<!-- Chain Replication demo: write a value, watch it flow head-to-tail. -->
<div class="controls">
<input id="val-input" type="text" maxlength="12" placeholder="e.g. hello" autocomplete="off" />
<button id="btn-write" type="button">Send Write</button>
</div>
<div id="chain" class="chain"></div>
<div class="action-row">
<button id="btn-fail" type="button" class="ghost">Fail a node</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="log" class="log"></div>
/* Styles for the chain replication demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; margin-bottom: .8rem; flex-wrap: wrap; }
input { padding: .4rem .7rem; border: 1px solid #ccc; border-radius: 8px;
font: 600 14px system-ui, sans-serif; width: 140px; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
/* Chain layout: nodes connected by arrows. */
.chain { display: flex; align-items: center; gap: 0; margin: .6rem 0 1rem; flex-wrap: wrap; }
.node { width: 80px; text-align: center; }
.node-box { border: 2px solid #1d3557; border-radius: 10px; padding: .4rem .2rem;
background: #e8eef3; position: relative; transition: background .25s, border-color .25s; }
.node-box.active { background: #ffd166; border-color: #e6a200; }
.node-box.failed { background: #f1f1f1; border-color: #bbb; color: #999; }
.node-label { font-size: .7rem; font-weight: 700; color: #1d3557; text-transform: uppercase;
letter-spacing: .05em; }
.node-box.failed .node-label { color: #aaa; }
.node-val { font: 700 18px ui-monospace, monospace; min-height: 1.5em; color: #1d3557; }
.node-box.failed .node-val { color: #ccc; }
.node-role { font-size: .65rem; color: #5a7088; margin-top: .15rem; }
.node-box.failed .node-role { color: #bbb; }
/* Arrow connector between nodes. */
.arrow { width: 32px; text-align: center; font-size: 1.3rem; color: #1d3557; flex-shrink: 0; }
.arrow.faded { color: #ccc; }
.action-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.status.info { color: #1d3557; }
/* Scrollable event log. */
.log { font-size: .78rem; font-family: ui-monospace, monospace; color: #555;
max-height: 120px; overflow-y: auto; border-top: 1px solid #ddd; padding-top: .4rem; }
.log-entry { margin-bottom: .15rem; }
.log-entry.ack { color: #0a7d33; }
.log-entry.fail { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chain Replication: writes flow left-to-right (head→tail). Only the tail commits.
// Node labels and roles — order is head, middle, tail.
const LABELS = ['Head', 'Middle', 'Tail'];
const ROLES_EN = ['head — accepts writes', 'middle — forwards writes', 'tail — answers reads'];
// Mutable state: each node tracks its label, role, current value, and failed flag.
let nodes = LABELS.map((label, i) => ({
label,
role: ROLES_EN[i],
value: '-',
failed: false
}));
// Append a timestamped line to the log panel.
function logMsg(text, cls) {
const log = document.getElementById('log');
const div = document.createElement('div');
div.className = 'log-entry' + (cls ? ' ' + cls : '');
div.textContent = text;
log.prepend(div);
// Keep log at most 40 lines to avoid unbounded growth.
while (log.children.length > 40) log.removeChild(log.lastChild);
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + (cls || '');
}
// Re-draw the full chain from the current state.
function render() {
const chain = document.getElementById('chain');
chain.innerHTML = '';
const alive = nodes.filter(n => !n.failed);
nodes.forEach((node, i) => {
// Build the DOM element for one node box.
const wrap = document.createElement('div');
wrap.className = 'node';
const box = document.createElement('div');
box.className = 'node-box' + (node.failed ? ' failed' : '');
box.id = 'node-' + i;
const labelEl = document.createElement('div');
labelEl.className = 'node-label';
labelEl.textContent = node.label;
const valEl = document.createElement('div');
valEl.className = 'node-val';
valEl.id = 'val-' + i;
valEl.textContent = node.failed ? '✕' : node.value;
const roleEl = document.createElement('div');
roleEl.className = 'node-role';
if (!node.failed) {
// Re-compute head/tail role from alive-node position.
const aliveIdx = alive.indexOf(node);
if (aliveIdx === 0) roleEl.textContent = 'head — accepts writes';
else if (aliveIdx === alive.length - 1) roleEl.textContent = 'tail — answers reads';
else roleEl.textContent = 'middle — forwards writes';
} else {
roleEl.textContent = 'failed';
}
box.appendChild(labelEl);
box.appendChild(valEl);
box.appendChild(roleEl);
wrap.appendChild(box);
chain.appendChild(wrap);
// Draw the → connector; grey it out if either neighbour has failed.
if (i < nodes.length - 1) {
const arr = document.createElement('div');
arr.className = 'arrow' + (node.failed || nodes[i + 1].failed ? ' faded' : '');
arr.textContent = '→';
chain.appendChild(arr);
}
});
}
// Step through alive nodes one by one with a 400 ms pause to visualise propagation.
// Animate a write flowing node by node, then ack flowing back.
function animateWrite(value, aliveNodes, onDone) {
let step = 0;
// Walk forwards through the alive list, highlighting each node.
function forward() {
if (step >= aliveNodes.length) {
// All nodes updated — send the ack back to the client.
logMsg(' ← Tail ack → client', 'ack');
onDone();
return;
}
const node = aliveNodes[step];
const origIdx = nodes.indexOf(node);
// Flash the node box yellow while it is processing the write.
const box = document.getElementById('node-' + origIdx);
if (box) box.classList.add('active');
logMsg(" " + fmt('→ %label%: stored "%value%"', { label: node.label, value: value }));
setTimeout(() => {
node.value = value;
const valEl = document.getElementById('val-' + origIdx);
if (valEl) valEl.textContent = value;
if (box) box.classList.remove('active');
step++;
forward();
}, 400);
}
forward();
}
// Handler for the Send Write button.
function doWrite() {
const input = document.getElementById('val-input');
const value = input.value.trim();
if (!value) { setStatus('Enter a value to write.', 'err'); return; }
const alive = nodes.filter(n => !n.failed);
if (alive.length === 0) { setStatus('All nodes have failed — reset to continue.', 'err'); return; }
document.getElementById('btn-write').disabled = true;
document.getElementById('btn-fail').disabled = true;
setStatus(fmt('Writing "%value%"...', { value: value }), 'info');
logMsg(fmt('Client → write "%value%"', { value: value }));
animateWrite(value, alive, () => {
setStatus(fmt('Committed "%value%" ✓', { value: value }), 'ok');
logMsg(fmt('Committed "%value%" at tail %label%', { value: value, label: alive[alive.length - 1].label }), 'ack');
input.value = '';
document.getElementById('btn-write').disabled = false;
document.getElementById('btn-fail').disabled = false;
});
}
// Randomly fail one alive node, then re-draw the chain.
function doFail() {
const alive = nodes.filter(n => !n.failed);
if (alive.length <= 1) { setStatus('Cannot fail: only one node remains.', 'err'); return; }
// Pick a random alive node to fail.
const target = alive[Math.floor(Math.random() * alive.length)];
target.failed = true;
logMsg(fmt('Node failed: %label%', { label: target.label }), 'fail');
setStatus(fmt('Failed %label% — chain re-linked automatically.', { label: target.label }), 'info');
render();
}
// Restore all nodes to their initial state.
function doReset() {
nodes = LABELS.map((label, i) => ({
label,
role: ROLES_EN[i],
value: '-',
failed: false
}));
document.getElementById('log').innerHTML = '';
setStatus('', '');
document.getElementById('val-input').value = '';
document.getElementById('btn-write').disabled = false;
document.getElementById('btn-fail').disabled = false;
render();
}
document.getElementById('btn-write').addEventListener('click', doWrite);
document.getElementById('btn-fail').addEventListener('click', doFail);
document.getElementById('btn-reset').addEventListener('click', doReset);
document.getElementById('val-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doWrite();
});
render();
<!-- Chain Replication demo: write a value, watch it flow head-to-tail. -->
<div class="controls">
<input id="val-input" type="text" maxlength="12" placeholder="e.g. hello" autocomplete="off" />
<button id="btn-write" type="button">Send Write</button>
</div>
<div id="chain" class="chain"></div>
<div class="action-row">
<button id="btn-fail" type="button" class="ghost">Fail a node</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="log" class="log"></div>
/* Styles for the chain replication demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; margin-bottom: .8rem; flex-wrap: wrap; }
input { padding: .4rem .7rem; border: 1px solid #ccc; border-radius: 8px;
font: 600 14px system-ui, sans-serif; width: 140px; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
/* Chain layout: nodes connected by arrows. */
.chain { display: flex; align-items: center; gap: 0; margin: .6rem 0 1rem; flex-wrap: wrap; }
.node { width: 80px; text-align: center; }
.node-box { border: 2px solid #1d3557; border-radius: 10px; padding: .4rem .2rem;
background: #e8eef3; position: relative; transition: background .25s, border-color .25s; }
.node-box.active { background: #ffd166; border-color: #e6a200; }
.node-box.failed { background: #f1f1f1; border-color: #bbb; color: #999; }
.node-label { font-size: .7rem; font-weight: 700; color: #1d3557; text-transform: uppercase;
letter-spacing: .05em; }
.node-box.failed .node-label { color: #aaa; }
.node-val { font: 700 18px ui-monospace, monospace; min-height: 1.5em; color: #1d3557; }
.node-box.failed .node-val { color: #ccc; }
.node-role { font-size: .65rem; color: #5a7088; margin-top: .15rem; }
.node-box.failed .node-role { color: #bbb; }
/* Arrow connector between nodes. */
.arrow { width: 32px; text-align: center; font-size: 1.3rem; color: #1d3557; flex-shrink: 0; }
.arrow.faded { color: #ccc; }
.action-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
.status.info { color: #1d3557; }
/* Scrollable event log. */
.log { font-size: .78rem; font-family: ui-monospace, monospace; color: #555;
max-height: 120px; overflow-y: auto; border-top: 1px solid #ddd; padding-top: .4rem; }
.log-entry { margin-bottom: .15rem; }
.log-entry.ack { color: #0a7d33; }
.log-entry.fail { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chain Replication: writes flow left-to-right (head→tail). Only the tail commits.
// Node labels and roles — order is head, middle, tail.
const LABELS = ['Head', 'Middle', 'Tail'];
const ROLES_EN = ['head — accepts writes', 'middle — forwards writes', 'tail — answers reads'];
// Mutable state: each node tracks its label, role, current value, and failed flag.
let nodes = LABELS.map((label, i) => ({
label,
role: ROLES_EN[i],
value: '-',
failed: false
}));
// Append a timestamped line to the log panel.
function logMsg(text, cls) {
const log = document.getElementById('log');
const div = document.createElement('div');
div.className = 'log-entry' + (cls ? ' ' + cls : '');
div.textContent = text;
log.prepend(div);
// Keep log at most 40 lines to avoid unbounded growth.
while (log.children.length > 40) log.removeChild(log.lastChild);
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + (cls || '');
}
// Re-draw the full chain from the current state.
function render() {
const chain = document.getElementById('chain');
chain.innerHTML = '';
const alive = nodes.filter(n => !n.failed);
nodes.forEach((node, i) => {
// Build the DOM element for one node box.
const wrap = document.createElement('div');
wrap.className = 'node';
const box = document.createElement('div');
box.className = 'node-box' + (node.failed ? ' failed' : '');
box.id = 'node-' + i;
const labelEl = document.createElement('div');
labelEl.className = 'node-label';
labelEl.textContent = node.label;
const valEl = document.createElement('div');
valEl.className = 'node-val';
valEl.id = 'val-' + i;
valEl.textContent = node.failed ? '✕' : node.value;
const roleEl = document.createElement('div');
roleEl.className = 'node-role';
if (!node.failed) {
// Re-compute head/tail role from alive-node position.
const aliveIdx = alive.indexOf(node);
if (aliveIdx === 0) roleEl.textContent = 'head — accepts writes';
else if (aliveIdx === alive.length - 1) roleEl.textContent = 'tail — answers reads';
else roleEl.textContent = 'middle — forwards writes';
} else {
roleEl.textContent = 'failed';
}
box.appendChild(labelEl);
box.appendChild(valEl);
box.appendChild(roleEl);
wrap.appendChild(box);
chain.appendChild(wrap);
// Draw the → connector; grey it out if either neighbour has failed.
if (i < nodes.length - 1) {
const arr = document.createElement('div');
arr.className = 'arrow' + (node.failed || nodes[i + 1].failed ? ' faded' : '');
arr.textContent = '→';
chain.appendChild(arr);
}
});
}
// Step through alive nodes one by one with a 400 ms pause to visualise propagation.
// Animate a write flowing node by node, then ack flowing back.
function animateWrite(value, aliveNodes, onDone) {
let step = 0;
// Walk forwards through the alive list, highlighting each node.
function forward() {
if (step >= aliveNodes.length) {
// All nodes updated — send the ack back to the client.
logMsg(' ← Tail ack → client', 'ack');
onDone();
return;
}
const node = aliveNodes[step];
const origIdx = nodes.indexOf(node);
// Flash the node box yellow while it is processing the write.
const box = document.getElementById('node-' + origIdx);
if (box) box.classList.add('active');
logMsg(" " + fmt('→ %label%: stored "%value%"', { label: node.label, value: value }));
setTimeout(() => {
node.value = value;
const valEl = document.getElementById('val-' + origIdx);
if (valEl) valEl.textContent = value;
if (box) box.classList.remove('active');
step++;
forward();
}, 400);
}
forward();
}
// Handler for the Send Write button.
function doWrite() {
const input = document.getElementById('val-input');
const value = input.value.trim();
if (!value) { setStatus('Enter a value to write.', 'err'); return; }
const alive = nodes.filter(n => !n.failed);
if (alive.length === 0) { setStatus('All nodes have failed — reset to continue.', 'err'); return; }
document.getElementById('btn-write').disabled = true;
document.getElementById('btn-fail').disabled = true;
setStatus(fmt('Writing "%value%"...', { value: value }), 'info');
logMsg(fmt('Client → write "%value%"', { value: value }));
animateWrite(value, alive, () => {
setStatus(fmt('Committed "%value%" ✓', { value: value }), 'ok');
logMsg(fmt('Committed "%value%" at tail %label%', { value: value, label: alive[alive.length - 1].label }), 'ack');
input.value = '';
document.getElementById('btn-write').disabled = false;
document.getElementById('btn-fail').disabled = false;
});
}
// Randomly fail one alive node, then re-draw the chain.
function doFail() {
const alive = nodes.filter(n => !n.failed);
if (alive.length <= 1) { setStatus('Cannot fail: only one node remains.', 'err'); return; }
// Pick a random alive node to fail.
const target = alive[Math.floor(Math.random() * alive.length)];
target.failed = true;
logMsg(fmt('Node failed: %label%', { label: target.label }), 'fail');
setStatus(fmt('Failed %label% — chain re-linked automatically.', { label: target.label }), 'info');
render();
}
// Restore all nodes to their initial state.
function doReset() {
nodes = LABELS.map((label, i) => ({
label,
role: ROLES_EN[i],
value: '-',
failed: false
}));
document.getElementById('log').innerHTML = '';
setStatus('', '');
document.getElementById('val-input').value = '';
document.getElementById('btn-write').disabled = false;
document.getElementById('btn-fail').disabled = false;
render();
}
document.getElementById('btn-write').addEventListener('click', doWrite);
document.getElementById('btn-fail').addEventListener('click', doFail);
document.getElementById('btn-reset').addEventListener('click', doReset);
document.getElementById('val-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doWrite();
});
render();
Comments
Loading comments...