Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
3e1d101
fix: a snapshot scrubs purged ids; a sealed record's op is skipped; h…
Alex-Dolid Oct 24, 2026
89c814c
docs(comparison): the Jazz column describes 2.0, not Jazz Classic
Alex-Dolid Oct 24, 2026
6fc88df
feat(sequence): the engine behind the list and text kinds
Alex-Dolid Oct 24, 2026
598a026
feat(ops): the list and text kinds in the record model
Alex-Dolid Oct 24, 2026
d3beb16
feat(storage): splice() and push() — lists and texts through the writ…
Alex-Dolid Oct 24, 2026
e57df25
test+docs(lists): lists and texts through the wire; the Lists and tex…
Alex-Dolid Oct 24, 2026
f579888
feat(lists): setAt() and move() — a movable list
Alex-Dolid Oct 24, 2026
9d28221
feat(text): updateText() — a whole new string becomes the splices tha…
Alex-Dolid Oct 24, 2026
fd870bb
feat(text): rich text — mark(), unmark(), spans()
Alex-Dolid Oct 24, 2026
1a62916
feat: cursors, the order subpath, the sequence bench, the matrix cells
Alex-Dolid Oct 24, 2026
bb9fca2
feat(tree): the tree field kind — treeMove(), treeRemove()
Alex-Dolid Oct 24, 2026
be05193
feat(log): droppedTs and pin() — what the log dropped, and what a rea…
Alex-Dolid Oct 24, 2026
22c4621
feat: history(id) and get(id, { at }) — the past as a fold of the log
Alex-Dolid Oct 24, 2026
4f782d1
feat: tags — named versions of the whole storage, kept on the node
Alex-Dolid Oct 24, 2026
ca802e9
feat(tags): views at a tag — list(), collection().select()/count(), s…
Alex-Dolid Oct 24, 2026
e8deb7b
feat: diff(), applyPatches(), restore() and the commit event
Alex-Dolid Oct 24, 2026
01ad153
feat(undo): @alexify/syncom/undo — an undo stack over the commit event
Alex-Dolid Oct 24, 2026
9199c9f
feat(branch): @alexify/syncom/branch — fork a storage, merge either way
Alex-Dolid Oct 24, 2026
b066681
docs(matrix): eight rows for what the engine does now; sizes remeasured
Alex-Dolid Oct 24, 2026
bb85654
bench(compare): typing into a text against Yjs, Automerge and Loro
Alex-Dolid Oct 24, 2026
f378727
test: INVARIANTS.md, the test that keeps it true, and a hazard walk
Alex-Dolid Oct 24, 2026
d0945fd
perf(sequence): the index built once per change and kept on the entry
Alex-Dolid Oct 24, 2026
2b433cc
bench(compare): the text scenarios remeasured after the index cache
Alex-Dolid Oct 24, 2026
3a92496
fix(engine): bounds and convergence at the edges (review №2, E1)
Alex-Dolid Oct 24, 2026
bedaaa2
fix(api): the contracts at the edges — branch, patches, tags (review …
Alex-Dolid Oct 24, 2026
c3ad4e2
fix(undo): a failed undo never inverts a record twice (review №2, E3)
Alex-Dolid Oct 24, 2026
e005d09
perf: the cheap wins of the hot paths (review №2, E4a)
Alex-Dolid Oct 24, 2026
ac01853
feat(ops): the kinds' precedence decides a clash on the op path too (…
Alex-Dolid Oct 24, 2026
d929514
fix(tags): an unreadable tags.json never blocks open (review №2, E3b)
Alex-Dolid Oct 24, 2026
e7e2a94
chore(governance): the 64 KB ceiling for v1 and the PROTOCOL 1 note (…
Alex-Dolid Oct 24, 2026
e576e1e
perf(sequence): a keystroke extends the held index; spans by a sweep;…
Alex-Dolid Oct 24, 2026
2bc037c
feat(storage): compact(), history paging, deferred record files, one …
Alex-Dolid Oct 24, 2026
d6fcb2f
feat(telemetry): spans for tags, history, versions and patches; the n…
Alex-Dolid Oct 24, 2026
4f25653
refactor(storage): unmarkById, and one way to read a version (review …
Alex-Dolid Oct 24, 2026
1386e85
refactor(storage): the field kinds' API and the versions API as path …
Alex-Dolid Oct 24, 2026
f20964b
test(scopes): filter the peers' streams by name, not by their first l…
Alex-Dolid Oct 24, 2026
0d25df7
refactor(sequence): the run-length content as src/rle.js (review №2, …
Alex-Dolid Oct 24, 2026
bb14358
perf(tags): a tag's catalog is read at its first use; the open reads …
Alex-Dolid Oct 24, 2026
7be9388
docs(agents): CLAUDE.md keeps the cross-module rules; the module head…
Alex-Dolid Oct 24, 2026
988db22
docs: the guides, the reference and the README in step with the CRDT …
Alex-Dolid Oct 24, 2026
98e4ba2
docs(vs-crdt): syncom compared with Yjs, Automerge and Loro as CRDT l…
Alex-Dolid Oct 24, 2026
5531e66
fix(commit): an array spliced into a text, or a string into a list, i…
Alex-Dolid Oct 24, 2026
d1a4663
fix(sequence): spans() reads a name that has both plain and stacking …
Alex-Dolid Oct 24, 2026
0e6453c
fix(versions): compact() keeps a text's marks, a counter at 0 and a f…
Alex-Dolid Oct 24, 2026
0e9a6a8
fix(ops): a counter stays a number every node reads alike
Alex-Dolid Oct 24, 2026
4f806cc
fix(undo): a failed undo never takes back part of a record twice
Alex-Dolid Oct 24, 2026
6e5c9f7
fix: small edges the docs audit found
Alex-Dolid Oct 24, 2026
d1fee85
fix(ops): a snapshot block is held to the node's sync.pendingMax
Alex-Dolid Oct 24, 2026
068d140
fix(storage): a live Record reads as get() after a change too
Alex-Dolid Oct 24, 2026
e7ae16b
fix(ops): a malformed register, counter or set payload is reported, n…
Alex-Dolid Oct 24, 2026
4633351
fix(types): the .d.ts files say what the code does
Alex-Dolid Oct 24, 2026
fb3b92b
fix(sync): a refused handshake push opens the feed anyway; a failed d…
Alex-Dolid Oct 24, 2026
6d26e4e
docs: the pages, the matrices and the sizes after the audit's fixes
Alex-Dolid Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 389 additions & 5 deletions CHANGELOG.md

Large diffs are not rendered by default.

247 changes: 220 additions & 27 deletions CLAUDE.md

Large diffs are not rendered by default.

46 changes: 37 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ pnpm test:coverage # c8 over src/ — thresholds 95 lines / 95 statements /
pnpm test:types # tsd against the .d.ts files
pnpm lint # oxlint
pnpm format # oxfmt (format:check in CI)
pnpm size # bundle-size report; the browser budget fails the run
pnpm size # bundle-size report; a browser budget exceeded fails the run
pnpm bench # every script in bench/ (--save → bench/results/engine.json)
pnpm bench:compare # against the alternatives (pnpm bench:compare:install first)
pnpm bench:size # the browser bundles of syncom and the alternatives (the same install)
pnpm bench:report # charts and tables of docs/guide/benchmarks.md and the README
pnpm docs:comparison # the matrix of docs/guide/comparison.md from docs/comparison.json
pnpm docs:dev # the VitePress site (docs:build / docs:preview too)
Expand All @@ -43,8 +44,10 @@ with `node --test --test-name-pattern 'swap'`.
`package.json#browser` plus `src/index.browser.js`. A new split module needs
an entry in both.
- **A `.d.ts` change and its `tsd` test land together.** `index.d.ts` is the
Node surface; `browser.d.ts` re-exports it and overrides only the three
crypto functions that are async in the browser.
Node surface; `browser.d.ts` re-exports it and overrides only the five
crypto functions that are async in the browser (`calculateHash`, `encrypt`,
`decrypt`, `sign`, `verify`). Each subpath has its own `.d.ts` and
`tests/<name>.test-d.ts`.
- **Anything in `tests/` not named `*.test.js` is a helper** and is skipped by
`node --test` discovery — that is why `tests/suites/`, `tests/utils/` and
`tests/package/` work the way they do.
Expand All @@ -54,9 +57,19 @@ with `node --test --test-name-pattern 'swap'`.
`tests/wrpc.integration.test.js` only, which skips when it is missing; nothing
under `src/` may `require` it. The adapter in `src/wrpc.js` duck-types the
client it is handed.
- **A field is a register, a counter or a set.** `set`/`update` write
registers, `inc` writes counters, `add`/`remove` write sets, and no two of
them meet on one field — do not add a path that lets them.
- **A field is one kind: a register, a counter, a set, a list, a text or a
tree.** `set`/`update` write registers, `inc` counters, `add`/`remove` sets,
`splice`/`push`/`updateText` lists and texts (`setAt`/`move` a list,
`mark`/`unmark`/`unmarkById` a text), `treeMove`/`treeRemove` trees. A local
write of another kind is refused before it is stamped; between peers the
kinds' precedence decides — register, counter, set, list, text, tree — by
the kinds alone, the same on the op path and in a snapshot merge. Do not add
a path that decides a clash another way.
- **An adapted algorithm brings its notice.** Code or a rule taken from an MIT
project is named in `THIRD_PARTY_NOTICES.md` — the project, the commit, the
files that carry it — with the upstream notice. Projects under other
licences (json-joy, Collabs, `fugue/fugue`) are read as specifications only;
nothing is copied from them.

## Tests

Expand All @@ -74,6 +87,16 @@ bundler honouring `package.json#browser` would. The sync engine is tested over
client at the boundary (JSON across, resume from the last event id on
reconnect), and once more over real wrpc in `tests/wrpc.integration.test.js`.

`INVARIANTS.md` names every guarantee — `SYNC-n`, `OPS-n`, `LIST-n`, `SEQ-n`,
`TREE-n`, `RLE-n`, `BOUND-n`, `LOG-n`, `VER-n`, `TAG-n`, `PATCH-n`, `UNDO-n`,
`BRANCH-n` — with the tests that hold it, and `tests/package/invariants.test.js`
fails when an id names no test or a named title is gone: rename a test and its
row together, and give a new guarantee an id, a test and a row. The seeded
hazard walk in `tests/suites/sync-engine.js` — dropped and duplicated pages, a
reopen mid-walk — is the one that must stay green whatever else moves.
`tests/package/telemetry-events.test.js` keeps every logged `event:`, span and
metric in the table of `docs/guide/telemetry.md`.

`tests/package/consistency.test.js` is the machine-checked half of the release
checklist: it fails if `exports`, `files`, the `browser` map, the `.d.ts`
files, the `scripts/size.js` rows or the docs keywords/version drift from
Expand All @@ -94,9 +117,14 @@ Benchmarks are not run in CI: they measure a machine, not a change.
## Bundle size

`pnpm size` prints one row per `exports` entry and enforces a min+gzip budget
on the browser entry. The budget is a **ratchet, not a target**: raise it
deliberately, in the same change that earns the bytes, and say why in a comment
next to the number.
on every browser entry — the main one and each subpath. The budget is a
**ratchet, not a target**: raise it deliberately, in the same change that earns
the bytes, and say why in a comment next to the number. The main entry's v1
ceiling of 64 KB was passed on 2026-10-24 for the module split of the second
review (the comment in `scripts/size.js` names it; the budget is 66 KB now);
every raise still names its feature, and a raise of more than a kilobyte is a
design decision (an engine subpath, an injected kind), taken in a plan, not in
the number.

## Release checklist

Expand Down
Loading
Loading