diff --git a/packages/atom/src/Atom.ts b/packages/atom/src/Atom.ts
index 55f10ce..08726e2 100644
--- a/packages/atom/src/Atom.ts
+++ b/packages/atom/src/Atom.ts
@@ -1802,19 +1802,23 @@ export const kvs = (options: {
const resultAtom = options.runtime.atom(
Effect.flatMap(
KeyValueStore.KeyValueStore,
- (store) => Effect.flatten(store.forSchema(options.schema).get(options.key))
+ (store) => store.forSchema(options.schema).get(options.key)
)
)
return writable(
(get) => {
get.mount(setAtom)
- const value = Result.value(get(resultAtom))
- if (Option.isSome(value)) {
- return value.value
- }
- const defaultValue = options.defaultValue()
- get.set(setAtom, defaultValue)
- return defaultValue
+ get.subscribe(resultAtom, (result) => {
+ if (!Result.isSuccess(result)) return
+ if (Option.isSome(result.value)) {
+ get.setSelf(result.value.value)
+ } else {
+ const value = Option.getOrElse(get.self(), options.defaultValue)
+ get.setSelf(value)
+ get.set(setAtom, value)
+ }
+ }, { immediate: true })
+ return Option.getOrElse(get.self(), options.defaultValue)
},
(ctx, value: A) => {
ctx.set(setAtom, value as any)
diff --git a/packages/atom/test/Atom.test.ts b/packages/atom/test/Atom.test.ts
index 5d76dc0..68ca9ab 100644
--- a/packages/atom/test/Atom.test.ts
+++ b/packages/atom/test/Atom.test.ts
@@ -2,6 +2,7 @@ import * as Atom from "@effect-atom/atom/Atom"
import * as Hydration from "@effect-atom/atom/Hydration"
import * as Registry from "@effect-atom/atom/Registry"
import * as Result from "@effect-atom/atom/Result"
+import * as KeyValueStore from "@effect/platform/KeyValueStore"
import { addEqualityTesters, afterEach, assert, beforeEach, describe, expect, it, test, vitest } from "@effect/vitest"
import { Cause, Either, Equal, FiberRef, Schema, Struct, Subscribable, SubscriptionRef } from "effect"
import * as Arr from "effect/Array"
@@ -1501,6 +1502,109 @@ describe("Atom", () => {
assert.strictEqual(result.value.b, 4)
assert.strictEqual(runs, 2)
})
+
+ describe("kvs", () => {
+ it("memoizes defaultValue while loading empty storage", async () => {
+ let calls = 0
+ const storage = new Map()
+
+ const DelayedKVS = Layer.succeed(
+ KeyValueStore.KeyValueStore,
+ KeyValueStore.makeStringOnly({
+ get: (key) =>
+ Effect.gen(function*() {
+ yield* Effect.sleep(20)
+ return Option.fromNullable(storage.get(key))
+ }),
+ set: (key, value) =>
+ Effect.sync(() => {
+ storage.set(key, value)
+ }),
+ remove: (key) =>
+ Effect.sync(() => {
+ storage.delete(key)
+ }),
+ clear: Effect.sync(() => storage.clear()),
+ size: Effect.sync(() => storage.size)
+ })
+ )
+
+ const kvsRuntime = Atom.runtime(DelayedKVS)
+ const atom = Atom.kvs({
+ runtime: kvsRuntime,
+ key: "default-value-key",
+ schema: Schema.Number,
+ defaultValue: () => {
+ calls++
+ return 0
+ }
+ })
+
+ const r = Registry.make()
+ r.mount(atom)
+
+ expect(r.get(atom)).toEqual(0)
+ expect(calls).toEqual(1)
+
+ await vitest.advanceTimersByTimeAsync(50)
+
+ expect(r.get(atom)).toEqual(0)
+ expect(calls).toEqual(1)
+ })
+
+ it("preserves existing value after async load completes", async () => {
+ vitest.useRealTimers()
+ // Create an in-memory store with a pre-existing value
+ const storage = new Map()
+ storage.set("test-key", JSON.stringify(42))
+
+ // Create a delayed KeyValueStore to simulate async loading
+ // Use KeyValueStore.make to get proper forSchema support
+ const DelayedKVS = Layer.succeed(
+ KeyValueStore.KeyValueStore,
+ KeyValueStore.makeStringOnly({
+ get: (key) =>
+ Effect.gen(function*() {
+ yield* Effect.sleep(20) // Short delay to create Initial state window
+ return Option.fromNullable(storage.get(key))
+ }),
+ set: (key, value) =>
+ Effect.sync(() => {
+ storage.set(key, value)
+ }),
+ remove: (key) =>
+ Effect.sync(() => {
+ storage.delete(key)
+ }),
+ clear: Effect.sync(() => storage.clear()),
+ size: Effect.sync(() => storage.size)
+ })
+ )
+
+ const kvsRuntime = Atom.runtime(DelayedKVS)
+ const atom = Atom.kvs({
+ runtime: kvsRuntime,
+ key: "test-key",
+ schema: Schema.Number,
+ defaultValue: () => 0
+ })
+
+ const r = Registry.make()
+ r.mount(atom)
+
+ // First read during Initial state returns default
+ const value = r.get(atom)
+ expect(value).toEqual(0)
+
+ // Wait for async load AND any set effects to complete
+ await new Promise((resolve) => setTimeout(resolve, 50))
+
+ // THE KEY ASSERTION: After load completes, storage should still have original value.
+ // The bug was that the default (0) would be written during Initial state,
+ // corrupting the storage before the async load could read it.
+ expect(storage.get("test-key")).toEqual(JSON.stringify(42))
+ })
+ })
})
interface BuildCounter {