rexfect 0.0.7

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 (190) hide show
  1. package/README.md +1756 -0
  2. package/dist/abortableContext.d.ts +3 -0
  3. package/dist/abortableContext.d.ts.map +1 -0
  4. package/dist/abortableContext.js +48 -0
  5. package/dist/abortableContext.js.map +1 -0
  6. package/dist/action.d.ts +64 -0
  7. package/dist/action.d.ts.map +1 -0
  8. package/dist/action.js +208 -0
  9. package/dist/action.js.map +1 -0
  10. package/dist/action.test.d.ts +2 -0
  11. package/dist/action.test.d.ts.map +1 -0
  12. package/dist/action.test.js +189 -0
  13. package/dist/action.test.js.map +1 -0
  14. package/dist/async/abortable-guard.d.ts +25 -0
  15. package/dist/async/abortable-guard.d.ts.map +1 -0
  16. package/dist/async/abortable-guard.js +33 -0
  17. package/dist/async/abortable-guard.js.map +1 -0
  18. package/dist/async/abortable.d.ts +331 -0
  19. package/dist/async/abortable.d.ts.map +1 -0
  20. package/dist/async/abortable.js +410 -0
  21. package/dist/async/abortable.js.map +1 -0
  22. package/dist/async/abortable.test.d.ts +2 -0
  23. package/dist/async/abortable.test.d.ts.map +1 -0
  24. package/dist/async/abortable.test.js +535 -0
  25. package/dist/async/abortable.test.js.map +1 -0
  26. package/dist/async/abortable.typeCheck.d.ts +8 -0
  27. package/dist/async/abortable.typeCheck.d.ts.map +1 -0
  28. package/dist/async/abortable.typeCheck.js +138 -0
  29. package/dist/async/abortable.typeCheck.js.map +1 -0
  30. package/dist/async/async.d.ts +18 -0
  31. package/dist/async/async.d.ts.map +1 -0
  32. package/dist/async/async.js +20 -0
  33. package/dist/async/async.js.map +1 -0
  34. package/dist/async/index.d.ts +15 -0
  35. package/dist/async/index.d.ts.map +1 -0
  36. package/dist/async/index.js +13 -0
  37. package/dist/async/index.js.map +1 -0
  38. package/dist/async/loadable.d.ts +7 -0
  39. package/dist/async/loadable.d.ts.map +1 -0
  40. package/dist/async/loadable.js +52 -0
  41. package/dist/async/loadable.js.map +1 -0
  42. package/dist/async/loadable.test.d.ts +2 -0
  43. package/dist/async/loadable.test.d.ts.map +1 -0
  44. package/dist/async/loadable.test.js +322 -0
  45. package/dist/async/loadable.test.js.map +1 -0
  46. package/dist/async/promiseCache.d.ts +14 -0
  47. package/dist/async/promiseCache.d.ts.map +1 -0
  48. package/dist/async/promiseCache.js +29 -0
  49. package/dist/async/promiseCache.js.map +1 -0
  50. package/dist/async/read.d.ts +120 -0
  51. package/dist/async/read.d.ts.map +1 -0
  52. package/dist/async/read.js +286 -0
  53. package/dist/async/read.js.map +1 -0
  54. package/dist/async/read.test.d.ts +2 -0
  55. package/dist/async/read.test.d.ts.map +1 -0
  56. package/dist/async/read.test.js +419 -0
  57. package/dist/async/read.test.js.map +1 -0
  58. package/dist/async/read.typeCheck.d.ts +6 -0
  59. package/dist/async/read.typeCheck.d.ts.map +1 -0
  60. package/dist/async/read.typeCheck.js +101 -0
  61. package/dist/async/read.typeCheck.js.map +1 -0
  62. package/dist/async/safe.d.ts +230 -0
  63. package/dist/async/safe.d.ts.map +1 -0
  64. package/dist/async/safe.js +247 -0
  65. package/dist/async/safe.js.map +1 -0
  66. package/dist/async/safe.test.d.ts +2 -0
  67. package/dist/async/safe.test.d.ts.map +1 -0
  68. package/dist/async/safe.test.js +447 -0
  69. package/dist/async/safe.test.js.map +1 -0
  70. package/dist/async/utils.d.ts +17 -0
  71. package/dist/async/utils.d.ts.map +1 -0
  72. package/dist/async/utils.js +38 -0
  73. package/dist/async/utils.js.map +1 -0
  74. package/dist/async/wait.d.ts +120 -0
  75. package/dist/async/wait.d.ts.map +1 -0
  76. package/dist/async/wait.js +112 -0
  77. package/dist/async/wait.js.map +1 -0
  78. package/dist/async/wait.test.d.ts +2 -0
  79. package/dist/async/wait.test.d.ts.map +1 -0
  80. package/dist/async/wait.test.js +122 -0
  81. package/dist/async/wait.test.js.map +1 -0
  82. package/dist/async/wait.typeCheck.d.ts +6 -0
  83. package/dist/async/wait.typeCheck.d.ts.map +1 -0
  84. package/dist/async/wait.typeCheck.js +104 -0
  85. package/dist/async/wait.typeCheck.js.map +1 -0
  86. package/dist/atom.d.ts +46 -0
  87. package/dist/atom.d.ts.map +1 -0
  88. package/dist/atom.js +86 -0
  89. package/dist/atom.js.map +1 -0
  90. package/dist/atom.test.d.ts +2 -0
  91. package/dist/atom.test.d.ts.map +1 -0
  92. package/dist/atom.test.js +75 -0
  93. package/dist/atom.test.js.map +1 -0
  94. package/dist/batch.d.ts +15 -0
  95. package/dist/batch.d.ts.map +1 -0
  96. package/dist/batch.js +45 -0
  97. package/dist/batch.js.map +1 -0
  98. package/dist/defer.d.ts +56 -0
  99. package/dist/defer.d.ts.map +1 -0
  100. package/dist/defer.js +49 -0
  101. package/dist/defer.js.map +1 -0
  102. package/dist/effect.d.ts +91 -0
  103. package/dist/effect.d.ts.map +1 -0
  104. package/dist/effect.js +311 -0
  105. package/dist/effect.js.map +1 -0
  106. package/dist/effect.test.d.ts +2 -0
  107. package/dist/effect.test.d.ts.map +1 -0
  108. package/dist/effect.test.js +123 -0
  109. package/dist/effect.test.js.map +1 -0
  110. package/dist/emitter.d.ts +129 -0
  111. package/dist/emitter.d.ts.map +1 -0
  112. package/dist/emitter.js +164 -0
  113. package/dist/emitter.js.map +1 -0
  114. package/dist/emitter.test.d.ts +2 -0
  115. package/dist/emitter.test.d.ts.map +1 -0
  116. package/dist/emitter.test.js +259 -0
  117. package/dist/emitter.test.js.map +1 -0
  118. package/dist/equality.d.ts +66 -0
  119. package/dist/equality.d.ts.map +1 -0
  120. package/dist/equality.js +145 -0
  121. package/dist/equality.js.map +1 -0
  122. package/dist/event.d.ts +18 -0
  123. package/dist/event.d.ts.map +1 -0
  124. package/dist/event.js +166 -0
  125. package/dist/event.js.map +1 -0
  126. package/dist/event.test.d.ts +2 -0
  127. package/dist/event.test.d.ts.map +1 -0
  128. package/dist/event.test.js +167 -0
  129. package/dist/event.test.js.map +1 -0
  130. package/dist/hooks.d.ts +152 -0
  131. package/dist/hooks.d.ts.map +1 -0
  132. package/dist/hooks.js +122 -0
  133. package/dist/hooks.js.map +1 -0
  134. package/dist/hooks.test.d.ts +2 -0
  135. package/dist/hooks.test.d.ts.map +1 -0
  136. package/dist/hooks.test.js +99 -0
  137. package/dist/hooks.test.js.map +1 -0
  138. package/dist/index.d.ts +33 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +35 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/isPromiseLike.d.ts +10 -0
  143. package/dist/isPromiseLike.d.ts.map +1 -0
  144. package/dist/isPromiseLike.js +15 -0
  145. package/dist/isPromiseLike.js.map +1 -0
  146. package/dist/pick.d.ts +22 -0
  147. package/dist/pick.d.ts.map +1 -0
  148. package/dist/pick.js +46 -0
  149. package/dist/pick.js.map +1 -0
  150. package/dist/react/index.d.ts +8 -0
  151. package/dist/react/index.d.ts.map +1 -0
  152. package/dist/react/index.js +8 -0
  153. package/dist/react/index.js.map +1 -0
  154. package/dist/react/useRx.d.ts +14 -0
  155. package/dist/react/useRx.d.ts.map +1 -0
  156. package/dist/react/useRx.js +110 -0
  157. package/dist/react/useRx.js.map +1 -0
  158. package/dist/react/useRx.test.d.ts +2 -0
  159. package/dist/react/useRx.test.d.ts.map +1 -0
  160. package/dist/react/useRx.test.js +457 -0
  161. package/dist/react/useRx.test.js.map +1 -0
  162. package/dist/strictModeTest.d.ts +11 -0
  163. package/dist/strictModeTest.d.ts.map +1 -0
  164. package/dist/strictModeTest.js +41 -0
  165. package/dist/strictModeTest.js.map +1 -0
  166. package/dist/types.d.ts +606 -0
  167. package/dist/types.d.ts.map +1 -0
  168. package/dist/types.js +5 -0
  169. package/dist/types.js.map +1 -0
  170. package/dist/untrack.d.ts +14 -0
  171. package/dist/untrack.d.ts.map +1 -0
  172. package/dist/untrack.js +17 -0
  173. package/dist/untrack.js.map +1 -0
  174. package/dist/utils/withUse.d.ts +10 -0
  175. package/dist/utils/withUse.d.ts.map +1 -0
  176. package/dist/utils/withUse.js +21 -0
  177. package/dist/utils/withUse.js.map +1 -0
  178. package/dist/utils/withUse.test.d.ts +2 -0
  179. package/dist/utils/withUse.test.d.ts.map +1 -0
  180. package/dist/utils/withUse.test.js +233 -0
  181. package/dist/utils/withUse.test.js.map +1 -0
  182. package/dist/utils.d.ts +7 -0
  183. package/dist/utils.d.ts.map +1 -0
  184. package/dist/utils.js +7 -0
  185. package/dist/utils.js.map +1 -0
  186. package/dist/utils.test.d.ts +2 -0
  187. package/dist/utils.test.d.ts.map +1 -0
  188. package/dist/utils.test.js +119 -0
  189. package/dist/utils.test.js.map +1 -0
  190. package/package.json +64 -0
