Skip to content

Effect migration for silo (without forking alien-signals)#85

Open
scottmessinger wants to merge 22 commits into
mainfrom
claude/cool-dijkstra-7YOtx
Open

Effect migration for silo (without forking alien-signals)#85
scottmessinger wants to merge 22 commits into
mainfrom
claude/cool-dijkstra-7YOtx

Conversation

@scottmessinger
Copy link
Copy Markdown
Member

Summary

Rebuilds @supergrain/silo's network/async layer on an internal Effect engine, remodels the reactive handle as a flat status-discriminated statechart with typed errors, and upgrades the kernel's reactive core alien-signals 2.0.7 → 3.2.1.

⚠️ Course-correction in this branch

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 an unwatched hook, 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 unwatched callback is a private closure argument to createReactiveSystem, and a separate system instance can't observe teardown driven by the library's own effect/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

  • Upgrade alien-signals 2.0.7 → 3.2.1. Breaking: effect(fn) now treats fn's return value as a cleanup function (matches useEffect); read-for-subscription with a statement body or void. useSignalEffect now accepts an optional cleanup return.
  • Internal rename getCurrentSub/setCurrentSubgetActiveSub/setActiveSub (re-exported from @supergrain/kernel/internal).

Silo (breaking)

  • Network/async layer on an internal Effect engine. Adapters stay Promise-first (Promise<unknown> | Effect.Effect<unknown, AdapterError>); a rejection becomes a typed AdapterError.
  • Typed errors: AdapterError / NotFoundError / ProcessorError (Data.TaggedError, union SiloError), exported from the root.
  • Per-model retry (Effect Schedule) and timeout (Duration).
  • DocumentHandle / QueryHandle are now a status-discriminated union ("pending" | "success" | "error") over flat orthogonal fields (value / error / isFetching / fetchedAt / promise). data/isPending/hasData and the old uppercase statuses are gone — see the silo changeset for the migration table.
  • No auto-cancellation. find/findQuery are 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. a timeout fires).

Queries (@supergrain/queries)

  • QueryAdapter.fetch is Promise-first with the same AdapterError boundary.

Checks

All green locally: pnpm test (608), pnpm run test:validate (5), pnpm run typecheck, pnpm lint, pnpm format.

Notes for review

  • Changesets describe each package's breaking surface.
  • I left a couple of pre-existing structural cleanup opportunities (the duplicated Promise→AdapterError boundary in silo/finder and queries/create-query; the repeated get-or-create-handle dance in store.ts) out of scope here — happy to follow up.

https://claude.ai/code/session_012E1i6gcCAVc5Qtkt9oxtrG


Generated by Claude Code

claude added 17 commits June 1, 2026 20:42
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
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a8a0c30) to head (6e35713).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

claude added 3 commits June 3, 2026 02:38
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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-signals 2.x → 3.2.1 and propagate the renamed subscriber APIs (getActiveSub/setActiveSub) plus the new effect() 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 getCurrentSubgetActiveSub 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 getCurrentSubgetActiveSub 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.

Comment thread README.md Outdated
Comment on lines +227 to +231
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.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/silo/src/store.ts Outdated
Comment on lines +164 to +170
* - `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.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/silo/src/react/index.ts Outdated
Comment on lines +168 to +169
// hook never imperatively subscribes. (The store exposes `subscribeDocument`
// for callers that want fetch cancellation; see its docs.)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

claude added 2 commits June 3, 2026 03:11
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
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.

3 participants