@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
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Tree-shaped benchmark: M leaf resources with K hooks each; the update
3
+ * scenario dispatches into a single leaf. React renders the leaves as
4
+ * memo'd components, so a clean sibling costs nothing; tap currently
5
+ * re-renders the whole tree (no subtree bailout). This bench exists to
6
+ * measure that gap and to validate the bailout work. Build first, then:
7
+ *
8
+ * pnpm build
9
+ * pnpm exec vitest bench --run --project prod src/__tests__/bench/tree.bench.tsx
10
+ */
11
+ /* oxlint-disable react/rules-of-hooks -- fixed-count hook loops, benchmark only */
12
+ import { bench, describe } from "vitest";
13
+ import { createElement, Fragment, memo, useState } from "react";
14
+ import { createRoot } from "react-dom/client";
15
+ import { flushSync } from "react-dom";
16
+ import {
17
+ createTapRoot,
18
+ flushTapSync,
19
+ resource,
20
+ useResource,
21
+ useTapRoot,
22
+ } from "@assistant-ui/tap";
23
+
24
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false;
25
+
26
+ const M = 500;
27
+ const K = 10;
28
+
29
+ type Bump = (updater: (v: number) => number) => void;
30
+ const leafSetters: Bump[] = [];
31
+
32
+ const useLeafBody = (id: number): number => {
33
+ let sum = 0;
34
+ let set!: Bump;
35
+ for (let i = 0; i < K; i++) {
36
+ const [v, setV] = useState(0);
37
+ sum += v;
38
+ set = setV;
39
+ }
40
+ leafSetters[id] = set;
41
+ return sum;
42
+ };
43
+
44
+ const Leaf = resource((props: { id: number }) => useLeafBody(props.id));
45
+
46
+ const useTree = () => {
47
+ let total = 0;
48
+ for (let i = 0; i < M; i++) {
49
+ total += useResource(Leaf({ id: i }));
50
+ }
51
+ return total;
52
+ };
53
+
54
+ const useTreeDeps = () => {
55
+ let total = 0;
56
+ for (let i = 0; i < M; i++) {
57
+ total += useResource(Leaf({ id: i }), [i]);
58
+ }
59
+ return total;
60
+ };
61
+
62
+ // Stable element identities, like React Compiler output.
63
+ const leafElements = Array.from({ length: M }, (_, id) => Leaf({ id }));
64
+ const useTreeStable = () => {
65
+ let total = 0;
66
+ for (let i = 0; i < M; i++) {
67
+ total += useResource(leafElements[i]!);
68
+ }
69
+ return total;
70
+ };
71
+
72
+ const LeafComponent = memo(function LeafComponent({ id }: { id: number }) {
73
+ useLeafBody(id);
74
+ return null;
75
+ });
76
+
77
+ const leafIds = Array.from({ length: M }, (_, i) => i);
78
+
79
+ type Host = { flush: (fn: () => void) => void; unmount: () => void };
80
+
81
+ const HOSTS: Record<string, () => Host> = {
82
+ react: () => {
83
+ function Tree() {
84
+ return createElement(
85
+ Fragment,
86
+ null,
87
+ leafIds.map((id) => createElement(LeafComponent, { key: id, id })),
88
+ );
89
+ }
90
+ const root = createRoot(document.createElement("div"));
91
+ flushSync(() => root.render(createElement(Tree)));
92
+ return {
93
+ flush: (fn) => flushSync(fn),
94
+ unmount: () => flushSync(() => root.unmount()),
95
+ };
96
+ },
97
+ tapRoot: () => {
98
+ function Probe() {
99
+ useTapRoot(function Root() {
100
+ return useTree();
101
+ });
102
+ return null;
103
+ }
104
+ const root = createRoot(document.createElement("div"));
105
+ flushSync(() => root.render(createElement(Probe)));
106
+ return {
107
+ flush: (fn) => flushTapSync(fn),
108
+ unmount: () => flushSync(() => root.unmount()),
109
+ };
110
+ },
111
+ createTapRoot: () => {
112
+ const root = createTapRoot(function Root() {
113
+ return useTree();
114
+ });
115
+ return {
116
+ flush: (fn) => flushTapSync(fn),
117
+ unmount: () => root.unmount(),
118
+ };
119
+ },
120
+ createTapRootDeps: () => {
121
+ const root = createTapRoot(function Root() {
122
+ return useTreeDeps();
123
+ });
124
+ return {
125
+ flush: (fn) => flushTapSync(fn),
126
+ unmount: () => root.unmount(),
127
+ };
128
+ },
129
+ createTapRootStable: () => {
130
+ const root = createTapRoot(function Root() {
131
+ return useTreeStable();
132
+ });
133
+ return {
134
+ flush: (fn) => flushTapSync(fn),
135
+ unmount: () => root.unmount(),
136
+ };
137
+ },
138
+ };
139
+
140
+ describe(`tree mount+unmount, ${M} leaves x ${K} hooks`, () => {
141
+ for (const [name, make] of Object.entries(HOSTS)) {
142
+ bench(name, () => {
143
+ make().unmount();
144
+ });
145
+ }
146
+ });
147
+
148
+ describe(`tree update: one leaf dispatch, ${M} leaves x ${K} hooks`, () => {
149
+ for (const [name, make] of Object.entries(HOSTS)) {
150
+ let host: Host;
151
+ bench(
152
+ name,
153
+ () => {
154
+ host.flush(() => leafSetters[M >> 1]!((v) => v + 1));
155
+ },
156
+ {
157
+ setup: () => {
158
+ host = make();
159
+ },
160
+ teardown: () => {
161
+ host.unmount();
162
+ },
163
+ },
164
+ );
165
+ }
166
+ });
@@ -1,7 +1,7 @@
1
1
  /* oxlint-disable react/exhaustive-deps -- tests deliberately exercise invalid dep arrays */
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { useEffect } from "../../hooks/useEffect";
4
- import { useState } from "../../hooks/useState";
3
+ import { useEffect } from "../../react-hooks/useEffect";
4
+ import { useState } from "../../react-hooks/useState";
5
5
  import { createTestResource, renderTest, unmountResource } from "../test-utils";
