@assistant-ui/tap 0.6.0 → 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 (215) 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 +17 -16
  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.d.ts +8 -10
  110. package/dist/react-shim/index.d.ts.map +1 -1
  111. package/dist/react-shim/index.js +19 -19
  112. package/dist/react-shim/index.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/__tests__/basic/resourceHandle.test.ts +32 -22
  115. package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
  116. package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
  117. package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
  118. package/src/__tests__/basic/tapState.basic.test.ts +11 -11
  119. package/src/__tests__/bench/hosts.bench.tsx +124 -0
  120. package/src/__tests__/bench/tree.bench.tsx +166 -0
  121. package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
  122. package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
  123. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
  124. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
  125. package/src/__tests__/parity/describeParity.tsx +217 -0
  126. package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
  127. package/src/__tests__/parity/parity.basics.test.tsx +281 -0
  128. package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
  129. package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
  130. package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
  131. package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
  132. package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
  133. package/src/__tests__/react/react-shim.test.tsx +1 -1
  134. package/src/__tests__/react/useResource.test.tsx +41 -26
  135. package/src/__tests__/react/useTapHost.test.tsx +233 -0
  136. package/src/__tests__/react-dispatcher.test.ts +4 -4
  137. package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
  138. package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
  139. package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
  140. package/src/__tests__/strictmode/strictmode.test.ts +39 -209
  141. package/src/__tests__/test-utils.ts +33 -23
  142. package/src/core/ResourceFiber.ts +43 -35
  143. package/src/core/createTapRoot.ts +45 -0
  144. package/src/core/helpers/commit.ts +12 -2
  145. package/src/core/helpers/execution-context.ts +4 -13
  146. package/src/core/helpers/root.ts +24 -12
  147. package/src/core/react-dispatcher.ts +14 -13
  148. package/src/core/resource.ts +5 -20
  149. package/src/core/scheduler.ts +1 -1
  150. package/src/core/types.ts +27 -21
  151. package/src/hooks/useResource.ts +18 -27
  152. package/src/hooks/useResources.ts +18 -42
  153. package/src/hooks/useTapHost.ts +60 -0
  154. package/src/hooks/useTapRoot.ts +135 -0
  155. package/src/hooks/utils/depsShallowEqual.ts +12 -2
  156. package/src/hooks/utils/useCell.ts +2 -2
  157. package/src/hooks/utils/useDevStrictMode.ts +34 -0
  158. package/src/hooks/utils/useRenderMemo.ts +27 -0
  159. package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
  160. package/src/index.ts +6 -3
  161. package/src/{hooks → react-hooks}/index.ts +4 -4
  162. package/src/react-hooks/useEffect.ts +58 -0
  163. package/src/{hooks → react-hooks}/useMemo.ts +1 -1
  164. package/src/react-hooks/useReducer.ts +254 -0
  165. package/src/{hooks → react-hooks}/useState.ts +2 -2
  166. package/src/react-shim/index.ts +24 -13
  167. package/dist/core/createResourceRoot.d.ts +0 -11
  168. package/dist/core/createResourceRoot.d.ts.map +0 -1
  169. package/dist/core/createResourceRoot.js +0 -31
  170. package/dist/core/createResourceRoot.js.map +0 -1
  171. package/dist/core/helpers/callResourceFn.d.ts +0 -1
  172. package/dist/core/helpers/callResourceFn.js +0 -19
  173. package/dist/core/helpers/callResourceFn.js.map +0 -1
  174. package/dist/hooks/use.js.map +0 -1
  175. package/dist/hooks/useCallback.d.ts.map +0 -1
  176. package/dist/hooks/useCallback.js.map +0 -1
  177. package/dist/hooks/useEffect.d.ts.map +0 -1
  178. package/dist/hooks/useEffect.js +0 -40
  179. package/dist/hooks/useEffect.js.map +0 -1
  180. package/dist/hooks/useEffectEvent.d.ts.map +0 -1
  181. package/dist/hooks/useEffectEvent.js.map +0 -1
  182. package/dist/hooks/useMemo.d.ts.map +0 -1
  183. package/dist/hooks/useMemo.js.map +0 -1
  184. package/dist/hooks/useMemoCache.d.ts.map +0 -1
  185. package/dist/hooks/useMemoCache.js.map +0 -1
  186. package/dist/hooks/useReducer.d.ts +0 -21
  187. package/dist/hooks/useReducer.d.ts.map +0 -1
  188. package/dist/hooks/useReducer.js +0 -81
  189. package/dist/hooks/useReducer.js.map +0 -1
  190. package/dist/hooks/useRef.d.ts.map +0 -1
  191. package/dist/hooks/useRef.js.map +0 -1
  192. package/dist/hooks/useResourceRoot.d.ts +0 -20
  193. package/dist/hooks/useResourceRoot.d.ts.map +0 -1
  194. package/dist/hooks/useResourceRoot.js +0 -77
  195. package/dist/hooks/useResourceRoot.js.map +0 -1
  196. package/dist/hooks/useState.d.ts.map +0 -1
  197. package/dist/hooks/useState.js.map +0 -1
  198. package/dist/react/hooks.d.ts +0 -25
  199. package/dist/react/hooks.d.ts.map +0 -1
  200. package/dist/react/hooks.js +0 -69
  201. package/dist/react/hooks.js.map +0 -1
  202. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
  203. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
  204. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
  205. package/src/core/createResourceRoot.ts +0 -53
  206. package/src/core/helpers/callResourceFn.ts +0 -21
  207. package/src/hooks/useEffect.ts +0 -72
  208. package/src/hooks/useReducer.ts +0 -160
  209. package/src/hooks/useResourceRoot.ts +0 -130
  210. package/src/react/hooks.ts +0 -112
  211. /package/src/{hooks → react-hooks}/use.ts +0 -0
  212. /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
  213. /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
  214. /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
  215. /package/src/{hooks → react-hooks}/useRef.ts +0 -0
