@assistant-ui/tap 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +9 -6
  2. package/dist/core/ResourceFiber.d.ts +5 -5
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +26 -18
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/createTapRoot.d.ts +9 -0
  7. package/dist/core/createTapRoot.d.ts.map +1 -0
  8. package/dist/core/createTapRoot.js +27 -0
  9. package/dist/core/createTapRoot.js.map +1 -0
  10. package/dist/core/helpers/commit.d.ts +1 -1
  11. package/dist/core/helpers/commit.d.ts.map +1 -1
  12. package/dist/core/helpers/commit.js +6 -1
  13. package/dist/core/helpers/commit.js.map +1 -1
  14. package/dist/core/helpers/execution-context.d.ts +4 -5
  15. package/dist/core/helpers/execution-context.d.ts.map +1 -1
  16. package/dist/core/helpers/execution-context.js +1 -7
  17. package/dist/core/helpers/execution-context.js.map +1 -1
  18. package/dist/core/helpers/root.d.ts +3 -2
  19. package/dist/core/helpers/root.d.ts.map +1 -1
  20. package/dist/core/helpers/root.js +19 -15
  21. package/dist/core/helpers/root.js.map +1 -1
  22. package/dist/core/react-dispatcher.d.ts.map +1 -1
  23. package/dist/core/react-dispatcher.js +14 -14
  24. package/dist/core/react-dispatcher.js.map +1 -1
  25. package/dist/core/resource.d.ts +2 -4
  26. package/dist/core/resource.d.ts.map +1 -1
  27. package/dist/core/resource.js +5 -10
  28. package/dist/core/resource.js.map +1 -1
  29. package/dist/core/scheduler.d.ts +2 -2
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +2 -2
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/types.d.ts +27 -25
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/hooks/useResource.d.ts +2 -2
  36. package/dist/hooks/useResource.d.ts.map +1 -1
  37. package/dist/hooks/useResource.js +14 -20
  38. package/dist/hooks/useResource.js.map +1 -1
  39. package/dist/hooks/useResources.d.ts +1 -1
  40. package/dist/hooks/useResources.d.ts.map +1 -1
  41. package/dist/hooks/useResources.js +18 -27
  42. package/dist/hooks/useResources.js.map +1 -1
  43. package/dist/hooks/useTapHost.d.ts +21 -0
  44. package/dist/hooks/useTapHost.d.ts.map +1 -0
  45. package/dist/hooks/useTapHost.js +30 -0
  46. package/dist/hooks/useTapHost.js.map +1 -0
  47. package/dist/hooks/useTapRoot.d.ts +18 -0
  48. package/dist/hooks/useTapRoot.d.ts.map +1 -0
  49. package/dist/hooks/useTapRoot.js +77 -0
  50. package/dist/hooks/useTapRoot.js.map +1 -0
  51. package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
  52. package/dist/hooks/utils/depsShallowEqual.js +5 -2
  53. package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
  54. package/dist/hooks/utils/useCell.d.ts +2 -2
  55. package/dist/hooks/utils/useCell.d.ts.map +1 -1
  56. package/dist/hooks/utils/useCell.js.map +1 -1
  57. package/dist/hooks/utils/useDevStrictMode.d.ts +5 -0
  58. package/dist/hooks/utils/useDevStrictMode.d.ts.map +1 -0
  59. package/dist/hooks/utils/useDevStrictMode.js +25 -0
  60. package/dist/hooks/utils/useDevStrictMode.js.map +1 -0
  61. package/dist/hooks/utils/useRenderMemo.d.ts +5 -0
  62. package/dist/hooks/utils/useRenderMemo.d.ts.map +1 -0
  63. package/dist/hooks/utils/useRenderMemo.js +25 -0
  64. package/dist/hooks/utils/useRenderMemo.js.map +1 -0
  65. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts +10 -0
  66. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts.map +1 -0
  67. package/dist/hooks/utils/useResourceFiberHostUtils.js +46 -0
  68. package/dist/hooks/utils/useResourceFiberHostUtils.js.map +1 -0
  69. package/dist/index.d.ts +7 -4
  70. package/dist/index.js +7 -4
  71. package/dist/{hooks → react-hooks}/index.d.ts +6 -6
  72. package/dist/{hooks → react-hooks}/index.js +5 -5
  73. package/dist/{hooks → react-hooks}/use.d.ts +1 -1
  74. package/dist/{hooks → react-hooks}/use.d.ts.map +1 -1
  75. package/dist/{hooks → react-hooks}/use.js +1 -1
  76. package/dist/react-hooks/use.js.map +1 -0
  77. package/dist/{hooks → react-hooks}/useCallback.d.ts +1 -1
  78. package/dist/react-hooks/useCallback.d.ts.map +1 -0
  79. package/dist/{hooks → react-hooks}/useCallback.js +1 -1
  80. package/dist/react-hooks/useCallback.js.map +1 -0
  81. package/dist/{hooks → react-hooks}/useEffect.d.ts +1 -1
  82. package/dist/react-hooks/useEffect.d.ts.map +1 -0
  83. package/dist/react-hooks/useEffect.js +35 -0
  84. package/dist/react-hooks/useEffect.js.map +1 -0
  85. package/dist/{hooks → react-hooks}/useEffectEvent.d.ts +1 -1
  86. package/dist/react-hooks/useEffectEvent.d.ts.map +1 -0
  87. package/dist/{hooks → react-hooks}/useEffectEvent.js +2 -2
  88. package/dist/react-hooks/useEffectEvent.js.map +1 -0
  89. package/dist/{hooks → react-hooks}/useMemo.d.ts +1 -1
  90. package/dist/react-hooks/useMemo.d.ts.map +1 -0
  91. package/dist/{hooks → react-hooks}/useMemo.js +3 -3
  92. package/dist/react-hooks/useMemo.js.map +1 -0
  93. package/dist/{hooks → react-hooks}/useMemoCache.d.ts +1 -1
  94. package/dist/react-hooks/useMemoCache.d.ts.map +1 -0
  95. package/dist/{hooks → react-hooks}/useMemoCache.js +1 -1
  96. package/dist/react-hooks/useMemoCache.js.map +1 -0
  97. package/dist/react-hooks/useReducer.d.ts +9 -0
  98. package/dist/react-hooks/useReducer.d.ts.map +1 -0
  99. package/dist/react-hooks/useReducer.js +120 -0
  100. package/dist/react-hooks/useReducer.js.map +1 -0
  101. package/dist/{hooks → react-hooks}/useRef.d.ts +1 -1
  102. package/dist/react-hooks/useRef.d.ts.map +1 -0
  103. package/dist/{hooks → react-hooks}/useRef.js +1 -1
  104. package/dist/react-hooks/useRef.js.map +1 -0
  105. package/dist/{hooks → react-hooks}/useState.d.ts +1 -1
  106. package/dist/react-hooks/useState.d.ts.map +1 -0
  107. package/dist/{hooks → react-hooks}/useState.js +3 -3
  108. package/dist/react-hooks/useState.js.map +1 -0
  109. package/dist/react-shim/index.js +11 -11
  110. package/dist/react-shim/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/__tests__/basic/resourceHandle.test.ts +32 -22
  113. package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
  114. package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
  115. package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
  116. package/src/__tests__/basic/tapState.basic.test.ts +11 -11
  117. package/src/__tests__/bench/hosts.bench.tsx +124 -0
  118. package/src/__tests__/bench/tree.bench.tsx +166 -0
  119. package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
  120. package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
  121. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
  122. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
  123. package/src/__tests__/parity/describeParity.tsx +217 -0
  124. package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
  125. package/src/__tests__/parity/parity.basics.test.tsx +281 -0
  126. package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
  127. package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
  128. package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
  129. package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
  130. package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
  131. package/src/__tests__/react/react-shim.test.tsx +1 -1
  132. package/src/__tests__/react/useResource.test.tsx +41 -26
  133. package/src/__tests__/react/useTapHost.test.tsx +233 -0
  134. package/src/__tests__/react-dispatcher.test.ts +4 -4
  135. package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
  136. package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
  137. package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
  138. package/src/__tests__/strictmode/strictmode.test.ts +39 -209
  139. package/src/__tests__/test-utils.ts +33 -23
  140. package/src/core/ResourceFiber.ts +43 -35
  141. package/src/core/createTapRoot.ts +45 -0
  142. package/src/core/helpers/commit.ts +12 -2
  143. package/src/core/helpers/execution-context.ts +4 -13
  144. package/src/core/helpers/root.ts +24 -12
  145. package/src/core/react-dispatcher.ts +10 -9
  146. package/src/core/resource.ts +5 -20
  147. package/src/core/scheduler.ts +1 -1
  148. package/src/core/types.ts +27 -21
  149. package/src/hooks/useResource.ts +18 -27
  150. package/src/hooks/useResources.ts +18 -42
  151. package/src/hooks/useTapHost.ts +60 -0
  152. package/src/hooks/useTapRoot.ts +135 -0
  153. package/src/hooks/utils/depsShallowEqual.ts +12 -2
  154. package/src/hooks/utils/useCell.ts +2 -2
  155. package/src/hooks/utils/useDevStrictMode.ts +34 -0
  156. package/src/hooks/utils/useRenderMemo.ts +27 -0
  157. package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
  158. package/src/index.ts +6 -3
  159. package/src/{hooks → react-hooks}/index.ts +4 -4
  160. package/src/react-hooks/useEffect.ts +58 -0
  161. package/src/{hooks → react-hooks}/useMemo.ts +1 -1
  162. package/src/react-hooks/useReducer.ts +254 -0
  163. package/src/{hooks → react-hooks}/useState.ts +2 -2
  164. package/src/react-shim/index.ts +1 -1
  165. package/dist/core/createResourceRoot.d.ts +0 -11
  166. package/dist/core/createResourceRoot.d.ts.map +0 -1
  167. package/dist/core/createResourceRoot.js +0 -31
  168. package/dist/core/createResourceRoot.js.map +0 -1
  169. package/dist/core/helpers/callResourceFn.d.ts +0 -1
  170. package/dist/core/helpers/callResourceFn.js +0 -19
  171. package/dist/core/helpers/callResourceFn.js.map +0 -1
  172. package/dist/hooks/use.js.map +0 -1
  173. package/dist/hooks/useCallback.d.ts.map +0 -1
  174. package/dist/hooks/useCallback.js.map +0 -1
  175. package/dist/hooks/useEffect.d.ts.map +0 -1
  176. package/dist/hooks/useEffect.js +0 -40
  177. package/dist/hooks/useEffect.js.map +0 -1
  178. package/dist/hooks/useEffectEvent.d.ts.map +0 -1
  179. package/dist/hooks/useEffectEvent.js.map +0 -1
  180. package/dist/hooks/useMemo.d.ts.map +0 -1
  181. package/dist/hooks/useMemo.js.map +0 -1
  182. package/dist/hooks/useMemoCache.d.ts.map +0 -1
  183. package/dist/hooks/useMemoCache.js.map +0 -1
  184. package/dist/hooks/useReducer.d.ts +0 -21
  185. package/dist/hooks/useReducer.d.ts.map +0 -1
  186. package/dist/hooks/useReducer.js +0 -81
  187. package/dist/hooks/useReducer.js.map +0 -1
  188. package/dist/hooks/useRef.d.ts.map +0 -1
  189. package/dist/hooks/useRef.js.map +0 -1
  190. package/dist/hooks/useResourceRoot.d.ts +0 -20
  191. package/dist/hooks/useResourceRoot.d.ts.map +0 -1
  192. package/dist/hooks/useResourceRoot.js +0 -77
  193. package/dist/hooks/useResourceRoot.js.map +0 -1
  194. package/dist/hooks/useState.d.ts.map +0 -1
  195. package/dist/hooks/useState.js.map +0 -1
  196. package/dist/react/hooks.d.ts +0 -25
  197. package/dist/react/hooks.d.ts.map +0 -1
  198. package/dist/react/hooks.js +0 -69
  199. package/dist/react/hooks.js.map +0 -1
  200. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
  201. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
  202. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
  203. package/src/core/createResourceRoot.ts +0 -53
  204. package/src/core/helpers/callResourceFn.ts +0 -21
  205. package/src/hooks/useEffect.ts +0 -72
  206. package/src/hooks/useReducer.ts +0 -160
  207. package/src/hooks/useResourceRoot.ts +0 -130
  208. package/src/react/hooks.ts +0 -112
  209. /package/src/{hooks → react-hooks}/use.ts +0 -0
  210. /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
  211. /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
  212. /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
  213. /package/src/{hooks → react-hooks}/useRef.ts +0 -0
