@assistant-ui/tap 0.5.14 → 0.6.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 (181) 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/execution-context.d.ts +2 -1
  14. package/dist/core/helpers/execution-context.d.ts.map +1 -1
  15. package/dist/core/helpers/execution-context.js +4 -1
  16. package/dist/core/helpers/execution-context.js.map +1 -1
  17. package/dist/core/react-dispatcher.d.ts +12 -0
  18. package/dist/core/react-dispatcher.d.ts.map +1 -0
  19. package/dist/core/react-dispatcher.js +63 -0
  20. package/dist/core/react-dispatcher.js.map +1 -0
  21. package/dist/core/scheduler.js +1 -1
  22. package/dist/core/scheduler.js.map +1 -1
  23. package/dist/core/types.d.ts +3 -3
  24. package/dist/hooks/index.d.ts +13 -0
  25. package/dist/hooks/index.js +13 -0
  26. package/dist/hooks/use.d.ts +9 -0
  27. package/dist/hooks/use.d.ts.map +1 -0
  28. package/dist/hooks/use.js +14 -0
  29. package/dist/hooks/use.js.map +1 -0
  30. package/dist/hooks/useCallback.d.ts +5 -0
  31. package/dist/hooks/useCallback.d.ts.map +1 -0
  32. package/dist/hooks/useCallback.js +9 -0
  33. package/dist/hooks/useCallback.js.map +1 -0
  34. package/dist/hooks/useEffect.d.ts +10 -0
  35. package/dist/hooks/useEffect.d.ts.map +1 -0
  36. package/dist/hooks/{tap-effect.js → useEffect.js} +7 -7
  37. package/dist/hooks/useEffect.js.map +1 -0
  38. package/dist/hooks/{tap-effect-event.d.ts → useEffectEvent.d.ts} +5 -5
  39. package/dist/hooks/useEffectEvent.d.ts.map +1 -0
  40. package/dist/hooks/{tap-effect-event.js → useEffectEvent.js} +12 -12
  41. package/dist/hooks/useEffectEvent.js.map +1 -0
  42. package/dist/hooks/useMemo.d.ts +5 -0
  43. package/dist/hooks/useMemo.d.ts.map +1 -0
  44. package/dist/hooks/{tap-memo.js → useMemo.js} +6 -6
  45. package/dist/hooks/useMemo.js.map +1 -0
  46. package/dist/hooks/useMemoCache.d.ts +10 -0
  47. package/dist/hooks/useMemoCache.d.ts.map +1 -0
  48. package/dist/hooks/useMemoCache.js +21 -0
  49. package/dist/hooks/useMemoCache.js.map +1 -0
  50. package/dist/hooks/useReducer.d.ts +21 -0
  51. package/dist/hooks/useReducer.d.ts.map +1 -0
  52. package/dist/hooks/{tap-reducer.js → useReducer.js} +10 -10
  53. package/dist/hooks/useReducer.js.map +1 -0
  54. package/dist/hooks/useRef.d.ts +11 -0
  55. package/dist/hooks/useRef.d.ts.map +1 -0
  56. package/dist/hooks/useRef.js +10 -0
  57. package/dist/hooks/useRef.js.map +1 -0
  58. package/dist/{react/use-resource.d.ts → hooks/useResource.d.ts} +3 -2
  59. package/dist/hooks/useResource.d.ts.map +1 -0
  60. package/dist/hooks/{tap-resource.js → useResource.js} +12 -12
  61. package/dist/hooks/useResource.js.map +1 -0
  62. package/dist/hooks/useResourceRoot.d.ts +20 -0
  63. package/dist/hooks/useResourceRoot.d.ts.map +1 -0
  64. package/dist/{tapResourceRoot.js → hooks/useResourceRoot.js} +30 -26
  65. package/dist/hooks/useResourceRoot.js.map +1 -0
  66. package/dist/hooks/{tap-resources.d.ts → useResources.d.ts} +4 -4
  67. package/dist/hooks/useResources.d.ts.map +1 -0
  68. package/dist/hooks/{tap-resources.js → useResources.js} +28 -23
  69. package/dist/hooks/useResources.js.map +1 -0
  70. package/dist/hooks/useState.d.ts +9 -0
  71. package/dist/hooks/useState.d.ts.map +1 -0
  72. package/dist/hooks/useState.js +11 -0
  73. package/dist/hooks/useState.js.map +1 -0
  74. package/dist/hooks/utils/useCell.d.ts +10 -0
  75. package/dist/hooks/utils/useCell.d.ts.map +1 -0
  76. package/dist/hooks/utils/{tapHook.js → useCell.js} +4 -4
  77. package/dist/hooks/utils/{tapHook.js.map → useCell.js.map} +1 -1
  78. package/dist/index.d.ts +3 -13
  79. package/dist/index.js +3 -13
  80. package/dist/react/hooks.d.ts +25 -0
  81. package/dist/react/hooks.d.ts.map +1 -0
  82. package/dist/react/hooks.js +69 -0
  83. package/dist/react/hooks.js.map +1 -0
  84. package/dist/react-shim/index.d.ts +17 -0
  85. package/dist/react-shim/index.d.ts.map +1 -0
  86. package/dist/react-shim/index.js +28 -0
  87. package/dist/react-shim/index.js.map +1 -0
  88. package/package.json +13 -16
  89. package/react-shim/package.json +4 -0
  90. package/src/__tests__/basic/resourceHandle.test.ts +7 -3
  91. package/src/__tests__/basic/tapEffect.basic.test.ts +19 -19
  92. package/src/__tests__/basic/tapReducer.basic.test.ts +14 -14
  93. package/src/__tests__/basic/tapResources.basic.test.ts +19 -14
  94. package/src/__tests__/basic/tapState.basic.test.ts +20 -20
  95. package/src/__tests__/errors/errors.effect-errors.test.ts +21 -21
  96. package/src/__tests__/errors/errors.render-errors.test.ts +18 -18
  97. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +25 -25
  98. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +17 -18
  99. package/src/__tests__/react/concurrent-mode.test.tsx +7 -7
  100. package/src/__tests__/react/react-shim.test.tsx +65 -0
  101. package/src/__tests__/react/useResource.test.tsx +172 -0
  102. package/src/__tests__/react-dispatcher.test.ts +74 -0
  103. package/src/__tests__/rules/rules.hook-count.test.ts +30 -29
  104. package/src/__tests__/rules/rules.hook-order.test.ts +27 -27
  105. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +1 -1
  106. package/src/__tests__/strictmode/strictmode.test.ts +42 -42
  107. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +55 -55
  108. package/src/__tests__/test-utils.ts +2 -2
  109. package/src/core/ResourceFiber.ts +4 -1
  110. package/src/core/context.ts +31 -9
  111. package/src/core/createResourceRoot.ts +4 -4
  112. package/src/core/helpers/execution-context.ts +4 -0
  113. package/src/core/react-dispatcher.ts +78 -0
  114. package/src/core/scheduler.ts +1 -1
  115. package/src/core/types.ts +3 -3
  116. package/src/hooks/index.ts +12 -0
  117. package/src/hooks/use.ts +13 -0
  118. package/src/hooks/useCallback.ts +9 -0
  119. package/src/hooks/{tap-effect.ts → useEffect.ts} +9 -9
  120. package/src/hooks/{tap-effect-event.ts → useEffectEvent.ts} +9 -9
  121. package/src/hooks/{tap-memo.ts → useMemo.ts} +3 -3
  122. package/src/hooks/useMemoCache.ts +25 -0
  123. package/src/hooks/{tap-reducer.ts → useReducer.ts} +23 -11
  124. package/src/hooks/useRef.ts +16 -0
  125. package/src/hooks/{tap-resource.ts → useResource.ts} +13 -12
  126. package/src/{tapResourceRoot.ts → hooks/useResourceRoot.ts} +26 -27
  127. package/src/hooks/{tap-resources.ts → useResources.ts} +21 -22
  128. package/src/hooks/useState.ts +29 -0
  129. package/src/hooks/utils/{tapHook.ts → useCell.ts} +1 -1
  130. package/src/index.ts +4 -24
  131. package/src/react/hooks.ts +112 -0
  132. package/src/react-shim/index.ts +75 -0
  133. package/dist/hooks/tap-callback.d.ts +0 -5
  134. package/dist/hooks/tap-callback.d.ts.map +0 -1
  135. package/dist/hooks/tap-callback.js +0 -9
  136. package/dist/hooks/tap-callback.js.map +0 -1
  137. package/dist/hooks/tap-const.d.ts +0 -5
  138. package/dist/hooks/tap-const.d.ts.map +0 -1
  139. package/dist/hooks/tap-const.js +0 -10
  140. package/dist/hooks/tap-const.js.map +0 -1
  141. package/dist/hooks/tap-effect-event.d.ts.map +0 -1
  142. package/dist/hooks/tap-effect-event.js.map +0 -1
  143. package/dist/hooks/tap-effect.d.ts +0 -10
  144. package/dist/hooks/tap-effect.d.ts.map +0 -1
  145. package/dist/hooks/tap-effect.js.map +0 -1
  146. package/dist/hooks/tap-memo.d.ts +0 -5
  147. package/dist/hooks/tap-memo.d.ts.map +0 -1
  148. package/dist/hooks/tap-memo.js.map +0 -1
  149. package/dist/hooks/tap-reducer.d.ts +0 -9
  150. package/dist/hooks/tap-reducer.d.ts.map +0 -1
  151. package/dist/hooks/tap-reducer.js.map +0 -1
  152. package/dist/hooks/tap-ref.d.ts +0 -11
  153. package/dist/hooks/tap-ref.d.ts.map +0 -1
  154. package/dist/hooks/tap-ref.js +0 -10
  155. package/dist/hooks/tap-ref.js.map +0 -1
  156. package/dist/hooks/tap-resource.d.ts +0 -8
  157. package/dist/hooks/tap-resource.d.ts.map +0 -1
  158. package/dist/hooks/tap-resource.js.map +0 -1
  159. package/dist/hooks/tap-resources.d.ts.map +0 -1
  160. package/dist/hooks/tap-resources.js.map +0 -1
  161. package/dist/hooks/tap-state.d.ts +0 -9
  162. package/dist/hooks/tap-state.d.ts.map +0 -1
  163. package/dist/hooks/tap-state.js +0 -11
  164. package/dist/hooks/tap-state.js.map +0 -1
  165. package/dist/hooks/utils/tapHook.d.ts +0 -10
  166. package/dist/hooks/utils/tapHook.d.ts.map +0 -1
  167. package/dist/react/index.d.ts +0 -2
  168. package/dist/react/index.js +0 -2
  169. package/dist/react/use-resource.d.ts.map +0 -1
  170. package/dist/react/use-resource.js +0 -46
  171. package/dist/react/use-resource.js.map +0 -1
  172. package/dist/tapResourceRoot.d.ts +0 -20
  173. package/dist/tapResourceRoot.d.ts.map +0 -1
  174. package/dist/tapResourceRoot.js.map +0 -1
  175. package/react/package.json +0 -5
  176. package/src/hooks/tap-callback.ts +0 -9
  177. package/src/hooks/tap-const.ts +0 -6
  178. package/src/hooks/tap-ref.ts +0 -16
  179. package/src/hooks/tap-state.ts +0 -29
  180. package/src/react/index.ts +0 -1
  181. 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,7 +13,7 @@ describe("Lifecycle - Mount/Unmount", () => {
13
13
  const effects = [vi.fn(), vi.fn(), vi.fn()];
14
14
 
15
15
  const resource = createTestResource(() => {
16
- effects.forEach((fn) => tapEffect(fn));
16
+ effects.forEach((fn) => useEffect(fn));
17
17
  return null;
18
18
  });
19
19
 
@@ -29,7 +29,7 @@ describe("Lifecycle - Mount/Unmount", () => {
29
29
 
30
30
  const resource = createTestResource(() => {
31
31
  cleanups.forEach((cleanup) => {
32
- tapEffect(() => cleanup);
32
+ useEffect(() => cleanup);
33
33
  });
34
34
  return null;
35
35
  });
@@ -45,9 +45,9 @@ describe("Lifecycle - Mount/Unmount", () => {
45
45
  const order: number[] = [];
46
46
 
47
47
  const resource = createTestResource(() => {
48
- tapEffect(() => () => order.push(1));
49
- tapEffect(() => () => order.push(2));
50
- tapEffect(() => () => order.push(3));
48
+ useEffect(() => () => order.push(1));
49
+ useEffect(() => () => order.push(2));
50
+ useEffect(() => () => order.push(3));
51
51
  return null;
52
52
  });
53
53
 
@@ -64,11 +64,11 @@ describe("Lifecycle - Mount/Unmount", () => {
64
64
 
65
65
  const resource = createTestResource((props: number) => {
66
66
  renderCount++;
67
- const [state, _setState] = tapState({ count: 0 });
67
+ const [state, _setState] = useState({ count: 0 });
68
68
  setState = _setState;
69
69
 
70
70
  // Simple effect that tracks runs
71
- tapEffect(() => {
71
+ useEffect(() => {
72
72
  effectRunCount++;
73
73
  });
74
74
 
@@ -100,18 +100,18 @@ describe("Lifecycle - Mount/Unmount", () => {
100
100
  const log: string[] = [];
101
101
 
102
102
  const resource = createTestResource(() => {
103
- const [mounted, setMounted] = tapState(false);
103
+ const [mounted, setMounted] = useState(false);
104
104
 
105
105
  log.push("render");
106
106
 
107
- tapEffect(() => {
107
+ useEffect(() => {
108
108
  log.push("effect-1");
109
109
  setMounted(true);
110
110
 
111
111
  return () => log.push("cleanup-1");
112
112
  });
113
113
 
114
- tapEffect(() => {
114
+ useEffect(() => {
115
115
  log.push("effect-2");
116
116
  return () => log.push("cleanup-2");
117
117
  });
@@ -128,8 +128,7 @@ describe("Lifecycle - Mount/Unmount", () => {
128
128
  // After commit: initial render + effects
129
129
  expect(log).toEqual(["render", "effect-1", "effect-2"]);
130
130
 
131
- // The setState in effect schedules a re-render
132
- // With the new architecture, we need to manually trigger it
131
+ // The setState in effect schedules a re-render; trigger it manually
133
132
  const ctx2 = renderResourceFiber(resource, undefined);
134
133
  commitResourceFiber(resource, ctx2);
135
134
 
@@ -158,10 +157,10 @@ describe("Lifecycle - Mount/Unmount", () => {
158
157
  const goodCleanup = vi.fn();
159
158
 
160
159
  const resource = createTestResource(() => {
161
- tapEffect(() => () => {
160
+ useEffect(() => () => {
162
161
  throw error;
163
162
  });
164
- tapEffect(() => goodCleanup);
163
+ useEffect(() => goodCleanup);
165
164
  return null;
166
165
  });
167
166
 
@@ -178,7 +177,7 @@ describe("Lifecycle - Mount/Unmount", () => {
178
177
 
179
178
  const resource = createTestResource(() => {
180
179
  if (!skipEffect) {
181
- tapEffect(() => cleanup);
180
+ useEffect(() => cleanup);
182
181
  }
183
182
  return null;
184
183
  });
@@ -194,7 +193,7 @@ describe("Lifecycle - Mount/Unmount", () => {
194
193
  const cleanup = vi.fn();
195
194
 
196
195
  const resource = createTestResource(() => {
197
- tapEffect(() => {
196
+ useEffect(() => {
198
197
  effect();
199
198
  return cleanup;
200
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
  });