docs(core): update crdt/encoding/fetch docs and lint config

This commit is contained in:
2026-06-15 16:55:07 +07:00
parent aa2938cb34
commit a147ec0730
11 changed files with 143 additions and 143 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# @robonen/crdt # @robonen/crdt
Framework-agnostic CRDT primitives — the convergence engine behind `@robonen/editor`, usable on their own. Zero runtime dependencies; pure TypeScript; runs in Node and the browser. Framework-agnostic CRDT primitives — the convergence engine behind `@robonen/writekit`, usable on their own. Zero runtime dependencies; pure TypeScript; runs in Node and the browser.
Every primitive is built so that **applying the same set of operations in any order, with duplicates, yields the same state** (commutative, idempotent, convergent), verified by property tests. Every primitive is built so that **applying the same set of operations in any order, with duplicates, yields the same state** (commutative, idempotent, convergent), verified by property tests.
@@ -50,7 +50,7 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — converged
- `compareOpId` is the single deterministic tie-break (higher clock wins; site id breaks ties) every primitive agrees on — that's what makes LWW and RGA converge. - `compareOpId` is the single deterministic tie-break (higher clock wins; site id breaks ties) every primitive agrees on — that's what makes LWW and RGA converge.
- `VersionVector` assumes **dense** per-site clocks (1, 2, 3, …). - `VersionVector` assumes **dense** per-site clocks (1, 2, 3, …).
- The v1 wire format is JSON encoded to bytes — simple and debuggable; a compact varint format is a later optimization with no API change. - The v1 wire format is JSON encoded to bytes — simple and debuggable; a compact varint format is a later optimization with no API change.
- An editor-specific composition of these primitives (blocks + text + marks ↔ editor steps) lives in `@robonen/editor` under `crdt/native/`, not here — this package stays domain-agnostic. - A writekit-specific composition of these primitives (blocks + text + marks ↔ writekit steps) lives in `@robonen/writekit` under `crdt/native/`, not here — this package stays domain-agnostic.
## Development ## Development
+20 -20
View File
@@ -179,13 +179,13 @@ const propsSrc = `// Commutative — order of application doesn't matter:
same survivor. That single shared decision is what lets a last-writer-wins register and a sequence same survivor. That single shared decision is what lets a last-writer-wins register and a sequence
CRDT, built by different code, nonetheless agree on the final document. CRDT, built by different code, nonetheless agree on the final document.
</p> </p>
<div class="my-4 rounded-lg border border-(--border) bg-(--bg-subtle) p-4"> <div class="my-4 rounded-lg border border-border bg-bg-subtle p-4">
<p class="m-0 text-sm leading-relaxed text-(--fg-muted)"> <p class="m-0 text-sm leading-relaxed text-fg-muted">
<strong class="text-(--fg)">Why one rule for everything?</strong> <strong class="text-fg">Why one rule for everything?</strong>
<code class="text-(--accent-text)">LwwRegister</code> uses <code class="text-accent-text">LwwRegister</code> uses
<code class="text-(--accent-text)">compareOpId</code> to pick the surviving value; <code class="text-accent-text">compareOpId</code> to pick the surviving value;
<code class="text-(--accent-text)">Rga</code> uses it to break ties between concurrent inserts at <code class="text-accent-text">Rga</code> uses it to break ties between concurrent inserts at
the same position; <code class="text-(--accent-text)">MarkStore</code> uses it to decide which the same position; <code class="text-accent-text">MarkStore</code> uses it to decide which
formatting wins per character. One total order, applied consistently, is what turns a pile of formatting wins per character. One total order, applied consistently, is what turns a pile of
independent primitives into a coherent, converging system. independent primitives into a coherent, converging system.
</p> </p>
@@ -223,11 +223,11 @@ const propsSrc = `// Commutative — order of application doesn't matter:
<DocsCode :code="vvWireSrc" lang="ts" /> <DocsCode :code="vvWireSrc" lang="ts" />
<div class="prose-docs"> <div class="prose-docs">
<div class="my-4 rounded-lg border border-amber-500/30 bg-amber-500/10 p-4"> <div class="my-4 rounded-lg border border-amber-500/30 bg-amber-500/10 p-4">
<p class="m-0 text-sm leading-relaxed text-(--fg-muted)"> <p class="m-0 text-sm leading-relaxed text-fg-muted">
<strong class="text-amber-700 dark:text-amber-400">Density matters.</strong> <strong class="text-amber-700 dark:text-amber-400">Density matters.</strong>
<code class="text-(--accent-text)">VersionVector</code> only works because clocks arrive without <code class="text-accent-text">VersionVector</code> only works because clocks arrive without
gaps. If you generate ids with a raw <code class="text-(--accent-text)">LamportClock</code>, deliver gaps. If you generate ids with a raw <code class="text-accent-text">LamportClock</code>, deliver
them in order per site (the <code class="text-(--accent-text)">Replica</code>'s causal buffer does them in order per site (the <code class="text-accent-text">Replica</code>'s causal buffer does
this for you) so a single high-water mark per site can stand in for the full set of seen ops. this for you) so a single high-water mark per site can stand in for the full set of seen ops.
</p> </p>
</div> </div>
@@ -242,23 +242,23 @@ const propsSrc = `// Commutative — order of application doesn't matter:
</div> </div>
<DocsCode :code="propsSrc" lang="ts" /> <DocsCode :code="propsSrc" lang="ts" />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Commutative</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Commutative</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Order of application doesn't change the result. A replica can integrate operations as they arrive, Order of application doesn't change the result. A replica can integrate operations as they arrive,
in whatever sequence the network delivers them. in whatever sequence the network delivers them.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Idempotent</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Idempotent</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Applying the same operation twice is the same as applying it once. Redelivery and retries are safe; Applying the same operation twice is the same as applying it once. Redelivery and retries are safe;
version vectors make them free. version vectors make them free.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Convergent</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Convergent</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Same set of operations, same final state — full stop. Two replicas that have seen the same ops are Same set of operations, same final state — full stop. Two replicas that have seen the same ops are
byte-for-byte identical. byte-for-byte identical.
</p> </p>
+30 -30
View File
@@ -198,33 +198,33 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
<!-- Map of the package --> <!-- Map of the package -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Registers</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Registers</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<code class="text-(--accent-text)">LwwRegister</code> and <code class="text-accent-text">LwwRegister</code> and
<code class="text-(--accent-text)">LwwMap</code> single values and keyed maps where the <code class="text-accent-text">LwwMap</code> single values and keyed maps where the
write with the highest op id wins. write with the highest op id wins.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Ordering</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Ordering</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<code class="text-(--accent-text)">keyBetween</code> / <code class="text-accent-text">keyBetween</code> /
<code class="text-(--accent-text)">keysBetween</code> fractional indexing to place or move <code class="text-accent-text">keysBetween</code> fractional indexing to place or move
an item with a single string key. an item with a single string key.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Sequence</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Sequence</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<code class="text-(--accent-text)">Rga</code> a replicated growable array: an ordered <code class="text-accent-text">Rga</code> a replicated growable array: an ordered
sequence CRDT with tombstones and a deterministic insert tie-break. sequence CRDT with tombstones and a deterministic insert tie-break.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Marks</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Marks</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<code class="text-(--accent-text)">MarkStore</code> lightweight Peritext formatting spans <code class="text-accent-text">MarkStore</code> lightweight Peritext formatting spans
anchored to character op ids, resolved per character by highest op id. anchored to character op ids, resolved per character by highest op id.
</p> </p>
</div> </div>
@@ -262,12 +262,12 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
</div> </div>
<DocsCode :code="lwwMap" lang="ts" /> <DocsCode :code="lwwMap" lang="ts" />
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-4"> <div class="rounded-lg border border-border bg-bg-subtle p-4">
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<strong class="text-(--fg)">Why keep tombstones?</strong> If a delete simply dropped the entry, <strong class="text-fg">Why keep tombstones?</strong> If a delete simply dropped the entry,
a concurrent <code class="text-(--accent-text)">set</code> arriving afterward would resurrect a concurrent <code class="text-accent-text">set</code> arriving afterward would resurrect
the key the two replicas would disagree on whether it exists. Retaining the delete as a the key the two replicas would disagree on whether it exists. Retaining the delete as a
timestamped tombstone lets <code class="text-(--accent-text)">compareOpId</code> decide the timestamped tombstone lets <code class="text-accent-text">compareOpId</code> decide the
winner deterministically, the same way it does for live values. winner deterministically, the same way it does for live values.
</p> </p>
</div> </div>
@@ -308,9 +308,9 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
<DocsCode :code="fractionalBatch" lang="ts" /> <DocsCode :code="fractionalBatch" lang="ts" />
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-4"> <div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-4">
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<strong class="text-amber-700 dark:text-amber-400">Heads up:</strong> <strong class="text-amber-700 dark:text-amber-400">Heads up:</strong>
<code class="text-(--accent-text)">keyBetween</code> requires <code>lower &lt; upper</code> <code class="text-accent-text">keyBetween</code> requires <code>lower &lt; upper</code>
and throws otherwise. Two replicas independently generating a key between the and throws otherwise. Two replicas independently generating a key between the
<em>same</em> neighbors can produce identical keys; pair the key with the item's op id as a <em>same</em> neighbors can produce identical keys; pair the key with the item's op id as a
secondary sort to keep ordering deterministic, or let secondary sort to keep ordering deterministic, or let
@@ -366,14 +366,14 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
</div> </div>
<DocsCode :code="rgaBuffer" lang="ts" /> <DocsCode :code="rgaBuffer" lang="ts" />
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-4"> <div class="rounded-lg border border-border bg-bg-subtle p-4">
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<strong class="text-(--fg)">Garbage collection.</strong> Tombstones accumulate. When every <strong class="text-fg">Garbage collection.</strong> Tombstones accumulate. When every
replica has fully synced and nothing is in flight, <code class="text-(--accent-text)">gc(stable, keep?)</code> replica has fully synced and nothing is in flight, <code class="text-accent-text">gc(stable, keep?)</code>
drops deleted nodes whose insert is covered by a stable drops deleted nodes whose insert is covered by a stable
<NuxtLink to="/crdt/version-vector">VersionVector</NuxtLink>, returning how many it removed. <NuxtLink to="/crdt/version-vector">VersionVector</NuxtLink>, returning how many it removed.
Run it only at quiescence a late op that uses a dropped node as its origin could no longer Run it only at quiescence a late op that uses a dropped node as its origin could no longer
integrate and pass <code class="text-(--accent-text)">keep</code> to protect ids still integrate and pass <code class="text-accent-text">keep</code> to protect ids still
referenced elsewhere, such as mark span endpoints. referenced elsewhere, such as mark span endpoints.
</p> </p>
</div> </div>
+8 -8
View File
@@ -249,12 +249,12 @@ a.replica.receive(ops);`;
</div> </div>
<!-- Why order does not matter --> <!-- Why order does not matter -->
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Why the order of the two deltas is irrelevant</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Why the order of the two deltas is irrelevant</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
You could swap the two <code class="text-(--accent-text)">receive</code> lines, run them You could swap the two <code class="text-accent-text">receive</code> lines, run them
repeatedly, or interleave them with more edits — the result is the same. Each side only ever repeatedly, or interleave them with more edits — the result is the same. Each side only ever
adds ops it hasn't seen, and <code class="text-(--accent-text)">compareOpId</code> places adds ops it hasn't seen, and <code class="text-accent-text">compareOpId</code> places
each op in its deterministic position regardless of arrival order. That is convergence, each op in its deterministic position regardless of arrival order. That is convergence,
and the property tests assert it across randomized schedules. and the property tests assert it across randomized schedules.
</p> </p>
@@ -346,11 +346,11 @@ a.replica.receive(ops);`;
<!-- Caveat callout --> <!-- Caveat callout -->
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-5"> <div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-5">
<h3 class="mb-1.5 text-sm font-semibold text-amber-700 dark:text-amber-400">Dense clocks are a precondition</h3> <h3 class="mb-1.5 text-sm font-semibold text-amber-700 dark:text-amber-400">Dense clocks are a precondition</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Version vectors assume each site's clocks are dense (1, 2, 3, ). That holds automatically Version vectors assume each site's clocks are dense (1, 2, 3, ). That holds automatically
when ids come from <code class="text-(--accent-text)">Replica.nextId()</code>. If you mint when ids come from <code class="text-accent-text">Replica.nextId()</code>. If you mint
ids yourself, never skip a value for a site a gap would make ids yourself, never skip a value for a site a gap would make
<code class="text-(--accent-text)">delta</code> believe a missing op was already delivered. <code class="text-accent-text">delta</code> believe a missing op was already delivered.
</p> </p>
</div> </div>
+36 -36
View File
@@ -260,17 +260,17 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
<ClientOnly> <ClientOnly>
<template #fallback> <template #fallback>
<div class="rounded-xl border border-(--border) bg-(--bg-subtle) p-8 text-center text-sm text-(--fg-subtle)"> <div class="rounded-xl border border-border bg-bg-subtle p-8 text-center text-sm text-fg-subtle">
Loading interactive demo Loading interactive demo
</div> </div>
</template> </template>
<div class="rounded-xl border border-(--border) bg-(--bg-subtle) p-4 sm:p-5"> <div class="rounded-xl border border-border bg-bg-subtle p-4 sm:p-5">
<div v-if="!ready" class="flex flex-col items-center gap-3 py-8 text-center"> <div v-if="!ready" class="flex flex-col items-center gap-3 py-8 text-center">
<p class="text-sm text-(--fg-muted)">Spin up two fresh replicas to start editing.</p> <p class="text-sm text-fg-muted">Spin up two fresh replicas to start editing.</p>
<button <button
type="button" type="button"
class="rounded-md bg-(--accent) px-4 py-2 text-sm font-medium text-(--accent-fg) hover:bg-(--accent-hover) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-fg hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-ring"
@click="start()" @click="start()"
> >
Start demo Start demo
@@ -281,82 +281,82 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
<!-- Two replica panes --> <!-- Two replica panes -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Replica A --> <!-- Replica A -->
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-elevated) p-3"> <div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-elevated p-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs font-semibold uppercase tracking-wider text-(--fg-muted)">Replica A</span> <span class="text-xs font-semibold uppercase tracking-wider text-fg-muted">Replica A</span>
<span class="rounded bg-(--bg-inset) px-1.5 py-0.5 font-mono text-[11px] text-(--fg-subtle)">site: A</span> <span class="rounded bg-bg-inset px-1.5 py-0.5 font-mono text-[11px] text-fg-subtle">site: A</span>
</div> </div>
<textarea <textarea
v-model="drafts.a" v-model="drafts.a"
rows="3" rows="3"
spellcheck="false" spellcheck="false"
class="resize-none rounded-md border border-(--border) bg-(--bg) px-3 py-2 font-mono text-sm text-(--fg) focus:border-(--border-strong) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="resize-none rounded-md border border-border bg-bg px-3 py-2 font-mono text-sm text-fg focus:border-border-strong focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Type on A…" placeholder="Type on A…"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
class="rounded-md border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-xs font-medium text-(--fg) hover:bg-(--bg-inset) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="rounded-md border border-border bg-bg-elevated px-3 py-1.5 text-xs font-medium text-fg hover:bg-bg-inset focus:outline-none focus:ring-2 focus:ring-ring"
@click="apply('a')" @click="apply('a')"
> >
Apply edits Apply edits
</button> </button>
<div class="ml-auto flex items-center gap-3 font-mono text-[11px] text-(--fg-subtle)"> <div class="ml-auto flex items-center gap-3 font-mono text-[11px] text-fg-subtle">
<span>ops {{ snapshot.a.ops }}</span> <span>ops {{ snapshot.a.ops }}</span>
<span>clock {{ snapshot.a.clock }}</span> <span>clock {{ snapshot.a.clock }}</span>
</div> </div>
</div> </div>
<div class="rounded-md bg-(--bg-inset) px-3 py-2 font-mono text-sm text-(--fg) break-all min-h-9"> <div class="rounded-md bg-bg-inset px-3 py-2 font-mono text-sm text-fg break-all min-h-9">
<span v-if="snapshot.a.text">{{ snapshot.a.text }}</span> <span v-if="snapshot.a.text">{{ snapshot.a.text }}</span>
<span v-else class="text-(--fg-subtle)">(empty)</span> <span v-else class="text-fg-subtle">(empty)</span>
</div> </div>
</div> </div>
<!-- Replica B --> <!-- Replica B -->
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-elevated) p-3"> <div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-elevated p-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs font-semibold uppercase tracking-wider text-(--fg-muted)">Replica B</span> <span class="text-xs font-semibold uppercase tracking-wider text-fg-muted">Replica B</span>
<span class="rounded bg-(--bg-inset) px-1.5 py-0.5 font-mono text-[11px] text-(--fg-subtle)">site: B</span> <span class="rounded bg-bg-inset px-1.5 py-0.5 font-mono text-[11px] text-fg-subtle">site: B</span>
</div> </div>
<textarea <textarea
v-model="drafts.b" v-model="drafts.b"
rows="3" rows="3"
spellcheck="false" spellcheck="false"
class="resize-none rounded-md border border-(--border) bg-(--bg) px-3 py-2 font-mono text-sm text-(--fg) focus:border-(--border-strong) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="resize-none rounded-md border border-border bg-bg px-3 py-2 font-mono text-sm text-fg focus:border-border-strong focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Type on B…" placeholder="Type on B…"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
class="rounded-md border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-xs font-medium text-(--fg) hover:bg-(--bg-inset) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="rounded-md border border-border bg-bg-elevated px-3 py-1.5 text-xs font-medium text-fg hover:bg-bg-inset focus:outline-none focus:ring-2 focus:ring-ring"
@click="apply('b')" @click="apply('b')"
> >
Apply edits Apply edits
</button> </button>
<div class="ml-auto flex items-center gap-3 font-mono text-[11px] text-(--fg-subtle)"> <div class="ml-auto flex items-center gap-3 font-mono text-[11px] text-fg-subtle">
<span>ops {{ snapshot.b.ops }}</span> <span>ops {{ snapshot.b.ops }}</span>
<span>clock {{ snapshot.b.clock }}</span> <span>clock {{ snapshot.b.clock }}</span>
</div> </div>
</div> </div>
<div class="rounded-md bg-(--bg-inset) px-3 py-2 font-mono text-sm text-(--fg) break-all min-h-9"> <div class="rounded-md bg-bg-inset px-3 py-2 font-mono text-sm text-fg break-all min-h-9">
<span v-if="snapshot.b.text">{{ snapshot.b.text }}</span> <span v-if="snapshot.b.text">{{ snapshot.b.text }}</span>
<span v-else class="text-(--fg-subtle)">(empty)</span> <span v-else class="text-fg-subtle">(empty)</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Sync bar --> <!-- Sync bar -->
<div class="flex flex-wrap items-center gap-3 border-t border-(--border) pt-3"> <div class="flex flex-wrap items-center gap-3 border-t border-border pt-3">
<button <button
type="button" type="button"
class="rounded-md bg-(--accent) px-4 py-2 text-sm font-medium text-(--accent-fg) hover:bg-(--accent-hover) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-fg hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-ring"
@click="sync()" @click="sync()"
> >
Sync Sync
</button> </button>
<button <button
type="button" type="button"
class="rounded-md px-3 py-2 text-sm text-(--fg-muted) hover:bg-(--bg-inset) hover:text-(--fg) focus:outline-none focus:ring-2 focus:ring-(--ring)" class="rounded-md px-3 py-2 text-sm text-fg-muted hover:bg-bg-inset hover:text-fg focus:outline-none focus:ring-2 focus:ring-ring"
@click="init()" @click="init()"
> >
Reset Reset
@@ -436,27 +436,27 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
</div> </div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Commutative</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Commutative</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
A-then-B and B-then-A produce the same sequence. Concurrent inserts at the same origin are A-then-B and B-then-A produce the same sequence. Concurrent inserts at the same origin are
ordered by <code class="text-(--accent-text)">compareOpId</code>, so order of arrival ordered by <code class="text-accent-text">compareOpId</code>, so order of arrival
doesn't matter. doesn't matter.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Idempotent</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Idempotent</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Receiving the same op twice is a no-op. The op log's version vector dedups on Receiving the same op twice is a no-op. The op log's version vector dedups on
<code class="text-(--accent-text)">id</code>, and <code class="text-(--accent-text)">integrateInsert</code> <code class="text-accent-text">id</code>, and <code class="text-accent-text">integrateInsert</code>
short-circuits if the id is already present. short-circuits if the id is already present.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Causal</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Causal</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
An insert can't integrate before its <code class="text-(--accent-text)">originLeft</code>, An insert can't integrate before its <code class="text-accent-text">originLeft</code>,
nor a delete before its target. <code class="text-(--accent-text)">receive</code> buffers nor a delete before its target. <code class="text-accent-text">receive</code> buffers
such ops and retries them, so out-of-order delivery still converges. such ops and retries them, so out-of-order delivery still converges.
</p> </p>
</div> </div>
+17 -17
View File
@@ -55,40 +55,40 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — converged`;
offline, with messages that arrive out of order or twice. A CRDT solves this by construction: offline, with messages that arrive out of order or twice. A CRDT solves this by construction:
every primitive here is <strong>commutative, idempotent, and convergent</strong>, so applying every primitive here is <strong>commutative, idempotent, and convergent</strong>, so applying
the same set of operations in any order yields the same state a property verified by the same set of operations in any order yields the same state a property verified by
property tests. It's the convergence engine behind <code>@robonen/editor</code>, but stays property tests. It's the convergence engine behind <code>@robonen/writekit</code>, but stays
fully domain-agnostic, ships zero runtime dependencies, and runs in both Node and the browser. fully domain-agnostic, ships zero runtime dependencies, and runs in both Node and the browser.
</p> </p>
</div> </div>
<!-- Feature cards --> <!-- Feature cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Convergent by construction</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Convergent by construction</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
One deterministic tie-break — <code class="text-(--accent-text)">compareOpId</code> (higher One deterministic tie-break — <code class="text-accent-text">compareOpId</code> (higher
Lamport clock wins; site id breaks ties) — is shared by every primitive, so LWW and RGA agree Lamport clock wins; site id breaks ties) — is shared by every primitive, so LWW and RGA agree
on the same final state. on the same final state.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Causal buffering built in</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Causal buffering built in</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
<code class="text-(--accent-text)">Replica.receive</code> dedups, holds ops whose dependencies <code class="text-accent-text">Replica.receive</code> dedups, holds ops whose dependencies
haven't arrived yet (an insert before its origin), and retries them automatically as they land. haven't arrived yet (an insert before its origin), and retries them automatically as they land.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Delta sync, not full state</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Delta sync, not full state</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
Version vectors let each side request exactly the ops it's missing via Version vectors let each side request exactly the ops it's missing via
<code class="text-(--accent-text)">delta(version)</code>, with a transport-agnostic wire format. <code class="text-accent-text">delta(version)</code>, with a transport-agnostic wire format.
</p> </p>
</div> </div>
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5"> <div class="rounded-lg border border-border bg-bg-subtle p-5">
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Zero dependencies, pure TS</h3> <h3 class="mb-1.5 text-sm font-semibold text-fg">Zero dependencies, pure TS</h3>
<p class="text-sm leading-relaxed text-(--fg-muted)"> <p class="text-sm leading-relaxed text-fg-muted">
No runtime deps, no framework lock-in. Compose the primitives yourself, or lean on No runtime deps, no framework lock-in. Compose the primitives yourself, or lean on
<code class="text-(--accent-text)">Replica</code> to tie a clock, op log, and buffer together. <code class="text-accent-text">Replica</code> to tie a clock, op log, and buffer together.
</p> </p>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,3 +1,3 @@
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint'; import { base, compose, imports, stylistic, tests, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic); export default compose(base, typescript, imports, stylistic, tests);
+12 -12
View File
@@ -27,36 +27,36 @@
<!-- Feature cards --> <!-- Feature cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">High-level QR in one call</h3> <h3 class="text-sm font-semibold text-fg">High-level QR in one call</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
<code>encodeText</code> and <code>encodeBinary</code> pick the smallest <code>encodeText</code> and <code>encodeBinary</code> pick the smallest
version and optimal segment modes for you, then hand back an immutable version and optimal segment modes for you, then hand back an immutable
<code>QrCode</code> grid. <code>QrCode</code> grid.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Render-agnostic output</h3> <h3 class="text-sm font-semibold text-fg">Render-agnostic output</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
A <code>QrCode</code> is just a square of modules. Read each one with A <code>QrCode</code> is just a square of modules. Read each one with
<code>getModule(x, y)</code> and draw to SVG, canvas, or anything else <code>getModule(x, y)</code> and draw to SVG, canvas, or anything else
no rendering opinions baked in. no rendering opinions baked in.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Standalone Reed-Solomon</h3> <h3 class="text-sm font-semibold text-fg">Standalone Reed-Solomon</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
The GF(2^8) error-correction core <code>multiply</code>, The GF(2^8) error-correction core <code>multiply</code>,
<code>computeDivisor</code>, <code>computeRemainder</code> is exported <code>computeDivisor</code>, <code>computeRemainder</code> is exported
on its own, reusable beyond QR. on its own, reusable beyond QR.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Zero dependencies, fully typed</h3> <h3 class="text-sm font-semibold text-fg">Zero dependencies, fully typed</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
Tree-shakeable ESM and CJS builds with no third-party runtime deps, hot Tree-shakeable ESM and CJS builds with no third-party runtime deps, hot
loops backed by typed arrays, and end-to-end TypeScript types. loops backed by typed arrays, and end-to-end TypeScript types.
</p> </p>
+2 -2
View File
@@ -1,4 +1,4 @@
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint'; import { base, compose, imports, stylistic, tests, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic, { export default compose(base, typescript, imports, stylistic, {
name: 'encoding/overrides', name: 'encoding/overrides',
@@ -10,4 +10,4 @@ export default compose(base, typescript, imports, stylistic, {
oldest register's seed/last write is intentionally dead — keep symmetry. */ oldest register's seed/last write is intentionally dead — keep symmetry. */
'no-useless-assignment': 'off', 'no-useless-assignment': 'off',
}, },
}); }, tests);
+12 -12
View File
@@ -59,35 +59,35 @@ const billing = api.extend({ baseURL: 'https://billing.example.com' });`;
</div> </div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Type-safe end to end</h3> <h3 class="text-sm font-semibold text-fg">Type-safe end to end</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
Response data, request options, and plugin-contributed fields are all inferred Response data, request options, and plugin-contributed fields are all inferred
the parsed body comes back typed, no casting required. the parsed body comes back typed, no casting required.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Smart bodies &amp; parsing</h3> <h3 class="text-sm font-semibold text-fg">Smart bodies &amp; parsing</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
Plain objects are JSON-serialized; <code>FormData</code>/<code>Blob</code>/streams Plain objects are JSON-serialized; <code>FormData</code>/<code>Blob</code>/streams
pass through untouched. Responses are decoded from <code>Content-Type</code> or pass through untouched. Responses are decoded from <code>Content-Type</code> or
forced via <code>responseType</code>. forced via <code>responseType</code>.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Retry, timeout &amp; errors</h3> <h3 class="text-sm font-semibold text-fg">Retry, timeout &amp; errors</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
Built-in retry and per-attempt timeout with sensible defaults, and non-2xx Built-in retry and per-attempt timeout with sensible defaults, and non-2xx
responses reject with a rich <code>FetchError</code> carrying status, request, responses reject with a rich <code>FetchError</code> carrying status, request,
and parsed body. and parsed body.
</p> </p>
</div> </div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5"> <div class="rounded-xl border border-border bg-bg-elevated p-5">
<h3 class="text-sm font-semibold text-(--fg)">Hooks &amp; plugins</h3> <h3 class="text-sm font-semibold text-fg">Hooks &amp; plugins</h3>
<p class="mt-1.5 text-sm text-(--fg-muted)"> <p class="mt-1.5 text-sm text-fg-muted">
Lifecycle hooks plus a typed, composable plugin system with onion-style Lifecycle hooks plus a typed, composable plugin system with onion-style
<code>execute</code> middleware composed once, with zero per-request overhead <code>execute</code> middleware composed once, with zero per-request overhead
beyond the hooks themselves. beyond the hooks themselves.
+2 -2
View File
@@ -1,3 +1,3 @@
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint'; import { base, compose, imports, stylistic, tests, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic); export default compose(base, typescript, imports, stylistic, tests);