| ... | @@ -5,6 +5,9 @@ import * as Option from "effect/Option"; | ... | @@ -5,6 +5,9 @@ import * as Option from "effect/Option"; |
| 5 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; | 5 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; |
| 6 | import { vi } from "vite-plus/test"; | 6 | import { vi } from "vite-plus/test"; |
| 7 | import { ProviderInstanceId } from "@t3tools/contracts"; | 7 | import { ProviderInstanceId } from "@t3tools/contracts"; |
| | 8 | import { RegistryContext, useAtomSet } from "@effect/atom-react"; |
| | 9 | import { createElement } from "react"; |
| | 10 | import { renderToString } from "react-dom/server"; |
| 8 | | 11 | |
| 9 | vi.mock("expo-secure-store", () => ({ | 12 | vi.mock("expo-secure-store", () => ({ |
| 10 | getItemAsync: vi.fn(), | 13 | getItemAsync: vi.fn(), |
| ... | @@ -125,12 +128,16 @@ describe("mobile preferences state", () => { | ... | @@ -125,12 +128,16 @@ describe("mobile preferences state", () => { |
| 125 | }), | 128 | }), |
| 126 | ); | 129 | ); |
| 127 | | 130 | |
| 128 | it.effect("keeps both favorites when updates are sent before a render", () => | 131 | it.effect("keeps both favorites when the React setter sends updates before a render", () => |
| 129 | Effect.gen(function* () { | 132 | Effect.gen(function* () { |
| 130 | let persisted: Preferences = { modelFavorites: [] }; | 133 | let persisted: Preferences = { modelFavorites: [] }; |
| 131 | const state = makePreferencesState({ | 134 | const state = makePreferencesState({ |
| 132 | load: Effect.succeed(persisted), | 135 | load: Effect.succeed(persisted), |
| 133 | savePatch: () => Effect.die(new Error("Favorite updates must use a transform.")), | 136 | savePatch: (patch) => |
| | 137 | Effect.sync(() => { |
| | 138 | persisted = { ...persisted, ...patch }; |
| | 139 | return persisted; |
| | 140 | }), |
| 134 | update: (transform) => | 141 | update: (transform) => |
| 135 | Effect.sync(() => { | 142 | Effect.sync(() => { |
| 136 | persisted = { ...persisted, ...transform(persisted) }; | 143 | persisted = { ...persisted, ...transform(persisted) }; |
| ... | @@ -142,13 +149,31 @@ describe("mobile preferences state", () => { | ... | @@ -142,13 +149,31 @@ describe("mobile preferences state", () => { |
| 142 | const unmountUpdate = registry.mount(state.updatePreferencesAtom); | 149 | const unmountUpdate = registry.mount(state.updatePreferencesAtom); |
| 143 | yield* AtomRegistry.getResult(registry, state.preferencesAtom, { suspendOnWaiting: true }); | 150 | yield* AtomRegistry.getResult(registry, state.preferencesAtom, { suspendOnWaiting: true }); |
| 144 | | 151 | |
| | 152 | function useSavePreferences() { |
| | 153 | return useAtomSet(state.updatePreferencesAtom); |
| | 154 | } |
| | 155 | const setters: Array<ReturnType<typeof useSavePreferences>> = []; |
| | 156 | function CaptureSetter() { |
| | 157 | setters.push(useSavePreferences()); |
| | 158 | return null; |
| | 159 | } |
| | 160 | // Exercise the real React setter, which treats bare functions as updates |
| | 161 | // to the atom's read value. Direct registry.set calls bypass that behavior. |
| | 162 | renderToString( |
| | 163 | createElement(RegistryContext.Provider, { value: registry }, createElement(CaptureSetter)), |
| | 164 | ); |
| | 165 | const savePreferences = setters[0]!; |
| 145 | const provider = ProviderInstanceId.make("codex"); | 166 | const provider = ProviderInstanceId.make("codex"); |
| 146 | registry.set(state.updatePreferencesAtom, (current) => ({ | 167 | savePreferences({ |
| 147 | modelFavorites: [...(current.modelFavorites ?? []), { provider, model: "astra" }], | 168 | transform: (current) => ({ |
| 148 | })); | 169 | modelFavorites: [...(current.modelFavorites ?? []), { provider, model: "astra" }], |
| 149 | registry.set(state.updatePreferencesAtom, (current) => ({ | 170 | }), |
| 150 | modelFavorites: [...(current.modelFavorites ?? []), { provider, model: "sol" }], | 171 | }); |
| 151 | })); | 172 | savePreferences({ |
| | 173 | transform: (current) => ({ |
| | 174 | modelFavorites: [...(current.modelFavorites ?? []), { provider, model: "sol" }], |
| | 175 | }), |
| | 176 | }); |
| 152 | yield* AtomRegistry.getResult(registry, state.updatePreferencesAtom, { | 177 | yield* AtomRegistry.getResult(registry, state.updatePreferencesAtom, { |
| 153 | suspendOnWaiting: true, | 178 | suspendOnWaiting: true, |
| 154 | }); | 179 | }); |
| ... | @@ -158,6 +183,18 @@ describe("mobile preferences state", () => { | ... | @@ -158,6 +183,18 @@ describe("mobile preferences state", () => { |
| 158 | { provider, model: "sol" }, | 183 | { provider, model: "sol" }, |
| 159 | ]); | 184 | ]); |
| 160 | | 185 | |
| | 186 | savePreferences({ |
| | 187 | transform: (current) => ({ |
| | 188 | modelFavorites: (current.modelFavorites ?? []).filter( |
| | 189 | (favorite) => favorite.model !== "astra", |
| | 190 | ), |
| | 191 | }), |
| | 192 | }); |
| | 193 | yield* AtomRegistry.getResult(registry, state.updatePreferencesAtom, { |
| | 194 | suspendOnWaiting: true, |
| | 195 | }); |
| | 196 | expect(persisted.modelFavorites).toEqual([{ provider, model: "sol" }]); |
| | 197 | |
| 161 | unmountUpdate(); | 198 | unmountUpdate(); |
| 162 | unmountPreferences(); | 199 | unmountPreferences(); |
| 163 | registry.dispose(); | 200 | registry.dispose(); |