Skip to content

feat: compare with RxDB feature by feature; fix what the comparison found - #2

Merged
Alex-Dolid merged 1 commit into
mainfrom
feature/rxdb-comparison
Oct 23, 2026
Merged

Alex-Dolid merged 1 commit into
mainfrom
feature/rxdb-comparison

Conversation

@Alex-Dolid

Copy link
Copy Markdown
Contributor

Summary

A new docs page compares syncom with RxDB 17.6.0 in 300 rows. Deciding the
syncom column from running code turned up 12 bugs in syncom. Each is fixed
here with a regression test, and the cells describe the code after the fixes.

Docs: /guide/vs-rxdb

  • Scope. 300 rows in eight sections: package and platform, storage, data
    model and writes, queries and reactivity, replication, security and
    integrity, extensibility and operations, project and ecosystem. A summary,
    the method, an RxDB → syncom concept map, speed charts and "which to
    choose" go with it.
  • How the RxDB column was decided. From the source at the 17.6.0 tag,
    from runs of the published package (with and without dev-mode), and from
    all 271 pages of rxdb.info. Two adversarial passes tried to refute the
    strongest claims about each project; none was refuted, and 13 were reworded.
  • Premium. Premium features are marked ⚠️ with their tier. They are
    closed source, so those cells follow the docs.
  • Rendering. docs/comparison-rxdb.json is rendered by
    scripts/comparison.js, which now renders both matrices. A group's
    section becomes its own ### table. .sc-matrix-duel styles the
    two-column table.
  • Charts. BenchCompare / BenchFigure accept only (library ids), and
    the model functions take an optional filter.
  • Main matrix. docs/comparison.json gets the RxDB and syncom cells the
    deeper reading corrected, and comparison.md / why.md no longer claim a
    syncom host stops a write its validator rejects (it relays it).

Fixes

Security:

  • Scopes bound what a peer writes, not only what it declares.
    • An update() took the scope of its delta. With a field-based
      sync.scope it was minted global and reached every peer. An op now
      takes the scope of its record as it leaves it, re-read after validation.
    • A host trusted the scope an op declared, so a peer granted Other
      overwrote a Doc record. A host's push now passes the grant to
      applyRemote as bound.
    • An op whose record is outside the grant, before or after the op, is now
      refused: outOfScope in the result, a refused event with
      reason: 'scope'.

Replication:

  • Offline writes past retention.ops reach the host. A dialing node
    keeps its own ops until every enabled registered node holds them
    (OpLog.retain(keep); acked in the registry, shown by listNodes()).
    A host that lacks ops the log no longer holds is reported once per hello
    as UNDELIVERABLE.
  • A restored directory no longer forks its node in silence. At hello the
    node notices the host holds more of its own stream, and reports REWOUND.
    It then moves past the host's seqs and bootstraps back what the lost ops
    wrote.

Storage:

  • Logged is committed. Before, a record file (or chain block) that failed
    after the log append rejected the write, though its op stayed logged and
    replicated. Now the write stands, the entry is held in memory, the batch is
    replayed onto the files at the next open, and UNPERSISTED is reported. A
    peer's batch is held the same way.
  • A torn record file is INTEGRITY, not a bare SyntaxError.

API:

  • Collections: a collection's get / update / delete / record act
    only on its own records (null / NOT_FOUND otherwise). An entity named
    like a storage member (get, close, log…) is refused at open().
  • Queries: cursor.one() no longer leaves limit(1) on the cursor.
    Indexed and unique object values match whatever their key order
    (canonical keyOf).
  • CRDT helpers: merge() / diff() walk own keys only and never take
    __proto__.
  • Node.js ESM: import { open } from '@alexify/syncom' works. Both
    barrels spell their exports out so cjs-module-lexer can read them.

Docs drift:

  • Failing listeners and watch runs log at warn, not debug.
  • An op that skips ahead is dropped as a gap, not set aside.
  • There is no read-only open for a second tab.
  • A host relays an op its validator or beforeApply skipped.
  • A nested object is one register.
  • The event tables list log, refused and every error code.
  • sync.op.outOfScope, which never existed, is gone from the table.

Behaviour changes

  • A record-file failure after the log append no longer rejects the write: it
    emits error with UNPERSISTED.
  • collection.get(id) of another collection's record returns null;
    update / delete / record reject with NOT_FOUND.
  • The applyRemote result and the apply / sync event payloads gain
    outOfScope.
  • New error codes: UNPERSISTED, REWOUND, UNDELIVERABLE. They are added
    to the SyncomErrorCode union and the codes table in api.md.
  • Browser budget 41 → 42 KB. The fixes add about 1.2 KB min+gzip; the
    entry is 41.9 KB. Size figures in the README and docs are re-measured
    (pnpm size, pnpm bench:size, pnpm bench:report).

Not covered

  • Ops written before any node was registered, and compacted since, still
    cannot reach a host. This is now reported (UNDELIVERABLE), not silent.
  • pnpm bench:compare was not re-run; only the size figures are re-measured.
  • RxDB Premium cells rest on its docs (closed source).

Testing

  • pnpm test: 752 tests, 0 failures.
  • pnpm test:coverage: 99.6 / 96.4 / 99.9 / 99.6 (stmts / branches / funcs /
    lines).
  • pnpm test:types, pnpm lint, pnpm format:check, pnpm size and
    pnpm docs:build (no dead links) all pass.
  • New tests: tests/sync.retention.test.js (own-op retention,
    UNDELIVERABLE, REWOUND), tests/package/esm.test.js (ESM named
    imports of every entry), plus additions to sync.scopes, storage.fsi,
    storage, crdt and the query suite.
  • I also re-ran the scripts that reproduced the scope bugs; both now show the
    bug gone.
  • I opened /guide/vs-rxdb in the browser at desktop and phone widths.

🤖 Generated with Claude Code

…ound

Docs:
- /guide/vs-rxdb: 300 rows in eight sections, syncom against RxDB 17.6.0,
  decided from its code at the tag, runs of the published package and all
  271 pages of rxdb.info; rendered from docs/comparison-rxdb.json by
  scripts/comparison.js (a group's `section` becomes its own table), with
  the benchmark charts filtered to the two (`only`).
- docs/comparison.json: RxDB and syncom cells corrected from the deeper read.

Fixes found while deciding the syncom column, each with a test:
- scopes: an op takes the scope of its record (an update's delta named
  none); a host refuses a pushed op whose record is outside the peer's
  grant (applyRemote `bound`, `outOfScope`)
- a dialer keeps its own ops until its registered nodes hold them
  (OpLog.retain, node.acked); UNDELIVERABLE when a host lacks ones the log
  no longer has
- REWOUND: a node restored from an older directory moves past the host's
  seqs and bootstraps back what it lost
- logged is committed: a record file that fails after the append is held in
  memory and replayed at the next open (UNPERSISTED)
- a torn record file is INTEGRITY; entity names the storage answers to are
  refused; collection methods are scoped to their collection; one() leaves
  the cursor alone; index keys are canonical; merge()/diff() skip
  __proto__; ESM named imports of the root work
- docs drift: log levels, event tables, gaps, read-only tabs, nested objects

Browser budget 41 -> 42 KB (entry 41.9 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
syncom Ready Ready Preview Oct 23, 2026 8:08pm UTC

@Alex-Dolid
Alex-Dolid merged commit 014f1f3 into main Oct 23, 2026
6 checks passed
@Alex-Dolid
Alex-Dolid deleted the feature/rxdb-comparison branch October 23, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant