@assistant-ui/tap 0.5.13 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +9 -8
  2. package/dist/core/ResourceFiber.d.ts.map +1 -1
  3. package/dist/core/ResourceFiber.js +3 -2
  4. package/dist/core/ResourceFiber.js.map +1 -1
  5. package/dist/core/context.d.ts +13 -6
  6. package/dist/core/context.d.ts.map +1 -1
  7. package/dist/core/context.js +19 -6
  8. package/dist/core/context.js.map +1 -1
  9. package/dist/core/createResourceRoot.d.ts +2 -1
  10. package/dist/core/createResourceRoot.d.ts.map +1 -1
  11. package/dist/core/createResourceRoot.js +2 -2
  12. package/dist/core/createResourceRoot.js.map +1 -1
  13. package/dist/core/helpers/commit.d.ts.map +1 -1
  14. package/dist/core/helpers/execution-context.d.ts +2 -1
  15. package/dist/core/helpers/execution-context.d.ts.map +1 -1
  16. package/dist/core/helpers/execution-context.js +4 -1
  17. package/dist/core/helpers/execution-context.js.map +1 -1
  18. package/dist/core/helpers/root.d.ts.map +1 -1
  19. package/dist/core/helpers/root.js.map +1 -1
  20. package/dist/core/react-dispatcher.d.ts +12 -0
  21. package/dist/core/react-dispatcher.d.ts.map +1 -0
  22. package/dist/core/react-dispatcher.js +62 -0
  23. package/dist/core/react-dispatcher.js.map +1 -0
  24. package/dist/core/resource.d.ts.map +1 -1
  25. package/dist/core/scheduler.d.ts.map +1 -1
  26. package/dist/core/scheduler.js +1 -1
  27. package/dist/core/scheduler.js.map +1 -1
  28. package/dist/core/types.d.ts +3 -3
  29. package/dist/core/withKey.d.ts.map +1 -1
  30. package/dist/hooks/index.d.ts +13 -0
  31. package/dist/hooks/index.js +13 -0
  32. package/dist/hooks/use.d.ts +9 -0
  33. package/dist/hooks/use.d.ts.map +1 -0
  34. package/dist/hooks/use.js +14 -0
  35. package/dist/hooks/use.js.map +1 -0
  36. package/dist/hooks/useCallback.d.ts +5 -0
  37. package/dist/hooks/useCallback.d.ts.map +1 -0
  38. package/dist/hooks/useCallback.js +9 -0
  39. package/dist/hooks/useCallback.js.map +1 -0
  40. package/dist/hooks/useEffect.d.ts +10 -0
  41. package/dist/hooks/useEffect.d.ts.map +1 -0
  42. package/dist/hooks/{tap-effect.js → useEffect.js} +7 -7
  43. package/dist/hooks/useEffect.js.map +1 -0
  44. package/dist/hooks/{tap-effect-event.d.ts → useEffectEvent.d.ts} +5 -5
  45. package/dist/hooks/useEffectEvent.d.ts.map +1 -0
  46. package/dist/hooks/{tap-effect-event.js → useEffectEvent.js} +12 -12
  47. package/dist/hooks/useEffectEvent.js.map +1 -0
  48. package/dist/hooks/useMemo.d.ts +5 -0
  49. package/dist/hooks/useMemo.d.ts.map +1 -0
  50. package/dist/hooks/{tap-memo.js → useMemo.js} +6 -6
  51. package/dist/hooks/useMemo.js.map +1 -0
  52. package/dist/hooks/useMemoCache.d.ts +10 -0
  53. package/dist/hooks/useMemoCache.d.ts.map +1 -0
  54. package/dist/hooks/useMemoCache.js +21 -0
  55. package/dist/hooks/useMemoCache.js.map +1 -0
  56. package/dist/hooks/useReducer.d.ts +21 -0
  57. package/dist/hooks/useReducer.d.ts.map +1 -0
  58. package/dist/hooks/{tap-reducer.js → useReducer.js} +10 -10
  59. package/dist/hooks/useReducer.js.map +1 -0
  60. package/dist/hooks/useRef.d.ts +11 -0
  61. package/dist/hooks/useRef.d.ts.map +1 -0
  62. package/dist/hooks/useRef.js +10 -0
  63. package/dist/hooks/useRef.js.map +1 -0
  64. package/dist/{react/use-resource.d.ts → hooks/useResource.d.ts} +3 -2
  65. package/dist/hooks/useResource.d.ts.map +1 -0
  66. package/dist/hooks/{tap-resource.js → useResource.js} +12 -12
  67. package/dist/hooks/useResource.js.map +1 -0
  68. package/dist/hooks/useResourceRoot.d.ts +20 -0
  69. package/dist/hooks/useResourceRoot.d.ts.map +1 -0
  70. package/dist/{tapResourceRoot.js → hooks/useResourceRoot.js} +30 -26
  71. package/dist/hooks/useResourceRoot.js.map +1 -0
  72. package/dist/hooks/{tap-resources.d.ts → useResources.d.ts} +4 -4
  73. package/dist/hooks/useResources.d.ts.map +1 -0
  74. package/dist/hooks/{tap-resources.js → useResources.js} +28 -23
  75. package/dist/hooks/useResources.js.map +1 -0
  76. package/dist/hooks/useState.d.ts +9 -0
  77. package/dist/hooks/useState.d.ts.map +1 -0
  78. package/dist/hooks/useState.js +11 -0
  79. package/dist/hooks/useState.js.map +1 -0
  80. package/dist/hooks/utils/useCell.d.ts +10 -0
  81. package/dist/hooks/utils/useCell.d.ts.map +1 -0
  82. package/dist/hooks/utils/{tapHook.js → useCell.js} +4 -4
  83. package/dist/hooks/utils/{tapHook.js.map → useCell.js.map} +1 -1
  84. package/dist/index.d.ts +3 -13
  85. package/dist/index.js +3 -13
  86. package/dist/react/hooks.d.ts +25 -0
  87. package/dist/react/hooks.d.ts.map +1 -0
  88. package/dist/react/hooks.js +69 -0
  89. package/dist/react/hooks.js.map +1 -0
  90. package/dist/react-shim/index.d.ts +19 -0
  91. package/dist/react-shim/index.d.ts.map +1 -0
  92. package/dist/react-shim/index.js +28 -0
  93. package/dist/react-shim/index.js.map +1 -0
  94. package/package.json +13 -16
  95. package/react-shim/package.json +4 -0
  96. package/src/__tests__/basic/resourceHandle.test.ts +7 -3
  97. package/src/__tests__/basic/tapEffect.basic.test.ts +19 -21
  98. package/src/__tests__/basic/tapReducer.basic.test.ts +14 -14
  99. package/src/__tests__/basic/tapResources.basic.test.ts +19 -14
  100. package/src/__tests__/basic/tapState.basic.test.ts +20 -20
  101. package/src/__tests__/errors/errors.effect-errors.test.ts +21 -21
  102. package/src/__tests__/errors/errors.render-errors.test.ts +18 -18
  103. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +25 -25
  104. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +17 -21
  105. package/src/__tests__/react/concurrent-mode.test.tsx +7 -7
  106. package/src/__tests__/react/react-shim.test.tsx +65 -0
  107. package/src/__tests__/react/useResource.test.tsx +172 -0
  108. package/src/__tests__/react-dispatcher.test.ts +74 -0
  109. package/src/__tests__/rules/rules.hook-count.test.ts +30 -29
  110. package/src/__tests__/rules/rules.hook-order.test.ts +27 -27
  111. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +1 -4
  112. package/src/__tests__/strictmode/strictmode.test.ts +42 -42
  113. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +55 -58
  114. package/src/__tests__/test-utils.ts +2 -3
  115. package/src/core/ResourceFiber.ts +4 -1
  116. package/src/core/context.ts +31 -9
  117. package/src/core/createResourceRoot.ts +4 -4
  118. package/src/core/helpers/execution-context.ts +4 -0
  119. package/src/core/helpers/root.ts +0 -1
  120. package/src/core/react-dispatcher.ts +78 -0
  121. package/src/core/scheduler.ts +1 -1
  122. package/src/core/types.ts +3 -3
  123. package/src/hooks/index.ts +12 -0
  124. package/src/hooks/use.ts +13 -0
  125. package/src/hooks/useCallback.ts +9 -0
  126. package/src/hooks/{tap-effect.ts → useEffect.ts} +9 -9
  127. package/src/hooks/{tap-effect-event.ts → useEffectEvent.ts} +9 -9
  128. package/src/hooks/{tap-memo.ts → useMemo.ts} +3 -3
  129. package/src/hooks/useMemoCache.ts +25 -0
  130. package/src/hooks/{tap-reducer.ts → useReducer.ts} +23 -11
  131. package/src/hooks/useRef.ts +16 -0
  132. package/src/hooks/{tap-resource.ts → useResource.ts} +13 -12
  133. package/src/{tapResourceRoot.ts → hooks/useResourceRoot.ts} +26 -29
  134. package/src/hooks/{tap-resources.ts → useResources.ts} +21 -22
  135. package/src/hooks/useState.ts +29 -0
  136. package/src/hooks/utils/{tapHook.ts → useCell.ts} +1 -1
  137. package/src/index.ts +4 -24
  138. package/src/react/hooks.ts +112 -0
  139. package/src/react-shim/index.ts +64 -0
  140. package/dist/hooks/tap-callback.d.ts +0 -5
  141. package/dist/hooks/tap-callback.d.ts.map +0 -1
  142. package/dist/hooks/tap-callback.js +0 -9
  143. package/dist/hooks/tap-callback.js.map +0 -1
  144. package/dist/hooks/tap-const.d.ts +0 -5
  145. package/dist/hooks/tap-const.d.ts.map +0 -1
  146. package/dist/hooks/tap-const.js +0 -10
  147. package/dist/hooks/tap-const.js.map +0 -1
  148. package/dist/hooks/tap-effect-event.d.ts.map +0 -1
  149. package/dist/hooks/tap-effect-event.js.map +0 -1
  150. package/dist/hooks/tap-effect.d.ts +0 -10
  151. package/dist/hooks/tap-effect.d.ts.map +0 -1
  152. package/dist/hooks/tap-effect.js.map +0 -1
  153. package/dist/hooks/tap-memo.d.ts +0 -5
  154. package/dist/hooks/tap-memo.d.ts.map +0 -1
  155. package/dist/hooks/tap-memo.js.map +0 -1
  156. package/dist/hooks/tap-reducer.d.ts +0 -9
  157. package/dist/hooks/tap-reducer.d.ts.map +0 -1
  158. package/dist/hooks/tap-reducer.js.map +0 -1
  159. package/dist/hooks/tap-ref.d.ts +0 -11
  160. package/dist/hooks/tap-ref.d.ts.map +0 -1
  161. package/dist/hooks/tap-ref.js +0 -10
  162. package/dist/hooks/tap-ref.js.map +0 -1
  163. package/dist/hooks/tap-resource.d.ts +0 -8
  164. package/dist/hooks/tap-resource.d.ts.map +0 -1
  165. package/dist/hooks/tap-resource.js.map +0 -1
  166. package/dist/hooks/tap-resources.d.ts.map +0 -1
  167. package/dist/hooks/tap-resources.js.map +0 -1
  168. package/dist/hooks/tap-state.d.ts +0 -9
  169. package/dist/hooks/tap-state.d.ts.map +0 -1
  170. package/dist/hooks/tap-state.js +0 -11
  171. package/dist/hooks/tap-state.js.map +0 -1
  172. package/dist/hooks/utils/tapHook.d.ts +0 -10
  173. package/dist/hooks/utils/tapHook.d.ts.map +0 -1
  174. package/dist/react/index.d.ts +0 -2
  175. package/dist/react/index.js +0 -2
  176. package/dist/react/use-resource.d.ts.map +0 -1
  177. package/dist/react/use-resource.js +0 -46
  178. package/dist/react/use-resource.js.map +0 -1
  179. package/dist/tapResourceRoot.d.ts +0 -20
  180. package/dist/tapResourceRoot.d.ts.map +0 -1
  181. package/dist/tapResourceRoot.js.map +0 -1
  182. package/react/package.json +0 -5
  183. package/src/hooks/tap-callback.ts +0 -9
  184. package/src/hooks/tap-const.ts +0 -6
  185. package/src/hooks/tap-ref.ts +0 -16
  186. package/src/hooks/tap-state.ts +0 -29
  187. package/src/react/index.ts +0 -1
  188. package/src/react/use-resource.ts +0 -61
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { tapEffect } from "../../hooks/tap-effect";
3
- import { tapState } from "../../hooks/tap-state";
2
+ import { useEffect } from "../../hooks/useEffect";
3
+ import { useState } from "../../hooks/useState";
4
4
  import { createTestResource, renderTest, unmountResource } from "../test-utils";
