atomirx 0.0.8 → 0.1.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.
- package/README.md +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.js +191 -151
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -42
package/src/core/atom.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { onCreateHook } from "./onCreateHook";
|
|
2
|
-
import { emitter } from "./emitter";
|
|
3
|
-
import { resolveEquality } from "./equality";
|
|
4
|
-
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
5
|
-
import {
|
|
6
|
-
AtomOptions,
|
|
7
|
-
MutableAtom,
|
|
8
|
-
SYMBOL_ATOM,
|
|
9
|
-
Equality,
|
|
10
|
-
Pipeable,
|
|
11
|
-
Atom,
|
|
12
|
-
} from "./types";
|
|
13
|
-
import { withUse } from "./withUse";
|
|
14
|
-
import { isPromiseLike } from "./isPromiseLike";
|
|
15
|
-
import { trackPromise } from "./promiseCache";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Context object passed to atom initializer functions.
|
|
19
|
-
* Provides utilities for cleanup and cancellation.
|
|
20
|
-
*/
|
|
21
|
-
export interface AtomContext extends Pipeable {
|
|
22
|
-
/**
|
|
23
|
-
* AbortSignal that is aborted when the atom value changes (via set or reset).
|
|
24
|
-
* Use this to cancel pending async operations.
|
|
25
|
-
*/
|
|
26
|
-
signal: AbortSignal;
|
|
27
|
-
/**
|
|
28
|
-
* Register a cleanup function that runs when the atom value changes or resets.
|
|
29
|
-
* Multiple cleanup functions can be registered; they run in FIFO order.
|
|
30
|
-
*
|
|
31
|
-
* @param cleanup - Function to run during cleanup
|
|
32
|
-
*/
|
|
33
|
-
onCleanup(cleanup: VoidFunction): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Creates a mutable atom - a reactive state container that holds a single value.
|
|
38
|
-
*
|
|
39
|
-
* MutableAtom is a raw storage container. It stores values as-is, including Promises.
|
|
40
|
-
* If you store a Promise, `.get()` returns the Promise object itself.
|
|
41
|
-
*
|
|
42
|
-
* Features:
|
|
43
|
-
* - Raw storage: stores any value including Promises
|
|
44
|
-
* - Lazy initialization: pass a function to defer computation
|
|
45
|
-
* - Equality checking: configurable equality for reducer-based updates
|
|
46
|
-
* - Plugin system: chainable `.use()` method for extensions
|
|
47
|
-
* - Subscriptions: `.on()` for change notifications
|
|
48
|
-
*
|
|
49
|
-
* @template T - The type of value stored in the atom
|
|
50
|
-
* @param valueOrInit - Initial value or lazy initializer function `() => T`
|
|
51
|
-
* @param options - Configuration options
|
|
52
|
-
* @param options.meta - Optional metadata for debugging/devtools
|
|
53
|
-
* @param options.equals - Equality strategy for change detection (default: strict)
|
|
54
|
-
* @returns A mutable atom with get, set/reset methods
|
|
55
|
-
*
|
|
56
|
-
* @example Synchronous value
|
|
57
|
-
* ```ts
|
|
58
|
-
* const count = atom(0);
|
|
59
|
-
* count.set(1);
|
|
60
|
-
* count.set(prev => prev + 1);
|
|
61
|
-
* console.log(count.get()); // 2
|
|
62
|
-
* ```
|
|
63
|
-
*
|
|
64
|
-
* @example Lazy initialization
|
|
65
|
-
* ```ts
|
|
66
|
-
* // Initial value computed at creation
|
|
67
|
-
* const config = atom(() => parseExpensiveConfig());
|
|
68
|
-
*
|
|
69
|
-
* // reset() re-runs the initializer for fresh values
|
|
70
|
-
* const timestamp = atom(() => Date.now());
|
|
71
|
-
* timestamp.reset(); // Gets new timestamp
|
|
72
|
-
*
|
|
73
|
-
* // To store a function as value, wrap it:
|
|
74
|
-
* const callback = atom(() => () => console.log('hello'));
|
|
75
|
-
* ```
|
|
76
|
-
*
|
|
77
|
-
* @example Async value (stores Promise as-is)
|
|
78
|
-
* ```ts
|
|
79
|
-
* const posts = atom(fetchPosts());
|
|
80
|
-
* posts.get(); // Promise<Post[]>
|
|
81
|
-
*
|
|
82
|
-
* // Refetch - set a new Promise
|
|
83
|
-
* posts.set(fetchPosts());
|
|
84
|
-
*
|
|
85
|
-
* // Reset with direct value - restores original Promise (does NOT refetch)
|
|
86
|
-
* // Reset with lazy init - re-runs initializer (DOES refetch)
|
|
87
|
-
* const lazyPosts = atom(() => fetchPosts());
|
|
88
|
-
* lazyPosts.reset(); // Refetches!
|
|
89
|
-
* ```
|
|
90
|
-
*
|
|
91
|
-
* @example With equals option
|
|
92
|
-
* ```ts
|
|
93
|
-
* const state = atom({ count: 0 }, { equals: "shallow" });
|
|
94
|
-
* state.set(prev => ({ ...prev })); // No notification (shallow equal)
|
|
95
|
-
* ```
|
|
96
|
-
*/
|
|
97
|
-
export function atom<T>(
|
|
98
|
-
valueOrInit: T | ((context: AtomContext) => T),
|
|
99
|
-
options: AtomOptions<T> = {}
|
|
100
|
-
): MutableAtom<T> {
|
|
101
|
-
const changeEmitter = emitter();
|
|
102
|
-
const eq = resolveEquality(options.equals as Equality<unknown>);
|
|
103
|
-
|
|
104
|
-
// Track current AbortController and cleanup emitter for init context
|
|
105
|
-
let abortController: AbortController | null = null;
|
|
106
|
-
const cleanupEmitter = emitter();
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Aborts the current signal and calls all registered cleanup functions.
|
|
110
|
-
*/
|
|
111
|
-
const abortAndCleanup = (reason: string) => {
|
|
112
|
-
// Abort the signal first
|
|
113
|
-
if (abortController) {
|
|
114
|
-
abortController.abort(reason);
|
|
115
|
-
abortController = null;
|
|
116
|
-
}
|
|
117
|
-
// Then call all registered cleanups
|
|
118
|
-
cleanupEmitter.emitAndClear();
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Creates a fresh AtomContext for initializer functions.
|
|
123
|
-
*/
|
|
124
|
-
const createContext = (): AtomContext => {
|
|
125
|
-
abortController = new AbortController();
|
|
126
|
-
return withUse({
|
|
127
|
-
signal: abortController.signal,
|
|
128
|
-
onCleanup: cleanupEmitter.on,
|
|
129
|
-
});
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Resolve initial value (supports lazy initialization with context)
|
|
133
|
-
const isInitFunction = typeof valueOrInit === "function";
|
|
134
|
-
const initialValue: T = isInitFunction
|
|
135
|
-
? (valueOrInit as (context: AtomContext) => T)(createContext())
|
|
136
|
-
: valueOrInit;
|
|
137
|
-
|
|
138
|
-
// Current value
|
|
139
|
-
let value: T = initialValue;
|
|
140
|
-
|
|
141
|
-
// Track if value has changed since init/reset
|
|
142
|
-
let isDirty = false;
|
|
143
|
-
|
|
144
|
-
isPromiseLike(value) && trackPromise(value);
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Schedules notification to all subscribers.
|
|
148
|
-
*/
|
|
149
|
-
const notify = () => {
|
|
150
|
-
changeEmitter.forEach((listener) => {
|
|
151
|
-
scheduleNotifyHook.current(listener);
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Updates the atom's value.
|
|
157
|
-
*
|
|
158
|
-
* @param newValue - New value or reducer function (prev) => newValue
|
|
159
|
-
*/
|
|
160
|
-
const set = (newValue: T | ((prev: T) => T)) => {
|
|
161
|
-
let nextValue: T;
|
|
162
|
-
|
|
163
|
-
if (typeof newValue === "function") {
|
|
164
|
-
// Reducer function
|
|
165
|
-
nextValue = (newValue as (prev: T) => T)(value);
|
|
166
|
-
} else {
|
|
167
|
-
nextValue = newValue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Check equality
|
|
171
|
-
if (eq(nextValue, value)) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Abort previous signal and run cleanups before changing value
|
|
176
|
-
abortAndCleanup("value changed");
|
|
177
|
-
|
|
178
|
-
value = nextValue;
|
|
179
|
-
isDirty = true;
|
|
180
|
-
isPromiseLike(value) && trackPromise(value);
|
|
181
|
-
notify();
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Resets the atom to its initial value and clears dirty flag.
|
|
186
|
-
*/
|
|
187
|
-
const reset = () => {
|
|
188
|
-
// Abort previous signal and run cleanups before resetting
|
|
189
|
-
abortAndCleanup("reset");
|
|
190
|
-
|
|
191
|
-
// Re-run initializer if function (with fresh context), otherwise use initial value
|
|
192
|
-
const nextValue: T = isInitFunction
|
|
193
|
-
? (valueOrInit as (context: AtomContext) => T)(createContext())
|
|
194
|
-
: valueOrInit;
|
|
195
|
-
|
|
196
|
-
// Track promise if needed
|
|
197
|
-
isPromiseLike(nextValue) && trackPromise(nextValue);
|
|
198
|
-
|
|
199
|
-
// Check if value actually changed
|
|
200
|
-
const changed = !eq(nextValue, value);
|
|
201
|
-
|
|
202
|
-
value = nextValue;
|
|
203
|
-
isDirty = false; // Always clear dirty flag on reset
|
|
204
|
-
|
|
205
|
-
if (changed) {
|
|
206
|
-
notify();
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Returns true if the value has changed since initialization or last reset().
|
|
212
|
-
*/
|
|
213
|
-
const dirty = (): boolean => {
|
|
214
|
-
return isDirty;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Create the atom object
|
|
218
|
-
const a = withUse({
|
|
219
|
-
[SYMBOL_ATOM]: true as const,
|
|
220
|
-
meta: options.meta,
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Get the current value (raw, including Promises).
|
|
224
|
-
*/
|
|
225
|
-
get(): any {
|
|
226
|
-
return value;
|
|
227
|
-
},
|
|
228
|
-
use: undefined as any,
|
|
229
|
-
set,
|
|
230
|
-
reset,
|
|
231
|
-
dirty,
|
|
232
|
-
/**
|
|
233
|
-
* Subscribe to value changes.
|
|
234
|
-
*/
|
|
235
|
-
on: changeEmitter.on,
|
|
236
|
-
}) as Pipeable & MutableAtom<T>;
|
|
237
|
-
|
|
238
|
-
// Notify devtools/plugins of atom creation
|
|
239
|
-
onCreateHook.current?.({
|
|
240
|
-
type: "mutable",
|
|
241
|
-
key: options.meta?.key,
|
|
242
|
-
meta: options.meta,
|
|
243
|
-
instance: a as MutableAtom<unknown>,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
return a;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Type utility to expose an atom as read-only when exporting from a module.
|
|
251
|
-
*
|
|
252
|
-
* This function returns the same atom instance but with a narrowed type (`Atom<T>`)
|
|
253
|
-
* that hides mutable methods like `set()` and `reset()`. Use this to encapsulate
|
|
254
|
-
* state mutations within a module while allowing external consumers to only read
|
|
255
|
-
* and subscribe to changes.
|
|
256
|
-
*
|
|
257
|
-
* **Note:** This is a compile-time restriction only. At runtime, the atom is unchanged.
|
|
258
|
-
* Consumers with access to the original reference can still mutate it.
|
|
259
|
-
*
|
|
260
|
-
* @param atom - The atom (or record of atoms) to expose as read-only
|
|
261
|
-
* @returns The same atom(s) with a read-only type signature
|
|
262
|
-
*
|
|
263
|
-
* @example Single atom
|
|
264
|
-
* ```ts
|
|
265
|
-
* const myModule = define(() => {
|
|
266
|
-
* const count$ = atom(0); // Internal mutable atom
|
|
267
|
-
*
|
|
268
|
-
* return {
|
|
269
|
-
* // Expose as read-only - consumers can't call set() or reset()
|
|
270
|
-
* count$: readonly(count$),
|
|
271
|
-
* // Mutations only possible through explicit actions
|
|
272
|
-
* increment: () => count$.set(prev => prev + 1),
|
|
273
|
-
* decrement: () => count$.set(prev => prev - 1),
|
|
274
|
-
* };
|
|
275
|
-
* });
|
|
276
|
-
*
|
|
277
|
-
* // Usage:
|
|
278
|
-
* const { count$, increment } = myModule();
|
|
279
|
-
* count$.get(); // ✅ OK - reading is allowed
|
|
280
|
-
* count$.on(console.log); // ✅ OK - subscribing is allowed
|
|
281
|
-
* count$.set(5); // ❌ TypeScript error - set() not available on Atom<T>
|
|
282
|
-
* increment(); // ✅ OK - use exposed action instead
|
|
283
|
-
* ```
|
|
284
|
-
*
|
|
285
|
-
* @example Record of atoms
|
|
286
|
-
* ```ts
|
|
287
|
-
* const myModule = define(() => {
|
|
288
|
-
* const count$ = atom(0);
|
|
289
|
-
* const name$ = atom('');
|
|
290
|
-
*
|
|
291
|
-
* return {
|
|
292
|
-
* // Expose multiple atoms as read-only at once
|
|
293
|
-
* ...readonly({ count$, name$ }),
|
|
294
|
-
* setName: (name: string) => name$.set(name),
|
|
295
|
-
* };
|
|
296
|
-
* });
|
|
297
|
-
*
|
|
298
|
-
* // Usage:
|
|
299
|
-
* const { count$, name$, setName } = myModule();
|
|
300
|
-
* count$.get(); // ✅ Atom<number>
|
|
301
|
-
* name$.get(); // ✅ Atom<string>
|
|
302
|
-
* name$.set(''); // ❌ TypeScript error
|
|
303
|
-
* ```
|
|
304
|
-
*/
|
|
305
|
-
export function readonly<T extends Atom<any> | Record<string, Atom<any>>>(
|
|
306
|
-
atom: T
|
|
307
|
-
): T extends Atom<infer V>
|
|
308
|
-
? Atom<V>
|
|
309
|
-
: { [K in keyof T]: T[K] extends Atom<infer V> ? Atom<V> : never } {
|
|
310
|
-
return atom as any;
|
|
311
|
-
}
|
|
@@ -1,342 +0,0 @@
|
|
|
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
|
-
});
|