6
6
  import {
7
7
  renderResourceFiber,
@@ -19,7 +19,7 @@ describe("Errors - Effect Errors", () => {
19
19
  return null;
20
20
  });
21
21
 
22
- expect(() => renderTest(resource, undefined)).toThrow(error);
22
+ expect(() => renderTest(resource)).toThrow(error);
23
23
  });
24
24
 
25
25
  it("should propagate errors from cleanup functions", () => {
@@ -39,14 +39,14 @@ describe("Errors - Effect Errors", () => {
39
39
  });
40
40
 
41
41
  // First render and commit - establishes the effect
42
- const ctx1 = renderResourceFiber(resource, undefined);
42
+ const ctx1 = renderResourceFiber(resource, []);
43
43
  commitResourceFiber(resource, ctx1);
44
44
 
45
45
  // Change dep to trigger cleanup on next render
46
46
  dep = 1;
47
47
 
48
48
  // Second render with different dep should trigger cleanup that throws
49
- const ctx2 = renderResourceFiber(resource, undefined);
49
+ const ctx2 = renderResourceFiber(resource, []);
50
50
  expect(() => commitResourceFiber(resource, ctx2)).toThrow(error);
51
51
  });
52
52
 
@@ -58,7 +58,7 @@ describe("Errors - Effect Errors", () => {
58
58
  return null;
59
59
  });
60
60
 
61
- expect(() => renderTest(resource, undefined)).toThrow(
61
+ expect(() => renderTest(resource)).toThrow(
62
62
  "An effect function must either return a cleanup function or nothing",
63
63
  );
64
64
  });
@@ -83,8 +83,7 @@ describe("Errors - Effect Errors", () => {
83
83
  });
84
84
 
85
85
  // Should throw aggregate error