package/dist/batch.js ADDED
@@ -0,0 +1,45 @@
1
+ import { withHooks } from "./hooks";
2
+ import { emitter } from "./emitter";
3
+ let batchDepth = 0;
4
+ /**
5
+ * Batches multiple state updates into a single reactive update.
6
+ *
7
+ * @param fn - Function containing multiple state updates
8
+ * @returns The return value of the function
9
+ *
10
+ * @example
11
+ * batch(() => {
12
+ * setA(1);
13
+ * setB(2);
14
+ * setC(3);
15
+ * }); // Effects run once after all updates
16
+ */
17
+ export function batch(fn) {
18
+ batchDepth++;
19
+ // First batch - set up the notification hook
20
+ if (batchDepth === 1) {
21
+ const onNotify = emitter();
22
+ try {
23
+ return withHooks({ scheduleNotify: onNotify.on }, fn);
24
+ }
25
+ finally {
26
+ batchDepth--;
27
+ // Keep hooks active while processing notifications
28
+ // This ensures effect re-runs are also batched
29
+ withHooks({ scheduleNotify: onNotify.on }, () => {
30
+ // Process until no more notifications (handles cascading updates)
31
+ while (onNotify.size > 0) {
32
+ onNotify.emitAndClear();
33
+ }
34
+ });
35
+ }
36
+ }
37
+ // Nested batch - just run the function
38
+ try {
39
+ return fn();
40
+ }
41
+ finally {
42
+ batchDepth--;
43
+ }
44
+ }
45
+ //# sourceMappingURL=batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,UAAU,GAAG,CAAC,CAAC;AACnB;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,KAAK,CAAI,EAAW;IAClC,UAAU,EAAE,CAAC;IAEb,6CAA6C;IAC7C,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,SAAS,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;gBAAS,CAAC;YACT,UAAU,EAAE,CAAC;YACb,mDAAmD;YACnD,+CAA+C;YAC/C,SAAS,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE;gBAC9C,kEAAkE;gBAClE,OAAO,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;oBACzB,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,UAAU,EAAE,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A deferred promise - a promise whose resolve/reject can be called externally.
3
+ *
4
+ * Unlike a regular Promise where resolve/reject are only accessible inside
5
+ * the executor function, a Deferred exposes them as properties, allowing
6
+ * the promise to be resolved/rejected from anywhere.
7
+ *
8
+ * @template T - The type of value the promise resolves to
9
+ */
10
+ export interface Deferred<T> {
11
+ /** The underlying promise that will be resolved/rejected */
12
+ readonly promise: Promise<T>;
13
+ /** Resolves the promise with the given value */
14
+ readonly resolve: (value: T) => void;
15
+ /** Rejects the promise with the given error */
16
+ readonly reject: (error: unknown) => void;
17
+ }
18
+ /**
19
+ * Creates a deferred promise - a promise with externally accessible resolve/reject.
20
+ *
21
+ * This is useful when you need to resolve a promise from a different context
22
+ * than where it was created, such as:
23
+ * - Waiting for an event to fire (`await action`)
24
+ * - Coordinating between async operations
25
+ * - Building thenable objects
26
+ *
27
+ * @template T - The type of value the promise resolves to
28
+ * @returns A Deferred object with { promise, resolve, reject }
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const deferred = defer<string>();
33
+ *
34
+ * // Somewhere else in the code...
35
+ * setTimeout(() => {
36
+ * deferred.resolve("done!");
37
+ * }, 1000);
38
+ *
39
+ * // Await the promise
40
+ * const result = await deferred.promise; // "done!"
41
+ * ```
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // Used internally by action() for thenable support:
46
+ * let nextDispatch = defer<T>();
47
+ *
48
+ * // When action is awaited:
49
+ * action.then(...) → nextDispatch.promise.then(...)
50
+ *
51
+ * // When action is dispatched:
52
+ * action(payload) → nextDispatch.resolve(payload)
53
+ * ```
54
+ */
55
+ export declare function defer<T>(): Deferred<T>;
56
+ //# sourceMappingURL=defer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defer.d.ts","sourceRoot":"","sources":["../src/defer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACrC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,KAAK,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAYtC"}
package/dist/defer.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Creates a deferred promise - a promise with externally accessible resolve/reject.
3
+ *
4
+ * This is useful when you need to resolve a promise from a different context
5
+ * than where it was created, such as:
6
+ * - Waiting for an event to fire (`await action`)
7
+ * - Coordinating between async operations
8
+ * - Building thenable objects
9
+ *
10
+ * @template T - The type of value the promise resolves to
11
+ * @returns A Deferred object with { promise, resolve, reject }
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const deferred = defer<string>();
16
+ *
17
+ * // Somewhere else in the code...
18
+ * setTimeout(() => {
19
+ * deferred.resolve("done!");
20
+ * }, 1000);
21
+ *
22
+ * // Await the promise
23
+ * const result = await deferred.promise; // "done!"
24
+ * ```
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // Used internally by action() for thenable support:
29
+ * let nextDispatch = defer<T>();
30
+ *
31
+ * // When action is awaited:
32
+ * action.then(...) → nextDispatch.promise.then(...)
33
+ *
34
+ * // When action is dispatched:
35
+ * action(payload) → nextDispatch.resolve(payload)
36
+ * ```
37
+ */
38
+ export function defer() {
39
+ // These will be assigned synchronously by the Promise executor
40
+ let resolve;
41
+ let reject;
42
+ // Create promise and capture resolve/reject from executor
43
+ const promise = new Promise((...args) => {
44
+ [resolve, reject] = args;
45
+ });
46
+ // Non-null assertions are safe because Promise executor runs synchronously
47
+ return { promise, resolve: resolve, reject: reject };
48
+ }
49
+ //# sourceMappingURL=defer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defer.js","sourceRoot":"","sources":["../src/defer.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,KAAK;IACnB,+DAA+D;IAC/D,IAAI,OAA2B,CAAC;IAChC,IAAI,MAAgC,CAAC;IAErC,0DAA0D;IAC1D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,IAAI,EAAE,EAAE;QACzC,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAQ,EAAE,MAAM,EAAE,MAAO,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,91 @@
1
+ import type { EffectContext, EffectOptions } from "./types";
2
+ /**
3
+ * Creates a reactive effect that runs when its dependencies change.
4
+ *
5
+ * ## How It Works
6
+ *
7
+ * 1. Effect runs immediately on creation
8
+ * 2. During execution, any atom/signal reads are automatically tracked
9
+ * 3. When any tracked dependency changes, effect re-runs
10
+ * 4. Before each re-run, cleanup functions are called
11
+ * 5. Dependencies are re-tracked on each run (supports conditional reads)
12
+ *
13
+ * ## Key Characteristics
14
+ *
15
+ * - **Synchronous only**: Effects cannot be async (throws if you return a Promise)
16
+ * - **Auto-tracking**: Dependencies detected automatically via signal reads
17
+ * - **Cleanup support**: Register cleanup via `ctx.onCleanup()`
18
+ * - **Error handling**: Register error handlers via `ctx.onError()`
19
+ * - **Deduplication**: Multiple dependency changes in same tick = single re-run
20
+ *
21
+ * @param fn - The effect function to run. Receives an EffectContext.
22
+ * @param options - Optional configuration (key for debugging)
23
+ * @returns A dispose function to stop the effect permanently
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const [count, setCount] = atom(0);
28
+ *
29
+ * const dispose = effect((ctx) => {
30
+ * console.log("Count:", count()); // count() is auto-tracked
31
+ * });
32
+ * // Output: "Count: 0"
33
+ *
34
+ * setCount(1);
35
+ * // Output: "Count: 1"
36
+ *
37
+ * dispose(); // Stop the effect
38
+ * setCount(2); // No output
39
+ * ```
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * // With cleanup for subscriptions/timers
44
+ * effect((ctx) => {
45
+ * const interval = setInterval(() => tick(), 1000);
46
+ * ctx.onCleanup(() => clearInterval(interval));
47
+ * });
48
+ * ```
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * // With error handling
53
+ * effect((ctx) => {
54
+ * ctx.onError((err) => console.error("Effect failed:", err));
55
+ * if (data() === "bad") throw new Error("Invalid data");
56
+ * });
57
+ * ```
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // Conditional tracking - only re-runs when relevant deps change
62
+ * effect(() => {
63
+ * if (showDetails()) {
64
+ * // details() only tracked when showDetails() is true
65
+ * console.log(details());
66
+ * }
67
+ * });
68
+ * ```
69
+ */
70
+ export declare function effect(fn: (context: EffectContext) => void, options?: EffectOptions): VoidFunction;
71
+ /**
72
+ * Creates an effect instance without immediately running it.
73
+ *
74
+ * This is the internal factory used by `effect()`. It's exposed separately
75
+ * for cases where you need to control when the effect first runs (e.g., testing,
76
+ * or deferred initialization).
77
+ *
78
+ * @internal
79
+ * @param fn - The effect function
80
+ * @param options - Optional configuration
81
+ * @returns An object with { options, run, dispose }
82
+ */
83
+ export declare function effectInstance(fn: (context: EffectContext) => void, options?: EffectOptions): {
84
+ /** Original options passed to effectInstance */
85
+ options: EffectOptions | undefined;
86
+ /** Manually trigger the effect to run */
87
+ run: () => void;
88
+ /** Stop the effect permanently */
89
+ dispose: () => void;
90
+ };
91
+ //# sourceMappingURL=effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAU,MAAM,SAAS,CAAC;AASpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,wBAAgB,MAAM,CACpB,EAAE,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE,aAAa,GACtB,YAAY,CAMd;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE,aAAa;IAsOrB,gDAAgD;;IAEhD,yCAAyC;;IAEzC,kCAAkC;;EAGrC"}
package/dist/effect.js ADDED
@@ -0,0 +1,311 @@
1
+ import { emitter } from "./emitter";
2
+ import { getHooks, withHooks } from "./hooks";
3
+ import { isPromiseLike } from "./isPromiseLike";
4
+ // ============================================================================
5
+ // Effect - Reactive Side Effects
6
+ // ============================================================================
7
+ /**
8
+ * Creates a reactive effect that runs when its dependencies change.
9
+ *
10
+ * ## How It Works
11
+ *
12
+ * 1. Effect runs immediately on creation
13
+ * 2. During execution, any atom/signal reads are automatically tracked
14
+ * 3. When any tracked dependency changes, effect re-runs
15
+ * 4. Before each re-run, cleanup functions are called
16
+ * 5. Dependencies are re-tracked on each run (supports conditional reads)
17
+ *
18
+ * ## Key Characteristics
19
+ *
20
+ * - **Synchronous only**: Effects cannot be async (throws if you return a Promise)
21
+ * - **Auto-tracking**: Dependencies detected automatically via signal reads
22
+ * - **Cleanup support**: Register cleanup via `ctx.onCleanup()`
23
+ * - **Error handling**: Register error handlers via `ctx.onError()`
24
+ * - **Deduplication**: Multiple dependency changes in same tick = single re-run
25
+ *
26
+ * @param fn - The effect function to run. Receives an EffectContext.
27
+ * @param options - Optional configuration (key for debugging)
28
+ * @returns A dispose function to stop the effect permanently
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const [count, setCount] = atom(0);
33
+ *
34
+ * const dispose = effect((ctx) => {
35
+ * console.log("Count:", count()); // count() is auto-tracked
36
+ * });
37
+ * // Output: "Count: 0"
38
+ *
39
+ * setCount(1);
40
+ * // Output: "Count: 1"
41
+ *
42
+ * dispose(); // Stop the effect
43
+ * setCount(2); // No output
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // With cleanup for subscriptions/timers
49
+ * effect((ctx) => {
50
+ * const interval = setInterval(() => tick(), 1000);
51
+ * ctx.onCleanup(() => clearInterval(interval));
52
+ * });
53
+ * ```
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // With error handling
58
+ * effect((ctx) => {
59
+ * ctx.onError((err) => console.error("Effect failed:", err));
60
+ * if (data() === "bad") throw new Error("Invalid data");
61
+ * });
62
+ * ```
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // Conditional tracking - only re-runs when relevant deps change
67
+ * effect(() => {
68
+ * if (showDetails()) {
69
+ * // details() only tracked when showDetails() is true
70
+ * console.log(details());
71
+ * }
72
+ * });
73
+ * ```
74
+ */
75
+ export function effect(fn, options) {
76
+ const instance = effectInstance(fn, options);
77
+ // Run immediately on creation
78
+ instance.run();
79
+ // Return dispose function for cleanup
80
+ return instance.dispose;
81
+ }
82
+ // ============================================================================
83
+ // Effect Instance - Internal Implementation
84
+ // ============================================================================
85
+ /**
86
+ * Creates an effect instance without immediately running it.
87
+ *
88
+ * This is the internal factory used by `effect()`. It's exposed separately
89
+ * for cases where you need to control when the effect first runs (e.g., testing,
90
+ * or deferred initialization).
91
+ *
92
+ * @internal
93
+ * @param fn - The effect function
94
+ * @param options - Optional configuration
95
+ * @returns An object with { options, run, dispose }
96
+ */
97
+ export function effectInstance(fn, options) {
98
+ const { key } = options ?? {};
99
+ // ---------------------------------------------------------------------------
100
+ // Internal State
101
+ // ---------------------------------------------------------------------------
102
+ /** True after dispose() is called - prevents further runs */
103
+ let disposed = false;
104
+ /**
105
+ * True when a re-run is scheduled but not yet executed.
106
+ * Used to deduplicate: if multiple deps change before next tick,
107
+ * we only run once.
108
+ */
109
+ let scheduled = false;
110
+ /**
111
+ * Error handler emitter. Lazily created when ctx.onError() is first called.
112
+ * If no error handler is registered and an error occurs, it propagates up.
113
+ */
114
+ let onErrorEmitter;
115
+ /**
116
+ * Cleanup handler emitter. Lazily created when ctx.onCleanup() is first called.
117
+ * Cleanup functions run:
118
+ * - Before each re-run (to clean up previous run's resources)
119
+ * - NOT on dispose (use dispose for final cleanup if needed)
120
+ */
121
+ let onCleanupEmitter;
122
+ /**
123
+ * Run counter. Starts at -1, incremented before each run.
124
+ * - First run: nth = 0
125
+ * - Second run: nth = 1
126
+ * - etc.
127
+ *
128
+ * Useful for skipping logic on first run:
129
+ * ```ts
130
+ * effect((ctx) => {
131
+ * if (ctx.nth === 0) return; // Skip first run
132
+ * // ... logic for subsequent runs only
133
+ * });
134
+ * ```
135
+ */
136
+ let nth = -1;
137
+ // ---------------------------------------------------------------------------
138
+ // Context Builders
139
+ // ---------------------------------------------------------------------------
140
+ /**
141
+ * Registers a cleanup function to run before the next effect execution.
142
+ *
143
+ * Note: Cleanup does NOT run on dispose - only before re-runs.
144
+ * This matches React's useEffect cleanup behavior.
145
+ */
146
+ const onCleanup = (listener) => {
147
+ // Lazy creation of cleanup emitter
148
+ if (!onCleanupEmitter)
149
+ onCleanupEmitter = emitter();
150
+ return onCleanupEmitter.on(listener);
151
+ };
152
+ /**
153
+ * Registers an error handler for this effect.
154
+ *
155
+ * If an error handler is registered, errors are caught and passed to handlers
156
+ * instead of propagating up. If no handler is registered, errors throw normally.
157
+ */
158
+ const onError = (listener) => {
159
+ // Lazy creation of error emitter
160
+ if (!onErrorEmitter)
161
+ onErrorEmitter = emitter();
162
+ return onErrorEmitter.on(listener);
163
+ };
164
+ // ---------------------------------------------------------------------------
165
+ // Core Run Logic
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * Executes the effect function and sets up dependency tracking.
169
+ *
170
+ * Execution flow:
171
+ * 1. Clear scheduled flag
172
+ * 2. Exit early if disposed
173
+ * 3. Run cleanup functions from previous run
174
+ * 4. Clear error handlers (fresh set for this run)
175
+ * 5. Increment run counter
176
+ * 6. Execute effect function with hooks for dependency tracking
177
+ * 7. Check for async (throw if Promise returned)
178
+ * 8. Subscribe to all tracked dependencies
179
+ */
180
+ const run = () => {
181
+ // Step 1: Clear scheduled flag (we're running now)
182
+ scheduled = false;
183
+ // Step 2: Exit if disposed
184
+ if (disposed)
185
+ return;
186
+ // Step 3: Run cleanup from previous run
187
+ // emitAndClear() calls all listeners then removes them
188
+ onCleanupEmitter?.emitAndClear();
189
+ // Step 4: Clear error handlers (user registers fresh ones each run)
190
+ onErrorEmitter?.clear();
191
+ // Step 5: Increment run counter
192
+ nth++;
193
+ // Track which signals are read during this execution
194
+ const signals = new Set();
195
+ try {
196
+ // Build the context object passed to the effect function
197
+ const ctx = {
198
+ nth,
199
+ key,
200
+ onError,
201
+ onCleanup,
202
+ /**
203
+ * Plugin system for extending context functionality.
204
+ * Same pattern as OnContext.use() in actions.
205
+ */
206
+ use(plugin, ...args) {
207
+ return plugin(ctx, ...args);
208
+ },
209
+ };
210
+ // Step 6: Execute effect with hooks enabled
211
+ // withHooks() sets up the tracking context so that:
212
+ // - Any signal.on() call during execution triggers track()
213
+ // - scheduleCleanup is available for nested subscriptions
214
+ const result = withHooks({
215
+ // Register cleanup handlers for subscriptions created during effect
216
+ scheduleCleanup: onCleanup,
217
+ // Track signals that are read during effect execution
218
+ track(signal) {
219
+ signals.add(signal);
220
+ },
221
+ }, () => fn(ctx));
222
+ // Step 7: Check for async functions (common mistake)
223
+ // Effects must be synchronous for predictable dependency tracking
224
+ if (isPromiseLike(result)) {
225
+ throw new Error("effect() received an async function. Effects must be synchronous. " +
226
+ "Use event().on() for async operations instead.");
227
+ }
228
+ }
229
+ catch (error) {
230
+ // Handle errors: if error handler registered, emit to it
231
+ // Otherwise, re-throw the error
232
+ if (onErrorEmitter && onErrorEmitter.size > 0) {
233
+ onErrorEmitter.emitAndClear(error);
234
+ }
235
+ else {
236
+ throw error;
237
+ }
238
+ }
239
+ finally {
240
+ // Step 8: Subscribe to all tracked dependencies
241
+ // When any dependency changes, schedule a re-run
242
+ // The subscription itself is registered as a cleanup, so it's
243
+ // automatically removed before the next run (then re-added with fresh deps)
244
+ signals.forEach((signal) => onCleanup(signal.on(scheduleRun)));
245
+ }
246
+ };
247
+ // ---------------------------------------------------------------------------
248
+ // Scheduling
249
+ // ---------------------------------------------------------------------------
250
+ /**
251
+ * Schedules an effect re-run on the next tick.
252
+ *
253
+ * This is called when any tracked dependency changes.
254
+ * Deduplication: if multiple deps change before next tick, only runs once.
255
+ *
256
+ * Uses hooks.scheduleNotify() which typically queues to microtask,
257
+ * allowing multiple sync updates to batch into single effect run.
258
+ */
259
+ const scheduleRun = () => {
260
+ // Skip if already scheduled or disposed
261
+ if (scheduled || disposed)
262
+ return;
263
+ scheduled = true;
264
+ // Queue the run for next microtask
265
+ getHooks().scheduleNotify(run);
266
+ };
267
+ // ---------------------------------------------------------------------------
268
+ // Disposal
269
+ // ---------------------------------------------------------------------------
270
+ /**
271
+ * Permanently stops the effect.
272
+ *
273
+ * After dispose:
274
+ * - No more runs will occur
275
+ * - Error and cleanup handlers are cleared
276
+ * - Subscriptions are NOT explicitly cleaned up here - they'll be
277
+ * garbage collected when the effect instance is no longer referenced
278
+ */
279
+ const dispose = () => {
280
+ if (disposed)
281
+ return;
282
+ disposed = true;
283
+ // Clear emitters to release handler references
284
+ onErrorEmitter?.clear();
285
+ onCleanupEmitter?.emitAndClear();
286
+ };
287
+ // ---------------------------------------------------------------------------
288
+ // Hook Integration
289
+ // ---------------------------------------------------------------------------
290
+ /**
291
+ * Register this effect's dispose with the current hooks context.
292
+ *
293
+ * This enables automatic cleanup when effects are created inside:
294
+ * - React components (via useRx internals)
295
+ * - Other effects (nested effects dispose with parent)
296
+ * - Test harnesses
297
+ *
298
+ * If no scheduleCleanup hook is set, this is a no-op.
299
+ */
300
+ getHooks().scheduleCleanup?.(dispose);
301
+ // Return the effect instance interface
302
+ return {
303
+ /** Original options passed to effectInstance */
304
+ options,
305
+ /** Manually trigger the effect to run */
306
+ run,
307
+ /** Stop the effect permanently */
308
+ dispose,
309
+ };
310
+ }
311
+ //# sourceMappingURL=effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.js","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=effect.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.test.d.ts","sourceRoot":"","sources":["../src/effect.test.ts"],"names":[],"mappings":""}