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