Kernel observation primitive (onUnobserved) → signals-native fetch cancellation in silo#84
Kernel observation primitive (onUnobserved) → signals-native fetch cancellation in silo#84scottmessinger wants to merge 2 commits into
Conversation
…n in silo Kernel now owns its reactive operator layer on top of `alien-signals/system`'s `createReactiveSystem` (faithful port of alien-signals 3.x) so it can fire per-node observation callbacks when a reactive node loses its last subscriber. New `onObservationChange` / `getObservationNode` (public) and `trackNode` / `isObserved` (internal); the dispatch is counter-gated so the hot path is unchanged when observation is unused. All kernel + husk imports repointed to the owned system module. Silo replaces its opt-in `subscribe*` ref-counting with observation-driven cancellation: each handle carries a dedicated liveness node that the rendering component subscribes to via `find`/`findQuery`; when the last observer unmounts, the in-flight fetch is interrupted after the `gcTimeMs` grace window (partial-batch rule honored). `useDocument` / `useQuery` stay pure reactive reads. Removed `subscribeDocument` / `subscribeQuery`. https://claude.ai/code/session_018FyYY6YYb1CW5LnBfPMgod
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## claude/relaxed-johnson-fEGH6 #84 +/- ##
================================================================
- Coverage 99.92% 99.81% -0.12%
================================================================
Files 33 34 +1
Lines 1359 1595 +236
Branches 283 324 +41
================================================================
+ Hits 1358 1592 +234
- Partials 1 3 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Perf gate: no regression ✅
Read: every per-benchmark delta is within (or below) that benchmark's own run-to-run stddev (the Caveat per Also functionally validated through the real prod bundle: js-krauset correctness
Generated by Claude Code |
…tion path Addresses self-review feedback on the observation primitive + silo cancellation: - Kernel: defer `onUnobserved` to a coalesced microtask flush that re-checks each node, so a `tracked()` re-render's transient unlink/re-link never fires (kills the per-render timer thrash and its scheduler race). Drop the `onObserved` hook and its `link` wrapper entirely — the hot path now calls `baseLink` directly, so observation adds zero per-`link` overhead. - Kernel: `getObservationNode` dedupes frozen targets via a WeakMap fallback (previously returned a fresh, un-deduped node — observation would silently break). Cover the `flush` error-recovery path with a real test and drop its c8-ignore (only the genuinely-unreachable dirty-recheck fallback remains). - Silo: collapse the per-key gc timers into one coalesced sweep over a pending set; drop the now-unused `onObserved`/`unregister` plumbing; store the liveness node directly. Replace the NUL-delimited `keyOf` with a JSON-encoded tuple — still collision-safe, but plain text so finder.ts stays diffable. - Changesets: stop editing #82's silo changeset; add a separate additive #83 changeset instead. Update the kernel changeset to match the onObserved-free API and the coalescing semantics. All five gates green (657 tests). silo finder/store 100%; kernel system.ts 100% lines/statements/functions. https://claude.ai/code/session_018FyYY6YYb1CW5LnBfPMgod
Perf validation (rigorous pass): no significant difference ✅Following up on the earlier 8×8 with a fuller run — and after the latest commit removed the Method: Read: no benchmark reaches significance at α=0.05 (nearest is append-1k, p=0.083); the deltas are a mix of + and − and the weighted total is −1.0% (optimized nominally faster this run, vs +0.6% last session — the sign flipping across sessions is the tell that this is noise). This is the expected null result for a faithful operator port with a zero-overhead hot path. Caveat unchanged: shared cloud container, so absolute times drift between sessions (create-1k was ~100ms last session, ~76ms now). The t-test controls for within-session variance; a final confirmation on a quiet machine is still worth doing pre-merge, but there is no evidence of regression. Generated by Claude Code |
Closes #83. Built on top of #82 (this PR targets the #82 branch so the diff is just #83's changes — retarget to
mainonce #82 lands).Part 1 — Kernel owns the reactive system
packages/kernel/src/system.tsreimplements the primitive layer onalien-signals/system'screateReactiveSystem(...)instead of importingsignal/computed/effectfromalien-signalsdirectly. It's a faithful port of alien-signals 3.x's operator layer (update/notify/unwatched+ the*Operfunctions); the graph algorithm (link/unlink/propagate/checkDirty) is still delegated to alien-signals. The only behavioral additions:unwatched(node)additionally fires a registeredonUnobservedhandler.link(dep, sub)additionally fires a registeredonObservedhandler on the unobserved→observed (first-subscriber) transition.Both are gated behind counters that stay
0until a handler is registered, so the hot path is byte-for-byte the upstream behavior when observation is unused (e.g. the js-framework-benchmark never touches silo).New public API
trackNode/isObservedare exposed from@supergrain/kernel/internal(sharp tools).All kernel imports of
signal/computed/effect/startBatch/endBatch/getActiveSub/setActiveSubwere repointed to the owned module (core.ts,read.ts,collections.ts,batch.ts,internal.ts,index.ts).@supergrain/huskalso moved to the kernel'seffect(otherwise its effects and kernel signals would live in separate graphs).Liveness-node design (the issue's open question)
Approach (1) — a dedicated liveness node per handle — but not
$VERSION, which bumps on every field write and would break fine-grained reactivity. The liveness node is never written;find/findQuerycalltrackNodeso any rendering component subscribes to it. Unmounting the last observer ⇒unwatched⇒ deferred cancel.Part 2 — Silo cancellation onto observation
subscribe/unsubscriberef-counting infinder.tswith observation-driven cancellation:onUnobservedschedules the chunk interrupt aftergcTimeMs;onObservedclears it on a quick re-subscribe;maybeInterrupthonors the partial-batch rule (only cancel when every key in an in-flight chunk is unobserved) and re-checksisObservedper key.subscribeDocument/subscribeQueryfrom theDocumentStoreinterface andstore.ts(cancellation is automatic now).useDocument/useQueryremain pure reactive reads — nouseEffect, no imperative subscription.tests/cancellation.test.tsto drive cancellation via observation, and addedtests/react/cancellation.test.tsxfor mount/unmount auto-cancel coverage.Acceptance
gcTimeMs; a surviving observer keeps it alive; partial-batch rule honored.gcTimeMsdoes not cancel.useDocument/useQuerycontain nouseEffect/imperative subscription.pnpm test(654),test:validate,typecheck,lint,format.src/coverage 100% (finder/store/transitions incl. all cancellation branches); kernelsystem.ts~98% (residual = upstream alien-signals graph internals,/* c8 ignore */+ justification).pnpm perf:statsbaseline vs optimized; will post results. (Note: the js-krauset prod build needed two pre-existing, unrelated fixes to run here — a gitignoredbootstrap.min.cssplaceholder, and an alias-ordering bug where@supergrain/kernelmatched before@supergrain/kernel/react; the latter is included.)🤖 Generated with Claude Code
Generated by Claude Code