86
- expect(() => renderTest(resource, undefined))
87
- .toThrowErrorMatchingInlineSnapshot(`
86
+ expect(() => renderTest(resource)).toThrowErrorMatchingInlineSnapshot(`
88
87
  [AggregateError: Errors during commit]
89
88
  `);
90
89
  expect(goodEffect).toHaveBeenCalledTimes(1);
@@ -105,7 +104,7 @@ describe("Errors - Effect Errors", () => {
105
104
  return null;
106
105
  });
107
106
 
108
- renderTest(resource, undefined);
107
+ renderTest(resource);
109
108
 
110
109
  // Unmount should throw the error but should still run all cleanups
111
110
  expect(() => unmountResource(resource)).toThrow(cleanupError);
@@ -140,7 +139,7 @@ describe("Errors - Effect Errors", () => {
140
139
 
141
140
  // The initial render will trigger setState which causes flushSync
142
141
  // The flushed re-render will throw the error
143
- expect(() => renderTest(resource, undefined)).toThrow(error);
142
+ expect(() => renderTest(resource)).toThrow(error);
144
143
  });
145
144
 
146
145
  it("should handle async errors in effects", async () => {
@@ -165,7 +164,7 @@ describe("Errors - Effect Errors", () => {
165
164
  });
166
165
 
167
166
  // This won't throw synchronously
168
- expect(() => renderTest(resource, undefined)).not.toThrow();
167
+ expect(() => renderTest(resource)).not.toThrow();
169
168
 
170
169
  // Wait for the async error to be handled
171
170
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -186,7 +185,7 @@ describe("Errors - Effect Errors", () => {
186
185
  return value;
187
186
  });
188
187
 
189
- expect(() => renderTest(resource, undefined)).toThrow(error);
188
+ expect(() => renderTest(resource)).toThrow(error);
190
189
  expect(effectRan).toBe(true);
191
190
 
192
191
  // Resource should not have committed state since commit failed
@@ -221,6 +220,6 @@ describe("Errors - Effect Errors", () => {
221
220
 
222
221
  // The initial render will trigger setState which causes flushSync
223
222
  // During the flush, the cleanup will run and throw
224
- expect(() => renderTest(resource, undefined)).toThrow(cleanupError);
223
+ expect(() => renderTest(resource)).toThrow(cleanupError);
225
224
  });
226
225
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { useEffect } from "../../hooks/useEffect";
3
- import { useState } from "../../hooks/useState";
2
+ import { useEffect } from "../../react-hooks/useEffect";
3
+ import { useState } from "../../react-hooks/useState";
4
4
  import { createTestResource, renderTest } from "../test-utils";
5
5
  import {
6
6
  renderResourceFiber,
@@ -16,7 +16,7 @@ describe("Errors - Render Errors", () => {
16
16
  throw error;
17
17
  });
18
18
 
19
- expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
19
+ expect(() => renderResourceFiber(resource, [])).toThrow(error);
20
20
  });
21
21
 
22
22
  it("should throw when hooks are called outside render context", () => {
@@ -40,24 +40,67 @@ describe("Errors - Render Errors", () => {
40
40
  return value;
41
41
  });
42
42
 
43
- expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
43
+ expect(() => renderResourceFiber(resource, [])).toThrow(error);
44
44
  });
45
45
 
46
- it("should detect render during render", () => {
46
+ it("should process setState during render as a render-phase update", () => {
47
47
  const resource = createTestResource(() => {
48
48
  const [count, setCount] = useState(0);
49
+ if (count < 5) setCount(count + 1);
50
+ return count;
51
+ });
49
52
 
50
- // This violates the rules - no state updates during render
51
- if (count < 5) {
52
- expect(() => setCount(count + 1)).toThrow(
53
- "Resource updated during render",
54
- );
55
- }
53
+ expect(renderResourceFiber(resource, []).value).toBe(5);
54
+ });
55
+
56
+ it("should throw when updating a different resource during render", () => {
57
+ let setOther!: (value: number) => void;
58
+ const other = createTestResource(() => {
59
+ const [value, setValue] = useState(0);
60
+ setOther = setValue;
61
+ return value;
62
+ });
63
+ renderTest(other);
64
+
65
+ const resource = createTestResource(() => {
66
+ setOther(1);
67
+ return null;
68
+ });
69
+
70
+ expect(() => renderResourceFiber(resource, [])).toThrow(
71
+ "Cannot update a resource while rendering a different resource",
72
+ );
73
+ });
74
+
75
+ it("should throw when a child updates its parent mid-render", () => {
76
+ let setParent!: (value: number) => void;
77
+ const child = createTestResource(() => {
78
+ setParent(1);
79
+ return null;
80
+ });
81
+
82
+ const parent = createTestResource(() => {
83
+ const [value, setValue] = useState(0);
84
+ setParent = setValue;
85
+ renderResourceFiber(child, []);
86
+ return value;
87
+ });
56
88
 
89
+ expect(() => renderResourceFiber(parent, [])).toThrow(
90
+ "Cannot update a resource while rendering a different resource",
91
+ );
92
+ });
93
+
94
+ it("should throw on unbounded render-phase updates", () => {
95
+ const resource = createTestResource(() => {
96
+ const [count, setCount] = useState(0);
97
+ setCount(count + 1);
57
98
  return count;
58
99
  });
59
100
 
60
- renderResourceFiber(resource, undefined);
101
+ expect(() => renderResourceFiber(resource, [])).toThrow(
102
+ "Too many re-renders",
103
+ );
61
104
  });
62
105
 
63
106
  it("should allow setState during commit (effects)", () => {
@@ -74,7 +117,7 @@ describe("Errors - Render Errors", () => {
74
117
  return count;
75
118
  });
76
119
 
77
- const ctx = renderResourceFiber(resource, undefined);
120
+ const ctx = renderResourceFiber(resource, []);
78
121
  // This should not throw - setState in effects is allowed
79
122
  expect(() => commitResourceFiber(resource, ctx)).not.toThrow();
80
123
  unmountResourceFiber(resource);
@@ -94,11 +137,11 @@ describe("Errors - Render Errors", () => {
94
137
  return null;
95
138
  });
96
139
 
97
- renderResourceFiber(resource, undefined);
140
+ renderResourceFiber(resource, []);
98
141
 
99
142
  useStateFirst = false;
100
143
 
101
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
144
+ expect(() => renderResourceFiber(resource, [])).toThrow(
102
145
  "Hook order changed between renders",
103
146
  );
104
147
  });
@@ -117,12 +160,12 @@ describe("Errors - Render Errors", () => {
117
160
  });
118
161
 
119
162
  // First successful render
120
- const result = renderTest(resource, undefined);
163
+ const result = renderTest(resource);
121
164
  expect(result).toBe(42);
122
165
 
123
166
  // Failed render
124
167
  shouldThrow = true;
125
- expect(() => renderTest(resource, undefined)).toThrow("Render failed");
168
+ expect(() => renderTest(resource)).toThrow("Render failed");
126
169
 
127
170
  // State should be unchanged after failed render
128
171
  // The resource state is preserved
@@ -153,19 +196,19 @@ describe("Errors - Render Errors", () => {
153
196
  });
154
197
 
155
198
  // Successful render
156
- renderTest(resource, undefined);
199
+ renderTest(resource);
157
200
 
158
201
  // Render error
159
202
  phase = "render-error";
160
- expect(() => renderTest(resource, undefined)).toThrow("Render error");
203
+ expect(() => renderTest(resource)).toThrow("Render error");
161
204
 
162
205
  // Hook order error
163
206
  phase = "hook-order";
164
- expect(() => renderTest(resource, undefined)).toThrow("Hook order changed");
207
+ expect(() => renderTest(resource)).toThrow("Hook order changed");
165
208
 
166
209
  // Effect error
167
210
  phase = "effect-error";
168
- expect(() => renderTest(resource, undefined)).toThrow("Effect error");
211
+ expect(() => renderTest(resource)).toThrow("Effect error");
169
212
  });
170
213
 
171
214
  it("should handle errors in nested hook calls", () => {
@@ -184,7 +227,7 @@ describe("Errors - Render Errors", () => {
184
227
  return feature;
185
228
  });
186
229
 
187
- const result = renderTest(resource, undefined);
230
+ const result = renderTest(resource);
188
231
  expect(result).toBe("feature");
189
232
  });
190
233
  });
@@ -1,7 +1,7 @@
1
1
  /* oxlint-disable react/exhaustive-deps -- tests deliberately exercise invalid dep arrays */
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { useEffect } from "../../hooks/useEffect";
4
- import { useState } from "../../hooks/useState";
3
+ import { useEffect } from "../../react-hooks/useEffect";
4
+ import { useState } from "../../react-hooks/useState";
5
5
  import { createTestResource, renderTest, waitForNextTick } from "../test-utils";
6
6
  import {
7
7
  renderResourceFiber,
@@ -22,7 +22,7 @@ describe("Lifecycle - Dependencies", () => {
22
22
  return dep;
23
23
  });
24
24
 
25
- renderTest(resource, undefined);
25
+ renderTest(resource);
26
26
  expect(effect).toHaveBeenCalledTimes(1);
27
27
 
28
28
  // Change dependency - this triggers automatic re-render
@@ -46,7 +46,7 @@ describe("Lifecycle - Dependencies", () => {
46
46
  return { count, dep };
47
47
  });
48
48
 
49
- renderTest(resource, undefined);
49
+ renderTest(resource);
50
50
  expect(effect).toHaveBeenCalledTimes(1);
51
51
 
52
52
  // Trigger re-render without changing dep
@@ -73,12 +73,12 @@ describe("Lifecycle - Dependencies", () => {
73
73
  return dep;
74
74
  });
75
75
 
76
- renderTest(resource, undefined);
76
+ renderTest(resource);
77
77
  expect(log).toEqual(["effect-1"]);
78
78
 
79
79
  // Change dep
80
80
  setDep(2);
81
- const ctx = renderResourceFiber(resource, undefined);
81
+ const ctx = renderResourceFiber(resource, []);
82
82
  commitResourceFiber(resource, ctx);
83
83
 
84
84
  expect(log).toEqual(["effect-1", "cleanup-1", "effect-2"]);
@@ -96,7 +96,7 @@ describe("Lifecycle - Dependencies", () => {
96
96
  return count;
97
97
  });
98
98
 
99
- renderTest(resource, undefined);
99
+ renderTest(resource);
100
100
  expect(effect).toHaveBeenCalledTimes(1);
101
101
 
102
102
  // Re-render
@@ -119,12 +119,12 @@ describe("Lifecycle - Dependencies", () => {
119
119
  return count;
120
120
  });
121
121
 
122
- renderTest(resource, undefined);
122
+ renderTest(resource);
123
123
  expect(effect).toHaveBeenCalledTimes(1);
124
124
 
125
125
  // Re-render
126
126
  triggerRerender(1);
127
- const ctx = renderResourceFiber(resource, undefined);
127
+ const ctx = renderResourceFiber(resource, []);
128
128
  commitResourceFiber(resource, ctx);
129
129
 
130
130
  expect(effect).toHaveBeenCalledTimes(1); // Should not re-run
@@ -145,26 +145,26 @@ describe("Lifecycle - Dependencies", () => {
145
145
  });
146
146
 
147
147
  // Initial render
148
- let ctx = renderResourceFiber(resource, undefined);
148
+ let ctx = renderResourceFiber(resource, []);
149
149
  commitResourceFiber(resource, ctx);
150
150
  expect(effect).toHaveBeenCalledTimes(1);
151
151
 
152
152
  // Change first dep
153
153
  setDep1("b");
154
- ctx = renderResourceFiber(resource, undefined);
154
+ ctx = renderResourceFiber(resource, []);
155
155
  commitResourceFiber(resource, ctx);
156
156
  expect(effect).toHaveBeenCalledTimes(2);
157
157
 
158
158
  // Change second dep
159
159
  setDep2(2);
160
- ctx = renderResourceFiber(resource, undefined);
160
+ ctx = renderResourceFiber(resource, []);
161
161
  commitResourceFiber(resource, ctx);
162
162
  expect(effect).toHaveBeenCalledTimes(3);
163
163
 
164
164
  // Change both deps
165
165
  setDep1("c");
166
166
  setDep2(3);
167
- ctx = renderResourceFiber(resource, undefined);
167
+ ctx = renderResourceFiber(resource, []);
168
168
  commitResourceFiber(resource, ctx);
169
169
  expect(effect).toHaveBeenCalledTimes(4);
170
170
 
@@ -183,12 +183,12 @@ describe("Lifecycle - Dependencies", () => {
183
183
  return obj;
184
184
  });
185
185
 
186
- renderTest(resource, undefined);
186
+ renderTest(resource);
187
187
  expect(effect).toHaveBeenCalledTimes(1);
188
188
 
189
189
  // Set to new object with same shape
190
190
  setObj({ value: 1 });
191
- const ctx = renderResourceFiber(resource, undefined);
191
+ const ctx = renderResourceFiber(resource, []);
192
192
  commitResourceFiber(resource, ctx);
193
193
 
194
194
  expect(effect).toHaveBeenCalledTimes(2); // Should re-run (different object)
@@ -206,11 +206,11 @@ describe("Lifecycle - Dependencies", () => {
206
206
  return value;
207
207
  });
208
208
 
209
- renderTest(resource, undefined);
209
+ renderTest(resource);
210
210
  expect(effect).toHaveBeenCalledTimes(1);
211
211
 
212
212
  // Set to NaN again
213
- const ctx = renderResourceFiber(resource, undefined);
213
+ const ctx = renderResourceFiber(resource, []);
214
214
  setValue(NaN);
215
215
  commitResourceFiber(resource, ctx);
216
216
 
@@ -229,13 +229,13 @@ describe("Lifecycle - Dependencies", () => {
229
229
  return null;
230
230
  });
231
231
 
232
- renderTest(resource, undefined);
232
+ renderTest(resource);
233
233
 
234
234
  // Change to no deps
235
235
  useDeps = false;
236
236
 
237
237
  // Error throws during render (fail-fast validation)
238
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
238
+ expect(() => renderResourceFiber(resource, [])).toThrow(
239
239
  "useEffect called with and without dependencies across re-renders",
240
240
  );
241
241
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { useEffect } from "../../hooks/useEffect";
3
- import { useState } from "../../hooks/useState";
2
+ import { useEffect } from "../../react-hooks/useEffect";
3
+ import { useState } from "../../react-hooks/useState";
4
4
  import { createTestResource, renderTest, unmountResource } from "../test-utils";
5
5
  import {
6
6
  renderResourceFiber,
@@ -17,7 +17,7 @@ describe("Lifecycle - Mount/Unmount", () => {
17
17
  return null;
18
18
  });
19
19
 
20
- renderTest(resource, undefined);
20
+ renderTest(resource);
21
21
 
22
22
  effects.forEach((fn) => {
23
23
  expect(fn).toHaveBeenCalledTimes(1);
@@ -34,7 +34,7 @@ describe("Lifecycle - Mount/Unmount", () => {
34
34
  return null;
35
35
  });
36
36
 
37
- renderTest(resource, undefined);
37
+ renderTest(resource);
38
38
  cleanups.forEach((fn) => expect(fn).not.toHaveBeenCalled());
39
39
 
40
40
  unmountResource(resource);
@@ -51,7 +51,7 @@ describe("Lifecycle - Mount/Unmount", () => {
51
51
  return null;
52
52
  });
53
53
 
54
- renderTest(resource, undefined);
54
+ renderTest(resource);
55
55
  unmountResource(resource);
56
56
 
57
57
  expect(order).toEqual([1, 2, 3]);
@@ -120,7 +120,7 @@ describe("Lifecycle - Mount/Unmount", () => {
120
120
  });
121
121
 
122
122
  // Initial render
123
- const ctx = renderResourceFiber(resource, undefined);
123
+ const ctx = renderResourceFiber(resource, []);
124
124
  expect(log).toEqual(["render"]);
125
125
 
126
126
  // Commit - effects will run
@@ -129,7 +129,7 @@ describe("Lifecycle - Mount/Unmount", () => {
129
129
  expect(log).toEqual(["render", "effect-1", "effect-2"]);
130
130
 
131
131
  // The setState in effect schedules a re-render; trigger it manually
132
- const ctx2 = renderResourceFiber(resource, undefined);
132
+ const ctx2 = renderResourceFiber(resource, []);
133
133
  commitResourceFiber(resource, ctx2);
134
134
 
135
135
  // Now we should see the re-render and cleanup/re-run of effects
@@ -138,10 +138,10 @@ describe("Lifecycle - Mount/Unmount", () => {
138
138
  "effect-1",
139
139
  "effect-2",
140
140
  "render", // Re-render triggered by setMounted(true)
141
- "cleanup-1", // Cleanup from first render
142
- "effect-1", // Effect from re-render
143
- "cleanup-2", // Cleanup from first render
144
- "effect-2", // Effect from re-render
141
+ "cleanup-1", // Cleanups from first render run first, like React
142
+ "cleanup-2",
143
+ "effect-1", // Then the re-render's effects
144
+ "effect-2",
145
145
  ]);
146
146
 
147
147
  // Clear log for unmount testing
@@ -164,7 +164,7 @@ describe("Lifecycle - Mount/Unmount", () => {
164
164
  return null;
165
165
  });
166
166
 
167
- renderTest(resource, undefined);
167
+ renderTest(resource);
168
168
 
169
169
  // Unmount should throw the error
170
170
  expect(() => unmountResource(resource)).toThrow(error);
@@ -182,7 +182,7 @@ describe("Lifecycle - Mount/Unmount", () => {
182
182
  return null;
183
183
  });
184
184
 
185
- renderTest(resource, undefined);
185
+ renderTest(resource);
186
186
  unmountResource(resource);
187
187
 
188
188
  expect(cleanup).not.toHaveBeenCalled();
@@ -200,7 +200,7 @@ describe("Lifecycle - Mount/Unmount", () => {
200
200
  return null;
201
201
  });
202
202
 
203
- const ctx = renderResourceFiber(resource, undefined);
203
+ const ctx = renderResourceFiber(resource, []);
204
204
  commitResourceFiber(resource, ctx);
205
205
  unmountResourceFiber(resource);
206
206