From 91aac3e67989f0bb5e0060a9dd7816bddc47738f Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Wed, 3 Dec 2025 13:24:02 +0100 Subject: [PATCH 1/2] fix: add Scope.Scope support to AtomRuntime.subscriptionRef and subscribable --- packages/atom/src/Atom.ts | 12 ++++++++---- packages/atom/test/Atom.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/atom/src/Atom.ts b/packages/atom/src/Atom.ts index 7fecfc2..7025693 100644 --- a/packages/atom/src/Atom.ts +++ b/packages/atom/src/Atom.ts @@ -622,18 +622,22 @@ export interface AtomRuntime extends Atom( create: - | Effect.Effect, E, R | AtomRegistry | Reactivity.Reactivity> + | Effect.Effect, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity> | (( get: Context - ) => Effect.Effect, E, R | AtomRegistry | Reactivity.Reactivity>) + ) => Effect.Effect, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity>) ) => Writable, A> readonly subscribable: ( create: - | Effect.Effect, E1, R | AtomRegistry | Reactivity.Reactivity> + | Effect.Effect, E1, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity> | (( get: Context - ) => Effect.Effect, E1, R | AtomRegistry | Reactivity.Reactivity>) + ) => Effect.Effect< + Subscribable.Subscribable, + E1, + Scope.Scope | R | AtomRegistry | Reactivity.Reactivity + >) ) => Atom> } diff --git a/packages/atom/test/Atom.test.ts b/packages/atom/test/Atom.test.ts index 987a33c..4771556 100644 --- a/packages/atom/test/Atom.test.ts +++ b/packages/atom/test/Atom.test.ts @@ -979,6 +979,30 @@ describe("Atom", () => { unmount() }) + it("SubscriptionRef/runtime/scoped", async () => { + let finalized = false + const atom = counterRuntime.subscriptionRef( + Effect.gen(function*() { + yield* Effect.addFinalizer(() => + Effect.sync(() => { + finalized = true + }) + ) + return yield* SubscriptionRef.make(0) + }) + ) + const r = Registry.make() + const unmount = r.mount(atom) + assert.deepStrictEqual(r.get(atom), Result.success(0, { waiting: true })) + r.set(atom, 1) + await new Promise((resolve) => resolve(null)) + assert.deepStrictEqual(r.get(atom), Result.success(1, { waiting: true })) + assert.strictEqual(finalized, false) + unmount() + await new Promise((resolve) => resolve(null)) + assert.strictEqual(finalized, true) + }) + it("setLazy(true)", async () => { const count = Atom.make(0).pipe(Atom.keepAlive) let rebuilds = 0 From 41ebf1ba480dcd22fe502119e7bf31d94f3b0897 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Wed, 3 Dec 2025 13:44:11 +0100 Subject: [PATCH 2/2] test: add test for subscriptionRef recomputation with derived state Add a test to verify that subscriptionRef atoms properly recompute when their dependencies change. This test was added based on a user report that subscriptionRef atoms don't recompute when dependencies accessed via get.some() change. The test verifies that both regular derived atoms and subscriptionRef atoms correctly recompute when an Option dependency changes value. --- packages/atom/test/Atom.test.ts | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/atom/test/Atom.test.ts b/packages/atom/test/Atom.test.ts index 4771556..692b60b 100644 --- a/packages/atom/test/Atom.test.ts +++ b/packages/atom/test/Atom.test.ts @@ -1446,6 +1446,80 @@ describe("Atom", () => { unmount2() }) + + it("subscriptionRef with runtime recomputes when dependencies change", async () => { + vitest.useRealTimers() + + // Test that subscriptionRef atoms properly recompute when their dependencies change + // This test was added based on a user report that subscriptionRef atoms don't recompute + // when dependencies accessed via get.some() change. + const chatIdAtom = Atom.make>(Option.none()).pipe(Atom.keepAlive) + + // A regular derived atom using Effect.fnUntraced + let derivedRecomputes = 0 + const derivedStateAtom = counterRuntime.atom( + Effect.fnUntraced(function*(get: Atom.Context) { + const chatId = yield* get.some(chatIdAtom) + derivedRecomputes++ + return chatId + }) + ) + + // A subscriptionRef atom that also depends on chatIdAtom + let subRefRecomputes = 0 + const stateAtom = counterRuntime.subscriptionRef((get) => + Effect.gen(function*() { + const chatId = yield* get.some(chatIdAtom) + subRefRecomputes++ + return yield* SubscriptionRef.make(chatId) + }) + ) + + const r = Registry.make() + const unmountDerived = r.mount(derivedStateAtom) + const unmountSubRef = r.mount(stateAtom) + + // Initially, chatIdAtom is None, so both should fail with NoSuchElementException + let derivedResult = r.get(derivedStateAtom) + let subRefResult = r.get(stateAtom) + expect(derivedRecomputes).toEqual(0) + expect(subRefRecomputes).toEqual(0) + + // Set chatIdAtom to Some("chat-1") + r.set(chatIdAtom, Option.some("chat-1")) + await new Promise((resolve) => resolve(null)) + + derivedResult = r.get(derivedStateAtom) + subRefResult = r.get(stateAtom) + + assert(Result.isSuccess(derivedResult)) + expect(derivedResult.value).toEqual("chat-1") + expect(derivedRecomputes).toEqual(1) + + assert(Result.isSuccess(subRefResult)) + expect(subRefResult.value).toEqual("chat-1") + expect(subRefRecomputes).toEqual(1) + + // Change chatIdAtom to Some("chat-2") - both atoms should recompute + r.set(chatIdAtom, Option.some("chat-2")) + await new Promise((resolve) => resolve(null)) + + derivedResult = r.get(derivedStateAtom) + subRefResult = r.get(stateAtom) + + // The derived atom should have recomputed + assert(Result.isSuccess(derivedResult)) + expect(derivedResult.value).toEqual("chat-2") + expect(derivedRecomputes).toEqual(2) + + // The subscriptionRef atom should ALSO have recomputed + assert(Result.isSuccess(subRefResult)) + expect(subRefResult.value).toEqual("chat-2") + expect(subRefRecomputes).toEqual(2) + + unmountDerived() + unmountSubRef() + }) }) interface BuildCounter {