@@ -1,687 +0,0 @@
1
- /**
2
- * Tests to verify when tap strict mode causes double-rendering
3
- * These tests should mirror the React strict mode behavior
4
- */
5
- /* oxlint-disable react/exhaustive-deps -- empty dep arrays are part of the test scenarios */
6
-
7
- import { afterEach, describe, it, expect, vi } from "vitest";
8
- import { resource } from "../../core/resource";
9
- import { useState } from "../../hooks/useState";
10
- import { useEffect } from "../../hooks/useEffect";
11
- import { createResourceRoot } from "../../core/createResourceRoot";
12
- import { flushResourcesSync } from "../../core/scheduler";
13
-
14
- describe("Tap Strict Mode - Rerender Sources", () => {
15
- describe("Callback invocation count", () => {
16
- it("should use the first return value when updater returns different values", () => {
17
- const events: string[] = [];
18
- let updaterCallCount = 0;
19
-
20
- const TestResource = resource(function TestResource() {
21
- const [count, setCount] = useState(0);
22
- events.push(`render count=${count}`);
23
-
24
- useEffect(() => {
25
- events.push("effect mount");
26
- setCount((prev) => {
27
- updaterCallCount++;
28
- events.push(`updater call #${updaterCallCount} with prev=${prev}`);
29
- // Return different values on each call
30
- if (updaterCallCount === 1) {
31
- return 100; // First call returns 100
32
- }
33
- return 200; // Second call returns 200
34
- });
35
-
36
- return () => {
37
- events.push("effect cleanup");
38
- };
39
- }, []);
40
-
41
- return { count };
42
- });
43
-
44
- const root = createResourceRoot();
45
- root.render(TestResource());
46
-
47
- // Tap behavior: updater called 4 times, uses FIRST return value per dispatch
48
- // Effect #1 dispatch: updater(0) → 100 (kept)
49
- // Effect #1 cleanup, Effect #2 mount
50
- // Effect #2 dispatch: updater(0) → 200 (kept... but wait, prev=100 from effect #1)
51
- // Updater double-invoke happens per-dispatch (matching React ordering)
52
- expect(updaterCallCount).toBe(4);
53
- expect(events).toEqual([
54
- "render count=0",
55
- "render count=0",
56
- "effect mount",
57
- "updater call #1 with prev=0",
58
- "effect cleanup",
59
- "effect mount",
60
- "updater call #2 with prev=0",
61
- "updater call #3 with prev=100",
62
- "updater call #4 with prev=100",
63
- "render count=200",
64
- "render count=200",
65
- ]);
66
- });
67
- });
68
-
69
- describe("Source 1: Initial render", () => {
70
- it("should double-render on initial mount", () => {
71
- const events: string[] = [];
72
-
73
- const TestResource = resource(function TestResource() {
74
- const [count] = useState(0);
75
- events.push(`render count=${count}`);
76
- return { count };
77
- });
78
-
79
- const root = createResourceRoot();
80
- root.render(TestResource());
81
-
82
- expect(events).toEqual(["render count=0", "render count=0"]);
83
- });
84
- });
85
-
86
- describe("Source 2: setState in useEffect", () => {
87
- it("should double-render after setState in useEffect", () => {
88
- const events: string[] = [];
89
-
90
- const TestResource = resource(function TestResource() {
91
- const [count, setCount] = useState(0);
92
- events.push(`render count=${count}`);
93
-
94
- useEffect(() => {
95
- events.push(`effect count=${count}`);
96
- if (count === 0) {
97
- setCount(1);
98
- }
99
- return () => {
100
- events.push(`cleanup count=${count}`);
101
- };
102
- }, [count]);
103
-
104
- return { count };
105
- });
106
-
107
- const root = createResourceRoot();
108
- root.render(TestResource());
109
-
110
- expect(events).toEqual([
111
- "render count=0",
112
- "render count=0",
113
- "effect count=0",
114
- "cleanup count=0",
115
- "effect count=0",
116
- "render count=1",
117
- "render count=1",
118
- "cleanup count=0",
119
- "effect count=1",
120
- ]);
121
- });
122
- });
123
-
124
- describe("Source 3: setState in flushResourcesSync (event handler analogue)", () => {
125
- it("should ALSO double-render after setState in flushResourcesSync", () => {
126
- const events: string[] = [];
127
-
128
- const TestResource = resource(function TestResource() {
129
- const [count, setCount] = useState(0);
130
- events.push(`render count=${count}`);
131
-
132
- return {
133
- count,
134
- increment: () => {
135
- events.push("increment");
136
- setCount(count + 1);
137
- },
138
- };
139
- });
140
-
141
- const root = createResourceRoot();
142
- const sub = root.render(TestResource());
143
-
144
- // Initial render is double
145
- expect(events).toEqual(["render count=0", "render count=0"]);
146
-
147
- events.length = 0; // Clear events
148
-
149
- // Call the method inside flushResourcesSync (like clicking a button)
150
- flushResourcesSync(() => {
151
- sub.getValue().increment();
152
- });
153
-
154
- // flushResourcesSync setState should ALSO double-render (matching React 19)
155
- expect(events).toEqual(["increment", "render count=1", "render count=1"]);
156
- });
157
-
158
- it("should double-render on ALL flushResourcesSync calls", () => {
159
- const events: string[] = [];
160
-
161
- const TestResource = resource(function TestResource() {
162
- const [count, setCount] = useState(0);
163
- events.push(`render count=${count}`);
164
-
165
- return {
166
- count,
167
- increment: () => {
168
- events.push("increment");
169
- setCount((c) => c + 1);
170
- },
171
- };
172
- });
173
-
174
- const root = createResourceRoot();
175
- const sub = root.render(TestResource());
176
-
177
- events.length = 0; // Clear initial renders
178
-
179
- // Multiple flushResourcesSync calls (like multiple button clicks)
180
- flushResourcesSync(() => {
181
- sub.getValue().increment();
182
- });
183
- flushResourcesSync(() => {
184
- sub.getValue().increment();
185
- });
186
- flushResourcesSync(() => {
187
- sub.getValue().increment();
188
- });
189
-
190
- // Each call should cause double render
191
- expect(events).toEqual([
192
- "increment",
193
- "render count=1",
194
- "render count=1",
195
- "increment",
196
- "render count=2",
197
- "render count=2",
198
- "increment",
199
- "render count=3",
200
- "render count=3",
201
- ]);
202
- });
203
- });
204
-
205
- describe("Source 4: setState in setTimeout", () => {
206
- afterEach(() => {
207
- vi.useRealTimers();
208
- });
209
-
210
- it("should double-render AND double-call setTimeout callback", async () => {
211
- vi.useFakeTimers();
212
-
213
- const events: string[] = [];
214
-
215
- const TestResource = resource(function TestResource() {
216
- const [count, setCount] = useState(0);
217
- events.push(`render count=${count}`);
218
-
219
- useEffect(() => {
220
- if (count === 0) {
221
- setTimeout(() => {
222
- events.push("setTimeout");
223
- setCount(1);
224
- }, 10);
225
- }
226
- }, [count]);
227
-
228
- return { count };
229
- });
230
-
231
- const root = createResourceRoot();
232
- root.render(TestResource());
233
-
234
- // Fire both setTimeout callbacks synchronously via fake timers
235
- vi.advanceTimersByTime(10);
236
- // Restore real timers and wait for the scheduler flush (via MessageChannel)
237
- vi.useRealTimers();
238
- await new Promise((resolve) => setTimeout(resolve, 50));
239
-
240
- // React behavior: setTimeout callbacks run TWICE, then renders double
241
- expect(events).toEqual([
242
- "render count=0",
243
- "render count=0",
244
- "setTimeout",
245
- "setTimeout",
246
- "render count=1",
247
- "render count=1",
248
- ]);
249
- });
250
- });
251
-
252
- describe("Source 5: setState in Promise/async", () => {
253
- it("should double-render AND double-call Promise callback", async () => {
254
- const events: string[] = [];
255
-
256
- const TestResource = resource(function TestResource() {
257
- const [count, setCount] = useState(0);
258
- events.push(`render count=${count}`);
259
-
260
- useEffect(() => {
261
- if (count === 0) {
262
- Promise.resolve().then(() => {
263
- events.push("promise");
264
- setCount(1);
265
- });
266
- }
267
- }, [count]);
268
-
269
- return { count };
270
- });
271
-
272
- const root = createResourceRoot();
273
- root.render(TestResource());
274
-
275
- // Wait for promise
276
- await new Promise((resolve) => setTimeout(resolve, 10));
277
-
278
- // Promise callback should run TWICE and renders should be DOUBLED
279
- expect(events).toEqual([
280
- "render count=0",
281
- "render count=0",
282
- "promise",
283
- "promise",
284
- "render count=1",
285
- "render count=1",
286
- ]);
287
- });
288
- });
289
-
290
- describe("Source 6: Multiple setState calls", () => {
291
- it("should batch multiple setState calls in flushResourcesSync (single double-render)", () => {
292
- const events: string[] = [];
293
-
294
- const TestResource = resource(function TestResource() {
295
- const [count1, setCount1] = useState(0);
296
- const [count2, setCount2] = useState(0);
297
- events.push(`render count1=${count1} count2=${count2}`);
298
-
299
- return {
300
- updateBoth: () => {
301
- events.push("updateBoth");
302
- setCount1(1);
303
- setCount2(2);
304
- },
305
- };
306
- });
307
-
308
- const root = createResourceRoot();
309
- const sub = root.render(TestResource());
310
-
311
- events.length = 0; // Clear initial renders
312
-
313
- flushResourcesSync(() => {
314
- sub.getValue().updateBoth();
315
- });
316
-
317
- // Both setState calls batched, but render is DOUBLED
318
- expect(events).toEqual([
319
- "updateBoth",
320
- "render count1=1 count2=2",
321
- "render count1=1 count2=2",
322
- ]);
323
- });
324
-
325
- it("should batch multiple setState calls in useEffect (single double-render)", () => {
326
- const events: string[] = [];
327
-
328
- const TestResource = resource(function TestResource() {
329
- const [count1, setCount1] = useState(0);
330
- const [count2, setCount2] = useState(0);
331
- events.push(`render count1=${count1} count2=${count2}`);
332
-
333
- useEffect(() => {
334
- if (count1 === 0 && count2 === 0) {
335
- setCount1(1);
336
- setCount2(2);
337
- }
338
- }, [count1, count2]);
339
-
340
- return {};
341
- });
342
-
343
- const root = createResourceRoot();
344
- root.render(TestResource());
345
-
346
- // Initial double-render, then batched setState causes another double-render
347
- expect(events).toEqual([
348
- "render count1=0 count2=0",
349
- "render count1=0 count2=0",
350
- "render count1=1 count2=2",
351
- "render count1=1 count2=2",
352
- ]);
353
- });
354
- });
355
-
356
- describe("Source 7: Simple resource double-render", () => {
357
- it("should double-render simple resources", () => {
358
- const events: string[] = [];
359
-
360
- const TestResource = resource(function TestResource() {
361
- const [count, setCount] = useState(0);
362
- events.push(`render count=${count}`);
363
-
364
- return {
365
- count,
366
- increment: () => setCount((c) => c + 1),
367
- };
368
- });
369
-
370
- const root = createResourceRoot();
371
- root.render(TestResource());
372
-
373
- // Resource renders should be doubled
374
- expect(events).toEqual(["render count=0", "render count=0"]);
375
- });
376
- });
377
-
378
- describe("Source 8: setState with function updater", () => {
379
- it("should double-render with function updater in flushResourcesSync", () => {
380
- const events: string[] = [];
381
-
382
- const TestResource = resource(function TestResource() {
383
- const [count, setCount] = useState(0);
384
- events.push(`render count=${count}`);
385
-
386
- return {
387
- count,
388
- increment: () => {
389
- events.push("increment");
390
- setCount((prevCount) => {
391
- events.push(`updater prevCount=${prevCount}`);
392
- return prevCount + 1;
393
- });
394
- },
395
- };
396
- });
397
-
398
- const root = createResourceRoot();
399
- const sub = root.render(TestResource());
400
-
401
- events.length = 0; // Clear initial renders
402
-
403
- flushResourcesSync(() => {
404
- sub.getValue().increment();
405
- });
406
-
407
- // React behavior: Updater function is called TWICE in strict mode
408
- expect(events).toEqual([
409
- "increment",
410
- "updater prevCount=0",
411
- "updater prevCount=0",
412
- "render count=1",
413
- "render count=1",
414
- ]);
415
- });
416
- });
417
-
418
- describe("Source 9: Complex effect patterns", () => {
419
- it("should handle effect with dependencies and setState", () => {
420
- const events: string[] = [];
421
-
422
- const TestResource = resource(function TestResource() {
423
- const [count, setCount] = useState(0);
424
- const [doubled, setDoubled] = useState(0);
425
- events.push(`render count=${count} doubled=${doubled}`);
426
-
427
- useEffect(() => {
428
- events.push(`effect count=${count}`);
429
- setDoubled(count * 2);
430
- return () => {
431
- events.push(`cleanup count=${count}`);
432
- };
433
- }, [count]);
434
-
435
- return {
436
- count,
437
- increment: () => setCount((c) => c + 1),
438
- };
439
- });
440
-
441
- const root = createResourceRoot();
442
- const sub = root.render(TestResource());
443
-
444
- // setDoubled(0*2) = setDoubled(0) is a no-op, so no extra render
445
- expect(events).toEqual([
446
- "render count=0 doubled=0",
447
- "render count=0 doubled=0",
448
- "effect count=0",
449
- "cleanup count=0",
450
- "effect count=0",
451
- ]);
452
-
453
- events.length = 0;
454
-
455
- // Trigger increment via flushResourcesSync
456
- flushResourcesSync(() => {
457
- sub.getValue().increment();
458
- });
459
-
460
- // Double-render with new count, effect sets doubled=2, triggers another double-render
461
- expect(events).toEqual([
462
- "render count=1 doubled=0",
463
- "render count=1 doubled=0",
464
- "cleanup count=0",
465
- "effect count=1",
466
- "render count=1 doubled=2",
467
- "render count=1 doubled=2",
468
- ]);
469
- });
470
- });
471
-
472
- describe("Source 10: useState initializer function", () => {
473
- it("should call useState initializer twice", () => {
474
- const events: string[] = [];
475
- let initCount = 0;
476
-
477
- const TestResource = resource(function TestResource() {
478
- const [value] = useState(() => {
479
- initCount++;
480
- events.push(`init call #${initCount}`);
481
- return initCount;
482
- });
483
-
484
- events.push(`render value=${value}`);
485
-
486
- return { value };
487
- });
488
-
489
- const root = createResourceRoot();
490
- root.render(TestResource());
491
-
492
- // useState initializer should be called twice, first value kept
493
- expect(events).toEqual([
494
- "init call #1",
495
- "init call #2",
496
- "render value=1",
497
- "render value=1",
498
- ]);
499
- });
500
- });
501
-
502
- describe("Source 11: Resource disposal and recreation", () => {
503
- it("should maintain double-render behavior after disposal and recreation", () => {
504
- const events: string[] = [];
505
-
506
- const TestResource = resource(function TestResource() {
507
- const [count, setCount] = useState(0);
508
- events.push(`render count=${count}`);
509
-
510
- return {
511
- count,
512
- increment: () => setCount((c) => c + 1),
513
- };
514
- });
515
-
516
- // Create first instance
517
- const root1 = createResourceRoot();
518
- root1.render(TestResource());
519
-
520
- expect(events).toEqual(["render count=0", "render count=0"]);
521
-
522
- events.length = 0;
523
-
524
- // Unmount
525
- root1.unmount();
526
-
527
- // Create second instance
528
- const root2 = createResourceRoot();
529
- const sub2 = root2.render(TestResource());
530
-
531
- // Should still double-render
532
- expect(events).toEqual(["render count=0", "render count=0"]);
533
-
534
- events.length = 0;
535
-
536
- // Method calls via flushResourcesSync should still double-render
537
- flushResourcesSync(() => {
538
- sub2.getValue().increment();
539
- });
540
-
541
- expect(events).toEqual(["render count=1", "render count=1"]);
542
- });
543
- });
544
-
545
- describe("Source 12: setState in effect edge cases", () => {
546
- it("should apply setState from first effect mount even when second mount doesn't call setState", () => {
547
- const events: string[] = [];
548
- let effectRunCount = 0;
549
-
550
- const TestResource = resource(function TestResource() {
551
- const [count, setCount] = useState(0);
552
- events.push(`render count=${count}`);
553
-
554
- useEffect(() => {
555
- effectRunCount++;
556
- events.push(`effect mount #${effectRunCount} count=${count}`);
557
-
558
- // Only call setState on first mount
559
- if (effectRunCount === 1) {
560
- events.push(`setState(1) called in effect #${effectRunCount}`);
561
- setCount(1);
562
- } else {
563
- events.push(`no setState in effect #${effectRunCount}`);
564
- }
565
-
566
- return () => {
567
- events.push(`effect cleanup #${effectRunCount} count=${count}`);
568
- };
569
- }, []);
570
-
571
- return { count };
572
- });
573
-
574
- const root = createResourceRoot();
575
- root.render(TestResource());
576
-
577
- // Expected: setState(1) from effect #1 should be applied
578
- // even though effect #1 was cleaned up
579
- expect(events).toEqual([
580
- "render count=0",
581
- "render count=0",
582
- "effect mount #1 count=0",
583
- "setState(1) called in effect #1",
584
- "effect cleanup #1 count=0",
585
- "effect mount #2 count=0",
586
- "no setState in effect #2",
587
- "render count=1", // setState(1) applied!
588
- "render count=1",
589
- ]);
590
- });
591
-
592
- it("should apply last setState when both effect mounts call setState with different values", () => {
593
- const events: string[] = [];
594
- let effectRunCount = 0;
595
-
596
- const TestResource = resource(function TestResource() {
597
- const [count, setCount] = useState(0);
598
- events.push(`render count=${count}`);
599
-
600
- useEffect(() => {
601
- effectRunCount++;
602
- events.push(`effect mount #${effectRunCount} count=${count}`);
603
-
604
- if (effectRunCount === 1) {
605
- events.push(`setState(1) called in effect #${effectRunCount}`);
606
- setCount(1);
607
- } else if (effectRunCount === 2) {
608
- events.push(`setState(2) called in effect #${effectRunCount}`);
609
- setCount(2);
610
- }
611
-
612
- return () => {
613
- events.push(`effect cleanup #${effectRunCount} count=${count}`);
614
- };
615
- }, []);
616
-
617
- return { count };
618
- });
619
-
620
- const root = createResourceRoot();
621
- root.render(TestResource());
622
-
623
- // Expected: Only setState(2) should be applied (last one wins)
624
- expect(events).toEqual([
625
- "render count=0",
626
- "render count=0",
627
- "effect mount #1 count=0",
628
- "setState(1) called in effect #1",
629
- "effect cleanup #1 count=0",
630
- "effect mount #2 count=0",
631
- "setState(2) called in effect #2",
632
- "render count=2", // Only setState(2) applied!
633
- "render count=2",
634
- ]);
635
- });
636
-
637
- it("should handle updater functions from both effect mounts", () => {
638
- const events: string[] = [];
639
- let effectRunCount = 0;
640
-
641
- const TestResource = resource(function TestResource() {
642
- const [count, setCount] = useState(0);
643
- events.push(`render count=${count}`);
644
-
645
- useEffect(() => {
646
- effectRunCount++;
647
- events.push(`effect mount #${effectRunCount} count=${count}`);
648
-
649
- setCount((prev) => {
650
- events.push(
651
- `setState updater called with prev=${prev} in effect #${effectRunCount}`,
652
- );
653
- return prev + effectRunCount;
654
- });
655
-
656
- return () => {
657
- events.push(`effect cleanup #${effectRunCount} count=${count}`);
658
- };
659
- }, []);
660
-
661
- return { count };
662
- });
663
-
664
- const root = createResourceRoot();
665
- root.render(TestResource());
666
-
667
- // Tap behavior: Both updaters are queued and executed, first value kept per dispatch
668
- // Updater double-invoke happens per-dispatch (matching React ordering)
669
- // Effect #1: updater(0) => 0 + 1 = 1 (kept)
670
- // Effect #2: updater(0) => 0 + 2 = 2... but prev=1 from effect #1
671
- // Final: 3
672
- expect(events).toEqual([
673
- "render count=0",
674
- "render count=0",
675
- "effect mount #1 count=0",
676
- "setState updater called with prev=0 in effect #1",
677
- "effect cleanup #1 count=0",
678
- "effect mount #2 count=0",
679
- "setState updater called with prev=0 in effect #2",
680
- "setState updater called with prev=1 in effect #2",
681
- "setState updater called with prev=1 in effect #2",
682
- "render count=3",
683
- "render count=3",
684
- ]);
685
- });
686
- });
687
- });