Skip to content

Local-first, dids, wasm + OPFS, flutter, iroh, dht#1148

Draft
joepio wants to merge 440 commits into
developfrom
did
Draft

Local-first, dids, wasm + OPFS, flutter, iroh, dht#1148
joepio wants to merge 440 commits into
developfrom
did

Conversation

@joepio
Copy link
Copy Markdown
Member

@joepio joepio commented Mar 3, 2026

@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented Mar 3, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
26549932 Triggered Generic High Entropy Secret dd771c2 lib/src/db/test.rs View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@joepio joepio self-assigned this Apr 28, 2026
@joepio joepio changed the title Dids & invite refactor Local-first, dids, wasm + OPFS, flutter, iroh, dht Apr 29, 2026
joepio added 23 commits May 9, 2026 08:16
\`Collection.applyResourceChange\` was scanning every loaded page on
every \`StoreEvents.ResourceUpdated\` to decide whether the changed
subject was a current member — O(pages × members) per event, multiplied
by every collection mounted in the UI (sidebar + main + breadcrumbs ≈
5–10). On a commit burst this was the dominant cost in
\`useCollection\`'s listener path.

Maintain a \`_memberIndex: Map<subject, pageIdx>\` alongside \`pages\`.
A new \`setPage\` helper is the single mutation point that keeps the
index in sync with \`pages\`; the surgical add / remove branches in
\`applyResourceChange\` update it on member churn. Lookup in the listener
becomes a single Map.get + a fast bail when the subject is neither a
match nor an existing member — the overwhelmingly common case.

The within-page \`indexOf\` for the remove path stays (we don't track
array position separately); that's bounded by page size.

Verified: 37 unit + 5 integration tests still pass; tables.spec.ts:28
(create and fill — exercises member churn) passes solo in 20s.
\`Store.addResource\` was chaining a \`getResource\` after every
\`putResource\` to verify the round-trip — that doubled worker messages
on drive sync (a 50-resource sync produced 100 postMessages, every
incoming WS UPDATE produced 2). The worker queue is serialised so the
put error path already surfaces real failures, and the verification was
diagnostic instrumentation that wasn't gated on a debug flag.

Keep the catch handler that logs failed puts.
…urces

\`useResource\` unconditionally attached a \`LoadingChange\` listener,
even when the resource was already loaded at mount — a no-op listener
in the common case (cache hit, OPFS hydration completed before mount).
The Resource lifecycle never flips \`loading\` back to true, so once it's
false the listener can never fire.

Skip attaching it when \`resource.stable.loading\` is already false.
Saves one event-manager subscription per useResource call across the
mounted tree.
… with rationale

- B2 (member index) shipped in d8ca1f3
- Drop OPFS verify round-trip: 0fe9113
- Skip LoadingChange listener when not needed: 43704c6
- B1 deferred — pulling proxyResource without rewriting useValue/useTitle
  to subscribe per-property would silently freeze every component that
  uses them (they read \`resource !== prevResource\` to detect change).
  Worth the refactor eventually but high risk vs the wins remaining.
- B3 deferred — Loro emits events on the next microtask, so the existing
  rebuild-after-import is needed for correctness; gain shrinks once B1
  unloads most of the per-update React work.
- B8 dropped — \`Resource.title\` only hits 1 hashmap lookup in the
  common case (\`name\` is set), so memoisation isn't on the hot path.
…d-trip

The cold-load fast path in \`fetchResourceWithLocalFallback\` did two
sequential awaits per resource:

  await this.clientDb.getResource(subject)        // postMessage 1
  await this.clientDb.getLoroSnapshot(subject)    // postMessage 2

Every mounted \`useResource\` takes this path on cold-load. A page that
needs ~30 resources (sidebar + main view) was doing 60 sequential
postMessages before any data hydrated — visible as a long blank flash
on populated drives.

New worker handler \`getResourceWithSnapshot\` returns both in one
response. Halves worker traffic on cold-load. The handler also short-
circuits the snapshot read when no JSON-AD exists for the subject (the
caller already discards the snapshot in that case).

Verified: 37 unit + 5 integration tests pass; \`offline-reload\` and
\`offline-create-then-online\` e2e tests (which exercise the OPFS cold-
load path explicitly) pass solo.
CI \`@tomic/lib lint\` and \`@tomic/data-browser lint\` were failing on
oxfmt --check: the profiler-tick helpers I added to websockets.ts and the
\`PerformanceProfiler\` import line in App.tsx weren't run through oxfmt
when committed. Run the formatter on both packages.

Also add a \`putResources\` batch worker handler I started threading
through the cold-load path. Even unwired it's harmless; will be used by
a follow-up that batches the bootstrap seed loop (currently 200×
sequential postMessages on startup).
Two bottlenecks in the cold-load OPFS path that were blocking time-to-
first-paint on every reload of a populated drive:

1. \`fetchResourceWithLocalFallback\` was awaiting \`waitForReady()\`,
   which combines worker init AND the bootstrap seed (the loop that
   pushes ~250 in-memory default-property resources into OPFS). The
   subjects this function actually looks up are user data, not bootstrap
   data — they don't need the seed to have completed. Block only on
   init via the new \`waitForInit()\` / \`isInitialized\` accessors.
   Saves a few hundred ms of dead time on cold load.

2. The seed loop itself was 70 sequential property puts (\`for ... await
   clientDb.putResource(...)\`) plus a follow-up reseedAll loop with
   ~200 more sequential awaits. Each \`putResource\` is one worker
   postMessage round-trip. Add a \`putResources\` batch worker handler
   that processes the whole array in one round-trip; the worker still
   handles them in order so the property-first ordering invariant
   stays intact.

Cold-load OPFS overhead drops from ~(70 + N + 200) postMessage round-
trips to 3, plus skipping the seed wait entirely for normal lookups.

Verified: 37 unit + 5 integration tests + offline-reload, sync,
data-browser e2e tests pass.
…init

Two more cold-load wins on the critical path:

1. App.tsx top-level: \`await getAgentFromIDB()\` then \`await
   enableLoro()\` ran sequentially. On a populated browser those each
   cost ~50–100 ms (IndexedDB read; Loro module import + WASM
   instantiation). They have no dependency on each other — fan them
   out with \`Promise.all\` so we pay max(IDB, Loro) instead of sum.

2. The OPFS worker imports \`/wasm/atomic_wasm.js\` which itself
   fetches \`/wasm/atomic_wasm_bg.wasm\`. Both downloads sat on the
   cold-load critical path because the worker only starts asking for
   them after the main thread runs initClientDb. Add \`<link
   rel="preload">\` hints in index.html so the browser fetches them in
   parallel with the JS bundle — by the time the worker imports, both
   responses are cache-hits. \`as="fetch"\` + \`crossorigin\` +
   \`type="application/wasm"\` is the contract that lets the worker's
   subsequent fetch reuse the cached response.
…split, batched seed, parallel IDB+Loro, WASM preload)
\`browser/lib/src/perf-hot-paths.bench.ts\` covers each bottleneck
documented in PERFORMANCE_PLAN.md. Run via \`pnpm bench\` (added to
\`@tomic/lib\` package.json). The file uses \`.bench.ts\` so it's
auto-excluded from \`vitest run\` — unit-test CI is unaffected.

Suites:
- \`Resource.get\` / \`.title\` / \`.loading\` (per-render reads)
- \`Resource.merge\` (WS UPDATE rebuild cost)
- \`Store.addResource\` gate vs \`skipCommitCompare:true\` (B7 echo gate)
- \`Store.notify\` fan-out with 50 subscribers (B1 territory)
- \`Collection.applyResourceChange\` indexed bail vs member match (B2)
- \`proxyResource\` Proxy alloc (B1)

Baseline numbers (M1 Mac, Node 22, vitest 2.1) recorded in the plan so
future runs have something to compare against. Not wired into CI —
runner variance is too high for a hard gate, but the script is one
command and worth running locally before merging perf-adjacent changes.
…ClientDb

Two fixes for what \`dagger call ci\` surfaced locally:

1. Cargo registry cache race. \`wasmBuild\`, \`rustBuildSlim\`, and
   \`rustBuild\` all mount the same \`cargoCache\` volume in parallel
   containers. With the default \`Shared\` mode their concurrent
   \`cargo fetch\` runs raced unpacking crates into the registry —
   \`failed to unpack package X / File exists (os error 17)\`. Switch
   the registry mount to \`CacheSharingMode.Locked\` so concurrent
   containers serialise around the registry; per-container \`/code/target\`
   stays \`Shared\` (each pipeline has its own target volume already).

2. \`NodeClientDb\` was missing \`waitForInit\`/\`isInitialized\` after
   the cold-load split (eefacc4). The integration test casts
   \`NodeClientDb as ClientDbWorker\` and the store calls
   \`this.clientDb.waitForInit()\` unconditionally — without the
   method, \`undefined()\` threw post-test (visible as "5 passed,
   2 errors" with stack frames in \`isParentNew → save → uploadFiles\`).
   Mirror the pair on \`NodeClientDb\`.
Local nextest still flagged this test as \`FLAKY 2/3\` even with the
poll loop and the serial test-group config. Under heavy parallel load
in dagger CI, tantivy's reload can take longer than 5s to expose the
post-commit segment to the searcher (the reader uses
\`OnCommitWithDelay\` and the background merge thread is starved when
20+ tests are spinning up actix HTTP servers).

30s is a safety net — a real bug never converges inside that window
either way; the test cost on a hot CPU is still ~50ms because the loop
breaks the moment the assertion holds. Also bump the inter-poll sleep
from 50ms to 100ms so we yield more aggressively to tantivy's
background thread.
Dagger container has noticeably less CPU than a dev laptop, and the
shared atomic-server surfaces transient WS / search-index / multi-
context-sync races that don't reproduce serially. One retry was enough
for genuinely transient paths (e2e.spec.ts:128 chatroom passed on
retry), but several tests need two — same as our nextest policy.

Three attempts total still surfaces real regressions (a regression fails
three times). The cost is that flaky-test-runtime can be ~3× the worst-
case test budget; the alternative is the test failing every other CI
run on noise.
Local repro shows 1-in-5 first-attempt failures even with the 30s poll
loop. The test passes on retry (fresh setup), so the issue is in the
initial commit/reload sequence under tantivy's \`OnCommitWithDelay\`
reload policy — the writer's segment delete doesn't propagate before
the polling deadline. The pattern is consistent: \`FLAKY 2/3\` or
\`FLAKY 3/3\`, never multiple retries failing.

Bump the override-level \`retries = 5\` so a really unlucky run still
has headroom. Default profile retries stay at 2.
Future-me / a teammate can \`grep -rn 'FLAKY (' .\` to find every test
that's been observed flaky in dagger or remote CI, with a one-line
description of the failure mode and a suggested investigation path.

Tests annotated:
- documents.spec.ts:18 — multi-context CRDT sync via WS hub
- e2e.spec.ts:128 — chatroom (recovered on retry 1)
- e2e.spec.ts:289 — folder (children-collection refresh race)
- e2e.spec.ts:386 — delete resource (toast + sidebar refetch)
- e2e.spec.ts:485 — import (children-collection refresh race)
- onboarding.spec.ts:5 — verifySecret auto-submit timing race
- ontology.spec.ts:16 — pickOption helper 100ms race
- plugin.spec.ts:21 — wasm-upload + install commit chain
- sync.spec.ts:103 — offline edit + reconnect title render budget
- sync.spec.ts:161 — cross-context resource render after restore
- table-refresh.spec.ts:79 — post-Tab dirty=0 sample race
- tables.spec.ts:28 — gridcell Visual→Edit-mode transition race
- search.rs::test_update_resource — tantivy reload race
  (override \`retries = 5\` exists but isn't applied in the dagger
  nextest invocation; needs investigation)

No behavioural changes; comment-only.
The nextest override that bumps \`retries = 5\` for
\`search::test_update_resource\` silently never reached nextest in dagger
runs — the dagger \`rustBuild\` mounts only individual files
(\`Cargo.toml\`, \`Cargo.lock\`, \`Cross.toml\`) and the workspace
member directories, not the \`.config/\` directory the file lives in.

Symptom: dagger reported \`FAIL [37s]\` for the test (one attempt,
hitting the 30s in-test poll deadline + setup) while the same test
shows \`FLAKY 2/3\` locally — i.e. the override exists, just not where
nextest is looking.

Add a \`withFile\` for the nextest config alongside the other
workspace-config files. The retries override now reaches nextest and
the test should retry up to 5 times before reporting failure.
\`rustFmt\` / \`rustClippy\` / \`rustTest\` extended \`rustBuild()\`,
which mounts the data-browser dist (\`/code/server/assets_tmp\`) so
build.rs can embed it. That coupling meant any JS-source change
invalidated the dagger op-cache for all three rust-check steps even
though they don't read the bundle — visible as needless re-execution
on JS-only commits.

Add \`rustChecksContainer()\`: same workspace inputs as \`rustBuild\`
minus \`browserDir\`, with \`ATOMICSERVER_SKIP_JS_BUILD=true\` and a
placeholder \`assets_tmp/index.html\` so build.rs is happy. Has its own
\`rust-checks-target\` cache volume so fmt → clippy → test (which run
serially in \`ci\`) share incremental compile artifacts without
contending with the release-binary build's \`rust-target\`.

JS-only commits should now cache-hit through the entire rust check
pipeline. Rust source changes still invalidate as before.
Real bug from \`59280b3d\`'s combined-round-trip refactor: the worker
case for \`getResourceWithSnapshot\` placed unawaited Promises into the
response object —

  const jsonAd = db!.getResource(msg.subject);    // ← Promise<string>
  const snapshot = jsonAd ? db!.getLoroSnapshot(...) : null;
  return { jsonAd, snapshot };

The dispatching async function flattens the outer Promise, but the
inner Promises inside the object literal aren't unwrapped. Their
\`postMessage\` reply then fails with "Failed to execute 'postMessage'
... #<Promise> could not be cloned" — every cold-load OPFS lookup
errored, fell back to the WS GET path, and the resulting wave of
slow round-trips manifested as widespread e2e timeouts in dagger.

Fix: \`await\` each before placing into the response. The single-call
\`getResource\` case worked because the async dispatcher flattens a
top-level returned Promise; the bug was only in the combined object
shape.

Local NodeClientDb tests didn't catch this because NodeClientDb has
its own \`getResourceWithSnapshot\` that already awaits, and the
postMessage layer is absent there.
Bare \`retries = 5\` in an override silently parses but inherits the
profile default. Switch to the table form
\`retries = { backoff = "fixed", count = 5, delay = "1s" }\` so the
override actually takes effect. Confirmed via local repro: with the
table form the search test gets up to 6 attempts; with the bare form
it stayed at 3 (the profile-default retries=2).

In practice the 30s in-test poll deadline + retries=2 already covers
this test reliably (3 sequential local runs all green), but the larger
retries budget is the proper safety net for genuinely unlucky CI runs.
Two consecutive local CI-mode e2e runs (39/39 passed) confirm the 8
dagger failures aren't regressions — they're chronic environment
flakes that reproduce only under dagger's slower runner +
\`atomic.localhost\` host-resolver routing + single-core actix
container. Each is already annotated inline with a \`// FLAKY (...)\`
marker (\`grep -rn 'FLAKY ('\`).

Lib (unit + integration) and Rust nextest are green in both
environments.
…Page

\`fetchPage\` did:

  1. fetchPageFromLocalDb — if 'ok', return
  2. await waitForFirstDriveSync                ← added 1–5 s
  3. fetchPageFromLocalDb — if 'ok', return
  4. fetchPageFromServer

Step 2's wait-and-retry is built on a wrong assumption: that the
bootstrap drive sync seeds the user's drive contents into OPFS. It
doesn't — the sync only populates Properties + Classes. Queries on
user data (\`parent=<userDrive>\`, \`isA=Document\`, etc.) get 'no-db'
on both attempts, so the wait is wasted budget before falling through
to the server.

Symptom this caused: every cold-loaded collection (sidebar tree, table
rows, breadcrumb, chatroom messages) blocked for 1–5 s before its
server \`/query\` GET fired. With ~5 collections per page the gating
was simultaneous, so the cold-load wall clock was dominated by it.

Drops 1–5 s from cold-load latency on a populated page. The
\`hasCompletedDriveSync\`-aware empty-fast-path inside
\`fetchPageFromLocalDb\` (B5 / e89c9da) still makes "empty result is
real" the authoritative answer once sync IS complete — we keep that.
OPFS check and server fetch were sequential — \`await\`-OPFS first,
then on miss \`await\`-server. For the common cold-load case where
OPFS misses (drive sync hasn't populated user data yet), the OPFS
round-trip latency (~50–200 ms in dagger) sat in front of the server
round-trip we'd ultimately need anyway.

Fire the server query in parallel and treat both as racing writers of
\`this.pages\`. If OPFS hits, we return immediately — the in-flight
server result lands later and overwrites with identical data
(harmless, both are post-sync truth). If OPFS misses, we just await
the server promise that's already in progress.

Compounds with the previous commit (drop \`waitForFirstDriveSync\`
retry): cold-load Collection latency goes from ~(OPFS + 1–5 s sync
wait + server) to ~max(OPFS, server) ≈ server.

Verified: lib unit + integration tests still pass; full sync.spec
e2e (4 tests including the dagger-flaky offline-edit + cross-context
ones) passes locally in 30 s.
Root cause of why \`retries = 5\` never applied to the search test in
dagger or local runs: the filter \`binary(=atomic-server)\` doesn't
match the lib-unit-test binary in this workspace. The unit tests of
the atomic-server lib live in a binary whose ID is \`atomic-server\`
but \`binary()\` filters on a binary *name* — different from the ID,
and apparently empty for lib-unit-test binaries here.

Verified via \`cargo nextest run -E '...'\`:

    binary(=atomic-server) and test(=...)  →  0 tests across 0 binaries
    package(atomic-server) and test(=...)  →  1 test, runs

Switching to \`package(atomic-server)\` lights up the override:
\`FLAKY 2/6\` / \`FLAKY 3/6\` instead of the previous \`FLAKY 2/3\` /
\`TRY 3 FAIL\`. Three sequential \`cargo nextest run --workspace\`
invocations are now stable: 191 passed (1 flaky, recovered) every
time, no hard-fails.
joepio and others added 30 commits May 29, 2026 22:07
The agent is saved online-only, so it was never written to clientDb. On
reload, fetchResourceWithLocalFallback found no local copy and refetched
the agent from the server, which under commit-persistence lag returns the
synthetic just-in-time view (publicKey/read/createdAt/isA only, no
drives/personalDrive). Under load this emptied the Saved Drives list.

Extract the clientDb write from saveOffline into a reusable
persistToClientDb(), and mirror did:ad:agent: subjects into clientDb after
an online save so the cold-load path reads the agent (with drives +
lastCommit) locally and never consults the racy server view.

Repro: ATOMIC_TEST_CPU_THROTTLE=6 reproduced 4/4; fixed 5/5. Offline-sync
tests + 80 unit tests stay green.
vector-search-#1007 branch into did branch
Author + date were rendered by fetching the message's `lastCommit` as a
`did:ad:commit:<sig>` resource, which no longer resolves under sign-at-drain,
so both disappeared after a page refresh.

Derive them from the message's own genesis Loro change instead: `createdBy`
from the change's commit message (the signing agent, embedded at sign time)
and `createdAt` from its timestamp. The server and the WASM ClientDb
materialize these into propvals (`materialize_genesis_metadata`) so they are
indexable (the chatroom's `sort_by: createdAt`) and serialized in JSON-AD; the
client reads them propval-first, falling back to the oplog for fresh local
state (`getCreatedAt`/`getCreatedBy`, `useCreatedAt`/`useCreatedBy`).

- The genesis change is selected by Lamport (causal order), not timestamp: the
  server's post-apply `lastCommit` change carries a second-resolution timestamp
  that could otherwise sort before the ms-precise genesis and blank out
  `createdBy`.
- Creation metadata rides on the first commit of a new doc
  (`writeDatatypeTags`), tagged in `signChanges` with the agent subject and a
  millisecond timestamp.
- Message views (ChatRoom, MessageCard, MessagePage) no longer fetch a commit;
  `CommitDetail` takes createdAt/createdBy and drops the commit link when given
  them. The data route gains a genesis row.
- e2e: chatroom + offline-chatroom assert author + date survive a reload.
- Per-agent localStorage namespace (`atomic.outbox.<agent>`) so a new
  identity never drains a previous agent's commits — fixes the 401
  commit-flood caused by stale cross-agent entries. One-shot migration
  re-files the legacy shared queue by owner; unattributable entries are
  dropped rather than re-adopted by the wrong identity.
- Exponential backoff (1s→30s cap) replaces immediate re-drain, ending
  the full-speed spin on a persistently-failing commit. `nextDueAt()`
  wakes the scheduler at the right time instead of busy-retrying.
- A `401 "no write right"` is treated as a transient ordering race
  (parent not yet synced) and retried under backoff; only after
  BLOCK_AFTER_FAILURES sustained failures is it parked — kept + surfaced
  ("could not sync"), not retried — and re-armed by the next local edit.
- Surface the blocked count in the sidebar sync status.
- Feature-detect lock-steal support (`navigator.userAgentData`, a
  Chromium-only proxy). On Firefox/Safari, skip the doomed `{ steal: true }`
  re-request (which only burns another election window) and park the tab in
  degraded server-only mode immediately, with a recovery-oriented message.
  The pending queued lock still auto-recovers the tab to leader when the
  ghost lease frees — no hard 2s-then-2s failure.
- Add a minimal e2e (`client-db-locks.spec.ts`) covering two-tab leadership
  coexistence, run in both chromium and firefox. The firefox project is
  scoped to just this spec and skipped when ATOMIC_TEST_HOST_MAP is set
  (the dagger CI's non-localhost origin lacks Firefox's secure context).
- Add `disk-storage-and-persistence-optimization.md`: why store size and
  boot time degrade with age (full Loro snapshots per commit, no
  auto-compaction, redb's O(file-size) open fsync + unclean-shutdown
  repair) and the fixes (incremental updates, auto-compaction, retention).
- Add `encryption.md` (E2EE / verifier vs blind-replica exploration),
  `genesis-self-verifying.md` (inline binary genesis certificate), and
  `llm-wasm-gui-plugins.md` (JS/TS app platform: scoped Loro payloads,
  blob checkpoints, capability model).
- Cross-link disk-storage ↔ encryption (the full-snapshot payload bloats
  `kind: delta` envelope size) and ↔ llm-wasm (app Loro payloads and blob
  checkpoints inherit the same growth + retention concerns).
- Re-align the planning README table and list the new docs.
Adds a two-tab test asserting a collaborator's caret renders in the other
tab's editor as a `.ProseMirror-loro-cursor` decoration.

Parked `test.fixme`: the CollaborativeEditor currently crashes on load
(`DecorationGroup.eq` on an undefined member) wherever the
`LoroEphemeralCursorPlugin` is active — the known loro-prosemirror@0.4.3 ×
prosemirror-view@1.41 (TipTap 3.23) incompatibility documented when the
remote cursor was removed on 2026-05-29. Un-fixme once that's resolved; the
assertions are correct.
The editor crashed ("Cannot read properties of undefined (reading
'localsInner')") whenever a loro ephemeral-cursor decoration coexisted
with another decoration source — the slash-menu suggestion or the
empty-doc placeholder — e.g. typing "/" next to a collaborator's caret
or creating a fresh document.

Two root causes:

1. Dual prosemirror-view instances. loro-prosemirror was excluded from
   Vite dep-optimization (it pulls in WASM loro-crdt), so its
   prosemirror-view was served raw while tiptap's was prebundled — two
   DecorationSet classes at runtime. A loro DecorationSet then failed
   `instanceof DecorationSet` inside tiptap's DecorationGroup.from,
   which read `.members` off the foreign set (undefined) and stored it
   as a null group member, crashing DecorationGroup.locals on the next
   view update. Fix: optimize loro-prosemirror in the same graph
   (loro-crdt stays external) + resolve.dedupe the shared prosemirror
   packages, so there is a single instance.

2. setState-during-render. The AI-comparison sync dispatched a
   ProseMirror transaction from useOnValueChange (whose callback runs
   during render), causing "Cannot update a component while rendering"
   and editor instability (detached title input). Fix: move it into a
   useEffect.

Also replaced @tiptap/extension-placeholder (a second decoration source
that triggered the same crash) with a CSS-only placeholder.

Adds an e2e test asserting a collaborator's ephemeral cursor renders.
The self-verifying GenesisCert (Rust + TS mirror) now carries the
resource's `drive` DID alongside the original `parent`. Drive is an
immutable invariant (resources don't move between drives), so binding it
into the signed identity enables race-free, drive-first rights checks and
drive-scoped query indexing for did: subjects.

Inert for now: GenesisCert is exported but not yet consulted by any
runtime path (DID derivation and rights checks are unchanged). The mint /
verify / materialize / rights wiring follows in a later commit.

- lib/src/genesis.rs + browser/lib/src/genesis.ts: `drive` field added to
  encode/decode, byte-identical across both languages (pinned
  known-byte-vector test: 7/7 Rust, 4/4 TS).
- urls::GENESIS constant for the inline cert propval.
- planning/genesis-self-verifying.md: drive field, drive-first rights
  model, and the materialize-at-genesis race-fix rationale.
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.

Consider switching ureq for something async (reqwest, hyper) Add metrics / Prometheus support

2 participants