Conversation
zenbu-labs#33) createProxy allocated a fresh Proxy + path array on every property access, so repeated calls on the same method path (e.g. `client.users.get` in a loop, or re-read across renders) re-allocated the whole chain each time. Each proxy node now caches its children in a Map keyed by property name, so re-traversing a path returns the same node; only the terminal apply (which must produce a unique request id per call) still allocates. Call behavior is unchanged — verified with identity + request-count tests. Micro-benchmark: traversal-only (no invocation) is ~4x faster; full round-trip calls (dominated by nanoid + JSON.stringify) see a smaller but still real ~1.4x.
…writes (zenbu-labs#31) Replica listeners lived in one flat set, so every write anywhere in the document invoked every subscriber's callback — a `useDb(root => root.app.settings)` hook re-ran its selector on a completely unrelated write elsewhere in the tree. `replica.subscribe(cb, getPaths?)` now takes an optional thunk returning the root paths a listener currently cares about; the notify fiber skips callbacks whose paths didn't change. The check is cheap and correct because `setAtPath`/`deleteAtPath` only clone containers along the write path — comparing `getAtPath(prevRoot, path)` against `getAtPath(nextRoot, path)` with `Object.is` is enough, no separate diff pass needed. Connect/disconnect transitions and listeners with no `getPaths` (the default) always fire, so this is fully additive. `useDb` derives its watched paths automatically via a read-tracking proxy (`react/read-tracking.ts`) wrapped around the root during the selector call, so `useDb(root => root.app.settings)` needs no API change to benefit. `client.<field>.subscribe(cb)` already knows its path statically and now passes it straight through. `useCollection` is unchanged — collections have their own subscription mechanism. Tests cover exact-path, descendant, sibling, and parent-path-with- unchanged-leaf edge cases at the replica level, plus useDb-level tests for nested-object selectors and the `root => root` identity case. Benchmark: 200 subscribers watching disjoint paths, 50 writes concentrated on one — 10000 callback invocations before, 50 after.
zenbu-labs#29) Every connect/reconnect sent the entire root document, even when the replica already had almost all of it and just needed the handful of writes it missed while offline. The database now tracks a monotonic root version plus a bounded log of the root ops that produced each version (root-version-log.ts). A replica's `connect` carries `sinceVersion` — its last known version — whenever it has one; the server replies `mode: "delta"` with just the missed ops if the log can serve them contiguously, or `mode: "full"` otherwise (first-ever connect, gap older than the retained log, or the client's version is ahead of the server's — e.g. a process restart). Both cases converge to the identical root. A single scalar counter is enough rather than a version vector: there's one canonical in-memory root per process and every write is already serialized through the same mutex, so there's no concurrent-writer history to reconcile — see spec.md's new "Reconnect (delta sync)" section for the full protocol writeup. `rootVersion` is tracked on its own `Ref`, deliberately outside `ClientState`/`stateRef` — it's protocol bookkeeping, not observable application data, so updating it never publishes to `stateRef.changes` and costs path-keyed or whole-state subscribers (zenbu-labs#31) nothing. Tests: root-version-log unit tests (contiguous delta, gap fallback, version-regression fallback, capacity-0 disables delta), integration tests covering exact convergence after a delta reconnect (including root.delete), the too-old-gap full-sync fallback, and a first-ever connect always getting a full sync. Benchmark: a representative ~40-field document reconnecting after one changed field ships 201B instead of 2776B (13.8x); a no-op reconnect still ships fewer bytes than a full sync even for a tiny document.
…; add regression test
… drift, getSnapshot perf Four fixes from the Devin review of the kyju perf PR (zenbu-labs#29/zenbu-labs#31/zenbu-labs#33): 1. read-tracking stale-after-edit (BUG): `pruneToLeafPaths` discarded a shallower subscription path whenever a deeper read extended it, which is unsound when the selector RETURNS a container it also read a child of (e.g. `root.messages.find(m => m.id === active)` then render `msg.text`). The returned object's path was pruned behind `msg.id`, so an edit to `msg.text` never re-rendered. Now track proxy→path, collect the paths of values the selector actually returns (collectReturnedPaths), and keep those paths regardless of any deeper read. Regression test added. 2. reconnect drift (BUG): an optimistic write the server REJECTED (WriteFailedError) stayed visible forever, because reconnect switched from full resync to delta sync (which layers missed ops on the local root instead of replacing it) and could no longer heal local drift. Roll the optimistic apply back the moment the rejection ack lands, scoped to the touched paths (write + write-batch), so local state can't diverge from the server and a delta reconnect stays sound. Regression tests added (rejected write rolled back; delta reconnect doesn't resurrect it; a rejected batch keeps accepted ops and drops the rest). 3. db.ts version-log comment (COMMENT): corrected the claim that a fresh process "starts at version 0 with an empty log" — startup migrations run through handleWrite and populate the log. The real guarantee is that a client reconnecting to a restarted process uses a brand-new replica (no sinceVersion → full sync); getOpsSince is the backstop. No behavior change (no reuse-across-restart path exists). 4. getSnapshot perf (PERF): measured. Typical selectors cost ~1.6µs/call (negligible). A large-collection scan was ~15ms/call, dominated by an O(n^2) prefix check in pruneToLeafPaths — rewritten to O(n·depth) (~11x faster on that path). Added a safe fast path: when neither the store root nor the selector reference changed, reuse the cached output and watched paths instead of rebuilding the proxy tree. Gated on selector identity so inline selectors that close over changed props still re-track correctly; memoizing a heavy selector makes the fast path apply.
|
@Ayush2k02 is attempting to deploy a commit to the zenbu-labs Team on Vercel. A member of the Team first needs to authorize it. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the three open perf issues: #29 delta sync on reconnect, #31 path-keyed subscriptions, #33 RPC proxy caching.
Why
Reconnects re-sent the whole root document, every subscriber re-rendered on any write regardless of what it actually read, and every RPC call re-created its proxy — all unnecessary work on the hot paths.
How
A monotonic root version counter + bounded op log lets reconnects request a delta instead of the full root; per-hook read-path tracking narrows re-renders to paths actually touched; the RPC proxy is cached and reused instead of rebuilt per call.
Suggested reading order
packages/kyju/src/v2/db/root-version-log.tspackages/kyju/src/v2/db/handlers/connect.tspackages/kyju/src/v2/db/handlers/write.tspackages/kyju/src/v2/react/read-tracking.tspackages/kyju/src/v2/replica/replica.tspackages/zenrpc/src/proxy.tsDerive doc
https://derive.to/artifacts/zenbu-js-perf-trio-pr-1-sm5o46te