@@ -1,12 +1,17 @@
1
+ /**
2
+ * tap-only strict-mode behaviors: nested child resources and withKey identity.
3
+ * Children render inline during the parent's render (unlike React components),
4
+ * so these sequences have no React analog to compare against; everything that
5
+ * does is covered differentially in strictmode-parity.test.tsx.
6
+ */
7
+
1
8
  import { describe, it, expect } from "vitest";
2
9
  import { resource } from "../../core/resource";
3
10
  import { isDevelopment } from "../../core/helpers/env";
4
- import { useRef } from "../../hooks/useRef";
5
- import { useState } from "../../hooks/useState";
6
- import { useEffect } from "../../hooks/useEffect";
7
- import { useMemo } from "../../hooks/useMemo";
11
+ import { useState } from "../../react-hooks/useState";
12
+ import { useEffect } from "../../react-hooks/useEffect";
8
13
  import { useResource } from "../../hooks/useResource";
9
- import { createResourceRoot } from "../../core/createResourceRoot";
14
+ import { createTapRoot } from "../../core/createTapRoot";
10
15
  import { withKey } from "../../core/withKey";
11
16
 
12
17
  describe("Strict Mode", () => {
@@ -14,209 +19,29 @@ describe("Strict Mode", () => {
14
19
  expect(isDevelopment).toBe(true);
15
20
  });
16
21
 
17
- it("should persist useMemo cache across strict mode double render", () => {
18
- const events: string[] = [];
19
- let outerCount = 0;
20
- let memoCount = 0;
21
-
22
- const TestResource = resource(function TestResource() {
23
- const idx = outerCount++;
24
- events.push(`outer-${idx}`);
25
-
26
- useMemo(() => {
27
- events.push(`memo-${memoCount++}`);
28
- return {};
29
- }, []);
30
-
31
- events.push(`outerend-${idx}`);
32
- });
33
-
34
- const root = createResourceRoot();
35
- root.render(TestResource());
36
-
37
- console.log("Events:", events);
38
-
39
- // useMemo factory runs twice during first render (strict mode double-call)
40
- // but should NOT run during second render (cache should persist)
41
- expect(events).toEqual([
42
- "outer-0",
43
- "memo-0",
44
- "memo-1",
45
- "outerend-0",
46
- "outer-1",
47
- // no memo call here — cache should be reused
48
- "outerend-1",
49
- ]);
50
- });
51
-
52
- it("should double-invoke useMemo factory and use the first result", () => {
53
- const events: string[] = [];
54
- let memoCallCount = 0;
55
-
56
- const TestResource = resource(function TestResource() {
57
- const memoValue = useMemo(() => {
58
- memoCallCount++;
59
- events.push(`memo-${memoCallCount}`);
60
- return memoCallCount;
61
- }, []);
62
-
63
- events.push(`render memoValue=${memoValue}`);
64
- });
65
-
66
- const root = createResourceRoot();
67
- root.render(TestResource());
68
-
69
- // Matches React useMemo behavior: factory is double-invoked,
70
- // first result is kept
71
- expect(events).toEqual([
72
- "memo-1",
73
- "memo-2",
74
- "render memoValue=1",
75
- "render memoValue=1",
76
- ]);
77
- });
78
-
79
- it("should double-render on first render", () => {
80
- let renderCount = 0;
81
-
82
- const TestResource = resource(function TestResource() {
83
- renderCount++;
84
- return { renderCount };
85
- });
86
-
87
- const root = createResourceRoot();
88
- const sub = root.render(TestResource());
89
- const output = sub.getValue();
90
-
91
- expect(renderCount).toBe(2);
92
- expect(output.renderCount).toBe(2);
93
- });
94
-
95
- it("should double-call hook fns", () => {
96
- let renderCount = 0;
97
-
98
- const TestResource = resource(function TestResource() {
99
- const ref = useRef(0);
100
- const [count] = useState(() => {
101
- renderCount++;
102
- return ++ref.current;
103
- });
104
- const [count2] = useState(() => {
105
- renderCount++;
106
- return ++ref.current;
107
- });
108
-
109
- expect(count).toBe(1);
110
- expect(count2).toBe(3);
111
- expect(ref.current).toBe(4);
112
- });
113
-
114
- const root = createResourceRoot();
115
- root.render(TestResource());
116
-
117
- expect(renderCount).toBe(4);
118
- });
119
-
120
- it("should double-commit effects", () => {
121
- const events: string[] = [];
122
- const TestResource = resource(function TestResource() {
123
- const ref = useRef(0);
124
- ref.current++;
125
- const count = ref.current;
126
-
127
- useEffect(() => {
128
- events.push("mount-1");
129
-
130
- return () => {
131
- events.push("unmount-1");
132
- };
133
- });
134
-
135
- useEffect(() => {
136
- events.push("mount-2");
137
-
138
- return () => {
139
- events.push("unmount-2");
140
- };
141
- }, []);
142
-
143
- useEffect(() => {
144
- expect(count).toBe(2);
145
-
146
- events.push("mount-3");
147
-
148
- return () => {
149
- events.push("unmount-3");
150
- };
151
- }, [count]);
152
- });
153
-
154
- const root = createResourceRoot();
155
- root.render(TestResource());
156
-
157
- expect(events).toEqual([
158
- "mount-1",
159
- "mount-2",
160
- "mount-3",
161
- "unmount-1",
162
- "unmount-2",
163
- "unmount-3",
164
- "mount-1",
165
- "mount-2",
166
- "mount-3",
167
- ]);
168
- });
169
-
170
22
  it("should double-render on child render", () => {
171
23
  let renderCount = 0;
172
24
 
173
- const TestChildResource = resource(function TestChildResource() {
25
+ const useTestChildResource = () => {
174
26
  renderCount++;
175
27
  return { renderCount };
176
- });
28
+ };
29
+
30
+ const TestChildResource = resource(useTestChildResource);
177
31
 
178
- const TestResource = resource(function TestResource() {
32
+ const useTestResource = () => {
179
33
  return useResource(TestChildResource());
180
- });
34
+ };
181
35
 
182
- const root = createResourceRoot();
183
- const sub = root.render(TestResource());
36
+ const sub = createTapRoot(function Root() {
37
+ return useTestResource();
38
+ });
184
39
  const output = sub.getValue();
185
40
 
186
41
  expect(renderCount).toBe(2);
187
42
  expect(output.renderCount).toBe(2);
188
43
  });
189
44
 
190
- it("should double-mount before handling state updates", () => {
191
- const events: string[] = [];
192
- const TestResource = resource(function TestResource() {
193
- const [id, setId] = useState(0);
194
- events.push(`render-${id}`);
195
- useEffect(() => {
196
- events.push(`mount-${id}`);
197
- setId(1);
198
- return () => {
199
- events.push(`unmount-${id}`);
200
- };
201
- });
202
- });
203
-
204
- const root = createResourceRoot();
205
- root.render(TestResource());
206
-
207
- expect(events).toEqual([
208
- "render-0",
209
- "render-0",
210
- "mount-0",
211
- "unmount-0",
212
- "mount-0",
213
- "render-1",
214
- "render-1",
215
- "unmount-0",
216
- "mount-1",
217
- ]);
218
- });
219
-
220
45
  it("should double-render on child render change", () => {
221
46
  let renderCount = 0;
222
47
  let fnCount = 0;
@@ -228,7 +53,7 @@ describe("Strict Mode", () => {
228
53
  return renderCount;
229
54
  };
230
55
 
231
- const TestChildResource = resource(function TestChildResource() {
56
+ const useTestChildResource = () => {
232
57
  const [fnState] = useState(() => {
233
58
  fnCount++;
234
59
  return fnCount;
@@ -244,18 +69,21 @@ describe("Strict Mode", () => {
244
69
  };
245
70
  }, [fnState, count]);
246
71
  return { renderCount, fnCount, fnState };
247
- });
72
+ };
248
73
 
249
- const TestResource = resource(function TestResource() {
74
+ const TestChildResource = resource(useTestChildResource);
75
+
76
+ const useTestResource = () => {
250
77
  const [id, setId] = useState(0);
251
78
  useEffect(() => {
252
79
  setId(1);
253
80
  });
254
81
  return useResource(withKey(id, TestChildResource()));
255
- });
82
+ };
256
83
 
257
- const root = createResourceRoot();
258
- const sub = root.render(TestResource());
84
+ const sub = createTapRoot(function Root() {
85
+ return useTestResource();
86
+ });
259
87
  const output = sub.getValue();
260
88
 
261
89
  expect(renderCount).toBe(4);
@@ -267,10 +95,10 @@ describe("Strict Mode", () => {
267
95
  expect(unmountCount).toBe(3);
268
96
  });
269
97
 
270
- it("should double-render on child render change", () => {
98
+ it("should sequence child remounts on key change", () => {
271
99
  let renderCount = 0;
272
100
  const events: string[] = [];
273
- const TestChildResource = resource(function TestChildResource() {
101
+ const useTestChildResource = () => {
274
102
  renderCount++;
275
103
  events.push(`render-${renderCount}`);
276
104
 
@@ -285,9 +113,11 @@ describe("Strict Mode", () => {
285
113
  events.push(`unmount-${count}`);
286
114
  };
287
115
  });
288
- });
116
+ };
289
117
 
290
- const TestResource = resource(function TestResource() {
118
+ const TestChildResource = resource(useTestChildResource);
119
+
120
+ const useTestResource = () => {
291
121
  const [id, setId] = useState(0);
292
122
  events.push(`outer-render-${id}`);
293
123
  useEffect(() => {
@@ -299,10 +129,11 @@ describe("Strict Mode", () => {
299
129
  };
300
130
  });
301
131
  return useResource(withKey(id, TestChildResource()));
302
- });
132
+ };
303
133
 
304
- const root = createResourceRoot();
305
- root.render(TestResource());
134
+ createTapRoot(function Root() {
135
+ return useTestResource();
136
+ });
306
137
 
307
138
  expect(events).toEqual([
308
139
  "outer-render-0",
@@ -324,12 +155,11 @@ describe("Strict Mode", () => {
324
155
  "outer-render-1",
325
156
  "render-4",
326
157
  "outer-unmount-0",
327
- "outer-mount-1",
328
158
  "unmount-2",
159
+ "outer-mount-1",
329
160
  "mount-4",
330
161
  "unmount-4",
331
162
  "mount-4",
332
163
  ]);
333
- // expect(renderCount).toBe(4);
334
164
  });
335
165
  });
@@ -1,5 +1,4 @@
1
1
  import { createResourceFiberRoot } from "../core/helpers/root";
2
- import { resource } from "../core/resource";
3
2
  import {
4
3
  createResourceFiber,
5
4
  unmountResourceFiber,
@@ -7,29 +6,33 @@ import {
7
6
  commitResourceFiber,
8
7
  } from "../core/ResourceFiber";
9
8
  import type { ResourceFiber } from "../core/types";
10
- import { useState } from "../hooks/useState";
9
+ import { useState } from "../react-hooks/useState";
11
10
 
12
11
  /**
13
12
  * Creates a test resource fiber for unit testing.
14
13
  * This is a low-level utility that creates a ResourceFiber directly.
15
14
  * Sets up a rerender callback that automatically re-renders when state changes.
16
15
  */
17
- export function createTestResource<R, P>(fn: (props: P) => R) {
16
+ export function createTestResource<R, A extends readonly unknown[]>(
17
+ fn: (...args: A) => R,
18
+ ) {
18
19
  const rerenderCallback = (callback: () => boolean) => {
19
20
  if (!callback()) return;
20
21
 
21
22
  // Re-render when state changes
22
23
  if (activeResources.has(fiber)) {
23
- const lastProps = propsMap.get(fiber);
24
- const result = renderResourceFiber(fiber, lastProps);
24
+ const lastArgs = propsMap.get(fiber);
25
+ const result = renderResourceFiber(fiber, lastArgs);
25
26
  commitResourceFiber(fiber, result);
26
27
  lastRenderResultMap.set(fiber, result);
27
28
  }
28
29
  };
29
30
 
30
31
  const fiber = createResourceFiber(
31
- resource(fn),
32
+ fn,
32
33
  createResourceFiberRoot(rerenderCallback),
34
+ undefined,
35
+ null,
33
36
  );
34
37
  return fiber;
35
38
  }
@@ -44,26 +47,31 @@ const lastRenderResultMap = new WeakMap<ResourceFiber<any, any>, any>();
44
47
  * - Tracks resources for cleanup
45
48
  * - Returns the current state after render
46
49
  */
47
- export function renderTest<R, P>(fiber: ResourceFiber<R, P>, props: P): R {
48
- propsMap.set(fiber, props);
50
+ export function renderTest<R, A extends readonly unknown[]>(
51
+ fiber: ResourceFiber<R, A>,
52
+ ...args: A
53
+ ): R {
54
+ propsMap.set(fiber, args);
49
55
 
50
56
  // Track resource for cleanup
51
57
  activeResources.add(fiber);
52
58
 
53
- // Render with new props
54
- const result = renderResourceFiber(fiber, props);
59
+ // Render with new args
60
+ const result = renderResourceFiber(fiber, args);
55
61
  commitResourceFiber(fiber, result);
56
62
  lastRenderResultMap.set(fiber, result);
57
63
 
58
64
  // Return the committed state from the result
59
65
  // This accounts for any re-renders that happened during commit
60
- return result.output;
66
+ return result.value;
61
67
  }
62
68
 
63
69
  /**
64
70
  * Unmounts a specific resource fiber and removes it from tracking.
65
71
  */
66
- export function unmountResource<R, P>(fiber: ResourceFiber<R, P>) {
72
+ export function unmountResource<R, A extends readonly unknown[]>(
73
+ fiber: ResourceFiber<R, A>,
74
+ ) {
67
75
  if (activeResources.has(fiber)) {
68
76
  unmountResourceFiber(fiber);
69
77
  activeResources.delete(fiber);
@@ -82,14 +90,16 @@ export function cleanupAllResources() {
82
90
  * Gets the current committed state of a resource fiber.
83
91
  * Returns the state from the last render/commit cycle.
84
92
  */
85
- export function getCommittedOutput<R, P>(fiber: ResourceFiber<R, P>): R {
93
+ export function getCommittedOutput<R, A extends readonly unknown[]>(
94
+ fiber: ResourceFiber<R, A>,
95
+ ): R {
86
96
  const lastResult = lastRenderResultMap.get(fiber);
87
97
  if (!lastResult) {
88
98
  throw new Error(
89
99
  "No render result found for fiber. Make sure to call renderResource first.",
90
100
  );
91
101
  }
92
- return lastResult.output;
102
+ return lastResult.value;
93
103
  }
94
104
 
95
105
  /**
@@ -104,10 +114,10 @@ export class TestSubscriber<T> {
104
114
  constructor(fiber: ResourceFiber<any, any>) {
105
115
  this.fiber = fiber;
106
116
  // Need to render once to get initial state
107
- const lastProps = propsMap.get(fiber) ?? undefined;
108
- const initialResult = renderResourceFiber(fiber, lastProps as any);
117
+ const lastArgs = propsMap.get(fiber) ?? [];
118
+ const initialResult = renderResourceFiber(fiber, lastArgs as any);
109
119
  commitResourceFiber(fiber, initialResult);
110
- this.lastState = initialResult.output;
120
+ this.lastState = initialResult.value;
111
121
  lastRenderResultMap.set(fiber, initialResult);
112
122
  activeResources.add(fiber);
113
123
  }
@@ -124,23 +134,23 @@ export class TestSubscriber<T> {
124
134
  * Helper class to manage resource lifecycle in tests with explicit control.
125
135
  * Useful when you need fine-grained control over mount/unmount timing.
126
136
  */
127
- export class TestResourceManager<R, P> {
137
+ export class TestResourceManager<R, A extends readonly unknown[]> {
128
138
  private isActive = false;
129
139
 
130
- constructor(public fiber: ResourceFiber<R, P>) {}
140
+ constructor(public fiber: ResourceFiber<R, A>) {}
131
141
 
132
- renderAndMount(props: P): R {
142
+ renderAndMount(...args: A): R {
133
143
  if (this.isActive) {
134
144
  throw new Error("Resource already active");
135
145
  }
136
146
 
137
147
  this.isActive = true;
138
148
  activeResources.add(this.fiber);
139
- propsMap.set(this.fiber, props);
140
- const result = renderResourceFiber(this.fiber, props);
149
+ propsMap.set(this.fiber, args);
150
+ const result = renderResourceFiber(this.fiber, args);
141
151
  commitResourceFiber(this.fiber, result);
142
152
  lastRenderResultMap.set(this.fiber, result);
143
- return result.output;
153
+ return result.value;
144
154
  }
145
155
 
146
156
  cleanup() {
@@ -1,30 +1,22 @@
1
- import type {
2
- ResourceFiber,
3
- RenderResult,
4
- Resource,
5
- ResourceFiberRoot,
6
- } from "./types";
1
+ import type { ResourceFiber, RenderResult, ResourceFiberRoot } from "./types";
7
2
  import { commitAllEffects, cleanupAllEffects } from "./helpers/commit";
8
- import {
9
- getDevStrictMode,
10
- withResourceFiber,
11
- } from "./helpers/execution-context";
12
- import { callResourceFn } from "./helpers/callResourceFn";
3
+ import { withResourceFiber } from "./helpers/execution-context";
13
4
  import { withReactDispatcher } from "./react-dispatcher";
14
5
  import { isDevelopment } from "./helpers/env";
15
6
 
16
- export function createResourceFiber<R, P>(
17
- type: Resource<R, P>,
7
+ export function createResourceFiber<R, A extends readonly unknown[]>(
8
+ hook: (...args: A) => R,
18
9
  root: ResourceFiberRoot,
19
10
  markDirty: (() => void) | undefined = undefined,
20
- strictMode: "root" | "child" | null = getDevStrictMode(false),
21
- ): ResourceFiber<R, P> {
11
+ strictMode: "root" | "child" | null,
12
+ ): ResourceFiber<R, A> {
22
13
  return {
23
- type,
14
+ hook,
24
15
  root,
25
16
  markDirty,
26
17
  devStrictMode: strictMode,
27
18
  cells: [],
19
+ renderPendingCells: null,
28
20
  currentIndex: 0,
29
21
  renderContext: undefined,
30
22
  isFirstRender: true,
@@ -33,7 +25,9 @@ export function createResourceFiber<R, P>(
33
25
  };
34
26
  }
35
27
 
36
- export function unmountResourceFiber<R, P>(fiber: ResourceFiber<R, P>): void {
28
+ export function unmountResourceFiber<R, A extends readonly unknown[]>(
29
+ fiber: ResourceFiber<R, A>,
30
+ ): void {
37
31
  if (!fiber.isMounted)
38
32
  throw new Error("Tried to unmount a fiber that is already unmounted");
39
33
 
@@ -41,32 +35,46 @@ export function unmountResourceFiber<R, P>(fiber: ResourceFiber<R, P>): void {
41
35
  cleanupAllEffects(fiber);
42
36
  }
43
37
 
44
- export function renderResourceFiber<R, P>(
45
- fiber: ResourceFiber<R, P>,
46
- props: P,
38
+ export function renderResourceFiber<R, A extends readonly unknown[]>(
39
+ fiber: ResourceFiber<R, A>,
40
+ args: Readonly<A>,
47
41
  ): RenderResult {
48
- const result = {
49
- effectTasks: [],
50
- props,
51
- output: undefined as R | undefined,
52
- };
42
+ // Discard render-phase actions left by a previous render
43
+ if (fiber.renderPendingCells !== null) {
44
+ for (const cell of fiber.renderPendingCells) cell.renderQueue = null;
45
+ fiber.renderPendingCells.clear();
46
+ }
53
47
 
54
- withResourceFiber(fiber, () => {
55
- fiber.renderContext = result;
56
- try {
57
- result.output = withReactDispatcher(() =>
58
- callResourceFn(fiber.type, props),
48
+ let passes = 0;
49
+ let result: RenderResult;
50
+ do {
51
+ if (++passes > 25) {
52
+ throw new Error(
53
+ "Too many re-renders. tap limits the number of renders to prevent " +
54
+ "an infinite loop.",
59
55
  );
60
- } finally {
61
- fiber.renderContext = undefined;
62
56
  }
63
- });
57
+
58
+ result = {
59
+ effectTasks: [],
60
+ value: undefined as R | undefined,
61
+ };
62
+
63
+ withResourceFiber(fiber, () => {
64
+ fiber.renderContext = result;
65
+ try {
66
+ result.value = withReactDispatcher(() => fiber.hook(...args));
67
+ } finally {
68
+ fiber.renderContext = undefined;
69
+ }
70
+ });
71
+ } while ((fiber.renderPendingCells?.size ?? 0) > 0);
64
72
 
65
73
  return result;
66
74
  }
67
75
 
68
- export function commitResourceFiber<R, P>(
69
- fiber: ResourceFiber<R, P>,
76
+ export function commitResourceFiber<R, A extends readonly unknown[]>(
77
+ fiber: ResourceFiber<R, A>,
70
78
  result: RenderResult,
71
79
  ): void {
72
80
  fiber.isMounted = true;
@@ -0,0 +1,45 @@
1
+ import {
2
+ createResourceFiber,
3
+ unmountResourceFiber,
4
+ renderResourceFiber,
5
+ commitResourceFiber,
6
+ } from "./ResourceFiber";
7
+ import { useTapRoot } from "../hooks/useTapRoot";
8
+ import { isDevelopment } from "./helpers/env";
9
+ import { flushTapSync, UpdateScheduler } from "./scheduler";
10
+ import { createResourceFiberRoot } from "./helpers/root";
11
+
12
+ export const createTapRoot = <R>(
13
+ render: () => R,
14
+ ): useTapRoot.Root<R> & { unmount: () => void } => {
15
+ const fiber = createResourceFiber(
16
+ useTapRoot,
17
+ createResourceFiberRoot((callback) => {
18
+ new UpdateScheduler(() => {
19
+ if (callback()) {
20
+ throw new Error("Unexpected rerender of createTapRoot outer fiber");
21
+ }
22
+ return false;
23
+ }).markDirty();
24
+ }),
25
+ undefined,
26
+ isDevelopment ? "root" : null,
27
+ );
28
+
29
+ // In strict mode, render twice to detect side effects
30
+ if (isDevelopment && fiber.devStrictMode === "root") {
31
+ void renderResourceFiber(fiber, [render]);
32
+ }
33
+
34
+ const rendered = renderResourceFiber(fiber, [render]);
35
+ flushTapSync(() => commitResourceFiber(fiber, rendered));
36
+
37
+ const root = rendered.value as useTapRoot.Root<R>;
38
+
39
+ return {
40
+ ...root,
41
+ unmount: () => {
42
+ unmountResourceFiber(fiber);
43
+ },
44
+ };
45
+ };
@@ -5,7 +5,15 @@ export function commitAllEffects(renderResult: RenderResult): void {
5
5
 
6
6
  for (const task of renderResult.effectTasks) {
7
7
  try {
8
- task();
8
+ task.cleanup();
9
+ } catch (error) {
10
+ errors.push(error);
11
+ }
12
+ }
13
+
14
+ for (const task of renderResult.effectTasks) {
15
+ try {
16
+ task.setup();
9
17
  } catch (error) {
10
18
  errors.push(error);
11
19
  }
@@ -23,7 +31,9 @@ export function commitAllEffects(renderResult: RenderResult): void {
23
31
  }
24
32
  }
25
33
 
26
- export function cleanupAllEffects<R, P>(executionContext: ResourceFiber<R, P>) {
34
+ export function cleanupAllEffects<R, A extends readonly unknown[]>(
35
+ executionContext: ResourceFiber<R, A>,
36
+ ) {
27
37
  const errors: unknown[] = [];
28
38
  for (const cell of executionContext.cells) {
29
39
  if (cell?.type === "effect") {