The demo below shows three nodes (A, B, C) exchanging writes. Each write carries a vector clock — a small array that records how many writes each node has seen. When a node receives a message it checks: are all the writes this message depends on already present? If yes, it delivers immediately; if not, it holds the message until the dependencies arrive.
<!-- Three-node causal consistency demo with vector clocks -->
<div class="hint">Three nodes share writes. Each write carries a <b>vector clock</b> tracking its causal dependencies. Causally related writes always arrive in order; concurrent writes may arrive in any order — both are correct.</div>
<div class="nodes-row" id="nodesRow"></div>
<div class="controls">
<label for="fromSel">From:</label>
<select id="fromSel"></select>
<label for="msgInput">Write:</label>
<input id="msgInput" type="text" placeholder="e.g. hello" maxlength="30" />
<button id="sendBtn" type="button">Send</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="logPanel" class="log-panel">
<div class="log-title">Event log</div>
<div id="logBody"></div>
</div>
/* Styles for node cards and log panel */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a2e; }
.hint { font-size: .85rem; color: #444; margin-bottom: .8rem; line-height: 1.5; }
.nodes-row { display: flex; gap: 10px; margin-bottom: .8rem; flex-wrap: wrap; }
.node-card {
flex: 1; min-width: 140px; border: 1.5px solid #cdd9e3;
border-radius: 10px; padding: 8px 10px; background: #f0f4f8;
}
.node-name { font-weight: 700; font-size: .95rem; margin-bottom: 4px; }
.vc-row { font-size: .75rem; color: #555; margin-bottom: 4px; font-family: ui-monospace, monospace; }
.inbox { font-size: .8rem; min-height: 28px; }
.inbox-item {
display: inline-block; margin: 2px 3px 2px 0; padding: 2px 7px;
border-radius: 12px; font-size: .78rem; font-weight: 600;
}
.delivered { background: #d4edda; color: #155724; border: 1px solid #b2dfc5; }
.pending { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
.controls label { font-size: .82rem; color: #444; }
select, input[type=text] {
padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 7px;
font-size: .85rem; background: #fff;
}
input[type=text] { min-width: 120px; }
button {
font: 600 13px system-ui; padding: .35rem .8rem;
border: 1px solid #1d3557; background: #1d3557;
color: #fff; border-radius: 7px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d3557; }
.log-panel { border: 1px solid #dde3ea; border-radius: 8px; padding: 8px; max-height: 140px; overflow-y: auto; }
.log-title { font-size: .78rem; font-weight: 700; color: #666; margin-bottom: 4px; }
.log-entry { font-size: .78rem; color: #333; padding: 2px 0; border-bottom: 1px solid #f0f0f0; }
.log-entry.causal { color: #155724; }
.log-entry.held { color: #856404; }
.log-entry.recv { color: #0c5460; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Causal consistency demo — vector clock implementation
// Each node tracks a vector clock vc[i] = number of writes from node i it has seen.
const NODE_NAMES = ['A','B','C'];
// Initialize three nodes, each with a zero vector clock
const state = {
nodes: NODE_NAMES.map(() => ({ vc: [0,0,0], delivered: [], pending: [] })),
msgSeq: 0
};
// Sending a write: increment sender’s own VC slot, tag the message, broadcast.
function sendMsg(fromIdx, text) {
const node = state.nodes[fromIdx];
node.vc[fromIdx]++;
const msg = {
id: ++state.msgSeq,
from: fromIdx,
text,
vc: [...node.vc]
};
// Sender delivers to itself immediately
deliverMsg(fromIdx, msg);
// Broadcast to all other nodes
NODE_NAMES.forEach((_, toIdx) => {
if (toIdx !== fromIdx) receiveMsg(toIdx, msg);
});
log(fmt(`Sent from %v%: "%text%" VC=[%vc%]`, { v: NODE_NAMES[fromIdx], text: text, vc: msg.vc }), 'recv');
render();
}
// A message is ready to deliver when all causal dependencies are satisfied.
function isReady(nodeIdx, msg) {
const nodeVc = state.nodes[nodeIdx].vc;
for (let i = 0; i < NODE_NAMES.length; i++) {
// For sender’s slot: must have seen one fewer than msg.vc[from]; others must be caught up.
if (i === msg.from) {
if (nodeVc[i] < msg.vc[i] - 1) return false;
} else {
if (nodeVc[i] < msg.vc[i]) return false;
}
}
return true;
}
function receiveMsg(toIdx, msg) {
state.nodes[toIdx].pending.push(msg);
flushPending(toIdx);
}
function deliverMsg(toIdx, msg) {
const node = state.nodes[toIdx];
for (let i = 0; i < NODE_NAMES.length; i++) {
if (msg.vc[i] > node.vc[i]) node.vc[i] = msg.vc[i];
}
node.delivered.push(msg);
}
function flushPending(nodeIdx) {
const node = state.nodes[nodeIdx];
let progress = true;
while (progress) {
progress = false;
for (let i = 0; i < node.pending.length; i++) {
const m = node.pending[i];
if (isReady(nodeIdx, m)) {
node.pending.splice(i, 1);
deliverMsg(nodeIdx, m);
log(fmt(`Delivered to %v%: "%text%"`, { v: NODE_NAMES[nodeIdx], text: m.text }), 'causal');
progress = true;
i--;
}
}
}
if (node.pending.length > 0) {
log(fmt(`Held (waiting) at %v%: %map%`, { v: NODE_NAMES[nodeIdx], map: node.pending.map(m => '"' + m.text + '"').join(', ') }), 'held');
}
}
// Re-render all node cards showing VC and delivered/pending messages.
function render() {
const row = document.getElementById('nodesRow');
row.innerHTML = '';
state.nodes.forEach((node, idx) => {
const card = document.createElement('div');
card.className = 'node-card';
const vcStr = `VC=[${node.vc.join(',')}]`;
card.innerHTML = fmt(`<div class="node-name">Node %v%</div>
<div class="vc-row">%vcStr%</div>
<div class="inbox">
%map%
%map2%
</div>`, { v: NODE_NAMES[idx], vcStr: vcStr, map: node.delivered.map(m => `<span class="inbox-item delivered" title="Delivered (causal deps satisfied)">${m.text}</span>`).join(''), map2: node.pending.map(m => `<span class="inbox-item pending" title="Pending (waiting for causal deps)">${m.text}</span>`).join('') });
row.appendChild(card);
});
}
function log(msg, cls) {
const body = document.getElementById('logBody');
const e = document.createElement('div');
e.className = 'log-entry ' + cls;
e.textContent = msg;
body.prepend(e);
}
// Build UI controls and attach event handlers
function initUI() {
const fromSel = document.getElementById('fromSel');
NODE_NAMES.forEach((n, i) => {
const o = document.createElement('option'); o.value = i; o.textContent = fmt(`Node %n%`, { n: n }); fromSel.appendChild(o);
});
document.getElementById('sendBtn').addEventListener('click', () => {
const text = document.getElementById('msgInput').value.trim();
if (!text) return;
sendMsg(Number(fromSel.value), text);
document.getElementById('msgInput').value = '';
});
document.getElementById('msgInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('sendBtn').click();
});
document.getElementById('resetBtn').addEventListener('click', () => {
state.nodes.forEach(n => { n.vc = [0,0,0]; n.delivered = []; n.pending = []; });
state.msgSeq = 0;
document.getElementById('logBody').innerHTML = '';
render();
});
render();
}
initUI();
<!-- Three-node causal consistency demo with vector clocks -->
<div class="hint">Three nodes share writes. Each write carries a <b>vector clock</b> tracking its causal dependencies. Causally related writes always arrive in order; concurrent writes may arrive in any order — both are correct.</div>
<div class="nodes-row" id="nodesRow"></div>
<div class="controls">
<label for="fromSel">From:</label>
<select id="fromSel"></select>
<label for="msgInput">Write:</label>
<input id="msgInput" type="text" placeholder="e.g. hello" maxlength="30" />
<button id="sendBtn" type="button">Send</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="logPanel" class="log-panel">
<div class="log-title">Event log</div>
<div id="logBody"></div>
</div>
/* Styles for node cards and log panel */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a2e; }
.hint { font-size: .85rem; color: #444; margin-bottom: .8rem; line-height: 1.5; }
.nodes-row { display: flex; gap: 10px; margin-bottom: .8rem; flex-wrap: wrap; }
.node-card {
flex: 1; min-width: 140px; border: 1.5px solid #cdd9e3;
border-radius: 10px; padding: 8px 10px; background: #f0f4f8;
}
.node-name { font-weight: 700; font-size: .95rem; margin-bottom: 4px; }
.vc-row { font-size: .75rem; color: #555; margin-bottom: 4px; font-family: ui-monospace, monospace; }
.inbox { font-size: .8rem; min-height: 28px; }
.inbox-item {
display: inline-block; margin: 2px 3px 2px 0; padding: 2px 7px;
border-radius: 12px; font-size: .78rem; font-weight: 600;
}
.delivered { background: #d4edda; color: #155724; border: 1px solid #b2dfc5; }
.pending { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
.controls label { font-size: .82rem; color: #444; }
select, input[type=text] {
padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 7px;
font-size: .85rem; background: #fff;
}
input[type=text] { min-width: 120px; }
button {
font: 600 13px system-ui; padding: .35rem .8rem;
border: 1px solid #1d3557; background: #1d3557;
color: #fff; border-radius: 7px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d3557; }
.log-panel { border: 1px solid #dde3ea; border-radius: 8px; padding: 8px; max-height: 140px; overflow-y: auto; }
.log-title { font-size: .78rem; font-weight: 700; color: #666; margin-bottom: 4px; }
.log-entry { font-size: .78rem; color: #333; padding: 2px 0; border-bottom: 1px solid #f0f0f0; }
.log-entry.causal { color: #155724; }
.log-entry.held { color: #856404; }
.log-entry.recv { color: #0c5460; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Causal consistency demo — vector clock implementation
// Each node tracks a vector clock vc[i] = number of writes from node i it has seen.
const NODE_NAMES = ['A','B','C'];
// Initialize three nodes, each with a zero vector clock
const state = {
nodes: NODE_NAMES.map(() => ({ vc: [0,0,0], delivered: [], pending: [] })),
msgSeq: 0
};
// Sending a write: increment sender’s own VC slot, tag the message, broadcast.
function sendMsg(fromIdx, text) {
const node = state.nodes[fromIdx];
node.vc[fromIdx]++;
const msg = {
id: ++state.msgSeq,
from: fromIdx,
text,
vc: [...node.vc]
};
// Sender delivers to itself immediately
deliverMsg(fromIdx, msg);
// Broadcast to all other nodes
NODE_NAMES.forEach((_, toIdx) => {
if (toIdx !== fromIdx) receiveMsg(toIdx, msg);
});
log(fmt(`Sent from %v%: "%text%" VC=[%vc%]`, { v: NODE_NAMES[fromIdx], text: text, vc: msg.vc }), 'recv');
render();
}
// A message is ready to deliver when all causal dependencies are satisfied.
function isReady(nodeIdx, msg) {
const nodeVc = state.nodes[nodeIdx].vc;
for (let i = 0; i < NODE_NAMES.length; i++) {
// For sender’s slot: must have seen one fewer than msg.vc[from]; others must be caught up.
if (i === msg.from) {
if (nodeVc[i] < msg.vc[i] - 1) return false;
} else {
if (nodeVc[i] < msg.vc[i]) return false;
}
}
return true;
}
function receiveMsg(toIdx, msg) {
state.nodes[toIdx].pending.push(msg);
flushPending(toIdx);
}
function deliverMsg(toIdx, msg) {
const node = state.nodes[toIdx];
for (let i = 0; i < NODE_NAMES.length; i++) {
if (msg.vc[i] > node.vc[i]) node.vc[i] = msg.vc[i];
}
node.delivered.push(msg);
}
function flushPending(nodeIdx) {
const node = state.nodes[nodeIdx];
let progress = true;
while (progress) {
progress = false;
for (let i = 0; i < node.pending.length; i++) {
const m = node.pending[i];
if (isReady(nodeIdx, m)) {
node.pending.splice(i, 1);
deliverMsg(nodeIdx, m);
log(fmt(`Delivered to %v%: "%text%"`, { v: NODE_NAMES[nodeIdx], text: m.text }), 'causal');
progress = true;
i--;
}
}
}
if (node.pending.length > 0) {
log(fmt(`Held (waiting) at %v%: %map%`, { v: NODE_NAMES[nodeIdx], map: node.pending.map(m => '"' + m.text + '"').join(', ') }), 'held');
}
}
// Re-render all node cards showing VC and delivered/pending messages.
function render() {
const row = document.getElementById('nodesRow');
row.innerHTML = '';
state.nodes.forEach((node, idx) => {
const card = document.createElement('div');
card.className = 'node-card';
const vcStr = `VC=[${node.vc.join(',')}]`;
card.innerHTML = fmt(`<div class="node-name">Node %v%</div>
<div class="vc-row">%vcStr%</div>
<div class="inbox">
%map%
%map2%
</div>`, { v: NODE_NAMES[idx], vcStr: vcStr, map: node.delivered.map(m => `<span class="inbox-item delivered" title="Delivered (causal deps satisfied)">${m.text}</span>`).join(''), map2: node.pending.map(m => `<span class="inbox-item pending" title="Pending (waiting for causal deps)">${m.text}</span>`).join('') });
row.appendChild(card);
});
}
function log(msg, cls) {
const body = document.getElementById('logBody');
const e = document.createElement('div');
e.className = 'log-entry ' + cls;
e.textContent = msg;
body.prepend(e);
}
// Build UI controls and attach event handlers
function initUI() {
const fromSel = document.getElementById('fromSel');
NODE_NAMES.forEach((n, i) => {
const o = document.createElement('option'); o.value = i; o.textContent = fmt(`Node %n%`, { n: n }); fromSel.appendChild(o);
});
document.getElementById('sendBtn').addEventListener('click', () => {
const text = document.getElementById('msgInput').value.trim();
if (!text) return;
sendMsg(Number(fromSel.value), text);
document.getElementById('msgInput').value = '';
});
document.getElementById('msgInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('sendBtn').click();
});
document.getElementById('resetBtn').addEventListener('click', () => {
state.nodes.forEach(n => { n.vc = [0,0,0]; n.delivered = []; n.pending = []; });
state.msgSeq = 0;
document.getElementById('logBody').innerHTML = '';
render();
});
render();
}
initUI();
Try sending writes in different orders. Notice that causally related writes always arrive in the right order on every node, but concurrent writes (ones with no causal link) may arrive in different orders at different nodes — and that is perfectly correct behavior under causal consistency.
Comments
Loading comments...