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 () => {