Effect migration for silo (without forking alien-signals)#85
Effect migration for silo (without forking alien-signals)#85scottmessinger wants to merge 22 commits into
Conversation
Design sketch comparing a tagged-union public DocumentHandle/QueryHandle (Data.TaggedEnum + $match) against keeping the flat handle shape with an internal statechart, as the first step toward migrating silo's network/async orchestration to Effect. No code wired up yet. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…roxy Achieves type safety (illegal states unrepresentable, exhaustive matching) AND per-field reactivity by separating runtime representation from type: keep the existing stable reactive proxy with in-place field mutation, but type the public handle as a status-discriminated union. Effect's Data.TaggedEnum statechart style is used internally for the transition reducer only. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
A single async enum can't represent stale-data-plus-background-refetch or stale-data-plus-refetch-error without throwing information away. Model the lifecycle as two orthogonal statechart regions instead: data availability (Absent | Present) and fetch activity (Idle | Fetching | Failed). All six combinations are meaningful; value/error stay type-narrowed per region; the two reactive cells preserve per-field reactivity. matchHandle collapses the product into the four common consumer cases. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
No collapsing into a Ready super-state. Consumers narrow each region directly; all six data x fetch combinations are first-class. Add the rationale for two regions over a flat six-variant enum: a single discriminant would force value-readers to subscribe to background fetch activity, re-rendering on every refetch — the factoring into two cells is what preserves per-field reactivity. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…ndle BREAKING CHANGE: adapters now return Effect instead of Promise; the DocumentHandle/QueryHandle flat fields are replaced by two orthogonal regions (data: Absent|Present, fetch: Idle|Fetching|Failed). - src/errors.ts: AdapterError/NotFoundError/ProcessorError (Data.TaggedError) - src/transitions.ts: HandleEvent tagged enum + pure region reducers + applyEvent (promise lifecycle) - src/store.ts, src/queries.ts: two-region handles, Effect adapters, per-model retry/timeout - src/finder.ts: Effect.forEach fan-out, typed catchAll, statechart settlement - effect as a peer dependency; READMEs, docstrings, changeset updated Tests are migrated in a follow-up commit. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…vity robustness - Translate all silo tests + queries test to Effect adapters and the two-region handle (data: Absent|Present, fetch: Idle|Fetching|Failed). - applyEvent: read handle state UNTRACKED (via unwrap) while writing through the reactive proxy, so insertDocument inside a tracked render no longer self-subscribes and loops; genuine readers are still notified. - Build events from lowercase factory + plain tagged literals (keeps the TaggedEnum type), resolve oxlint findings across errors/finder/transitions. - queries: Effect.succeed stub adapters + effect devDependency. All green: silo 144, queries 32, doc-tests 5; typecheck/lint/format clean. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…d handle
silo:
- Handle is now a status-discriminated union over flat fields
(value/error/isFetching/fetchedAt/status/promise). value/error coexist in
the success arm (stale-while-revalidate); isFetching is orthogonal to status
so status doesn't flip on a background refetch; per-field reactivity intact.
- insertDocument again clears a prior error (fresh value supersedes it).
- Statechart unit tests + retry/timeout + error tests restore src to 100%
coverage (167 tests).
queries:
- QueryAdapter.fetch returns Effect<_, AdapterError>; create-query runs it via
Effect.runPromise(Effect.either(...)); pagination/backoff unchanged.
husk:
- reactivePromise/reactiveTask accept Effect programs, run via
Effect.runPromise(Effect.either(effect), { signal }) bridging the existing
abort model to Effect interruption; typed error channel.
effect added as a peer dependency of silo, queries, and husk. Docs + changeset
updated to the flat union shape.
https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…ngine Keep Effect as the internal engine but take it off the public boundary, so adopting the libraries no longer requires writing Effect. silo: - DocumentAdapter.find returns Promise<unknown> | Effect<unknown, AdapterError>. The common case is a plain Promise; the finder normalizes it onto the Effect engine (Effect.isEffect ? use : Effect.tryPromise) and a rejection becomes an AdapterError (passed through untouched if the adapter already threw one). Effect adapters still work as-is for typed errors / custom retries / resources. - Engine (batching, retry/timeout, statechart) and typed errors unchanged. queries: - QueryAdapter.fetch returns Promise | Effect; create-query normalizes the same way before funneling through Either into its backoff loop. husk: - Reverted to the Promise-based reactivePromise/reactiveTask (no Effect engine to keep; its boundary IS the primitive). Drops the effect dependency. Promise/Effect boundary tests added (finder + create-query); silo executable src stays at 100% coverage. Docs + changeset updated to Promise-first. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
The engine now runs entirely on Effect, with subscriber-gated request
cancellation — the capability the fiber foundation was for.
- Batch window runs on Effect.sleep (was setTimeout), so the whole pipeline is
on Effect's clock and deterministic under TestClock.
- Each chunk runs on its own interruptible fiber, tracked as in-flight.
- The store ref-counts subscribers per key: subscribeDocument/subscribeQuery
return an unsubscribe fn; useDocument/useQuery call them on mount/unmount.
When the last subscriber for every key in an in-flight chunk leaves, the
chunk's fiber is interrupted and its handles reset to idle (new Abort event)
so renewed interest refetches.
- Adapters receive an AbortSignal: find(ids, { signal }) — thread it into fetch
for a real network abort, or ignore it (interruption still discards the
result, no stale write). Effect adapters interrupt natively.
- gcTimeMs config (default 0 = next tick) defers the interrupt so a synchronous
re-subscribe (StrictMode remount, fast nav-back) cancels it.
New tests: cancellation.test.ts (subscriber-gating, partial-batch, re-subscribe,
refetch, signal-ignored discard, query cancellation, ref-count edges, evicted
handle/bucket) and test-clock.test.ts (TestClock window). silo src 100% covered;
184 silo tests pass.
https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
Two changeset claims lacked tests: - React hooks cancel on unmount: add browser tests proving useDocument's subscribe/unsubscribe wiring aborts the in-flight fetch when the last consumer unmounts, and keeps it alive while another component still wants the same doc (shared-cache ref-counting, end-to-end through StrictMode). - 'Effect adapters interrupt natively': add a node test where an Effect adapter hangs on Effect.never with an onInterrupt finalizer; cancellation runs the adapter's own finalizer and resets the handle. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…er seam Thermo-nuclear review follow-ups on the cancellation machinery: - Collapse the subscriber ref-count store from a nested Record<Surface, Map<type, Map<key, count>>> to a single flat Map<string, number> keyed by the same 'surface type key' string already used for gc timers. Unifies the keying convention, shortens subscribe/unsubscribe/ subCount, and deletes the 'unknown type' special-case branch (the flat map handles a missing key uniformly). - Remove FinderScheduler: a production injection seam whose only consumer was test-clock.test.ts, and which diverged from silo's canonical fake-timer test pattern. The batch window stays on Effect.sleep; the fake-timer cancellation and finder tests already prove its behavior deterministically. Reverts the three call sites to Effect.runFork/runPromise and drops the TestClock test. No behavior change. silo src 100% covered; 186 tests pass. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…second serializer useQuery computed its own JSON.stringify(params) for the effect dependency while the store keys queries with stableStringify. Two serializers for one concept: order-sensitive vs order-independent, so the subscription key and the rendered handle could diverge (latent correctness hazard), and reordered-param re-renders churned the subscription needlessly. findQuery already returns the SAME reactive handle for deep-equal params (it owns the one canonical key). Depend on that stable identity instead — the signals-native key. No second serialization, nothing to drift, no churn: deep-equal params keep the same handle (effect doesn't re-run); a different params yields a different handle (re-subscribes). Pin the order-independence contract the hook now relies on with a findQuery test that reorders param keys and asserts a stable handle. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…lation opt-in The hooks no longer use useEffect to drive an imperative subscription. They are pure reactive reads now: `return store.find(...)` / `return store.findQuery(...)`. The whole point of useQuery is that consumers don't reach for useEffect — and neither should the hook, when the value it adds is just a reactive read. Fetch cancellation stays as an explicit, tested capability on the store (subscribeDocument / subscribeQuery + fiber interruption + AbortSignal), for callers that want to wire unmount-driven cancellation themselves. It is no longer auto-wired through React: a signals-native 'cancel when a handle has no reactive observers' wants an observation-lifecycle primitive in the kernel core (alien-signals 2.0.7 has none; effect-cleanup landed in 3.x) — a separate change to the reactive core, not silo. - Drop useEffect + the hook-level subscription from useDocument/useQuery. - Remove the hook-driven cancellation tests (covered behavior is gone); the store-level cancellation tests remain and still exercise the finder machinery. - Update store/README/changeset docs to frame cancellation as opt-in. silo src 100% covered; 185 tests pass. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
Upgrade the reactive core. 3.x renamed getCurrentSub/setCurrentSub → getActiveSub/setActiveSub (renamed throughout; kernel/internal re-exports the new names) and moved ReactiveNode to the alien-signals/system subpath. The notable behavioral change: effect(fn) now treats fn's return value as a cleanup function (runs before each re-run and on dispose) — the React-useEffect model. Callbacks that returned a value relying on 2.x's lenient ignore now throw 'cleanup is not a function' on re-run; fixed the affected test/bench callbacks to read-for-subscription via void. useSignalEffect now accepts and wires through a cleanup return. Documented the semantics on the public effect re-export. All reactive semantics (fine-grained tracking, batching, Map/Set notification coalescing) verified unchanged: 621 tests across kernel/mill/husk/silo, doc validation, typecheck, lint, format all green. https://claude.ai/code/session_016FNH6xY9sFPmBaThUcHPAc
…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
…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
The kernel had vendored a 558-line port of alien-signals' operator layer
(`system.ts`) solely to expose an `unwatched` hook, which silo used to
cancel in-flight fetches when a handle lost its last reactive observer.
alien-signals exposes no public seam for "node lost its last subscriber"
(the `unwatched` callback is a private closure arg to `createReactiveSystem`),
so the only way to get it was to own the operator layer — a permanent
maintenance tax that must be re-ported on every upgrade.
Remove the fork entirely and rely on alien-signals' published primitives:
- kernel: delete `system.ts`; import `signal`/`computed`/`effect`,
`getActiveSub`/`setActiveSub`, `startBatch`/`endBatch` directly from
`alien-signals` (the 2.0.7 -> 3.2.1 upgrade stays). Drop the observation
primitives (`onObservationChange`/`getObservationNode`/`trackNode`/
`isObserved`/`createObservationNode`/`$OBSERVE`) and their tests.
- silo: drop auto-cancellation. `find`/`findQuery` are pure reactive reads;
remove the liveness-node observation, gc sweep, and fiber-interrupt path
from the finder, the `gcTimeMs` config, and the `Abort` handle event.
Adapters still receive `{ signal }`, which now aborts only on adapter-Effect
interruption (e.g. a per-model `timeout`).
The Effect migration, typed errors, statechart handle, and per-model
retry/timeout are unchanged. Docs/changesets updated to match.
https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #85 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 31 33 +2
Lines 1312 1286 -26
Branches 256 264 +8
=========================================
- Hits 1312 1286 -26 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
It documented the observation-primitive approach that was abandoned with the alien-signals fork; keeping it risks tempting a rebuild of the fork. https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
The Promise-adapter branch of create-query's Effect boundary rejected with a pre-built AdapterError was uncovered (codecov patch miss). Add a test asserting such an error is passed through untouched rather than re-wrapped. https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
Address two structural findings from the code-quality review: - Extract the Promise→AdapterError boundary (`coerceAdapter`) into `@supergrain/silo`'s errors module and reuse it from both the silo finder (`toAdapterEffect`, which now only layers the AbortController wiring on top) and `@supergrain/queries`' create-query. The "Promise wrapped, Effect as-is, existing AdapterError passed through" rule now lives in exactly one place. - Collapse the repeated get-or-create-handle dance in `store.ts` into a `getOrCreateHandle` helper (used by find/findQuery/insertDocument/ insertQueryResult) and fold the duplicated idle-check + fetch-trigger of `find`/`findQuery` into a shared `findOrFetch`. No behavior change; all changed files remain at 100% coverage. https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
There was a problem hiding this comment.
Pull request overview
This PR migrates @supergrain/silo’s async/batching pipeline onto an internal Effect runtime, reshapes document/query handles into a status-discriminated union over flat orthogonal fields (value/error/isFetching/fetchedAt/promise), introduces typed error boundaries (AdapterError/NotFoundError/ProcessorError), and upgrades the reactive core (alien-signals) to v3.2.1 across the monorepo.
Changes:
- Upgrade
alien-signals2.x → 3.2.1 and propagate the renamed subscriber APIs (getActiveSub/setActiveSub) plus the neweffect()cleanup semantics across kernel/react usage. - Rebuild silo’s finder/network layer on Effect (Promise-first adapter boundary preserved), adding per-model/query
retry+timeout, AbortSignal interruption wiring, and typed errors. - Update docs and expand tests to pin the new handle statechart/promise lifecycle and the adapter boundary semantics (Promise vs Effect).
Reviewed changes
Copilot reviewed 49 out of 50 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates top-level documentation/examples for silo handles, adapters, and Suspense usage. |
| pnpm-lock.yaml | Locks alien-signals@3.2.1 and adds effect@3.21.2 plus transitive deps. |
| packages/silo/tests/types.test-d.ts | Updates public type-surface assertions for new handle fields and error typing. |
| packages/silo/tests/transitions.test.ts | Adds unit tests for the new handle reducer/statechart and promise lifecycle. |
| packages/silo/tests/store.test.ts | Updates store behavior tests to the new value/error/isFetching/status model. |
| packages/silo/tests/resilience.test.ts | Adds tests for per-model/query retry and timeout behavior via Effect. |
| packages/silo/tests/react/json-api.test.tsx | Updates React JSON-API relationship hook tests for the new handle shape; removes write-only tracked seeds to avoid loops. |
| packages/silo/tests/react/index.test.tsx | Updates React integration tests for new handle fields and error behavior; adjusts seed components. |
| packages/silo/tests/queries.test.ts | Updates query handle tests and adds param key-order stability assertion. |
| packages/silo/tests/property.test.ts | Updates property-based tests to the new adapter boundary (Effect-wrapped adapters). |
| packages/silo/tests/finder.test.ts | Updates finder contract tests and adds explicit Promise-vs-Effect adapter boundary tests. |
| packages/silo/tests/example-app.ts | Updates example adapters to demonstrate Effect-wrapped adapters and typed errors. |
| packages/silo/tests/errors.test.ts | Adds tests pinning the new tagged error classes’ tags/messages/causes. |
| packages/silo/tests/adapters.test.ts | Updates adapter tests to read from value instead of data. |
| packages/silo/src/transitions.ts | Introduces the handle statechart reducer (applyEvent) and promise plumbing (withResolvers). |
| packages/silo/src/store.ts | Refactors store to use the new statechart, new handle types, and Effect-native config surface (retry/timeout). |
| packages/silo/src/react/json-api.ts | Updates docs/examples for relationship hooks to use value. |
| packages/silo/src/react/index.ts | Updates React binding comments around pure reactive reads. |
| packages/silo/src/queries.ts | Updates QueryAdapter/QueryHandle types to match Promise-first + optional Effect and new handle union. |
| packages/silo/src/index.ts | Re-exports new error types and HandleStatus from the root. |
| packages/silo/src/finder.ts | Rebuilds finder batching/chunking around Effect, AbortSignal wiring, typed errors, and statechart settlement. |
| packages/silo/src/errors.ts | Adds typed errors and shared Promise→AdapterError boundary (coerceAdapter). |
| packages/silo/README.md | Updates package docs for Promise-first adapters, optional Effect, typed errors, and new handle fields. |
| packages/silo/package.json | Adds effect as a peer dependency and dev dependency for silo. |
| packages/queries/tests/create-query.test.ts | Updates queries tests to accept Effect-returning adapters and typed errors. |
| packages/queries/src/types.ts | Updates query adapter typing to Promise-first + optional Effect, introduces QueryEnvelope. |
| packages/queries/src/create-query.ts | Uses coerceAdapter to normalize Promise/Effect results and route typed failures consistently. |
| packages/queries/package.json | Adds effect peer+dev dependencies for queries. |
| packages/mill/tests/operators.test.ts | Updates effect subscription tests to avoid returning non-function values under alien-signals 3.x cleanup semantics. |
| packages/mill/package.json | Bumps alien-signals dependency to ^3.2.1. |
| packages/kernel/vite.config.ts | Adds alien-signals/system to build config externals/entries. |
| packages/kernel/tests/read/tracking-isolation.test.ts | Updates tests for getActiveSub/setActiveSub rename and kernel re-export usage. |
| packages/kernel/tests/core/contracts.test.ts | Updates contract tests to ensure renamed internals aren’t exported from the root. |
| packages/kernel/tests/core/collections.test.ts | Updates effect subscription tests to use void reads (cleanup semantics). |
| packages/kernel/src/read.ts | Swaps getCurrentSub → getActiveSub usage. |
| packages/kernel/src/react/use-signal-effect.ts | Updates hook signature to support cleanup returns (alien-signals 3.x semantics). |
| packages/kernel/src/react/tracked.ts | Updates reactive node import path and subscriber API rename usage. |
| packages/kernel/src/react/for.ts | Updates subscriber API rename usage. |
| packages/kernel/src/internal.ts | Re-exports getActiveSub/setActiveSub instead of old names. |
| packages/kernel/src/index.ts | Documents alien-signals 3.x effect cleanup semantics; continues to re-export safe primitives only. |
| packages/kernel/src/collections.ts | Swaps getCurrentSub → getActiveSub in Map/Set tracking logic. |
| packages/kernel/package.json | Bumps alien-signals dependency to ^3.2.1. |
| packages/kernel/benchmarks/additional.bench.ts | Updates bench subscriptions to avoid returning values from effects. |
| packages/kernel/ARCHITECTURE.md | Updates architecture doc for renamed subscriber primitives. |
| packages/js-krauset/vite.config.ts | Adds alias for @supergrain/kernel/internal and reorders aliases. |
| packages/js-krauset/analyze-gap.ts | Updates printed text to the renamed subscriber API. |
| packages/husk/src/resource.ts | Updates to use kernel-exported effect and renamed subscriber APIs. |
| package.json | Bumps root alien-signals dependency to ^3.2.1. |
| .changeset/silo-effect-migration.md | Adds a major changeset describing silo’s breaking API migration and adapter boundary. |
| .changeset/alien-signals-3.md | Adds a major changeset describing alien-signals 3.x upgrade and cleanup semantics. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| The promise resolves exactly once on first success — later `insertDocument` calls update the `data` region in place but the promise reference stays stable, so `use()` doesn't re-suspend. After a first-load error, a recovery `insertDocument` produces a **new** resolved promise so a Suspense boundary nested in an error boundary can recover. | ||
|
|
||
| Because fetches are batched, naive `use(user.promise)` calls sprinkled through a list **don't waterfall** — the three `<UserCard>`s above collapse into one `userAdapter.find(["1", "2", "3"])` call before suspending. This is the piece that usually makes Suspense unusable at scale; here it's the default. | ||
|
|
||
| Want inline loading UI instead? Drop the `use(user.promise)` line and read `user.isPending` / `user.error` directly. Same hook, same handle, no config switch. | ||
| Want inline loading UI instead? Drop the `use(user.promise!)` line and branch on `user.data` / `user.fetch` directly. Same hook, same handle, no config switch. |
There was a problem hiding this comment.
Fixed in 6e35713. Updated the Suspense section to the new handle shape: data region → value, "branch on user.data / user.fetch" → "branch on user.value / user.isFetching / user.error (or user.status)", and corrected the prose use(user.promise) to use(user.promise!) to match the non-null assertion in the code example. Docs-only; test:validate still passes.
Generated by Claude Code
| * - `find` receives a chunk of at most `DocumentStoreConfig.batchSize` ids, | ||
| * grouped by type and deduped (no duplicate ids in one call). | ||
| * - A rejected Promise / failed Effect fails every deferred waiting on that | ||
| * chunk (as an `AdapterError`). | ||
| * - `ctx.signal` aborts when the fetch is no longer needed (every subscriber | ||
| * for the chunk has gone away). Thread it into `fetch(url, { signal })` for a | ||
| * real network abort; ignore it and interruption simply discards the result. |
There was a problem hiding this comment.
Fixed in 6e35713. The DocumentAdapter JSDoc now states ctx.signal aborts when the adapter Effect is interrupted (e.g. a per-model timeout fires, or a retry abandons the prior attempt) rather than "when every subscriber has gone away" — matching the actual semantics now that auto-cancellation is dropped. Applied the same correction to QueryAdapter in queries.ts for consistency (it had the same stale "abandoned" wording).
Generated by Claude Code
| // hook never imperatively subscribes. (The store exposes `subscribeDocument` | ||
| // for callers that want fetch cancellation; see its docs.) |
There was a problem hiding this comment.
Fixed in 6e35713. Removed the reference to the non-existent subscribeDocument API. The comment now just notes that useDocument is a pure reactive read and that an in-flight fetch isn't cancelled on unmount (it completes and caches) — consistent with this PR dropping auto-cancellation.
Generated by Claude Code
Address Copilot review on #85 — leftover docs from before auto-cancellation was dropped and the handle was reshaped: - DocumentAdapter / QueryAdapter JSDoc: `ctx.signal` aborts on adapter-Effect interruption (e.g. a timeout), not "when every subscriber has gone away". - react useDocument comment: drop the reference to a non-existent `subscribeDocument` API; note that an in-flight fetch isn't cancelled on unmount. - README Suspense section: `data`/`fetch` → `value`/`isFetching`/`error`/`status`; fix the missing non-null assertion in `use(user.promise!)`. Docs only; no behavior change. https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
- queries.ts QueryHandle JSDoc: `data`/`fetch` regions → the flat `value`/`error`/`isFetching`/`fetchedAt` fields (matches the reshaped handle). - Add a changeset for @supergrain/queries (published): the Promise-first `QueryAdapter.fetch` boundary and `AdapterError`-typed `Query.error`. https://claude.ai/code/session_018CvsdvgjWHvFe8jTxMKMam
Summary
Rebuilds
@supergrain/silo's network/async layer on an internal Effect engine, remodels the reactive handle as a flatstatus-discriminated statechart with typed errors, and upgrades the kernel's reactive corealien-signals2.0.7 → 3.2.1.An earlier revision of this branch vendored a 558-line port of alien-signals' operator layer into the kernel (
packages/kernel/src/system.ts) purely to expose anunwatchedhook, which silo used for signals-native fetch cancellation (cancel an in-flight fetch when a handle lost its last reactive observer).That fork has been removed. alien-signals exposes no public seam for "a node lost its last subscriber" — the
unwatchedcallback is a private closure argument tocreateReactiveSystem, and a separate system instance can't observe teardown driven by the library's owneffect/unlink. The only way to get the hook was to own the entire operator layer, which is a standing maintenance tax (re-port on every upgrade). Not worth it.The kernel now imports alien-signals' published primitives directly, and silo's auto-cancellation is dropped (see below).
What's in the PR
Kernel
alien-signals2.0.7 → 3.2.1. Breaking:effect(fn)now treatsfn's return value as a cleanup function (matchesuseEffect); read-for-subscription with a statement body orvoid.useSignalEffectnow accepts an optional cleanup return.getCurrentSub/setCurrentSub→getActiveSub/setActiveSub(re-exported from@supergrain/kernel/internal).Silo (breaking)
Promise<unknown> | Effect.Effect<unknown, AdapterError>); a rejection becomes a typedAdapterError.AdapterError/NotFoundError/ProcessorError(Data.TaggedError, unionSiloError), exported from the root.retry(EffectSchedule) andtimeout(Duration).DocumentHandle/QueryHandleare now astatus-discriminated union ("pending" | "success" | "error") over flat orthogonal fields (value/error/isFetching/fetchedAt/promise).data/isPending/hasDataand the old uppercase statuses are gone — see the silo changeset for the migration table.find/findQueryare pure reactive reads; an in-flight fetch is not cancelled on unmount (it completes and caches). Adapters still receive{ signal }, which aborts when the adapter Effect is interrupted (e.g. atimeoutfires).Queries (
@supergrain/queries)QueryAdapter.fetchis Promise-first with the sameAdapterErrorboundary.Checks
All green locally:
pnpm test(608),pnpm run test:validate(5),pnpm run typecheck,pnpm lint,pnpm format.Notes for review
AdapterErrorboundary in silo/finder and queries/create-query; the repeated get-or-create-handle dance instore.ts) out of scope here — happy to follow up.https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG
Generated by Claude Code