5
5
  import {
6
6
  renderResourceFiber,
@@ -13,8 +13,7 @@ describe("Lifecycle - Mount/Unmount", () => {
13
13
  const effects = [vi.fn(), vi.fn(), vi.fn()];
14
14
 
15
15
  const resource = createTestResource(() => {
16
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
17
- effects.forEach((fn) => tapEffect(fn));
16
+ effects.forEach((fn) => useEffect(fn));
18
17
  return null;
19
18
  });
20
19
 
@@ -30,17 +29,15 @@ describe("Lifecycle - Mount/Unmount", () => {
30
29
 
31
30
  const resource = createTestResource(() => {
32
31
  cleanups.forEach((cleanup) => {
33
- tapEffect(() => cleanup);
32
+ useEffect(() => cleanup);
34
33
  });
35
34
  return null;
36
35
  });
37
36
 
38
37
  renderTest(resource, undefined);
39
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
40
38
  cleanups.forEach((fn) => expect(fn).not.toHaveBeenCalled());
41
39
 
42
40
  unmountResource(resource);
43
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
44
41
  cleanups.forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
45
42
  });
46
43
 
@@ -48,9 +45,9 @@ describe("Lifecycle - Mount/Unmount", () => {
48
45
  const order: number[] = [];
49
46
 
50
47
  const resource = createTestResource(() => {
51
- tapEffect(() => () => order.push(1));
52
- tapEffect(() => () => order.push(2));
53
- tapEffect(() => () => order.push(3));
48
+ useEffect(() => () => order.push(1));
49
+ useEffect(() => () => order.push(2));
50
+ useEffect(() => () => order.push(3));
54
51
  return null;
55
52
  });
56
53
 
@@ -67,11 +64,11 @@ describe("Lifecycle - Mount/Unmount", () => {
67
64
 
68
65
  const resource = createTestResource((props: number) => {
69
66
  renderCount++;
70
- const [state, _setState] = tapState({ count: 0 });
67
+ const [state, _setState] = useState({ count: 0 });
71
68
  setState = _setState;
72
69
 
73
70
  // Simple effect that tracks runs
74
- tapEffect(() => {
71
+ useEffect(() => {
75
72
  effectRunCount++;
76
73
  });
77
74
 
@@ -103,18 +100,18 @@ describe("Lifecycle - Mount/Unmount", () => {
103
100
  const log: string[] = [];
104
101
 
105
102
  const resource = createTestResource(() => {
106
- const [mounted, setMounted] = tapState(false);
103
+ const [mounted, setMounted] = useState(false);
107
104
 
108
105
  log.push("render");
109
106
 
110
- tapEffect(() => {
107
+ useEffect(() => {
111
108
  log.push("effect-1");
112
109
  setMounted(true);
113
110
 
114
111
  return () => log.push("cleanup-1");
115
112
  });
116
113
 
117
- tapEffect(() => {
114
+ useEffect(() => {
118
115
  log.push("effect-2");
119
116
  return () => log.push("cleanup-2");
120
117
  });
@@ -131,8 +128,7 @@ describe("Lifecycle - Mount/Unmount", () => {
131
128
  // After commit: initial render + effects
132
129
  expect(log).toEqual(["render", "effect-1", "effect-2"]);
133
130
 
134
- // The setState in effect schedules a re-render
135
- // With the new architecture, we need to manually trigger it
131
+ // The setState in effect schedules a re-render; trigger it manually
136
132
  const ctx2 = renderResourceFiber(resource, undefined);
137
133
  commitResourceFiber(resource, ctx2);
138
134
 
@@ -161,10 +157,10 @@ describe("Lifecycle - Mount/Unmount", () => {
161
157
  const goodCleanup = vi.fn();
162
158
 
163
159
  const resource = createTestResource(() => {
164
- tapEffect(() => () => {
160
+ useEffect(() => () => {
165
161
  throw error;
166
162
  });
167
- tapEffect(() => goodCleanup);
163
+ useEffect(() => goodCleanup);
168
164
  return null;
169
165
  });
170
166
 
@@ -181,7 +177,7 @@ describe("Lifecycle - Mount/Unmount", () => {
181
177
 
182
178
  const resource = createTestResource(() => {
183
179
  if (!skipEffect) {
184
- tapEffect(() => cleanup);
180
+ useEffect(() => cleanup);
185
181
  }
186
182
  return null;
187
183
  });
@@ -197,7 +193,7 @@ describe("Lifecycle - Mount/Unmount", () => {
197
193
  const cleanup = vi.fn();
198
194
 
199
195
  const resource = createTestResource(() => {
200
- tapEffect(() => {
196
+ useEffect(() => {
201
197
  effect();
202
198
  return cleanup;
203
199
  });
@@ -2,17 +2,17 @@ import { describe, it, expect } from "vitest";
2
2
  import { render, screen, act } from "@testing-library/react";
3
3
  import { Suspense, startTransition, use, useState } from "react";
4
4
  import { resource } from "../../core/resource";
5
- import { useResource } from "../../react/use-resource";
6
- import { tapState } from "../../hooks/tap-state";
5
+ import { useResource } from "../../react/hooks";
6
+ import { useState as useResourceState } from "../../hooks/useState";
7
7
 
8
8
  const ShouldNeverFallback = () => {
9
9
  throw new Error("should never fallback");
10
10
  };
11
11
 
12
12
  describe("Concurrent Mode with useResource", () => {
13
- it("should not commit tapState updates when render is discarded", async () => {
14
- const TestResource = resource(() => {
15
- return tapState(false);
13
+ it("should not commit useResourceState updates when render is discarded", async () => {
14
+ const TestResource = resource(function TestResource() {
15
+ return useResourceState(false);
16
16
  });
17
17
 
18
18
  let resolve: (value: number) => void;
@@ -76,7 +76,7 @@ describe("Concurrent Mode with useResource", () => {
76
76
  expect(screen.getByTestId("message").textContent).toBe("hello");
77
77
  });
78
78
 
79
- it("react should not commit tapState updates when render is discarded", async () => {
79
+ it("react should not commit useResourceState updates when render is discarded", async () => {
80
80
  let resolve: (value: number) => void;
81
81
 
82
82
  const suspendPromise = new Promise<number>((r) => {
@@ -142,7 +142,7 @@ describe("Concurrent Mode with useResource", () => {
142
142
  let resolve: () => void;
143
143
  let shouldSuspend = false;
144
144
 
145
- const TestResource = resource((props: { id: number }) => {
145
+ const TestResource = resource(function TestResource(props: { id: number }) {
146
146
  if (shouldSuspend) {
147
147
  throw new Promise<void>((r) => {
148
148
  resolve = r;
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { render, screen, act, cleanup } from "@testing-library/react";
3
+ import {
4
+ createTestResource,
5
+ renderTest,
6
+ cleanupAllResources,
7
+ getCommittedOutput,
8
+ waitForNextTick,
9
+ } from "../test-utils";
10
+ import { useState, useEffect } from "../../react-shim";
11
+
12
+ describe("@assistant-ui/tap/react-shim", () => {
13
+ afterEach(() => {
14
+ cleanupAllResources();
15
+ cleanup();
16
+ });
17
+
18
+ describe("inside a tap resource", () => {
19
+ it("useState routes to useState and useEffect to useEffect", async () => {
20
+ let setCount: ((n: number) => void) | null = null;
21
+ const effectLog: number[] = [];
22
+
23
+ const testFiber = createTestResource(() => {
24
+ const [count, set] = useState(0);
25
+ useEffect(() => {
26
+ setCount = set;
27
+ effectLog.push(count);
28
+ }, [count]);
29
+ return count;
30
+ });
31
+
32
+ renderTest(testFiber, undefined);
33
+ expect(getCommittedOutput(testFiber)).toBe(0);
34
+ expect(effectLog).toEqual([0]);
35
+
36
+ setCount!(5);
37
+ await waitForNextTick();
38
+ expect(getCommittedOutput(testFiber)).toBe(5);
39
+ expect(effectLog).toEqual([0, 5]);
40
+ });
41
+ });
42
+
43
+ describe("inside a React component", () => {
44
+ it("useState routes to React.useState", () => {
45
+ function Counter() {
46
+ const [count, setCount] = useState(0);
47
+ return (
48
+ <button
49
+ type="button"
50
+ data-testid="btn"
51
+ onClick={() => setCount(count + 1)}
52
+ >
53
+ {count}
54
+ </button>
55
+ );
56
+ }
57
+
58
+ render(<Counter />);
59
+ const btn = screen.getByTestId("btn");
60
+ expect(btn.textContent).toBe("0");
61
+ act(() => btn.click());
62
+ expect(btn.textContent).toBe("1");
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { render, screen, act, cleanup } from "@testing-library/react";
3
+ import {
4
+ createTestResource,
5
+ renderTest,
6
+ cleanupAllResources,
7
+ } from "../test-utils";
8
+ import { resource } from "../../core/resource";
9
+ import { withKey } from "../../core/withKey";
10
+ import { useState } from "react";
11
+ import { useState as useResourceState } from "../../hooks/useState";
12
+ import { useEffect as useResourceEffect } from "../../hooks/useEffect";
13
+ import {
14
+ useResource,
15
+ useResources,
16
+ useResourceRoot,
17
+ flushResourcesSync,
18
+ } from "../../index";
19
+
20
+ describe("@assistant-ui/tap/react resource API", () => {
21
+ afterEach(() => {
22
+ cleanupAllResources();
23
+ cleanup();
24
+ });
25
+
26
+ describe("useResource", () => {
27
+ it("routes to useResource inside a tap resource", () => {
28
+ const Child = resource(function Child(props: { n: number }) {
29
+ return props.n * 2;
30
+ });
31
+ const parent = createTestResource(() => useResource(Child({ n: 21 })));
32
+ expect(renderTest(parent, undefined)).toBe(42);
33
+ });
34
+
35
+ it("routes to the React bridge inside a component", () => {
36
+ const CounterResource = resource(function CounterResource() {
37
+ const [count, setCount] = useResourceState(0);
38
+ return { count, setCount };
39
+ });
40
+
41
+ let api: { count: number; setCount: (n: number) => void } | null = null;
42
+ function App() {
43
+ api = useResource(CounterResource());
44
+ return <div data-testid="count">{api.count}</div>;
45
+ }
46
+
47
+ render(<App />);
48
+ expect(screen.getByTestId("count").textContent).toBe("0");
49
+ act(() => api!.setCount(3));
50
+ expect(screen.getByTestId("count").textContent).toBe("3");
51
+ });
52
+ });
53
+
54
+ describe("useResources", () => {
55
+ it("hosts a keyed list inside a tap resource", () => {
56
+ const Item = resource(function Item(p: { n: number }) {
57
+ return p.n * 10;
58
+ });
59
+ const parent = createTestResource(() =>
60
+ useResources(() => [
61
+ withKey("a", Item({ n: 1 })),
62
+ withKey("b", Item({ n: 2 })),
63
+ ]),
64
+ );
65
+ expect(renderTest(parent, undefined)).toEqual([10, 20]);
66
+ });
67
+
68
+ it("hosts a keyed list inside a React component and tracks deps", () => {
69
+ const Item = resource(function Item(p: { n: number }) {
70
+ const [v] = useResourceState(p.n * 10);
71
+ return v;
72
+ });
73
+
74
+ let setCount: (n: number) => void = () => {};
75
+ function App() {
76
+ const [count, setCountState] = useState(2);
77
+ setCount = setCountState;
78
+ const items = useResources(
79
+ () =>
80
+ Array.from({ length: count }, (_, i) =>
81
+ withKey(i, Item({ n: i + 1 })),
82
+ ),
83
+ [count],
84
+ );
85
+ return <div data-testid="list">{items.join(",")}</div>;
86
+ }
87
+
88
+ render(<App />);
89
+ expect(screen.getByTestId("list").textContent).toBe("10,20");
90
+ act(() => setCount(3));
91
+ expect(screen.getByTestId("list").textContent).toBe("10,20,30");
92
+ });
93
+ });
94
+
95
+ describe("useResourceRoot", () => {
96
+ it("exposes a subscribable inside a tap resource", () => {
97
+ const Root = resource(function Root() {
98
+ const [n] = useResourceState(7);
99
+ return n;
100
+ });
101
+ const parent = createTestResource(() =>
102
+ useResourceRoot(Root()).getValue(),
103
+ );
104
+ expect(renderTest(parent, undefined)).toBe(7);
105
+ });
106
+
107
+ // A root is push-based: host it in one place and observe it via getValue/
108
+ // subscribe elsewhere. (Hosting AND re-rendering off its own value in the same
109
+ // component self-feeds, since useResourceHost re-renders the root on every host render
110
+ // and the root notifies on output change — so this test observes the store
111
+ // directly rather than through a same-component useSyncExternalStore.)
112
+ it("hosts a subscribable root inside a React component", () => {
113
+ const CounterRoot = resource(function CounterRoot() {
114
+ const [count, setCount] = useResourceState(0);
115
+ return { count, setCount };
116
+ });
117
+
118
+ let store: ReturnType<
119
+ typeof useResourceRoot<{
120
+ count: number;
121
+ setCount: (n: number) => void;
122
+ }>
123
+ > | null = null;
124
+ function App() {
125
+ store = useResourceRoot(CounterRoot());
126
+ return null;
127
+ }
128
+
129
+ render(<App />);
130
+ expect(store!.getValue().count).toBe(0);
131
+
132
+ let notified = 0;
133
+ const unsubscribe = store!.subscribe(() => {
134
+ notified++;
135
+ });
136
+
137
+ // The root drives updates through tap's own (macrotask) scheduler, so flush
138
+ // synchronously to observe.
139
+ flushResourcesSync(() => store!.getValue().setCount(5));
140
+ expect(store!.getValue().count).toBe(5);
141
+ expect(notified).toBeGreaterThan(0);
142
+
143
+ unsubscribe();
144
+ });
145
+ });
146
+
147
+ describe("useResource key remount (React bridge)", () => {
148
+ it("remounts the hosted resource when the element key changes", () => {
149
+ const mounts: number[] = [];
150
+ const Keyed = resource(function Keyed(p: { id: number }) {
151
+ // oxlint-disable-next-line react/exhaustive-deps -- capture the mount id once per fiber to assert remount on key change
152
+ useResourceEffect(() => void mounts.push(p.id), []);
153
+ return p.id;
154
+ });
155
+
156
+ let setId: (n: number) => void = () => {};
157
+ function App() {
158
+ const [id, setIdState] = useState(1);
159
+ setId = setIdState;
160
+ const out = useResource(withKey(id, Keyed({ id })));
161
+ return <div data-testid="keyed">{out}</div>;
162
+ }
163
+
164
+ render(<App />);
165
+ expect(screen.getByTestId("keyed").textContent).toBe("1");
166
+ expect(mounts).toEqual([1]);
167
+ act(() => setId(2));
168
+ expect(screen.getByTestId("keyed").textContent).toBe("2");
169
+ expect(mounts).toEqual([1, 2]);
170
+ });
171
+ });
172
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import * as React from "react";
3
+ // react/compiler-runtime exports `c` (= useMemoCache) at runtime but ships no types for it.
4
+ // @ts-expect-error -- runtime-only export
5
+ import { c as _c } from "react/compiler-runtime";
6
+ import { renderResourceFiber } from "../core/ResourceFiber";
7
+ import {
8
+ createTestResource,
9
+ renderTest,
10
+ getCommittedOutput,
11
+ cleanupAllResources,
12
+ waitForNextTick,
13
+ } from "./test-utils";
14
+
15
+ // These resources author their hooks with the *real* `react` module (no shim, no
16
+ // build transform). tap's React dispatcher, installed around every resource body
17
+ // render, is what routes them to tap.
18
+ describe("react dispatcher", () => {
19
+ afterEach(() => {
20
+ cleanupAllResources();
21
+ });
22
+
23
+ it("routes React.useState to tap state", async () => {
24
+ let set!: (n: number) => void;
25
+ const fiber = createTestResource(() => {
26
+ const [n, setN] = React.useState(10);
27
+ set = setN;
28
+ return n;
29
+ });
30
+
31
+ expect(renderTest(fiber, undefined)).toBe(10);
32
+ set(42);
33
+ await waitForNextTick();
34
+ expect(getCommittedOutput(fiber)).toBe(42);
35
+ });
36
+
37
+ it("routes React.useMemo with deps memoization", () => {
38
+ let runs = 0;
39
+ const fiber = createTestResource((p: { x: number }) =>
40
+ React.useMemo(() => {
41
+ runs++;
42
+ return p.x * 2;
43
+ }, [p.x]),
44
+ );
45
+
46
+ expect(renderTest(fiber, { x: 2 })).toBe(4);
47
+ expect(runs).toBe(1);
48
+ renderTest(fiber, { x: 2 }); // same dep -> memoized
49
+ expect(runs).toBe(1);
50
+ expect(renderTest(fiber, { x: 3 })).toBe(6);
51
+ expect(runs).toBe(2);
52
+ });
53
+
54
+ it("backs react/compiler-runtime's useMemoCache so compiled resources work", () => {
55
+ const SENTINEL = Symbol.for("react.memo_cache_sentinel");
56
+ const fiber = createTestResource(() => {
57
+ // exactly what React Compiler emits: const $ = _c(n)
58
+ const $ = _c(3);
59
+ if ($[0] === SENTINEL) $[0] = "computed-once";
60
+ return $[0];
61
+ });
62
+
63
+ expect(renderTest(fiber, undefined)).toBe("computed-once");
64
+ // re-render: the cache persists across renders, slot already filled
65
+ expect(renderTest(fiber, undefined)).toBe("computed-once");
66
+ });
67
+
68
+ it("throws for a hook tap does not implement", () => {
69
+ const fiber = createTestResource(() => React.useId());
70
+ // render directly: a mid-render throw must not leave a tracked, unmounted
71
+ // fiber for `cleanupAllResources` to choke on.
72
+ expect(() => renderResourceFiber(fiber, undefined)).toThrow();
73
+ });
74
+ });
@@ -1,17 +1,18 @@
1
+ /* oxlint-disable react/rules-of-hooks -- tests deliberately exercise conditional/nested hook patterns */
1
2
  import { describe, it, expect } from "vitest";
2
- import { tapEffect } from "../../hooks/tap-effect";
3
- import { tapState } from "../../hooks/tap-state";
3
+ import { useEffect } from "../../hooks/useEffect";
4
+ import { useState } from "../../hooks/useState";
4
5
  import { createTestResource, renderTest } from "../test-utils";
5
6
  import { renderResourceFiber } from "../../core/ResourceFiber";
6
7
 
7
8
  describe("Rules of Hooks - Hook Count", () => {
8
9
  it("should establish hook count on first render", () => {
9
10
  const resource = createTestResource(() => {
10
- const [a] = tapState(1);
11
- const [b] = tapState(2);
12
- const [c] = tapState(3);
13
- tapEffect(() => {});
14
- tapEffect(() => {});
11
+ const [a] = useState(1);
12
+ const [b] = useState(2);
13
+ const [c] = useState(3);
14
+ useEffect(() => {});
15
+ useEffect(() => {});
15
16
 
16
17
  return { a, b, c };
17
18
  });
@@ -29,11 +30,11 @@ describe("Rules of Hooks - Hook Count", () => {
29
30
  let addExtraHook = false;
30
31
 
31
32
  const resource = createTestResource(() => {
32
- tapState(1);
33
- tapState(2);
33
+ useState(1);
34
+ useState(2);
34
35
 
35
36
  if (addExtraHook) {
36
- tapState(3); // Extra hook
37
+ useState(3); // Extra hook
37
38
  }
38
39
 
39
40
  return null;
@@ -54,13 +55,13 @@ describe("Rules of Hooks - Hook Count", () => {
54
55
  let skipHook = false;
55
56
 
56
57
  const resource = createTestResource(() => {
57
- tapState(1);
58
+ useState(1);
58
59
 
59
60
  if (!skipHook) {
60
- tapState(2);
61
+ useState(2);
61
62
  }
62
63
 
63
- tapState(3);
64
+ useState(3);
64
65
  return null;
65
66
  });
66
67
 
@@ -79,11 +80,11 @@ describe("Rules of Hooks - Hook Count", () => {
79
80
  let includeEffect = true;
80
81
 
81
82
  const resource = createTestResource(() => {
82
- tapState(1);
83
- tapState(2);
83
+ useState(1);
84
+ useState(2);
84
85
 
85
86
  if (includeEffect) {
86
- tapEffect(() => {});
87
+ useEffect(() => {});
87
88
  }
88
89
  return null;
89
90
  });
@@ -114,7 +115,7 @@ describe("Rules of Hooks - Hook Count", () => {
114
115
 
115
116
  const resource = createTestResource(() => {
116
117
  for (let i = 0; i < hookCount; i++) {
117
- tapState(i);
118
+ useState(i);
118
119
  }
119
120
  return null;
120
121
  });
@@ -134,9 +135,9 @@ describe("Rules of Hooks - Hook Count", () => {
134
135
 
135
136
  const resource = createTestResource(() => {
136
137
  renderCount++;
137
- const [a] = tapState(1);
138
- const [b] = tapState(2);
139
- tapEffect(() => {});
138
+ const [a] = useState(1);
139
+ const [b] = useState(2);
140
+ useEffect(() => {});
140
141
 
141
142
  return { a, b, renderCount };
142
143
  });
@@ -151,16 +152,16 @@ describe("Rules of Hooks - Hook Count", () => {
151
152
 
152
153
  it("should track count separately for different resource instances", () => {
153
154
  const resource1 = createTestResource(() => {
154
- tapState(1);
155
- tapState(2);
155
+ useState(1);
156
+ useState(2);
156
157
  return "two hooks";
157
158
  });
158
159
 
159
160
  const resource2 = createTestResource(() => {
160
- tapState(1);
161
- tapState(2);
162
- tapState(3);
163
- tapEffect(() => {});
161
+ useState(1);
162
+ useState(2);
163
+ useState(3);
164
+ useEffect(() => {});
164
165
  return "four hooks";
165
166
  });
166
167
 
@@ -177,14 +178,14 @@ describe("Rules of Hooks - Hook Count", () => {
177
178
  let useExtraHooks = false;
178
179
 
179
180
  const useFeature = () => {
180
- tapState("feature");
181
+ useState("feature");
181
182
  if (useExtraHooks) {
182
- tapState("extra");
183
+ useState("extra");
183
184
  }
184
185
  };
185
186
 
186
187
  const resource = createTestResource(() => {
187
- tapState("main");
188
+ useState("main");
188
189
  useFeature();
189
190
  return null;
190
191
  });