docs(core): update crdt/encoding/fetch docs and lint config
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
# @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.
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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
|
||||
CRDT, built by different code, nonetheless agree on the final document.
|
||||
</p>
|
||||
<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)">
|
||||
<strong class="text-(--fg)">Why one rule for everything?</strong>
|
||||
<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)">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
|
||||
<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">
|
||||
<strong class="text-fg">Why one rule for everything?</strong>
|
||||
<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">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
|
||||
formatting wins per character. One total order, applied consistently, is what turns a pile of
|
||||
independent primitives into a coherent, converging system.
|
||||
</p>
|
||||
@@ -223,11 +223,11 @@ const propsSrc = `// Commutative — order of application doesn't matter:
|
||||
<DocsCode :code="vvWireSrc" lang="ts" />
|
||||
<div class="prose-docs">
|
||||
<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>
|
||||
<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
|
||||
them in order per site (the <code class="text-(--accent-text)">Replica</code>'s causal buffer does
|
||||
<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
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -242,23 +242,23 @@ const propsSrc = `// Commutative — order of application doesn't matter:
|
||||
</div>
|
||||
<DocsCode :code="propsSrc" lang="ts" />
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<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,
|
||||
in whatever sequence the network delivers them.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<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;
|
||||
version vectors make them free.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<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
|
||||
byte-for-byte identical.
|
||||
</p>
|
||||
|
||||
@@ -198,33 +198,33 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
|
||||
|
||||
<!-- Map of the package -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">LwwRegister</code> and
|
||||
<code class="text-(--accent-text)">LwwMap</code> — single values and keyed maps where the
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<code class="text-accent-text">LwwRegister</code> and
|
||||
<code class="text-accent-text">LwwMap</code> — single values and keyed maps where the
|
||||
write with the highest op id wins.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">keyBetween</code> /
|
||||
<code class="text-(--accent-text)">keysBetween</code> — fractional indexing to place or move
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<code class="text-accent-text">keyBetween</code> /
|
||||
<code class="text-accent-text">keysBetween</code> — fractional indexing to place or move
|
||||
an item with a single string key.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">Rga</code> — a replicated growable array: an ordered
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<code class="text-accent-text">Rga</code> — a replicated growable array: an ordered
|
||||
sequence CRDT with tombstones and a deterministic insert tie-break.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">MarkStore</code> — lightweight Peritext formatting spans
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<code class="text-accent-text">MarkStore</code> — lightweight Peritext formatting spans
|
||||
anchored to character op ids, resolved per character by highest op id.
|
||||
</p>
|
||||
</div>
|
||||
@@ -262,12 +262,12 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
|
||||
</div>
|
||||
<DocsCode :code="lwwMap" lang="ts" />
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-4">
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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
|
||||
<div class="rounded-lg border border-border bg-bg-subtle p-4">
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<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
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -308,9 +308,9 @@ store.resolve(order).map(m => m.get('strong')); // [true, true, true, true]`;
|
||||
<DocsCode :code="fractionalBatch" lang="ts" />
|
||||
|
||||
<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>
|
||||
<code class="text-(--accent-text)">keyBetween</code> requires <code>lower < upper</code>
|
||||
<code class="text-accent-text">keyBetween</code> requires <code>lower < upper</code>
|
||||
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
|
||||
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>
|
||||
<DocsCode :code="rgaBuffer" lang="ts" />
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-4">
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<div class="rounded-lg border border-border bg-bg-subtle p-4">
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<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>
|
||||
drops deleted nodes whose insert is covered by a stable
|
||||
<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
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -249,12 +249,12 @@ a.replica.receive(ops);`;
|
||||
</div>
|
||||
|
||||
<!-- Why order does not matter -->
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
You could swap the two <code class="text-(--accent-text)">receive</code> lines, run them
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
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
|
||||
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,
|
||||
and the property tests assert it across randomized schedules.
|
||||
</p>
|
||||
@@ -346,11 +346,11 @@ a.replica.receive(ops);`;
|
||||
<!-- Caveat callout -->
|
||||
<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>
|
||||
<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
|
||||
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
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -260,17 +260,17 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
|
||||
|
||||
<ClientOnly>
|
||||
<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…
|
||||
</div>
|
||||
</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">
|
||||
<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
|
||||
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()"
|
||||
>
|
||||
Start demo
|
||||
@@ -281,82 +281,82 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
|
||||
<!-- Two replica panes -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- 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">
|
||||
<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="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>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="drafts.a"
|
||||
rows="3"
|
||||
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…"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<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')"
|
||||
>
|
||||
Apply edits
|
||||
</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>clock {{ snapshot.a.clock }}</span>
|
||||
</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-else class="text-(--fg-subtle)">(empty)</span>
|
||||
<span v-else class="text-fg-subtle">(empty)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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="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>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="drafts.b"
|
||||
rows="3"
|
||||
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…"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<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')"
|
||||
>
|
||||
Apply edits
|
||||
</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>clock {{ snapshot.b.clock }}</span>
|
||||
</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-else class="text-(--fg-subtle)">(empty)</span>
|
||||
<span v-else class="text-fg-subtle">(empty)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
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()"
|
||||
>
|
||||
Sync ↔
|
||||
</button>
|
||||
<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()"
|
||||
>
|
||||
Reset
|
||||
@@ -436,27 +436,27 @@ a.rga.toArray().join('') === b.rga.toArray().join(''); // true — CONVERGED`;
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<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
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<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
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
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
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
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
|
||||
such ops and retries them, so out-of-order delivery still converges.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+17
-17
@@ -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:
|
||||
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
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature cards -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
One deterministic tie-break — <code class="text-(--accent-text)">compareOpId</code> (higher
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
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
|
||||
on the same final state.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">Replica.receive</code> dedups, holds ops whose dependencies
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<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>
|
||||
<p class="text-sm leading-relaxed text-fg-muted">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,36 +27,36 @@
|
||||
|
||||
<!-- Feature cards -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
<code>encodeText</code> and <code>encodeBinary</code> pick the smallest
|
||||
version and optimal segment modes for you, then hand back an immutable
|
||||
<code>QrCode</code> grid.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="text-sm font-semibold text-(--fg)">Render-agnostic output</h3>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<div class="rounded-xl border border-border bg-bg-elevated p-5">
|
||||
<h3 class="text-sm font-semibold text-fg">Render-agnostic output</h3>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
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 —
|
||||
no rendering opinions baked in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="text-sm font-semibold text-(--fg)">Standalone Reed-Solomon</h3>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<div class="rounded-xl border border-border bg-bg-elevated p-5">
|
||||
<h3 class="text-sm font-semibold text-fg">Standalone Reed-Solomon</h3>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
The GF(2^8) error-correction core — <code>multiply</code>,
|
||||
<code>computeDivisor</code>, <code>computeRemainder</code> — is exported
|
||||
on its own, reusable beyond QR.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
Tree-shakeable ESM and CJS builds with no third-party runtime deps, hot
|
||||
loops backed by typed arrays, and end-to-end TypeScript types.
|
||||
</p>
|
||||
|
||||
@@ -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, {
|
||||
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. */
|
||||
'no-useless-assignment': 'off',
|
||||
},
|
||||
});
|
||||
}, tests);
|
||||
|
||||
+12
-12
@@ -59,35 +59,35 @@ const billing = api.extend({ baseURL: 'https://billing.example.com' });`;
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<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>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
Response data, request options, and plugin-contributed fields are all inferred —
|
||||
the parsed body comes back typed, no casting required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="text-sm font-semibold text-(--fg)">Smart bodies & parsing</h3>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<div class="rounded-xl border border-border bg-bg-elevated p-5">
|
||||
<h3 class="text-sm font-semibold text-fg">Smart bodies & parsing</h3>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
Plain objects are JSON-serialized; <code>FormData</code>/<code>Blob</code>/streams
|
||||
pass through untouched. Responses are decoded from <code>Content-Type</code> or
|
||||
forced via <code>responseType</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="text-sm font-semibold text-(--fg)">Retry, timeout & errors</h3>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<div class="rounded-xl border border-border bg-bg-elevated p-5">
|
||||
<h3 class="text-sm font-semibold text-fg">Retry, timeout & errors</h3>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
Built-in retry and per-attempt timeout with sensible defaults, and non-2xx
|
||||
responses reject with a rich <code>FetchError</code> carrying status, request,
|
||||
and parsed body.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="text-sm font-semibold text-(--fg)">Hooks & plugins</h3>
|
||||
<p class="mt-1.5 text-sm text-(--fg-muted)">
|
||||
<div class="rounded-xl border border-border bg-bg-elevated p-5">
|
||||
<h3 class="text-sm font-semibold text-fg">Hooks & plugins</h3>
|
||||
<p class="mt-1.5 text-sm text-fg-muted">
|
||||
Lifecycle hooks plus a typed, composable plugin system with onion-style
|
||||
<code>execute</code> middleware — composed once, with zero per-request overhead
|
||||
beyond the hooks themselves.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user