From 48d6b5e81730d90645621b5be8ef1f1c5d620cea Mon Sep 17 00:00:00 2001 From: MarkXian Date: Thu, 23 Jul 2026 13:00:17 +0800 Subject: [PATCH] fix: hydrate atom reactivity initial values --- .changeset/fix-hydrated-atom-reactivity.md | 5 +++ .../src/unstable/reactivity/AtomRegistry.ts | 21 +++++++--- packages/effect/test/reactivity/Atom.test.ts | 39 ++++++++++++++++++- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-hydrated-atom-reactivity.md diff --git a/.changeset/fix-hydrated-atom-reactivity.md b/.changeset/fix-hydrated-atom-reactivity.md new file mode 100644 index 00000000000..8846b187d6b --- /dev/null +++ b/.changeset/fix-hydrated-atom-reactivity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix hydrated serializable atoms with reactivity so their invalidation callbacks are registered before mutations. diff --git a/packages/effect/src/unstable/reactivity/AtomRegistry.ts b/packages/effect/src/unstable/reactivity/AtomRegistry.ts index 0747de7bdb1..358fc2992dc 100644 --- a/packages/effect/src/unstable/reactivity/AtomRegistry.ts +++ b/packages/effect/src/unstable/reactivity/AtomRegistry.ts @@ -317,6 +317,14 @@ const SerializableTypeId: Atom.SerializableTypeId = "~effect-atom/atom/Atom/Seri const atomKey = (atom: Atom.Atom): Atom.Atom | string => SerializableTypeId in atom ? (atom as Atom.Serializable)[SerializableTypeId].key : atom +const initialValueTarget = (atom: Atom.Atom): Atom.Atom => { + let target = atom + while (target.initialValueTarget) { + target = target.initialValueTarget + } + return target +} + class RegistryImpl implements AtomRegistry { readonly [TypeId]: TypeId readonly timeoutResolution: number @@ -346,11 +354,7 @@ class RegistryImpl implements AtomRegistry { } if (initialValues !== undefined) { for (const [atom, value] of initialValues) { - let target = atom - while (target.initialValueTarget) { - target = target.initialValueTarget - } - this.ensureNode(target).setInitialValue(value) + this.ensureNode(initialValueTarget(atom)).setInitialValue(value) } } } @@ -435,7 +439,12 @@ class RegistryImpl implements AtomRegistry { const encoded = this.preloadedSerializable.get(key) this.preloadedSerializable.delete(key) const decoded = (atom as any as Atom.Serializable)[SerializableTypeId].decode(encoded) - node.setValue(decoded) + const target = initialValueTarget(atom) + if (target === atom) { + node.setInitialValue(decoded) + } else { + this.ensureNode(target).setInitialValue(decoded) + } } return node } diff --git a/packages/effect/test/reactivity/Atom.test.ts b/packages/effect/test/reactivity/Atom.test.ts index 3ded384d86f..bd54d1816b6 100644 --- a/packages/effect/test/reactivity/Atom.test.ts +++ b/packages/effect/test/reactivity/Atom.test.ts @@ -15,7 +15,7 @@ import { } from "effect" import { TestClock } from "effect/testing" import { KeyValueStore } from "effect/unstable/persistence" -import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity" +import { AsyncResult, Atom, AtomRegistry, Hydration } from "effect/unstable/reactivity" declare const global: any @@ -2249,6 +2249,43 @@ describe.sequential("Atom", () => { assert.strictEqual(r.get(atom), 11) assert.strictEqual(rebuilds, 2) }) + + it("rebuilds on mutation with a hydrated serializable initial value", async () => { + let rebuilds = 0 + let value = 0 + const atom = Atom.make(() => { + rebuilds++ + return value + }).pipe( + Atom.withReactivity(["counter"]), + Atom.serializable({ key: "hydrated-counter", schema: Schema.Number }), + Atom.keepAlive + ) + const r = AtomRegistry.make() + const fn = counterRuntime.fn( + Effect.fn(function*() { + }), + { reactivityKeys: ["counter"] } + ) + const state = [{ + "~effect/reactivity/DehydratedAtom": true as const, + key: "hydrated-counter", + value: 10, + dehydratedAt: 0 + }] + + Hydration.hydrate(r, state) + r.mount(atom) + + assert.strictEqual(r.get(atom), 10) + assert.strictEqual(rebuilds, 1) + + value = 11 + r.set(fn, void 0) + + assert.strictEqual(r.get(atom), 11) + assert.strictEqual(rebuilds, 2) + }) }) it("Atom.Interrupt", async () => {