atomirx 0.0.7 → 0.1.0

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 (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -30
  33. package/dist/react/index.js +206 -791
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. package/vite.config.ts +0 -39
@@ -0,0 +1,428 @@
1
+ # Derived Patterns
2
+
3
+ ## .get() Returns Promise, But Execution Can Be Sync
4
+
5
+ `.get()` **ALWAYS returns `Promise<T>`** — this is the API contract.
6
+
7
+ **BUT** internally, computation is sync or async depending on dependencies:
8
+
9
+ ```typescript
10
+ const count$ = atom(0); // Sync value
11
+ const user$ = atom(fetchUser()); // Async value (Promise)
12
+
13
+ const doubled$ = derived(({ read }) => read(count$) * 2);
14
+ const greeting$ = derived(({ read }) => `Hello, ${read(user$).name}`);
15
+
16
+ // API always returns Promise
17
+ await doubled$.get(); // Promise<number>
18
+ await greeting$.get(); // Promise<string>
19
+ ```
20
+
21
+ ## Why This Matters: Sync Mental Model in UI
22
+
23
+ **In SelectContext (useSelector, derived, effect):**
24
+
25
+ - Everything runs **sync** — no awaits in your code
26
+ - Suspense handles async dependencies automatically
27
+ - You don't care if dependency is sync/async
28
+
29
+ ```tsx
30
+ const doubled$ = derived(({ read }) => read(count$) * 2);
31
+
32
+ function MyComponent() {
33
+ // Always sync execution — no Promise throw if count$ is sync
34
+ const doubled = useSelector(doubled$);
35
+
36
+ // Even with async dependency — still sync code, Suspense handles it
37
+ const user = useSelector(user$);
38
+
39
+ return (
40
+ <div>
41
+ {doubled} - {user.name}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ // Wrap with Suspense to handle async deps
47
+ <Suspense fallback={<Loading />}>
48
+ <MyComponent />
49
+ </Suspense>;
50
+ ```
51
+
52
+ **Effects work the same way:**
53
+
54
+ ```typescript
55
+ effect(({ read }) => {
56
+ const count = read(count$); // Sync
57
+ const user = read(user$); // Async — effect waits, no await needed
58
+ console.log(count, user.name);
59
+ });
60
+ ```
61
+
62
+ ## Outside SelectContext: Use await
63
+
64
+ Services, utilities, event handlers — use `.get()` with await:
65
+
66
+ ```typescript
67
+ // In service/utility
68
+ async function processData() {
69
+ const user = await user$.get(); // Await because outside selector
70
+ return transform(user);
71
+ }
72
+
73
+ // In event handler
74
+ const handleClick = async () => {
75
+ const count = await count$.get();
76
+ sendAnalytics(count);
77
+ };
78
+ ```
79
+
80
+ ## Do Utils Need Sync Values?
81
+
82
+ **No.** If a utility needs sync value, it should:
83
+
84
+ 1. Accept the value as parameter (not atom)
85
+ 2. Read from atom that contains sync value directly
86
+
87
+ ```typescript
88
+ // ❌ Don't make utils depend on atoms
89
+ function formatCount() {
90
+ return count$.get(); // Returns Promise, awkward
91
+ }
92
+
93
+ // ✅ Accept value as parameter
94
+ function formatCount(count: number) {
95
+ return `Count: ${count}`;
96
+ }
97
+
98
+ // Usage in selector
99
+ useSelector(({ read }) => formatCount(read(count$)));
100
+ ```
101
+
102
+ ## Summary
103
+
104
+ | Context | Execution | Async Handling |
105
+ | ---------------------------- | ---------- | -------------- |
106
+ | useSelector/derived/effect | Sync code | Suspense/wait |
107
+ | Outside (services, handlers) | Async code | await .get() |
108
+
109
+ ## When to Use
110
+
111
+ **Use `derived()` for:**
112
+
113
+ - Combining/transforming atoms
114
+ - Computed values that auto-update
115
+ - Handling sync + async atoms uniformly
116
+
117
+ **NEVER use for:**
118
+
119
+ - **Updating atoms** — use `effect()`
120
+ - Side effects — use `effect()`
121
+ - User actions — use plain functions
122
+
123
+ ## Core API
124
+
125
+ | Property/Method | Signature | Description |
126
+ | --------------- | ------------------- | ------------------------ |
127
+ | `get()` | `() => Promise<T>` | Get computed value |
128
+ | `staleValue` | `T \| undefined` | Last resolved / fallback |
129
+ | `state()` | `() => AtomState` | State without throwing |
130
+ | `refresh()` | `() => void` | Force recomputation |
131
+ | `on()` | `(listener) => sub` | Subscribe |
132
+
133
+ ## Selector Rules (CRITICAL)
134
+
135
+ ### NEVER Update Atoms
136
+
137
+ ```typescript
138
+ // ❌ FORBIDDEN
139
+ derived(({ read }) => {
140
+ const items = read(cartItems$);
141
+ cartTotal$.set(items.reduce((s, i) => s + i.price, 0)); // NEVER
142
+ return total;
143
+ });
144
+
145
+ // ✅ Use effect()
146
+ effect(
147
+ ({ read }) => {
148
+ const items = read(cartItems$);
149
+ cartTotal$.set(items.reduce((s, i) => s + i.price, 0));
150
+ },
151
+ { meta: { key: "compute.cartTotal" } }
152
+ );
153
+
154
+ // ✅ Or compute in derived
155
+ const cartTotal$ = derived(({ read }) =>
156
+ read(cartItems$).reduce((s, i) => s + i.price, 0)
157
+ );
158
+ ```
159
+
160
+ ### MUST Return Sync Value
161
+
162
+ ```typescript
163
+ // ❌ FORBIDDEN
164
+ derived(async ({ read }) => await fetch("/api"));
165
+ derived(({ read }) => fetch("/api").then((r) => r.json()));
166
+
167
+ // ✅ REQUIRED
168
+ const data$ = atom(fetch("/api").then((r) => r.json()));
169
+ derived(({ read }) => read(data$));
170
+ ```
171
+
172
+ ### NEVER try/catch — Use safe()
173
+
174
+ ```typescript
175
+ // ❌ FORBIDDEN — catches Promise
176
+ derived(({ read }) => {
177
+ try {
178
+ return read(asyncAtom$);
179
+ } catch (e) {
180
+ return "fallback";
181
+ } // Breaks Suspense
182
+ });
183
+
184
+ // ✅ REQUIRED
185
+ derived(({ read, safe }) => {
186
+ const [err, data] = safe(() => read(asyncAtom$));
187
+ if (err) return "error fallback";
188
+ return data;
189
+ });
190
+ ```
191
+
192
+ ## staleValue
193
+
194
+ Last resolved value, or fallback if not yet resolved / error occurred.
195
+
196
+ ```typescript
197
+ // Async atom dependency
198
+ const user$ = atom(fetchUser()); // Promise<User>
199
+
200
+ // Without fallback
201
+ const userName$ = derived(({ read }) => read(user$).name);
202
+ userName$.staleValue; // undefined — async not resolved yet
203
+ await userName$.get();
204
+ userName$.staleValue; // "John" — last resolved value
205
+
206
+ // With fallback — used when:
207
+ // 1. Async dependency not resolved yet
208
+ // 2. Computation error (async rejects or transform fails)
209
+ const userPosts$ = derived(({ read }) => read(userPostsAsync$).length, {
210
+ fallback: 0,
211
+ });
212
+ userPosts$.staleValue; // 0 — async not resolved yet
213
+ await userPosts$.get();
214
+ userPosts$.staleValue; // 42 — resolved value
215
+
216
+ // If async rejects or computation throws
217
+ userPosts$.staleValue; // 0 — fallback used on error
218
+ ```
219
+
220
+ | Scenario | Without fallback | With fallback |
221
+ | -------------- | ---------------- | -------------- |
222
+ | Before resolve | `undefined` | fallback value |
223
+ | After resolve | Last value | Last value |
224
+ | On error | `undefined` | fallback value |
225
+
226
+ ### Show Cached While Loading
227
+
228
+ ```tsx
229
+ function PostCount() {
230
+ const state = useSelector(({ state }) => state(postCount$));
231
+ const stale = postCount$.staleValue;
232
+
233
+ if (state.status === "loading")
234
+ return <div className="loading">{stale ?? "..."}</div>;
235
+ return <div>{state.value}</div>;
236
+ }
237
+ ```
238
+
239
+ ## state()
240
+
241
+ Get state without Suspense:
242
+
243
+ ```typescript
244
+ data$.state();
245
+ // { status: "ready", value: T }
246
+ // { status: "error", error: unknown }
247
+ // { status: "loading", promise: Promise<T> }
248
+ ```
249
+
250
+ ## refresh()
251
+
252
+ Force recomputation:
253
+
254
+ ```tsx
255
+ function DataList() {
256
+ const stable = useStable({ onRefresh: () => data$.refresh() });
257
+ return (
258
+ <PullToRefresh onRefresh={stable.onRefresh}>
259
+ <List />
260
+ </PullToRefresh>
261
+ );
262
+ }
263
+ ```
264
+
265
+ ## Options
266
+
267
+ ```typescript
268
+ interface DerivedOptions<T> {
269
+ meta?: { key?: string };
270
+ equals?: Equality<T>;
271
+ fallback?: T;
272
+ onError?: (error: unknown) => void;
273
+ }
274
+
275
+ // Shallow equality
276
+ const user$ = derived(({ read }) => ({ ...read(userData$) }), {
277
+ equals: "shallow",
278
+ });
279
+
280
+ // Custom equality
281
+ const data$ = derived(({ read }) => read(source$), {
282
+ equals: (a, b) => a.id === b.id,
283
+ });
284
+
285
+ // Error callback
286
+ const risky$ = derived(({ read }) => JSON.parse(read(raw$)), {
287
+ onError: (err) => Sentry.captureException(err),
288
+ });
289
+ ```
290
+
291
+ ## Common Patterns
292
+
293
+ ### Computed Property
294
+
295
+ ```typescript
296
+ const fullName$ = derived(({ read }) => {
297
+ const { firstName, lastName } = read(user$);
298
+ return `${firstName} ${lastName}`;
299
+ });
300
+ ```
301
+
302
+ ### Filtered List
303
+
304
+ ```typescript
305
+ const filteredTodos$ = derived(({ read }) => {
306
+ const todos = read(todos$);
307
+ const filter = read(filter$);
308
+ switch (filter) {
309
+ case "active":
310
+ return todos.filter((t) => !t.completed);
311
+ case "completed":
312
+ return todos.filter((t) => t.completed);
313
+ default:
314
+ return todos;
315
+ }
316
+ });
317
+ ```
318
+
319
+ ### Combined Async
320
+
321
+ ```typescript
322
+ const dashboard$ = derived(({ all }) => {
323
+ const [user, posts, notifications] = all([user$, posts$, notifications$]);
324
+ return { user, posts, notifications };
325
+ });
326
+ ```
327
+
328
+ ### Conditional Dependencies
329
+
330
+ ```typescript
331
+ const data$ = derived(({ read, and }) => {
332
+ if (!and([isLoggedIn$, hasPermission$])) return null;
333
+ return read(expensiveData$);
334
+ });
335
+ ```
336
+
337
+ ### Error-Resilient
338
+
339
+ ```typescript
340
+ const dashboard$ = derived(({ settled }) => {
341
+ const [userR, postsR, statsR] = settled([user$, posts$, stats$]);
342
+ return {
343
+ user: userR.status === "ready" ? userR.value : null,
344
+ posts: postsR.status === "ready" ? postsR.value : [],
345
+ errors: [userR, postsR, statsR]
346
+ .filter((r) => r.status === "error")
347
+ .map((r) => r.error),
348
+ };
349
+ });
350
+ ```
351
+
352
+ ### Race Cache vs API
353
+
354
+ ```typescript
355
+ const article$ = derived(({ race }) => {
356
+ const result = race({ cache: cachedArticle$, network: fetchedArticle$ });
357
+ console.log(`From: ${result.key}`);
358
+ return result.value;
359
+ });
360
+ ```
361
+
362
+ ### From Pool
363
+
364
+ ```typescript
365
+ const currentUser$ = derived(({ read, ready, from }) => {
366
+ const userId = ready(currentUserId$);
367
+ const user$ = from(userPool, userId);
368
+ return read(user$);
369
+ });
370
+ ```
371
+
372
+ ### Non-Reactive Config (untrack)
373
+
374
+ Read values without creating dependencies — useful for config or initial values that shouldn't trigger re-computation:
375
+
376
+ ```typescript
377
+ // Config doesn't change often — don't re-compute when it does
378
+ const formatted$ = derived(({ read, untrack }) => {
379
+ const data = read(data$); // Re-compute when data changes
380
+ const format = untrack(format$); // DON'T re-compute when format changes
381
+ return formatData(data, format);
382
+ });
383
+
384
+ // Snapshot multiple atoms at once
385
+ const snapshot$ = derived(({ read, untrack }) => {
386
+ const trigger = read(trigger$); // Only re-compute on trigger
387
+ return untrack(() => ({
388
+ a: read(a$),
389
+ b: read(b$),
390
+ c: read(c$),
391
+ timestamp: Date.now(),
392
+ }));
393
+ });
394
+
395
+ // Initial value pattern
396
+ const counter$ = derived(({ read, untrack }) => {
397
+ const count = read(count$);
398
+ const initial = untrack(initialValue$); // Don't re-run if initial changes
399
+ return count - initial;
400
+ });
401
+ ```
402
+
403
+ | Use Case | Method |
404
+ | -------- | ------ |
405
+ | Value that triggers re-compute | `read()` |
406
+ | Config/settings rarely changing | `untrack()` |
407
+ | Snapshot of multiple atoms | `untrack(() => ...)` |
408
+ | Initial/reference values | `untrack()` |
409
+
410
+ ## Effect vs Derived
411
+
412
+ | Aspect | derived() | effect() |
413
+ | ------------ | ----------------- | ------------------- |
414
+ | Returns | `Promise<T>` | void |
415
+ | Execution | Lazy (on access) | Eager (immediately) |
416
+ | Purpose | Transform/combine | Sync, persist, log |
417
+ | **Can set** | **❌ NEVER** | **✅ Yes** |
418
+ | Subscription | When accessed | Always active |
419
+
420
+ ## When to Use What
421
+
422
+ | Scenario | Solution |
423
+ | -------------------------------- | -------------- |
424
+ | User clicks → modify atoms | Plain function |
425
+ | React to changes → compute value | `derived()` |
426
+ | React to changes → side effects | `effect()` |
427
+ | Combine multiple atoms | `derived()` |
428
+ | Persist/log/sync on changes | `effect()` |