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.
- package/README.md +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic function type that accepts any arguments and returns any value.
|
|
3
|
+
* Used internally for type-safe function handling.
|
|
4
|
+
*/
|
|
5
|
+
export type AnyFunc = (...args: any[]) => any;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unique symbol used to identify atom instances.
|
|
9
|
+
* Uses Symbol.for() to ensure the same symbol across different module instances.
|
|
10
|
+
*/
|
|
11
|
+
export const SYMBOL_ATOM = Symbol.for("atomirx.atom");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Symbol to identify derived atoms.
|
|
15
|
+
*/
|
|
16
|
+
export const SYMBOL_DERIVED = Symbol.for("atomirx.derived");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for objects that support the `.use()` plugin pattern.
|
|
20
|
+
*
|
|
21
|
+
* The `.use()` method enables chainable transformations via plugins.
|
|
22
|
+
* Return type behavior:
|
|
23
|
+
* - `void` → returns original source (side-effect only)
|
|
24
|
+
* - Object with `.use` → returns as-is (already pipeable)
|
|
25
|
+
* - Object without `.use` → wraps with Pipeable
|
|
26
|
+
* - Primitive → returns directly (not chainable)
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const enhanced = atom(0)
|
|
31
|
+
* .use(source => ({ ...source, double: () => source.value * 2 }))
|
|
32
|
+
* .use(source => ({ ...source, triple: () => source.value * 3 }));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export interface Pipeable {
|
|
36
|
+
use<TNew = void>(
|
|
37
|
+
plugin: (source: this) => TNew
|
|
38
|
+
): void extends TNew
|
|
39
|
+
? this
|
|
40
|
+
: TNew extends object
|
|
41
|
+
? TNew extends { use: any }
|
|
42
|
+
? TNew
|
|
43
|
+
: Pipeable & TNew
|
|
44
|
+
: TNew;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional metadata for atoms.
|
|
49
|
+
*/
|
|
50
|
+
export interface AtomMeta {
|
|
51
|
+
key?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Base interface for all atoms.
|
|
57
|
+
* Represents a reactive value container with subscription capability.
|
|
58
|
+
*
|
|
59
|
+
* @template T - The type of value stored in the atom
|
|
60
|
+
*/
|
|
61
|
+
export interface Atom<T> {
|
|
62
|
+
/** Symbol marker to identify atom instances */
|
|
63
|
+
readonly [SYMBOL_ATOM]: true;
|
|
64
|
+
/** The current value */
|
|
65
|
+
readonly value: T;
|
|
66
|
+
/** Optional metadata for the atom */
|
|
67
|
+
readonly meta?: AtomMeta;
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to value changes.
|
|
70
|
+
* @param listener - Callback invoked when value changes
|
|
71
|
+
* @returns Unsubscribe function
|
|
72
|
+
*/
|
|
73
|
+
on(listener: VoidFunction): VoidFunction;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A mutable atom that can be updated via `set()` and reset to initial state.
|
|
78
|
+
*
|
|
79
|
+
* MutableAtom is a raw storage container. It stores values as-is, including Promises.
|
|
80
|
+
* Unlike DerivedAtom, it does not automatically unwrap or track Promise states.
|
|
81
|
+
*
|
|
82
|
+
* @template T - The type of value stored in the atom
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // Sync value
|
|
87
|
+
* const count = atom(0);
|
|
88
|
+
* count.set(5); // Direct value
|
|
89
|
+
* count.set(n => n + 1); // Reducer function
|
|
90
|
+
* count.reset(); // Back to 0
|
|
91
|
+
*
|
|
92
|
+
* // Async value (stores Promise as-is)
|
|
93
|
+
* const posts = atom(fetchPosts());
|
|
94
|
+
* posts.value; // Promise<Post[]>
|
|
95
|
+
* posts.set(fetchPosts()); // Store new Promise
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export interface MutableAtom<T> extends Atom<T>, Pipeable {
|
|
99
|
+
/** Reset atom to its initial state (also clears dirty flag) */
|
|
100
|
+
reset(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Update the atom's value.
|
|
103
|
+
*
|
|
104
|
+
* @param value - New value or reducer function (prev) => newValue
|
|
105
|
+
*/
|
|
106
|
+
set(value: T | ((prev: T) => T)): void;
|
|
107
|
+
/**
|
|
108
|
+
* Returns `true` if the value has changed since initialization or last `reset()`.
|
|
109
|
+
*
|
|
110
|
+
* Useful for:
|
|
111
|
+
* - Tracking unsaved changes
|
|
112
|
+
* - Enabling/disabling save buttons
|
|
113
|
+
* - Detecting form modifications
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const form$ = atom({ name: "", email: "" });
|
|
118
|
+
*
|
|
119
|
+
* form$.dirty(); // false - just initialized
|
|
120
|
+
*
|
|
121
|
+
* form$.set({ name: "John", email: "" });
|
|
122
|
+
* form$.dirty(); // true - value changed
|
|
123
|
+
*
|
|
124
|
+
* form$.reset();
|
|
125
|
+
* form$.dirty(); // false - reset clears dirty flag
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
dirty(): boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A derived (computed) atom that always returns Promise<T> for its value.
|
|
133
|
+
*
|
|
134
|
+
* DerivedAtom computes its value from other atoms. The computation is
|
|
135
|
+
* re-run whenever dependencies change. The `.value` always returns a Promise,
|
|
136
|
+
* even for synchronous computations.
|
|
137
|
+
*
|
|
138
|
+
* @template T - The resolved type of the computed value
|
|
139
|
+
* @template F - Whether fallback is provided (affects staleValue type)
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* // Without fallback
|
|
144
|
+
* const double$ = derived(({ get }) => get(count$) * 2);
|
|
145
|
+
* await double$.value; // number
|
|
146
|
+
* double$.staleValue; // number | undefined
|
|
147
|
+
* double$.state(); // { status: "ready", value: 10 }
|
|
148
|
+
*
|
|
149
|
+
* // With fallback - during loading
|
|
150
|
+
* const double$ = derived(({ get }) => get(count$) * 2, { fallback: 0 });
|
|
151
|
+
* double$.staleValue; // number (guaranteed)
|
|
152
|
+
* double$.state(); // { status: "loading", promise } during loading
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export interface DerivedAtom<T, F extends boolean = false> extends Atom<
|
|
156
|
+
Promise<T>
|
|
157
|
+
> {
|
|
158
|
+
/** Symbol marker to identify derived atom instances */
|
|
159
|
+
readonly [SYMBOL_DERIVED]: true;
|
|
160
|
+
/** Re-run the computation */
|
|
161
|
+
refresh(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Get the current state of the derived atom.
|
|
164
|
+
* Returns a discriminated union with status, value/error, and stale flag.
|
|
165
|
+
*/
|
|
166
|
+
state(): AtomState<T>;
|
|
167
|
+
/**
|
|
168
|
+
* The stale value - fallback or last resolved value.
|
|
169
|
+
* - Without fallback: T | undefined
|
|
170
|
+
* - With fallback: T (guaranteed)
|
|
171
|
+
*/
|
|
172
|
+
readonly staleValue: F extends true ? T : T | undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Union type for any atom (mutable or derived).
|
|
177
|
+
*/
|
|
178
|
+
export type AnyAtom<T> = MutableAtom<T> | DerivedAtom<T, boolean>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract the value type from an atom.
|
|
182
|
+
* For DerivedAtom, returns the awaited type.
|
|
183
|
+
*/
|
|
184
|
+
export type AtomValue<A> =
|
|
185
|
+
A extends DerivedAtom<infer V, boolean>
|
|
186
|
+
? V
|
|
187
|
+
: A extends Atom<infer V>
|
|
188
|
+
? Awaited<V>
|
|
189
|
+
: never;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Represents the state of an atom as a discriminated union.
|
|
193
|
+
*
|
|
194
|
+
* Uses intuitive state terms:
|
|
195
|
+
* - `ready` - Value is available
|
|
196
|
+
* - `error` - Computation failed
|
|
197
|
+
* - `loading` - Waiting for async value
|
|
198
|
+
*
|
|
199
|
+
* @template T - The type of the atom's value
|
|
200
|
+
*/
|
|
201
|
+
export type AtomState<T> =
|
|
202
|
+
| { status: "ready"; value: T }
|
|
203
|
+
| { status: "error"; error: unknown }
|
|
204
|
+
| { status: "loading"; promise: Promise<T> };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Result type for settled operations.
|
|
208
|
+
*/
|
|
209
|
+
export type SettledResult<T> =
|
|
210
|
+
| { status: "ready"; value: T }
|
|
211
|
+
| { status: "error"; error: unknown };
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Configuration options for creating a mutable atom.
|
|
215
|
+
*
|
|
216
|
+
* @template T - The type of value stored in the atom
|
|
217
|
+
*/
|
|
218
|
+
export interface AtomOptions<T> {
|
|
219
|
+
/** Optional metadata for the atom */
|
|
220
|
+
meta?: MutableAtomMeta;
|
|
221
|
+
/** Equality strategy for change detection (default: "strict") */
|
|
222
|
+
equals?: Equality<T>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface MutableAtomMeta extends AtomMeta {}
|
|
226
|
+
|
|
227
|
+
export interface DerivedAtomMeta extends AtomMeta {}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Configuration options for creating a derived atom.
|
|
231
|
+
*
|
|
232
|
+
* @template T - The type of the derived value
|
|
233
|
+
*/
|
|
234
|
+
export interface DerivedOptions<T> {
|
|
235
|
+
/** Optional metadata for the atom */
|
|
236
|
+
meta?: DerivedAtomMeta;
|
|
237
|
+
/** Equality strategy for change detection (default: "strict") */
|
|
238
|
+
equals?: Equality<T>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Configuration options for effects.
|
|
243
|
+
*/
|
|
244
|
+
export interface EffectOptions {
|
|
245
|
+
/** Optional key for debugging */
|
|
246
|
+
key?: string;
|
|
247
|
+
/** Error handler for uncaught errors in the effect */
|
|
248
|
+
onError?: (error: Error) => void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A function that returns a value when called.
|
|
253
|
+
* Used for lazy evaluation in derived atoms.
|
|
254
|
+
*
|
|
255
|
+
* @template T - The type of value returned
|
|
256
|
+
*/
|
|
257
|
+
export type Getter<T> = () => T;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Built-in equality strategy names.
|
|
261
|
+
*
|
|
262
|
+
* Used with atoms to control when subscribers are notified:
|
|
263
|
+
* - `"strict"` - Object.is (default, fastest)
|
|
264
|
+
* - `"shallow"` - Compare object keys/array items with Object.is
|
|
265
|
+
* - `"shallow2"` - 2 levels deep
|
|
266
|
+
* - `"shallow3"` - 3 levels deep
|
|
267
|
+
* - `"deep"` - Full recursive comparison (slowest)
|
|
268
|
+
*/
|
|
269
|
+
export type EqualityShorthand =
|
|
270
|
+
| "strict"
|
|
271
|
+
| "shallow"
|
|
272
|
+
| "shallow2"
|
|
273
|
+
| "shallow3"
|
|
274
|
+
| "deep";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Equality strategy for change detection.
|
|
278
|
+
*
|
|
279
|
+
* Can be a shorthand string or custom comparison function.
|
|
280
|
+
* Used by atoms to determine if value has "changed" -
|
|
281
|
+
* if equal, subscribers won't be notified.
|
|
282
|
+
*
|
|
283
|
+
* @template T - Type of values being compared
|
|
284
|
+
*/
|
|
285
|
+
export type Equality<T = unknown> =
|
|
286
|
+
| EqualityShorthand
|
|
287
|
+
| ((a: T, b: T) => boolean);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Prettify a type by adding all properties to the type.
|
|
291
|
+
* @template T - The type to prettify
|
|
292
|
+
*/
|
|
293
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
294
|
+
|
|
295
|
+
export interface ModuleMeta {}
|
|
296
|
+
|
|
297
|
+
export type Listener<T> = (value: T) => void;
|
|
298
|
+
|
|
299
|
+
export type SingleOrMultipleListeners<T> = Listener<T> | Listener<T>[];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Type guard to check if a value is an Atom.
|
|
303
|
+
*/
|
|
304
|
+
export declare function isAtom<T>(value: unknown): value is Atom<T>;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Type guard to check if a value is a DerivedAtom.
|
|
308
|
+
*/
|
|
309
|
+
export declare function isDerived<T>(
|
|
310
|
+
value: unknown
|
|
311
|
+
): value is DerivedAtom<T, boolean>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { withUse } from "./withUse";
|
|
3
|
+
|
|
4
|
+
describe("withUse", () => {
|
|
5
|
+
describe("basic functionality", () => {
|
|
6
|
+
it("should add use method to object", () => {
|
|
7
|
+
const obj = { value: 1 };
|
|
8
|
+
const result = withUse(obj);
|
|
9
|
+
|
|
10
|
+
expect(result.value).toBe(1);
|
|
11
|
+
expect(typeof result.use).toBe("function");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should preserve original object properties", () => {
|
|
15
|
+
const obj = { a: 1, b: "hello", c: true };
|
|
16
|
+
const result = withUse(obj);
|
|
17
|
+
|
|
18
|
+
expect(result.a).toBe(1);
|
|
19
|
+
expect(result.b).toBe("hello");
|
|
20
|
+
expect(result.c).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should work with arrays", () => {
|
|
24
|
+
const arr = [1, 2, 3];
|
|
25
|
+
const result = withUse(arr);
|
|
26
|
+
|
|
27
|
+
expect(result[0]).toBe(1);
|
|
28
|
+
expect(result.length).toBe(3);
|
|
29
|
+
expect(typeof result.use).toBe("function");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("use() transformations", () => {
|
|
34
|
+
it("should transform object with plugin", () => {
|
|
35
|
+
const obj = withUse({ value: 10 });
|
|
36
|
+
|
|
37
|
+
const transformed = obj.use((source) => ({
|
|
38
|
+
...source,
|
|
39
|
+
doubled: source.value * 2,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
expect(transformed.value).toBe(10);
|
|
43
|
+
expect(transformed.doubled).toBe(20);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return source when plugin returns void", () => {
|
|
47
|
+
const obj = withUse({ value: 1 });
|
|
48
|
+
const sideEffect = vi.fn();
|
|
49
|
+
|
|
50
|
+
const result = obj.use((source) => {
|
|
51
|
+
sideEffect(source.value);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(sideEffect).toHaveBeenCalledWith(1);
|
|
55
|
+
expect(result).toBe(obj);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return source when plugin returns undefined", () => {
|
|
59
|
+
const obj = withUse({ value: 1 });
|
|
60
|
+
|
|
61
|
+
const result = obj.use(() => undefined);
|
|
62
|
+
|
|
63
|
+
expect(result).toBe(obj);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return source when plugin returns null", () => {
|
|
67
|
+
const obj = withUse({ value: 1 });
|
|
68
|
+
|
|
69
|
+
const result = obj.use(() => null as any);
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(obj);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return source when plugin returns false", () => {
|
|
75
|
+
const obj = withUse({ value: 1 });
|
|
76
|
+
|
|
77
|
+
const result = obj.use(() => false as any);
|
|
78
|
+
|
|
79
|
+
expect(result).toBe(obj);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return source when plugin returns empty string", () => {
|
|
83
|
+
const obj = withUse({ value: 1 });
|
|
84
|
+
|
|
85
|
+
const result = obj.use(() => "" as any);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(obj);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return source when plugin returns 0", () => {
|
|
91
|
+
const obj = withUse({ value: 1 });
|
|
92
|
+
|
|
93
|
+
const result = obj.use(() => 0 as any);
|
|
94
|
+
|
|
95
|
+
expect(result).toBe(obj);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("chaining", () => {
|
|
100
|
+
it("should allow chaining by wrapping result with use method", () => {
|
|
101
|
+
const obj = withUse({ value: 1 });
|
|
102
|
+
|
|
103
|
+
// First transformation - result gets wrapped with use()
|
|
104
|
+
const withA = obj.use((source) => ({ ...source, a: "first" }));
|
|
105
|
+
expect(withA.value).toBe(1);
|
|
106
|
+
expect(withA.a).toBe("first");
|
|
107
|
+
expect(typeof withA.use).toBe("function");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should pass the transformed object to the next use() in chain", () => {
|
|
111
|
+
const obj = withUse({ value: 1 });
|
|
112
|
+
|
|
113
|
+
// When chaining, each use() receives the result of the previous transformation
|
|
114
|
+
const result = obj
|
|
115
|
+
.use((source) => ({ original: source.value, a: "first" }))
|
|
116
|
+
.use((source) => ({ ...source, b: "second" }));
|
|
117
|
+
|
|
118
|
+
expect(result.original).toBe(1);
|
|
119
|
+
expect(result.a).toBe("first");
|
|
120
|
+
expect(result.b).toBe("second");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should wrap result with withUse if it does not have use method", () => {
|
|
124
|
+
const obj = withUse({ value: 1 });
|
|
125
|
+
|
|
126
|
+
const result = obj.use(() => ({ newProp: "test" }));
|
|
127
|
+
|
|
128
|
+
expect(result.newProp).toBe("test");
|
|
129
|
+
expect(typeof result.use).toBe("function");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should return as-is if result already has use method", () => {
|
|
133
|
+
const obj = withUse({ value: 1 });
|
|
134
|
+
const existingWithUse = withUse({ other: 2 });
|
|
135
|
+
|
|
136
|
+
const result = obj.use(() => existingWithUse);
|
|
137
|
+
|
|
138
|
+
expect(result).toBe(existingWithUse);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should allow fluent chaining with independent transformations", () => {
|
|
142
|
+
const obj = withUse({ value: 1 });
|
|
143
|
+
|
|
144
|
+
// Each use() receives the result of the previous use()
|
|
145
|
+
const result = obj
|
|
146
|
+
.use((source) => ({ value: source.value, doubled: source.value * 2 }))
|
|
147
|
+
.use((source) => ({ ...source, tripled: source.value * 3 }));
|
|
148
|
+
|
|
149
|
+
expect(result.value).toBe(1);
|
|
150
|
+
expect(result.doubled).toBe(2);
|
|
151
|
+
expect(result.tripled).toBe(3);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("primitive return values", () => {
|
|
156
|
+
it("should return primitive number directly", () => {
|
|
157
|
+
const obj = withUse({ value: 1 });
|
|
158
|
+
|
|
159
|
+
const result = obj.use(() => 42);
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(42);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should return primitive string directly", () => {
|
|
165
|
+
const obj = withUse({ value: 1 });
|
|
166
|
+
|
|
167
|
+
const result = obj.use(() => "hello");
|
|
168
|
+
|
|
169
|
+
expect(result).toBe("hello");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should return primitive boolean directly", () => {
|
|
173
|
+
const obj = withUse({ value: 1 });
|
|
174
|
+
|
|
175
|
+
const result = obj.use(() => true);
|
|
176
|
+
|
|
177
|
+
expect(result).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("function return values", () => {
|
|
182
|
+
it("should wrap function result with withUse if no use method", () => {
|
|
183
|
+
const obj = withUse({ value: 1 });
|
|
184
|
+
const fn = () => 42;
|
|
185
|
+
|
|
186
|
+
const result = obj.use(() => fn);
|
|
187
|
+
|
|
188
|
+
expect(result()).toBe(42);
|
|
189
|
+
expect(typeof result.use).toBe("function");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should return function as-is if it has use method", () => {
|
|
193
|
+
const obj = withUse({ value: 1 });
|
|
194
|
+
const fnWithUse = Object.assign(() => 42, { use: () => {} });
|
|
195
|
+
|
|
196
|
+
const result = obj.use(() => fnWithUse);
|
|
197
|
+
|
|
198
|
+
expect(result).toBe(fnWithUse);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("real-world patterns", () => {
|
|
203
|
+
it("should support adding methods to an object", () => {
|
|
204
|
+
const counter = withUse({ count: 0 });
|
|
205
|
+
|
|
206
|
+
const enhanced = counter.use((source) => ({
|
|
207
|
+
...source,
|
|
208
|
+
increment: () => {
|
|
209
|
+
source.count++;
|
|
210
|
+
},
|
|
211
|
+
decrement: () => {
|
|
212
|
+
source.count--;
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
enhanced.increment();
|
|
217
|
+
expect(counter.count).toBe(1);
|
|
218
|
+
|
|
219
|
+
enhanced.decrement();
|
|
220
|
+
expect(counter.count).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should support middleware-like pattern", () => {
|
|
224
|
+
const logger: string[] = [];
|
|
225
|
+
|
|
226
|
+
const api = withUse({
|
|
227
|
+
fetch: (url: string) => `data from ${url}`,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const withLogging = api.use((source) => ({
|
|
231
|
+
...source,
|
|
232
|
+
fetch: (url: string) => {
|
|
233
|
+
logger.push(`fetching: ${url}`);
|
|
234
|
+
const result = source.fetch(url);
|
|
235
|
+
logger.push(`fetched: ${result}`);
|
|
236
|
+
return result;
|
|
237
|
+
},
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const result = withLogging.fetch("/api/users");
|
|
241
|
+
|
|
242
|
+
expect(result).toBe("data from /api/users");
|
|
243
|
+
expect(logger).toEqual([
|
|
244
|
+
"fetching: /api/users",
|
|
245
|
+
"fetched: data from /api/users",
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Pipeable } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds a chainable `.use()` method to any object, enabling plugin-based transformations.
|
|
5
|
+
*
|
|
6
|
+
* The `.use()` method accepts a plugin function that receives the source object
|
|
7
|
+
* and can return a transformed version. Supports several return patterns:
|
|
8
|
+
*
|
|
9
|
+
* - **Void/falsy**: Returns the original source unchanged (side-effect only plugins)
|
|
10
|
+
* - **Object/function with `.use`**: Returns as-is (already chainable)
|
|
11
|
+
* - **Object/function without `.use`**: Wraps with `withUse()` for continued chaining
|
|
12
|
+
* - **Primitive**: Returns the value directly
|
|
13
|
+
*
|
|
14
|
+
* @template TSource - The type of the source object being enhanced
|
|
15
|
+
* @param source - The object to add `.use()` method to
|
|
16
|
+
* @returns The source object with `.use()` method attached
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Basic usage with atom tuple
|
|
20
|
+
* const mappable = withUse([signal, setter]);
|
|
21
|
+
* const transformed = mappable.use(([sig, set]) => ({
|
|
22
|
+
* sig,
|
|
23
|
+
* set: (v: string) => set(Number(v))
|
|
24
|
+
* }));
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Chaining multiple transformations
|
|
28
|
+
* atom(0)
|
|
29
|
+
* .use(([sig, set]) => [sig, (v: number) => set(v * 2)])
|
|
30
|
+
* .use(([sig, set]) => [sig, (v: number) => set(v + 1)]);
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Side-effect only plugin (returns void)
|
|
34
|
+
* mappable.use((source) => {
|
|
35
|
+
* console.log('Source:', source);
|
|
36
|
+
* // returns undefined - original source is returned
|
|
37
|
+
* });
|
|
38
|
+
*/
|
|
39
|
+
export function withUse<TSource extends object>(source: TSource) {
|
|
40
|
+
return Object.assign(source, {
|
|
41
|
+
use<TNew = void>(plugin: (source: TSource) => TNew): any {
|
|
42
|
+
const result = plugin(source);
|
|
43
|
+
// Void/falsy: return original source (side-effect only plugins)
|
|
44
|
+
if (!result) return source;
|
|
45
|
+
// Object or function: check if already has .use(), otherwise wrap
|
|
46
|
+
if (typeof result === "object" || typeof result === "function") {
|
|
47
|
+
if ("use" in result) {
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
return withUse(result);
|
|
51
|
+
}
|
|
52
|
+
// Primitive values: return directly (not chainable)
|
|
53
|
+
return result;
|
|
54
|
+
},
|
|
55
|
+
}) as TSource & Pipeable;
|
|
56
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
atom,
|
|
4
|
+
batch,
|
|
5
|
+
define,
|
|
6
|
+
derived,
|
|
7
|
+
effect,
|
|
8
|
+
emitter,
|
|
9
|
+
isAtom,
|
|
10
|
+
isDerived,
|
|
11
|
+
select,
|
|
12
|
+
getAtomState,
|
|
13
|
+
isPending,
|
|
14
|
+
} from "./index";
|
|
15
|
+
|
|
16
|
+
describe("atomirx exports", () => {
|
|
17
|
+
it("should export atom", () => {
|
|
18
|
+
expect(typeof atom).toBe("function");
|
|
19
|
+
const count = atom(0);
|
|
20
|
+
expect(count.value).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should export batch", () => {
|
|
24
|
+
expect(typeof batch).toBe("function");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should export define", () => {
|
|
28
|
+
expect(typeof define).toBe("function");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should export derived", async () => {
|
|
32
|
+
expect(typeof derived).toBe("function");
|
|
33
|
+
const count = atom(5);
|
|
34
|
+
const doubled = derived(({ get }) => get(count) * 2);
|
|
35
|
+
expect(await doubled.value).toBe(10);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should export effect", () => {
|
|
39
|
+
expect(typeof effect).toBe("function");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should export emitter", () => {
|
|
43
|
+
expect(typeof emitter).toBe("function");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should export isAtom", () => {
|
|
47
|
+
expect(typeof isAtom).toBe("function");
|
|
48
|
+
const count = atom(0);
|
|
49
|
+
expect(isAtom(count)).toBe(true);
|
|
50
|
+
expect(isAtom({})).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should export isDerived", () => {
|
|
54
|
+
expect(typeof isDerived).toBe("function");
|
|
55
|
+
const count = atom(0);
|
|
56
|
+
const doubled = derived(({ get }) => get(count) * 2);
|
|
57
|
+
expect(isDerived(count)).toBe(false);
|
|
58
|
+
expect(isDerived(doubled)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should export select", () => {
|
|
62
|
+
expect(typeof select).toBe("function");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should export getAtomState", () => {
|
|
66
|
+
expect(typeof getAtomState).toBe("function");
|
|
67
|
+
const count = atom(42);
|
|
68
|
+
const state = getAtomState(count);
|
|
69
|
+
expect(state.status).toBe("ready");
|
|
70
|
+
if (state.status === "ready") {
|
|
71
|
+
expect(state.value).toBe(42);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should export isPending", () => {
|
|
76
|
+
expect(typeof isPending).toBe("function");
|
|
77
|
+
expect(isPending(42)).toBe(false);
|
|
78
|
+
expect(isPending(new Promise(() => {}))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|