atomirx 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
@@ -0,0 +1,342 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { atomState } from "./atomState";
3
+
4
+ describe("atomState", () => {
5
+ describe("initial state", () => {
6
+ it("should start with undefined value", () => {
7
+ const state = atomState<number>();
8
+ expect(state.getValue()).toBeUndefined();
9
+ });
10
+
11
+ it("should start with loading false", () => {
12
+ const state = atomState<number>();
13
+ expect(state.getLoading()).toBe(false);
14
+ });
15
+
16
+ it("should start with undefined error", () => {
17
+ const state = atomState<number>();
18
+ expect(state.getError()).toBeUndefined();
19
+ });
20
+
21
+ it("should start with version 0", () => {
22
+ const state = atomState<number>();
23
+ expect(state.getVersion()).toBe(0);
24
+ });
25
+ });
26
+
27
+ describe("setValue", () => {
28
+ it("should set the value", () => {
29
+ const state = atomState<number>();
30
+ state.setValue(42);
31
+ expect(state.getValue()).toBe(42);
32
+ });
33
+
34
+ it("should clear loading state", () => {
35
+ const state = atomState<number>();
36
+ state.setLoading(Promise.resolve(1));
37
+ expect(state.getLoading()).toBe(true);
38
+
39
+ state.setValue(42);
40
+ expect(state.getLoading()).toBe(false);
41
+ });
42
+
43
+ it("should clear error state", () => {
44
+ const state = atomState<number>();
45
+ state.setError(new Error("test"));
46
+ expect(state.getError()).toBeDefined();
47
+
48
+ state.setValue(42);
49
+ expect(state.getError()).toBeUndefined();
50
+ });
51
+
52
+ it("should bump version", () => {
53
+ const state = atomState<number>();
54
+ const v1 = state.getVersion();
55
+ state.setValue(42);
56
+ expect(state.getVersion()).toBe(v1 + 1);
57
+ });
58
+
59
+ it("should notify listeners", () => {
60
+ const state = atomState<number>();
61
+ const listener = vi.fn();
62
+ state.on(listener);
63
+
64
+ state.setValue(42);
65
+ expect(listener).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it("should not notify if value is equal (strict equality)", () => {
69
+ const state = atomState<number>();
70
+ state.setValue(42);
71
+
72
+ const listener = vi.fn();
73
+ state.on(listener);
74
+
75
+ state.setValue(42);
76
+ expect(listener).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("should use custom equality function", () => {
80
+ const state = atomState<{ id: number; name: string }>({
81
+ equals: (a, b) => a?.id === b?.id,
82
+ });
83
+ state.setValue({ id: 1, name: "John" });
84
+
85
+ const listener = vi.fn();
86
+ state.on(listener);
87
+
88
+ // Same id, different name - should not notify
89
+ state.setValue({ id: 1, name: "Jane" });
90
+ expect(listener).not.toHaveBeenCalled();
91
+
92
+ // Different id - should notify
93
+ state.setValue({ id: 2, name: "Jane" });
94
+ expect(listener).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ it("should create a resolved promise", async () => {
98
+ const state = atomState<number>();
99
+ state.setValue(42);
100
+
101
+ const value = await state.getPromise();
102
+ expect(value).toBe(42);
103
+ });
104
+ });
105
+
106
+ describe("setLoading", () => {
107
+ it("should set loading to true", () => {
108
+ const state = atomState<number>();
109
+ state.setLoading(Promise.resolve(1));
110
+ expect(state.getLoading()).toBe(true);
111
+ });
112
+
113
+ it("should clear value", () => {
114
+ const state = atomState<number>();
115
+ state.setValue(42);
116
+ state.setLoading(Promise.resolve(1));
117
+ expect(state.getValue()).toBeUndefined();
118
+ });
119
+
120
+ it("should clear error", () => {
121
+ const state = atomState<number>();
122
+ state.setError(new Error("test"));
123
+ state.setLoading(Promise.resolve(1));
124
+ expect(state.getError()).toBeUndefined();
125
+ });
126
+
127
+ it("should bump version", () => {
128
+ const state = atomState<number>();
129
+ const v1 = state.getVersion();
130
+ state.setLoading(Promise.resolve(1));
131
+ expect(state.getVersion()).toBe(v1 + 1);
132
+ });
133
+
134
+ it("should notify listeners", () => {
135
+ const state = atomState<number>();
136
+ const listener = vi.fn();
137
+ state.on(listener);
138
+
139
+ state.setLoading(Promise.resolve(1));
140
+ expect(listener).toHaveBeenCalledTimes(1);
141
+ });
142
+
143
+ it("should store the promise", () => {
144
+ const state = atomState<number>();
145
+ const promise = Promise.resolve(42);
146
+ state.setLoading(promise);
147
+ expect(state.getPromise()).toBe(promise);
148
+ });
149
+ });
150
+
151
+ describe("setError", () => {
152
+ it("should set the error", () => {
153
+ const state = atomState<number>();
154
+ const error = new Error("test");
155
+ state.setError(error);
156
+ expect(state.getError()).toBe(error);
157
+ });
158
+
159
+ it("should set loading to false", () => {
160
+ const state = atomState<number>();
161
+ state.setLoading(Promise.resolve(1));
162
+ state.setError(new Error("test"));
163
+ expect(state.getLoading()).toBe(false);
164
+ });
165
+
166
+ it("should clear value", () => {
167
+ const state = atomState<number>();
168
+ state.setValue(42);
169
+ state.setError(new Error("test"));
170
+ expect(state.getValue()).toBeUndefined();
171
+ });
172
+
173
+ it("should bump version", () => {
174
+ const state = atomState<number>();
175
+ const v1 = state.getVersion();
176
+ state.setError(new Error("test"));
177
+ expect(state.getVersion()).toBe(v1 + 1);
178
+ });
179
+
180
+ it("should notify listeners", () => {
181
+ const state = atomState<number>();
182
+ const listener = vi.fn();
183
+ state.on(listener);
184
+
185
+ state.setError(new Error("test"));
186
+ expect(listener).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it("should not notify if same error", () => {
190
+ const state = atomState<number>();
191
+ const error = new Error("test");
192
+ state.setError(error);
193
+
194
+ const listener = vi.fn();
195
+ state.on(listener);
196
+
197
+ state.setError(error);
198
+ expect(listener).not.toHaveBeenCalled();
199
+ });
200
+ });
201
+
202
+ describe("race condition handling", () => {
203
+ it("should detect stale versions", () => {
204
+ const state = atomState<number>();
205
+ const v1 = state.getVersion();
206
+
207
+ state.setValue(1);
208
+ expect(state.isVersionStale(v1)).toBe(true);
209
+ expect(state.isVersionStale(state.getVersion())).toBe(false);
210
+ });
211
+
212
+ it("should ignore stale promise resolution", async () => {
213
+ const state = atomState<number>();
214
+
215
+ let resolve1: (value: number) => void;
216
+ const promise1 = new Promise<number>((r) => {
217
+ resolve1 = r;
218
+ });
219
+
220
+ state.setLoading(promise1);
221
+ const v1 = state.getVersion();
222
+
223
+ // Set a new value before promise resolves
224
+ state.setValue(100);
225
+
226
+ // Now resolve the old promise
227
+ resolve1!(42);
228
+ await new Promise((r) => setTimeout(r, 0));
229
+
230
+ // Value should still be 100, not 42
231
+ expect(state.getValue()).toBe(100);
232
+ expect(state.isVersionStale(v1)).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe("subscriptions", () => {
237
+ it("should return unsubscribe function", () => {
238
+ const state = atomState<number>();
239
+ const listener = vi.fn();
240
+
241
+ const unsubscribe = state.on(listener);
242
+ state.setValue(1);
243
+ expect(listener).toHaveBeenCalledTimes(1);
244
+
245
+ unsubscribe();
246
+ state.setValue(2);
247
+ expect(listener).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ it("should support multiple listeners", () => {
251
+ const state = atomState<number>();
252
+ const listener1 = vi.fn();
253
+ const listener2 = vi.fn();
254
+
255
+ state.on(listener1);
256
+ state.on(listener2);
257
+
258
+ state.setValue(1);
259
+
260
+ expect(listener1).toHaveBeenCalledTimes(1);
261
+ expect(listener2).toHaveBeenCalledTimes(1);
262
+ });
263
+ });
264
+
265
+ describe("reset", () => {
266
+ it("should reset to initial state", () => {
267
+ const state = atomState<number>();
268
+ state.setValue(42);
269
+
270
+ state.reset();
271
+
272
+ expect(state.getValue()).toBeUndefined();
273
+ expect(state.getLoading()).toBe(false);
274
+ expect(state.getError()).toBeUndefined();
275
+ });
276
+
277
+ it("should bump version on reset", () => {
278
+ const state = atomState<number>();
279
+ state.setValue(42);
280
+ const v1 = state.getVersion();
281
+
282
+ state.reset();
283
+ expect(state.getVersion()).toBe(v1 + 1);
284
+ });
285
+
286
+ it("should notify listeners on reset", () => {
287
+ const state = atomState<number>();
288
+ state.setValue(42);
289
+
290
+ const listener = vi.fn();
291
+ state.on(listener);
292
+
293
+ state.reset();
294
+ expect(listener).toHaveBeenCalledTimes(1);
295
+ });
296
+
297
+ it("should not notify if already in initial state", () => {
298
+ const state = atomState<number>();
299
+ const listener = vi.fn();
300
+ state.on(listener);
301
+
302
+ state.reset();
303
+ expect(listener).not.toHaveBeenCalled();
304
+ });
305
+ });
306
+
307
+ describe("equals options", () => {
308
+ it("should use shallow equality when specified", () => {
309
+ const state = atomState<{ a: number }>({ equals: "shallow" });
310
+ state.setValue({ a: 1 });
311
+
312
+ const listener = vi.fn();
313
+ state.on(listener);
314
+
315
+ // Same content - no notification
316
+ state.setValue({ a: 1 });
317
+ expect(listener).not.toHaveBeenCalled();
318
+
319
+ // Different content - should notify
320
+ state.setValue({ a: 2 });
321
+ expect(listener).toHaveBeenCalledTimes(1);
322
+ });
323
+
324
+ it("should use deep equality when specified", () => {
325
+ const state = atomState<{ nested: { value: number } }>({
326
+ equals: "deep",
327
+ });
328
+ state.setValue({ nested: { value: 1 } });
329
+
330
+ const listener = vi.fn();
331
+ state.on(listener);
332
+
333
+ // Same deep content - no notification
334
+ state.setValue({ nested: { value: 1 } });
335
+ expect(listener).not.toHaveBeenCalled();
336
+
337
+ // Different deep content - should notify
338
+ state.setValue({ nested: { value: 2 } });
339
+ expect(listener).toHaveBeenCalledTimes(1);
340
+ });
341
+ });
342
+ });
@@ -0,0 +1,256 @@
1
+ import { emitter, Emitter } from "./emitter";
2
+ import { resolveEquality } from "./equality";
3
+ import { scheduleNotifyHook } from "./scheduleNotifyHook";
4
+ import { Equality } from "./types";
5
+
6
+ /**
7
+ * Options for creating an atomState.
8
+ */
9
+ export interface AtomStateOptions<T, TFallback = undefined> {
10
+ /** Equality strategy for change detection (default: "strict") */
11
+ equals?: Equality<T>;
12
+ /**
13
+ * Fallback value to use during loading or error states.
14
+ * When set, enables "stale" mode where value is never undefined.
15
+ */
16
+ fallback?: TFallback;
17
+ /**
18
+ * Whether fallback mode is enabled.
19
+ * When true, getValue() returns fallback/lastResolved during loading/error.
20
+ */
21
+ hasFallback?: boolean;
22
+ }
23
+
24
+ /**
25
+ * API for managing atom state with async support.
26
+ */
27
+ export interface AtomStateAPI<T, TFallback = undefined> {
28
+ /** Get the current value (undefined if loading or error, unless fallback mode) */
29
+ getValue(): TFallback extends undefined ? T | undefined : T | TFallback;
30
+ /** Get the loading state */
31
+ getLoading(): boolean;
32
+ /** Get the error (undefined if no error) */
33
+ getError(): any;
34
+ /** Get the current promise */
35
+ getPromise(): PromiseLike<T>;
36
+
37
+ /** Set the value (clears loading and error, notifies if changed) */
38
+ setValue(value: T, silent?: boolean): void;
39
+ /** Set loading state with a promise (clears value and error, notifies) */
40
+ setLoading(promise: PromiseLike<T>, silent?: boolean): void;
41
+ /** Set error state (clears value and loading, notifies if changed) */
42
+ setError(error: any, silent?: boolean): void;
43
+ /** Reset to initial state (notifies if was not already initial) */
44
+ reset(): void;
45
+
46
+ /** Get current version (for race condition handling) */
47
+ getVersion(): number;
48
+ /** Check if a version is stale (older than current) */
49
+ isVersionStale(version: number): boolean;
50
+
51
+ /**
52
+ * Returns true if fallback mode is enabled AND (loading OR error).
53
+ * When true, getValue() returns fallback or last resolved value.
54
+ */
55
+ stale(): boolean;
56
+
57
+ /**
58
+ * Returns true if value has been changed by setValue() (not during init).
59
+ */
60
+ isDirty(): boolean;
61
+
62
+ /**
63
+ * Marks the state as dirty (called when set() is used).
64
+ */
65
+ markDirty(): void;
66
+
67
+ /**
68
+ * Clears the dirty flag (called on reset).
69
+ */
70
+ clearDirty(): void;
71
+
72
+ /** Subscribe to state changes */
73
+ on: Emitter["on"];
74
+ }
75
+
76
+ /**
77
+ * Creates a state container for atoms with async support.
78
+ *
79
+ * Handles:
80
+ * - Value, loading, and error states
81
+ * - Version tracking for race condition handling
82
+ * - Equality checking for change detection
83
+ * - Notification scheduling
84
+ * - Fallback mode for stale-while-revalidate pattern
85
+ *
86
+ * @template T - The type of value stored
87
+ * @template TFallback - The type of fallback value (default: undefined)
88
+ * @param options - Configuration options
89
+ * @returns AtomStateAPI for managing the state
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const state = atomState<number>();
94
+ *
95
+ * state.setValue(42);
96
+ * console.log(state.getValue()); // 42
97
+ *
98
+ * state.setLoading(fetchData());
99
+ * console.log(state.getLoading()); // true
100
+ *
101
+ * state.on(() => console.log('State changed!'));
102
+ * ```
103
+ *
104
+ * @example With fallback
105
+ * ```ts
106
+ * const state = atomState<User, User>({
107
+ * fallback: { name: 'Guest' },
108
+ * hasFallback: true
109
+ * });
110
+ *
111
+ * state.setLoading(fetchUser());
112
+ * console.log(state.getValue()); // { name: 'Guest' }
113
+ * console.log(state.isStale()); // true
114
+ * ```
115
+ */
116
+ export function atomState<T, TFallback = undefined>(
117
+ options: AtomStateOptions<T, TFallback> = {}
118
+ ): AtomStateAPI<T, TFallback> {
119
+ const changeEmitter = emitter();
120
+ const eq = resolveEquality(options.equals as Equality<unknown>);
121
+
122
+ // Fallback configuration
123
+ const hasFallback = options.hasFallback ?? false;
124
+ const fallbackValue = options.fallback as TFallback;
125
+
126
+ // Internal state
127
+ let value: T | undefined = undefined;
128
+ let loading = false;
129
+ let error: any = undefined;
130
+ let promise: PromiseLike<T> = Promise.resolve(undefined as T);
131
+ let version = 0;
132
+
133
+ // Track last resolved value for stale-while-revalidate
134
+ let lastResolvedValue: T | undefined = undefined;
135
+
136
+ // Track if value has been modified by set()
137
+ let dirty = false;
138
+
139
+ /**
140
+ * Schedules notification to all subscribers.
141
+ * Each listener is scheduled individually to enable deduping in batch().
142
+ */
143
+ const notify = () => {
144
+ changeEmitter.forEach((listener) => {
145
+ scheduleNotifyHook.current(listener);
146
+ });
147
+ };
148
+
149
+ /**
150
+ * Checks if state is in initial state (no value, not loading, no error).
151
+ */
152
+ const isInitialState = () => {
153
+ return value === undefined && !loading && error === undefined;
154
+ };
155
+
156
+ const api: AtomStateAPI<T, TFallback> = {
157
+ getValue: () => {
158
+ // If fallback mode enabled and in loading/error state
159
+ if (hasFallback && (loading || error !== undefined)) {
160
+ // Return last resolved value if available, otherwise fallback
161
+ return (lastResolvedValue ?? fallbackValue) as any;
162
+ }
163
+ return value as any;
164
+ },
165
+ getLoading: () => loading,
166
+ getError: () => error,
167
+ getPromise: () => promise,
168
+ getVersion: () => version,
169
+
170
+ isVersionStale: (v: number) => v !== version,
171
+
172
+ stale: () => {
173
+ // Returns true if fallback mode enabled AND (loading OR error)
174
+ return hasFallback && (loading || error !== undefined);
175
+ },
176
+
177
+ isDirty: () => dirty,
178
+
179
+ markDirty: () => {
180
+ dirty = true;
181
+ },
182
+
183
+ clearDirty: () => {
184
+ dirty = false;
185
+ },
186
+
187
+ setValue: (newValue: T, silent = false) => {
188
+ // Check equality before updating (only if we have a previous value)
189
+ if (
190
+ value !== undefined &&
191
+ eq(newValue, value) &&
192
+ !loading &&
193
+ error === undefined
194
+ ) {
195
+ return;
196
+ }
197
+
198
+ value = newValue;
199
+ loading = false;
200
+ error = undefined;
201
+ promise = Promise.resolve(newValue);
202
+ version++;
203
+
204
+ // Track last resolved value for fallback mode
205
+ lastResolvedValue = newValue;
206
+
207
+ if (!silent) notify();
208
+ },
209
+
210
+ setLoading: (newPromise: PromiseLike<T>, silent = false) => {
211
+ // In fallback mode, don't clear value - it will be returned via getValue()
212
+ // But internally we track that we're loading
213
+ value = undefined;
214
+ loading = true;
215
+ error = undefined;
216
+ promise = newPromise;
217
+ version++;
218
+ if (!silent) notify();
219
+ },
220
+
221
+ setError: (newError: any, silent = false) => {
222
+ // Check if error is the same
223
+ if (Object.is(error, newError) && !loading && value === undefined) {
224
+ return;
225
+ }
226
+
227
+ value = undefined;
228
+ loading = false;
229
+ error = newError;
230
+ version++;
231
+ if (!silent) notify();
232
+ },
233
+
234
+ reset: () => {
235
+ // Don't notify if already in initial state
236
+ if (isInitialState() && lastResolvedValue === undefined && !dirty) {
237
+ return;
238
+ }
239
+
240
+ value = undefined;
241
+ loading = false;
242
+ error = undefined;
243
+ promise = Promise.resolve(undefined as T);
244
+ version++;
245
+ // Clear last resolved value on reset
246
+ lastResolvedValue = undefined;
247
+ // Clear dirty flag on reset
248
+ dirty = false;
249
+ notify();
250
+ },
251
+
252
+ on: changeEmitter.on,
253
+ };
254
+
255
+ return api;
256
+ }