0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
A laptop and a phone edit one picnic list through a hand-rolled sequence CRDT (RGA), and the demo opens mid-conflict: the link is already cut, both devices have rewritten the same line, and 29 operations sit in their outboxes. Hit Reconnect and the ops stream across the wire, merged characters glow in the color of the device that typed them, and both state fingerprints land on the same six hex digits — no prompt, no "which version do you want", nothing lost. It converges because every character is an immutable element with a unique id, a Lamport timestamp, and a permanent memory of the character it was typed after, so replicas that have seen the same operations rebuild byte-identical sequences no matter what order those operations arrived in. Try Run the classic conflict first, then cut the link yourself and type into both editors at once — and open Show character ids to watch "bread" sit there as five struck-through tombstones beside the phone's "focaccia".
Source — the code you see is the code that just ran 924 lines 42.2 KB index.html
<!doctype html>
<html lang="en" data-theme="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Conflict-Free Sync</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end token block ──────────────────────────────────────────────────────── */
/* per-replica origin colors (never green — green is earned) */
:root { --sL: #C05600; --sP: #2C5FC4; --tint: 26%; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --sL: #FFB454; --sP: #7FA8F5; --tint: 32%; }
}
:root[data-theme="dark"] { --sL: #FFB454; --sP: #7FA8F5; --tint: 32%; }
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; min-height: 100vh; overflow-x: hidden; }
.sr { position:absolute; width:1px; height:1px; padding:0; margin:-1px;
overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
.wrap { max-width: 1180px; margin: 0 auto; padding: 18px 15px 24px; }
/* ── top rail ─────────────────────────────────────────────────────────────── */
.rail { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
font: 500 11.5px var(--font-mono); color: var(--muted); margin-bottom: 14px; }
.rail .sep { opacity:.5; }
.rail .spacer { flex:1 1 auto; }
.pulse { width:7px; height:7px; border-radius:50%; background: var(--accent);
box-shadow: 0 0 0 0 var(--accent); animation: pulse 2.6s ease-out infinite; }
@keyframes pulse { 0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);}
70%{box-shadow:0 0 0 7px transparent;} 100%{box-shadow:0 0 0 0 transparent;} }
header h1 { margin: 0 0 6px; font-size: 21px; font-weight: 650; letter-spacing: -.015em; }
header .lede { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--muted); max-width: 74ch; }
header .lede b { color: var(--ink); font-weight: 600; }
/* ── buttons ──────────────────────────────────────────────────────────────── */
.btn { font: 600 13px/1 var(--font-sans); color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 8px; padding: 9px 12px;
display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
white-space: nowrap; transition: border-color .12s, color .12s, background .12s; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--surface); }
.btn.primary:hover { filter: brightness(1.08); color: var(--surface); }
.btn.stop { background: var(--danger); border-color: var(--danger); color: var(--surface); }
.btn.stop:hover { filter: brightness(1.08); color: var(--surface); }
.btn.sm { padding: 7px 10px; font-size: 12px; }
.btn .gl { font: 600 11px var(--font-mono); line-height: 1; opacity: .85; }
.btn .cnt { font: 650 11px var(--font-mono); }
.bar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:16px 0 10px; }
.bar .grow { flex:1 1 6px; }
/* ── status strip ─────────────────────────────────────────────────────────── */
.strip { display:flex; flex-wrap:wrap; align-items:center; gap:7px 14px;
font: 500 11.5px var(--font-mono); color: var(--muted);
border: 1px solid var(--line); background: var(--surface);
border-radius: var(--radius); padding: 9px 12px; }
.strip b { color: var(--ink); font-weight: 650; }
.pill { font: 650 10.5px var(--font-mono); letter-spacing:.11em; padding: 3px 8px;
border-radius: 999px; border: 1px solid var(--line); color: var(--muted);
background: var(--surface); white-space: nowrap; }
.pill[data-s="CONVERGED"] { color: var(--run); border-color: var(--run); background: var(--run-soft); }
.pill[data-s="DIVERGED"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.pill[data-s="SYNCING"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
/* ── stage ────────────────────────────────────────────────────────────────── */
.stage { display:grid; grid-template-columns: minmax(0,1fr) 164px minmax(0,1fr);
gap: 12px; align-items: stretch; margin-top: 12px; }
@media (max-width: 900px) { .stage { grid-template-columns: minmax(0,1fr); } }
.pane { border: 1px solid var(--line); background: var(--surface);
border-radius: var(--radius); display:flex; flex-direction:column; overflow:hidden; min-width:0; }
.pane .head { display:flex; align-items:center; gap:8px; padding: 9px 11px;
border-bottom: 1px solid var(--line); font: 650 12px var(--font-mono); }
.pane .head .dot { width:9px; height:9px; border-radius:2px; flex:0 0 auto; }
.pane[data-site="L"] .head .dot { background: var(--sL); }
.pane[data-site="P"] .head .dot { background: var(--sP); }
.pane .head .fp { margin-left:auto; font: 500 10.5px var(--font-mono); color: var(--muted); }
.pane .head .fp b { color: var(--ink); font-weight: 650; }
.pane .head .fp b.ok { color: var(--run); }
.ed { position: relative; height: 126px; }
.ed .hl, .ed textarea {
position: absolute; top:0; left:0; right:0; bottom:0; width:100%;
margin:0; padding: 11px 12px; border:0;
font: 500 13px/1.62 var(--font-mono); letter-spacing: 0; tab-size: 2;
white-space: pre-wrap; overflow-wrap: break-word;
}
.ed textarea { background: transparent; color: var(--ink); resize: none;
overflow: auto; scrollbar-width: thin; caret-color: var(--accent); }
.ed textarea:focus { outline: none; }
.ed .hl { color: transparent; pointer-events: none; user-select: none;
overflow: hidden; z-index: 0; }
.ed:focus-within { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 2px; }
.ed .hl span { border-radius: 2px; transition: background-color .6s ease .1s; }
.ed .hl span.lit.o-L { background-color: color-mix(in srgb, var(--sL) var(--tint), transparent); }
.ed .hl span.lit.o-P { background-color: color-mix(in srgb, var(--sP) var(--tint), transparent); }
.pane .foot { border-top: 1px dashed var(--line); padding: 8px 11px 9px;
font: 500 11px var(--font-mono); color: var(--muted); }
.pane .foot .hd { display:flex; align-items:center; gap:6px; }
.pane .foot .hd .n { color: var(--ink); font-weight: 650; }
.pane .foot .hd .tag { margin-left:auto; font-size:10px; letter-spacing:.09em; }
.pane[data-q="1"] .foot { border-top-color: var(--accent); background: var(--accent-soft); }
.pane[data-q="1"] .foot .hd .n, .pane[data-q="1"] .foot .hd .tag { color: var(--accent); }
.oplist { list-style:none; margin: 6px 0 0; padding: 0; max-height: 66px;
overflow: auto; overflow-x: auto; scrollbar-width: thin; }
.oplist li { white-space: nowrap; line-height: 1.65; color: var(--ink); opacity: .82; }
.oplist li .k { color: var(--muted); }
.oplist li .ch { color: var(--accent); }
.oplist.idle { color: var(--muted); }
.oplist:focus-visible, .logbox ol:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* ── link column ──────────────────────────────────────────────────────────── */
.link { display:flex; flex-direction:column; align-items:center; gap:9px;
padding: 10px 8px; border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); min-width: 0; }
.link .lpill { font: 650 10.5px var(--font-mono); letter-spacing:.11em; padding: 3px 9px;
border-radius: 999px; border: 1px solid var(--accent); color: var(--accent);
background: var(--accent-soft); }
.link.off .lpill { border-style: dashed; border-color: var(--line); color: var(--muted); background: transparent; }
.link .lanes { width:100%; display:flex; flex-direction:column; gap:8px; margin-top:2px; }
.lanewrap { width:100%; }
.lanewrap .dir { display:block; font: 500 9.5px var(--font-mono); color: var(--muted);
letter-spacing:.04em; margin-bottom: 3px; }
.lane { position: relative; height: 22px; border-radius: 6px; overflow: hidden;
border: 1px dashed var(--line); background: color-mix(in srgb, var(--ink) 3%, transparent); }
.lane::after { content: "idle"; position:absolute; inset:0; display:flex;
align-items:center; justify-content:center; font: 500 9px var(--font-mono);
color: var(--muted); opacity:.6; letter-spacing:.08em; }
.link.off .lane::after { content: "no link"; }
.lane.busy::after { opacity: 0; }
.lane .chip { position:absolute; top:3px; left:0; font: 650 10px/1 var(--font-mono);
padding: 3px 5px; border-radius: 4px; white-space: nowrap; opacity: 0; }
.lane .chip.L { color: var(--sL); background: color-mix(in srgb, var(--sL) 16%, var(--surface));
border: 1px solid color-mix(in srgb, var(--sL) 42%, transparent); }
.lane .chip.P { color: var(--sP); background: color-mix(in srgb, var(--sP) 16%, var(--surface));
border: 1px solid color-mix(in srgb, var(--sP) 42%, transparent); }
.link .hint { font: 500 10px var(--font-mono); color: var(--muted); text-align:center; line-height:1.5; }
@media (max-width: 900px) {
.link { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 8px 12px; }
.link .lanes { flex: 1 1 220px; }
.link .hint { flex: 1 1 100%; }
}
/* ── why callout ──────────────────────────────────────────────────────────── */
.why { margin-top: 12px; border: 1px solid var(--line); border-left: 3px solid var(--accent);
background: var(--surface); border-radius: 8px; padding: 10px 13px;
font-size: 13px; line-height: 1.55; }
.why b { font-weight: 650; }
.why code { font: 600 12px var(--font-mono); color: var(--accent); }
/* ── id inspector ─────────────────────────────────────────────────────────── */
.ids { margin-top: 12px; border: 1px solid var(--line); background: var(--surface);
border-radius: var(--radius); padding: 10px 12px; }
.ids[hidden] { display: none; }
.ids .cap { font: 500 11px var(--font-mono); color: var(--muted); margin-bottom: 8px;
display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
.ids .cap .sw { display:inline-flex; align-items:center; gap:5px; }
.ids .cap .sw i { width:8px; height:8px; border-radius:2px; display:block; }
.cells { display:flex; flex-wrap:wrap; gap:3px; }
.cell { display:flex; flex-direction:column; align-items:center; gap:2px;
border:1px solid var(--line); border-radius:5px; padding:3px 4px 2px; min-width: 22px; }
.cell .c { font: 600 12.5px/1 var(--font-mono); color: var(--ink); }
.cell .i { font: 500 8.5px/1 var(--font-mono); color: var(--muted); }
.cell[data-o="L"] { border-color: color-mix(in srgb, var(--sL) 45%, var(--line)); }
.cell[data-o="P"] { border-color: color-mix(in srgb, var(--sP) 45%, var(--line)); }
.cell[data-o="L"] .i { color: var(--sL); }
.cell[data-o="P"] .i { color: var(--sP); }
.cell.dead { border-style: dashed; opacity: .5; }
.cell.dead .c { text-decoration: line-through; }
/* ── event log ────────────────────────────────────────────────────────────── */
.logbox { margin-top: 12px; border: 1px solid var(--line); background: var(--surface);
border-radius: var(--radius); padding: 9px 12px; }
.logbox .cap { font: 600 10.5px var(--font-mono); letter-spacing:.1em; color: var(--muted); margin-bottom: 6px; }
.logbox ol { list-style:none; margin:0; padding:0; max-height: 112px; overflow:auto;
scrollbar-width: thin; font: 500 11.5px var(--font-mono); }
.logbox li { display:flex; gap:9px; line-height:1.75; color: var(--muted); }
.logbox li .t { color: var(--muted); opacity:.75; flex:0 0 auto; }
.logbox li .m { color: var(--ink); opacity:.85; }
.logbox li:first-child .m { opacity: 1; }
.logbox li[data-k="ok"] .m { color: var(--run); }
.logbox li[data-k="on"] .m { color: var(--accent); }
footer.foot { margin-top: 26px; padding-top: 13px; border-top: 1px solid var(--line);
font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
footer.foot .spacer { flex:1 1 auto; }
@media (max-width: 420px) {
header h1 { font-size: 19px; }
.bar .btn { flex: 1 1 auto; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important;
animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="rail">
<span class="pulse" aria-hidden="true"></span>
<span>sequence CRDT · RGA</span>
<span class="sep">·</span>
<span>two replicas, no server</span>
<span class="spacer"></span>
<button class="btn sm" id="themeBtn" type="button" title="Toggle light / dark">
<span aria-hidden="true">◐</span> <span id="themeLbl">Dark</span>
</button>
</div>
<header>
<h1>Conflict-Free Sync</h1>
<p class="lede">
Two devices editing the same note while the connection is down. Right now the
<b>laptop</b> and the <b>phone</b> hold <b>different text</b> and a pile of queued
operations. Hit <b>Reconnect</b> and watch them merge — no prompts, no "which version
do you want", no lost characters.
</p>
</header>
<div class="bar">
<button class="btn primary" id="runBtn" type="button">
<span class="gl" aria-hidden="true">▶</span> Run the classic conflict
</button>
<button class="btn" id="resetBtn" type="button">
<span class="gl" aria-hidden="true">↻</span> Reset both
</button>
<span class="grow"></span>
<button class="btn" id="idsBtn" type="button" aria-pressed="false" aria-controls="ids">
<span class="gl" aria-hidden="true">#</span> Show character ids
</button>
</div>
<div class="strip">
<span class="pill" id="convPill" data-s="DIVERGED">DIVERGED</span>
<span>link <b id="sLink">OFFLINE</b></span>
<span>chars <b id="sChars">0</b></span>
<span>ops applied <b id="sOps">0</b></span>
<span>tombstones <b id="sTomb">0</b></span>
<span>queued <b id="sPend">0</b></span>
<span id="sMsg"></span>
</div>
<p class="sr" id="announce" role="status" aria-live="polite"></p>
<div class="stage">
<section class="pane" data-site="L" aria-label="laptop replica">
<div class="head">
<span class="dot" aria-hidden="true"></span> laptop
<span class="fp">state <b id="fpL">------</b></span>
</div>
<div class="ed">
<div class="hl" id="hlL" aria-hidden="true"></div>
<textarea id="taL" spellcheck="false" aria-label="laptop note — type to edit"></textarea>
</div>
<div class="foot">
<div class="hd"><span class="n" id="qnL">0</span> pending ops <span class="tag" id="qtL">IN SYNC</span></div>
<ol class="oplist" id="qlL"></ol>
</div>
</section>
<div class="link" id="link">
<span class="lpill" id="linkPill">OFFLINE</span>
<button class="btn sm primary" id="linkBtn" type="button" aria-pressed="true">Reconnect</button>
<div class="lanes">
<div class="lanewrap">
<span class="dir">laptop → phone</span>
<div class="lane" id="laneLP" aria-hidden="true"></div>
</div>
<div class="lanewrap">
<span class="dir">phone → laptop</span>
<div class="lane" id="lanePL" aria-hidden="true"></div>
</div>
</div>
<div class="hint" id="linkHint">ops waiting</div>
</div>
<section class="pane" data-site="P" aria-label="phone replica">
<div class="head">
<span class="dot" aria-hidden="true"></span> phone
<span class="fp">state <b id="fpP">------</b></span>
</div>
<div class="ed">
<div class="hl" id="hlP" aria-hidden="true"></div>
<textarea id="taP" spellcheck="false" aria-label="phone note — type to edit"></textarea>
</div>
<div class="foot">
<div class="hd"><span class="n" id="qnP">0</span> pending ops <span class="tag" id="qtP">IN SYNC</span></div>
<ol class="oplist" id="qlP"></ol>
</div>
</section>
</div>
<p class="why">
<b>Why it always converges:</b> every character gets a unique id like
<code>L:14</code> and permanently remembers which character it was typed after, so
both devices rebuild the exact same order from the same set of operations —
whatever order those operations happen to arrive in.
</p>
<div class="ids" id="ids" hidden>
<div class="cap">
<span>merged sequence, tombstones included</span>
<span class="sw"><i style="background:var(--sL)"></i> typed on laptop</span>
<span class="sw"><i style="background:var(--sP)"></i> typed on phone</span>
<span class="sw">dashed + struck = deleted, kept as a tombstone</span>
</div>
<div class="cells" id="cells"></div>
</div>
<div class="logbox">
<div class="cap">EVENT LOG</div>
<ol id="log"></ol>
</div>
<footer class="foot">
<span>demo · sloprun.dev</span>
<span class="spacer"></span>
<span id="perf"></span>
</footer>
</div>
<script>
(function () {
"use strict";
var T0 = (window.performance && performance.now) ? performance.now() : Date.now();
function now(){ return ((window.performance && performance.now) ? performance.now() : Date.now()) - T0; }
function $(id){ return document.getElementById(id); }
function esc(s){ return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
var mqm = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
var reduce = !!(mqm && mqm.matches);
if (mqm){
var onmm = function(){ reduce = mqm.matches; };
if (mqm.addEventListener) mqm.addEventListener("change", onmm);
else if (mqm.addListener) mqm.addListener(onmm);
}
/* ═══ the CRDT ══════════════════════════════════════════════════════════════
RGA (replicated growable array). Every character is an immutable element:
{ id:"L:14", ts:14, site:"L", ch:"o", after:"L:13", del:false }
ts is a Lamport clock, so an element's ts is always greater than the ts of
the element it was inserted after. Remote inserts are placed just after
their origin, skipping any element that sorts higher under (ts, site).
That skip rule is total and deterministic, so replicas that have seen the
same set of operations hold byte-identical sequences. Deletes only flip a
tombstone flag, which is idempotent and commutative. */
function cmp(a, b){
if (a.ts !== b.ts) return a.ts - b.ts;
return a.site < b.site ? -1 : (a.site > b.site ? 1 : 0);
}
function Rep(site){
this.site = site;
this.clock = 0;
this.items = []; /* full sequence, tombstones included */
this.index = Object.create(null); /* id -> element */
this.seen = Object.create(null); /* op id -> 1 (dedupe) */
this.out = []; /* ops not yet handed to the peer */
this.defer = []; /* ops waiting on a missing origin */
this.applied = 0;
}
Rep.prototype.tick = function(){ return ++this.clock; };
Rep.prototype.observe = function(ts){ if (ts > this.clock) this.clock = ts; };
Rep.prototype.visible = function(){
var v = [], i;
for (i = 0; i < this.items.length; i++) if (!this.items[i].del) v.push(this.items[i]);
return v;
};
Rep.prototype.text = function(){
var s = "", i;
for (i = 0; i < this.items.length; i++) if (!this.items[i].del) s += this.items[i].ch;
return s;
};
Rep.prototype.tombs = function(){
var n = 0, i;
for (i = 0; i < this.items.length; i++) if (this.items[i].del) n++;
return n;
};
Rep.prototype.applyIns = function(op){
if (this.index[op.id]) { this.observe(op.ts); return true; }
var i = 0;
if (op.after != null){
var a = this.index[op.after];
if (!a) return false; /* origin not here yet — defer */
i = this.items.indexOf(a) + 1;
}
while (i < this.items.length && cmp(this.items[i], op) > 0) i++;
var el = { id: op.id, ts: op.ts, site: op.site, ch: op.ch, after: op.after, del: false };
this.items.splice(i, 0, el);
this.index[op.id] = el;
this.observe(op.ts);
this.applied++;
return true;
};
Rep.prototype.applyDel = function(op){
var el = this.index[op.target];
if (!el) return false;
if (!el.del){ el.del = true; this.applied++; }
this.observe(op.ts);
return true;
};
Rep.prototype.localInsert = function(visIdx, text){
var vis = this.visible(), ops = [], i, ts, op;
var after = visIdx > 0 && vis[visIdx - 1] ? vis[visIdx - 1].id : null;
for (i = 0; i < text.length; i++){
ts = this.tick();
op = { k:"i", id: this.site + ":" + ts, ts: ts, site: this.site, ch: text.charAt(i), after: after };
this.applyIns(op); this.seen[op.id] = 1;
after = op.id; ops.push(op);
}
this.out = this.out.concat(ops);
return ops;
};
Rep.prototype.localDelete = function(visIdx, count){
var vis = this.visible(), ops = [], i, ts, op, el;
for (i = visIdx; i < visIdx + count && i < vis.length; i++){
el = vis[i]; ts = this.tick();
op = { k:"d", id: this.site + ":" + ts, ts: ts, site: this.site, target: el.id, ch: el.ch };
this.applyDel(op); this.seen[op.id] = 1; ops.push(op);
}
this.out = this.out.concat(ops);
return ops;
};
Rep.prototype.fp = function(){ /* FNV-1a over the whole state */
var h = 0x811c9dc5, i, j, s;
for (i = 0; i < this.items.length; i++){
s = this.items[i].id + (this.items[i].del ? "-" : "+") + this.items[i].ch;
for (j = 0; j < s.length; j++){ h ^= s.charCodeAt(j); h = (h * 0x01000193) >>> 0; }
}
return ("00000000" + h.toString(16)).slice(-6);
};
/* delivery: retries deferred ops until no further progress (causal healing) */
function deliver(rep, ops){
var pool = rep.defer.concat(ops), applied = [], rest, moved = true, i, op, ok;
rep.defer = [];
while (moved){
moved = false; rest = [];
for (i = 0; i < pool.length; i++){
op = pool[i];
if (rep.seen[op.id]) continue;
ok = (op.k === "i") ? rep.applyIns(op) : rep.applyDel(op);
if (ok){ rep.seen[op.id] = 1; applied.push(op); moved = true; }
else rest.push(op);
}
pool = rest;
}
rep.defer = pool;
return applied;
}
/* ═══ state ═════════════════════════════════════════════════════════════════ */
var BASE = "Picnic Saturday. Bring: bread, cheese";
var epoch = 0, online = true, flight = 0, L, P;
var panes = {
L: { key:"L", name:"laptop", ta:$("taL"), hl:$("hlL"), fp:$("fpL"),
qn:$("qnL"), qt:$("qtL"), ql:$("qlL"), box:document.querySelector('.pane[data-site="L"]'),
last:"", flash:Object.create(null), flying:null },
P: { key:"P", name:"phone", ta:$("taP"), hl:$("hlP"), fp:$("fpP"),
qn:$("qnP"), qt:$("qtP"), ql:$("qlP"), box:document.querySelector('.pane[data-site="P"]'),
last:"", flash:Object.create(null), flying:null }
};
var timers = [], sTimers = [], scriptOn = false, unflashTimer = null;
function later(ms, fn){
var id = setTimeout(function(){
var k = timers.indexOf(id); if (k >= 0) timers.splice(k, 1);
fn();
}, ms);
timers.push(id); return id;
}
function sLater(ms, fn){ var id = setTimeout(fn, ms); sTimers.push(id); return id; }
function killScriptTimers(){ var i; for (i = 0; i < sTimers.length; i++) clearTimeout(sTimers[i]); sTimers = []; }
/* ═══ log ═══════════════════════════════════════════════════════════════════ */
var logEl = $("log");
function note(msg, kind){
var li = document.createElement("li");
if (kind) li.setAttribute("data-k", kind);
var t = document.createElement("span"); t.className = "t";
t.textContent = "+" + (now() / 1000).toFixed(1) + "s";
var m = document.createElement("span"); m.className = "m"; m.textContent = msg;
li.appendChild(t); li.appendChild(m);
logEl.insertBefore(li, logEl.firstChild);
while (logEl.children.length > 40) logEl.removeChild(logEl.lastChild);
}
function clearLog(){ logEl.innerHTML = ""; }
/* ═══ rendering ═════════════════════════════════════════════════════════════ */
function shown(ch){ return ch === " " ? "␣" : (ch === "\n" ? "⏎" : ch); }
function remap(oldT, newT, pos){
var p = 0, m = Math.min(oldT.length, newT.length), s = 0;
while (p < m && oldT.charCodeAt(p) === newT.charCodeAt(p)) p++;
if (pos <= p) return Math.min(pos, newT.length);
while (s < oldT.length - p && s < newT.length - p &&
oldT.charCodeAt(oldT.length - 1 - s) === newT.charCodeAt(newT.length - 1 - s)) s++;
if (pos >= oldT.length - s) return Math.max(0, pos + (newT.length - oldT.length));
return Math.min(pos, newT.length);
}
function paintHL(p){
var vis = p.rep.visible(), html = "", buf = "", cur = null, i, k;
function flush(){
if (!buf) return;
html += cur ? '<span class="lit o-' + cur + '">' + esc(buf) + "</span>"
: "<span>" + esc(buf) + "</span>";
buf = "";
}
for (i = 0; i < vis.length; i++){
k = p.flash[vis[i].id] ? vis[i].site : null;
if (k !== cur){ flush(); cur = k; }
buf += vis[i].ch;
}
flush();
p.hl.innerHTML = html + "\n";
p.hl.style.right = Math.max(0, p.ta.offsetWidth - p.ta.clientWidth) + "px";
p.hl.scrollTop = p.ta.scrollTop;
}
function fmtOp(op){
if (op.k === "i")
return '<span class="k">' + op.id + '</span> ins <span class="ch">"' + esc(shown(op.ch)) +
'"</span> <span class="k">after</span> ' + (op.after || "HEAD");
return '<span class="k">' + op.id + '</span> del ' + op.target +
' <span class="ch">"' + esc(shown(op.ch)) + '"</span>';
}
function renderPane(p){
var txt = p.rep.text();
if (p.ta.value !== txt){
var focused = document.activeElement === p.ta;
var a = p.ta.selectionStart, b = p.ta.selectionEnd, old = p.ta.value;
p.ta.value = txt;
if (focused){
try { p.ta.setSelectionRange(remap(old, txt, a), remap(old, txt, b)); } catch (e) {}
}
}
p.last = txt;
paintHL(p);
p.fp.textContent = p.rep.fp();
var q = p.rep.out, fly = p.flying || [], i, li;
var list = q.length ? q : fly; /* on the wire = still worth showing */
p.qn.textContent = list.length;
p.box.setAttribute("data-q", list.length ? "1" : "0");
p.qt.textContent = q.length ? "QUEUED LOCALLY"
: (fly.length ? "IN FLIGHT" : (online ? "IN SYNC" : "NOTHING PENDING"));
p.ql.innerHTML = "";
p.ql.className = "oplist" + (list.length ? "" : " idle");
if (!list.length){
li = document.createElement("li");
li.innerHTML = '<span class="k">' + (online ? "every edit sent immediately" : "no local edits since going offline") + "</span>";
p.ql.appendChild(li);
} else {
for (i = Math.max(0, list.length - 14); i < list.length; i++){
li = document.createElement("li");
li.innerHTML = fmtOp(list[i]);
p.ql.appendChild(li);
}
if (list.length > 14){
li = document.createElement("li");
li.innerHTML = '<span class="k">… ' + (list.length - 14) + " earlier ops</span>";
p.ql.insertBefore(li, p.ql.firstChild);
}
}
}
function renderStatus(){
var tl = L.text(), tp = P.text(), same = (L.fp() === P.fp());
var pend = L.out.length + P.out.length;
$("sChars").textContent = (tl.length === tp.length && tl === tp) ? String(tl.length)
: (tl.length + " / " + tp.length);
$("sOps").textContent = L.applied + P.applied;
$("sTomb").textContent = L.tombs() + " / " + P.tombs();
$("sPend").textContent = pend;
$("sLink").textContent = online ? "ONLINE" : "OFFLINE";
var pill = $("convPill"), msg = $("sMsg");
if (flight > 0){
pill.textContent = "SYNCING"; pill.setAttribute("data-s", "SYNCING");
msg.textContent = flight + " op" + (flight === 1 ? "" : "s") + " in flight";
} else if (same && pend === 0){
pill.textContent = "CONVERGED"; pill.setAttribute("data-s", "CONVERGED");
msg.textContent = "both replicas hold the identical sequence";
} else {
pill.textContent = "DIVERGED"; pill.setAttribute("data-s", "DIVERGED");
msg.textContent = same ? "waiting to exchange" : "texts differ until the next exchange";
}
var okfp = same && pend === 0 && flight === 0;
panes.L.fp.className = okfp ? "ok" : "";
panes.P.fp.className = okfp ? "ok" : "";
$("link").className = "link" + (online ? "" : " off");
$("linkPill").textContent = online ? "ONLINE" : "OFFLINE";
$("linkBtn").textContent = online ? "Go offline" : "Reconnect";
$("linkBtn").setAttribute("aria-pressed", online ? "false" : "true");
$("linkBtn").className = "btn sm" + (online ? "" : " primary");
$("linkHint").textContent = online
? (flight > 0 ? "in flight…" : "every op crosses instantly")
: (pend ? pend + " op" + (pend === 1 ? "" : "s") + " waiting" : "nothing to send yet");
var ann = pill.textContent + ", link " + (online ? "online" : "offline");
if (ann !== lastAnn){ lastAnn = ann; $("announce").textContent = ann; }
if (idsOpen) renderCells();
}
var lastAnn = "";
var idsOpen = false;
function renderCells(){
var host = $("cells"), src = L.items, frag = document.createDocumentFragment(), i, el, d, c, s;
var max = Math.min(src.length, 220);
host.innerHTML = "";
for (i = 0; i < max; i++){
el = src[i];
d = document.createElement("div");
d.className = "cell" + (el.del ? " dead" : "");
d.setAttribute("data-o", el.site);
c = document.createElement("span"); c.className = "c"; c.textContent = shown(el.ch);
s = document.createElement("span"); s.className = "i"; s.textContent = el.id;
d.appendChild(c); d.appendChild(s);
frag.appendChild(d);
}
host.appendChild(frag);
if (src.length > max){
var more = document.createElement("div");
more.className = "cell"; more.innerHTML = '<span class="c">…</span><span class="i">+' + (src.length - max) + "</span>";
host.appendChild(more);
}
}
function renderAll(){ renderPane(panes.L); renderPane(panes.P); renderStatus(); }
/* ═══ flash (merged characters, tinted by origin) ═══════════════════════════ */
function markFlash(p, ops){
var i;
for (i = 0; i < ops.length; i++) if (ops[i].k === "i") p.flash[ops[i].id] = 1;
}
function clearFlash(p){
p.flash = Object.create(null);
var lit = p.hl.querySelectorAll("span.lit"), i;
for (i = 0; i < lit.length; i++) lit[i].className = lit[i].className.replace("lit ", "");
}
function scheduleUnflash(){
if (unflashTimer) clearTimeout(unflashTimer);
unflashTimer = setTimeout(function(){ clearFlash(panes.L); clearFlash(panes.P); }, 2600);
}
/* ═══ the wire ══════════════════════════════════════════════════════════════ */
function spawnChips(lane, ops, dir){
var n = Math.min(ops.length, 8), w = lane.clientWidth || 130, dur = 520, stag = 68, i;
if (!n) return 0;
lane.className = "lane busy";
setTimeout(function(){ if (!lane.querySelector(".chip")) lane.className = "lane"; }, dur + (n - 1) * stag + 40);
for (i = 0; i < n; i++){
(function (op, k){
var el = document.createElement("span");
el.className = "chip " + op.site;
el.textContent = (op.k === "i" ? "+" + shown(op.ch) : "−" + shown(op.ch));
lane.appendChild(el);
var cw = 30;
var from = dir > 0 ? 2 : Math.max(2, w - cw - 2);
var to = dir > 0 ? Math.max(2, w - cw - 2) : 2;
if (!el.animate){
el.style.opacity = "1"; el.style.transform = "translateX(" + ((from + to) / 2) + "px)";
setTimeout(function(){ if (el.parentNode) el.parentNode.removeChild(el); }, dur + k * stag);
return;
}
var a = el.animate([
{ transform:"translateX(" + from + "px)", opacity:0 },
{ transform:"translateX(" + (from + (to - from) * 0.2) + "px)", opacity:1, offset:0.2 },
{ transform:"translateX(" + (from + (to - from) * 0.8) + "px)", opacity:1, offset:0.8 },
{ transform:"translateX(" + to + "px)", opacity:0 }
], { duration: dur, delay: k * stag, easing:"linear" });
a.onfinish = function(){ if (el.parentNode) el.parentNode.removeChild(el); };
})(ops[i], i);
}
return dur + (n - 1) * stag;
}
function syncNow(animate){
var a = L.out, b = P.out;
if (!a.length && !b.length) return;
L.out = []; P.out = [];
var my = epoch, n = a.length + b.length;
var commit = function (){
if (my !== epoch) return;
panes.L.flying = null; panes.P.flying = null;
flight -= n;
if (flight < 0) flight = 0;
var toP = deliver(P, a), toL = deliver(L, b);
markFlash(panes.P, toP); markFlash(panes.L, toL);
renderAll(); scheduleUnflash();
if (toP.length || toL.length){
var conv = (L.fp() === P.fp());
note(toP.length + " op" + (toP.length === 1 ? "" : "s") + " → phone, " +
toL.length + " op" + (toL.length === 1 ? "" : "s") + " → laptop", "on");
if (conv && !L.out.length && !P.out.length && flight === 0)
note("converged — both replicas now read “" + L.text() + "”", "ok");
}
};
flight += n;
if (animate && !reduce){
panes.L.flying = a; panes.P.flying = b;
var d1 = spawnChips($("laneLP"), a, 1), d2 = spawnChips($("lanePL"), b, -1);
renderAll();
later(Math.max(d1, d2, 120), commit);
} else {
commit();
}
}
function setLink(on, log){
online = !!on;
if (log) note(online ? "link → ONLINE" : "link → OFFLINE (edits queue locally)", "on");
renderAll();
if (online) syncNow(true);
}
/* ═══ setup / scenarios ═════════════════════════════════════════════════════ */
function fresh(){
epoch++; flight = 0;
if (unflashTimer){ clearTimeout(unflashTimer); unflashTimer = null; }
killScriptTimers();
var i; for (i = 0; i < timers.length; i++) clearTimeout(timers[i]); timers = [];
L = new Rep("L"); P = new Rep("P");
panes.L.rep = L; panes.P.rep = P;
panes.L.flash = Object.create(null); panes.P.flash = Object.create(null);
panes.L.flying = null; panes.P.flying = null;
online = true;
L.localInsert(0, BASE);
var seed = L.out; L.out = [];
deliver(P, seed);
}
function diverge(){
online = false;
var t = P.text(), at = t.indexOf("bread");
if (at >= 0){ P.localDelete(at, 5); P.localInsert(at, "focaccia"); }
note("phone: swapped “bread” for “focaccia”");
L.localInsert(L.text().length, ", olives");
note("laptop: appended “, olives”");
P.localInsert(P.text().length, ", grapes");
note("phone: appended “, grapes” — same spot, no idea about the olives");
}
function boot(){
fresh(); clearLog();
note("both devices in sync — " + BASE.length + " chars");
note("link → OFFLINE (edits queue locally)", "on");
diverge();
renderAll();
}
function resetAll(){
stopScript();
fresh(); clearLog();
note("reset — both replicas rebuilt from " + BASE.length + " characters", "on");
renderAll();
note("converged — both replicas now read “" + L.text() + "”", "ok");
renderStatus();
}
/* scripted scenario */
function setRunBtn(running){
var b = $("runBtn");
b.className = "btn " + (running ? "stop" : "primary");
b.innerHTML = running
? '<span class="gl" aria-hidden="true">■</span> Stop'
: '<span class="gl" aria-hidden="true">▶</span> Run the classic conflict';
}
function stopScript(){
if (!scriptOn) return;
scriptOn = false; killScriptTimers(); setRunBtn(false);
}
function step(fn){ return function(){ if (!scriptOn) return; fn(); }; }
function runScript(){
stopScript();
fresh(); clearLog();
scriptOn = true; setRunBtn(true);
note("both devices in sync — " + BASE.length + " chars");
renderAll();
var t = 0;
var add = function (ms, fn){ t += ms; sLater(t, step(fn)); };
add(700, function(){ setLink(false, true); });
add(900, function(){
var at = P.text().indexOf("bread");
if (at >= 0){ P.localDelete(at, 5); P.localInsert(at, "focaccia"); }
note("phone: swapped “bread” for “focaccia”"); renderAll();
});
add(1100, function(){
L.localInsert(L.text().length, ", olives");
note("laptop: appended “, olives”"); renderAll();
});
add(1100, function(){
P.localInsert(P.text().length, ", grapes");
note("phone: appended “, grapes” — same spot, no idea about the olives"); renderAll();
});
add(1000, function(){ note("two devices, two different notes, one shared insert point"); });
add(900, function(){ setLink(true, true); });
add(2200, function(){ stopScript(); });
}
/* ═══ typing ════════════════════════════════════════════════════════════════ */
var lastTypeLog = -9999, lastTypeKey = "";
function onInput(key){
return function (){
stopScript();
var p = panes[key], rep = p.rep, oldT = p.last, newT = p.ta.value;
if (oldT === newT) return;
var pre = 0, m = Math.min(oldT.length, newT.length), suf = 0;
while (pre < m && oldT.charCodeAt(pre) === newT.charCodeAt(pre)) pre++;
while (suf < oldT.length - pre && suf < newT.length - pre &&
oldT.charCodeAt(oldT.length - 1 - suf) === newT.charCodeAt(newT.length - 1 - suf)) suf++;
var delN = oldT.length - pre - suf, ins = newT.slice(pre, newT.length - suf);
if (delN > 0) rep.localDelete(pre, delN);
if (ins) rep.localInsert(pre, ins);
p.last = newT;
if (now() - lastTypeLog > 1400 || lastTypeKey !== key){
lastTypeLog = now(); lastTypeKey = key;
note(p.name + ": editing" + (online ? "" : " while offline"));
}
renderAll();
if (online) syncNow(true);
};
}
panes.L.ta.addEventListener("input", onInput("L"));
panes.P.ta.addEventListener("input", onInput("P"));
panes.L.ta.addEventListener("scroll", function(){ panes.L.hl.scrollTop = panes.L.ta.scrollTop; });
panes.P.ta.addEventListener("scroll", function(){ panes.P.hl.scrollTop = panes.P.ta.scrollTop; });
/* ═══ controls ══════════════════════════════════════════════════════════════ */
$("linkBtn").addEventListener("click", function (){ stopScript(); setLink(!online, true); });
$("resetBtn").addEventListener("click", resetAll);
$("runBtn").addEventListener("click", function (){
if (scriptOn){ stopScript(); note("scenario stopped — the replicas keep their state"); return; }
runScript();
});
$("idsBtn").addEventListener("click", function (){
idsOpen = !idsOpen;
this.setAttribute("aria-pressed", idsOpen ? "true" : "false");
$("ids").hidden = !idsOpen;
if (idsOpen) renderCells();
});
/* ═══ theme ═════════════════════════════════════════════════════════════════ */
var root = document.documentElement, themeBtn = $("themeBtn"), themeLbl = $("themeLbl");
function effective(){
var a = root.getAttribute("data-theme");
if (a === "dark" || a === "light") return a;
return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
}
function applyTheme(t){
if (t === "dark" || t === "light") root.setAttribute("data-theme", t);
else root.removeAttribute("data-theme");
themeLbl.textContent = effective() === "dark" ? "Light" : "Dark";
}
themeBtn.addEventListener("click", function (){ applyTheme(effective() === "dark" ? "light" : "dark"); });
window.addEventListener("message", function (e){
var d = e && e.data;
if (d && d.type === "sloprun:theme") applyTheme(d.theme);
});
if (window.matchMedia){
var mqd = window.matchMedia("(prefers-color-scheme: dark)");
var onScheme = function(){ themeLbl.textContent = effective() === "dark" ? "Light" : "Dark"; };
if (mqd.addEventListener) mqd.addEventListener("change", onScheme);
else if (mqd.addListener) mqd.addListener(onScheme);
}
applyTheme(root.getAttribute("data-theme") || null);
/* ═══ go ════════════════════════════════════════════════════════════════════ */
window.addEventListener("resize", function (){ paintHL(panes.L); paintHL(panes.P); });
boot();
$("perf").textContent = "interactive in " + Math.max(1, Math.round(now())) + "ms · 0 network requests";
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.