@assistant-ui/tap 0.3.3 → 0.3.5

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 (107) hide show
  1. package/dist/core/ResourceFiber.d.ts +1 -1
  2. package/dist/core/ResourceFiber.d.ts.map +1 -1
  3. package/dist/core/ResourceFiber.js +35 -40
  4. package/dist/core/ResourceFiber.js.map +1 -1
  5. package/dist/core/callResourceFn.js +15 -12
  6. package/dist/core/callResourceFn.js.map +1 -1
  7. package/dist/core/commit.d.ts +1 -1
  8. package/dist/core/commit.d.ts.map +1 -1
  9. package/dist/core/commit.js +57 -54
  10. package/dist/core/commit.js.map +1 -1
  11. package/dist/core/context.js +16 -21
  12. package/dist/core/context.js.map +1 -1
  13. package/dist/core/createResource.d.ts +1 -1
  14. package/dist/core/createResource.d.ts.map +1 -1
  15. package/dist/core/createResource.js +54 -61
  16. package/dist/core/createResource.js.map +1 -1
  17. package/dist/core/execution-context.d.ts +1 -1
  18. package/dist/core/execution-context.d.ts.map +1 -1
  19. package/dist/core/execution-context.js +21 -25
  20. package/dist/core/execution-context.js.map +1 -1
  21. package/dist/core/resource.d.ts +1 -1
  22. package/dist/core/resource.d.ts.map +1 -1
  23. package/dist/core/resource.js +8 -12
  24. package/dist/core/resource.js.map +1 -1
  25. package/dist/core/scheduler.js +73 -72
  26. package/dist/core/scheduler.js.map +1 -1
  27. package/dist/core/types.d.ts +3 -3
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/core/types.js +1 -0
  30. package/dist/core/types.js.map +1 -1
  31. package/dist/hooks/depsShallowEqual.js +8 -10
  32. package/dist/hooks/depsShallowEqual.js.map +1 -1
  33. package/dist/hooks/tap-callback.js +2 -6
  34. package/dist/hooks/tap-callback.js.map +1 -1
  35. package/dist/hooks/tap-effect-event.js +21 -10
  36. package/dist/hooks/tap-effect-event.js.map +1 -1
  37. package/dist/hooks/tap-effect.js +30 -31
  38. package/dist/hooks/tap-effect.js.map +1 -1
  39. package/dist/hooks/tap-inline-resource.d.ts +1 -1
  40. package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
  41. package/dist/hooks/tap-inline-resource.js +2 -6
  42. package/dist/hooks/tap-inline-resource.js.map +1 -1
  43. package/dist/hooks/tap-memo.js +10 -14
  44. package/dist/hooks/tap-memo.js.map +1 -1
  45. package/dist/hooks/tap-ref.js +5 -9
  46. package/dist/hooks/tap-ref.js.map +1 -1
  47. package/dist/hooks/tap-resource.d.ts +1 -1
  48. package/dist/hooks/tap-resource.d.ts.map +1 -1
  49. package/dist/hooks/tap-resource.js +13 -28
  50. package/dist/hooks/tap-resource.js.map +1 -1
  51. package/dist/hooks/tap-resources.d.ts +1 -1
  52. package/dist/hooks/tap-resources.d.ts.map +1 -1
  53. package/dist/hooks/tap-resources.js +63 -64
  54. package/dist/hooks/tap-resources.js.map +1 -1
  55. package/dist/hooks/tap-state.js +47 -44
  56. package/dist/hooks/tap-state.js.map +1 -1
  57. package/dist/index.d.ts +14 -14
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +18 -31
  60. package/dist/index.js.map +1 -1
  61. package/dist/react/index.d.ts +1 -1
  62. package/dist/react/index.d.ts.map +1 -1
  63. package/dist/react/index.js +1 -5
  64. package/dist/react/index.js.map +1 -1
  65. package/dist/react/use-resource.d.ts +1 -1
  66. package/dist/react/use-resource.d.ts.map +1 -1
  67. package/dist/react/use-resource.js +16 -26
  68. package/dist/react/use-resource.js.map +1 -1
  69. package/package.json +44 -30
  70. package/react/package.json +5 -0
  71. package/src/__tests__/basic/resourceHandle.test.ts +56 -0
  72. package/src/__tests__/basic/tapEffect.basic.test.ts +247 -0
  73. package/src/__tests__/basic/tapResources.basic.test.ts +222 -0
  74. package/src/__tests__/basic/tapState.basic.test.ts +240 -0
  75. package/src/__tests__/errors/errors.effect-errors.test.ts +222 -0
  76. package/src/__tests__/errors/errors.render-errors.test.ts +190 -0
  77. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +241 -0
  78. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +211 -0
  79. package/src/__tests__/rules/rules.hook-count.test.ts +200 -0
  80. package/src/__tests__/rules/rules.hook-order.test.ts +192 -0
  81. package/src/__tests__/test-utils.ts +219 -0
  82. package/src/core/ResourceFiber.ts +58 -0
  83. package/src/core/callResourceFn.ts +21 -0
  84. package/src/core/commit.ts +73 -0
  85. package/src/core/context.ts +28 -0
  86. package/src/core/createResource.ts +116 -0
  87. package/src/core/execution-context.ts +34 -0
  88. package/src/core/resource.ts +16 -0
  89. package/src/core/scheduler.ts +95 -0
  90. package/src/core/types.ts +59 -0
  91. package/src/hooks/depsShallowEqual.ts +10 -0
  92. package/src/hooks/tap-callback.ts +8 -0
  93. package/src/hooks/tap-effect-event.ts +29 -0
  94. package/src/hooks/tap-effect.ts +59 -0
  95. package/src/hooks/tap-inline-resource.ts +8 -0
  96. package/src/hooks/tap-memo.ts +16 -0
  97. package/src/hooks/tap-ref.ts +16 -0
  98. package/src/hooks/tap-resource.ts +44 -0
  99. package/src/hooks/tap-resources.ts +112 -0
  100. package/src/hooks/tap-state.ts +83 -0
  101. package/src/index.ts +31 -0
  102. package/src/react/index.ts +1 -0
  103. package/src/react/use-resource.ts +35 -0
  104. package/dist/__tests__/test-utils.d.ts +0 -79
  105. package/dist/__tests__/test-utils.d.ts.map +0 -1
  106. package/dist/__tests__/test-utils.js +0 -138
  107. package/dist/__tests__/test-utils.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { tapState } from "../../hooks/tap-state";
3
+ import { tapEffect } from "../../hooks/tap-effect";
4
+ import {
5
+ createTestResource,
6
+ renderTest,
7
+ cleanupAllResources,
8
+ waitForNextTick,
9
+ getCommittedState,
10
+ } from "../test-utils";
11
+
12
+ describe("tapState - Basic Functionality", () => {
13
+ afterEach(() => {
14
+ cleanupAllResources();
15
+ });
16
+
17
+ describe("Initialization", () => {
18
+ it("should initialize with direct value", () => {
19
+ const testFiber = createTestResource(() => {
20
+ const [count] = tapState(42);
21
+ return count;
22
+ });
23
+
24
+ const result = renderTest(testFiber, undefined);
25
+ expect(result).toBe(42);
26
+ });
27
+
28
+ it("should initialize with lazy value function", () => {
29
+ let initCalled = 0;
30
+
31
+ const testFiber = createTestResource(() => {
32
+ const [count] = tapState(() => {
33
+ initCalled++;
34
+ return 100;
35
+ });
36
+ return count;
37
+ });
38
+
39
+ // First render
40
+ const result = renderTest(testFiber, undefined);
41
+ expect(result).toBe(100);
42
+ expect(initCalled).toBe(1);
43
+
44
+ // Re-render should not call initializer again
45
+ renderTest(testFiber, undefined);
46
+ expect(initCalled).toBe(1);
47
+ });
48
+
49
+ it("should handle undefined initial state", () => {
50
+ const testFiber = createTestResource(() => {
51
+ const [value] = tapState<string>();
52
+ return value;
53
+ });
54
+
55
+ const result = renderTest(testFiber, undefined);
56
+ expect(result).toBeUndefined();
57
+ });
58
+ });
59
+
60
+ describe("State Updates", () => {
61
+ it("should update state and trigger re-render", async () => {
62
+ let renderCount = 0;
63
+ let setCountFn: ((value: number) => void) | null = null;
64
+
65
+ const testFiber = createTestResource(() => {
66
+ renderCount++;
67
+ const [count, setCount] = tapState(0);
68
+
69
+ // Capture setter on first render
70
+ if (!setCountFn) {
71
+ setCountFn = setCount;
72
+ }
73
+
74
+ return { count, renderCount };
75
+ });
76
+
77
+ // Initial render
78
+ const result1 = renderTest(testFiber, undefined);
79
+ expect(result1).toEqual({ count: 0, renderCount: 1 });
80
+
81
+ // Update state
82
+ setCountFn!(10);
83
+
84
+ // Wait for re-render
85
+ await waitForNextTick();
86
+
87
+ // Check that state was updated
88
+ expect(getCommittedState(testFiber)).toEqual({
89
+ count: 10,
90
+ renderCount: 2,
91
+ });
92
+ });
93
+
94
+ it("should not re-render for same value (Object.is comparison)", async () => {
95
+ let renderCount = 0;
96
+ let setCountFn: ((value: number) => void) | null = null;
97
+
98
+ const testFiber = createTestResource(() => {
99
+ renderCount++;
100
+ const [count, setCount] = tapState(42);
101
+
102
+ tapEffect(() => {
103
+ setCountFn = setCount;
104
+ });
105
+
106
+ return { count, renderCount };
107
+ });
108
+
109
+ // Initial render
110
+ renderTest(testFiber, undefined);
111
+ expect(renderCount).toBe(1);
112
+
113
+ // Set same value
114
+ setCountFn!(42);
115
+
116
+ // Wait to ensure no re-render happens
117
+ await waitForNextTick();
118
+
119
+ // Should not trigger re-render
120
+ expect(renderCount).toBe(1);
121
+ });
122
+
123
+ it("should handle functional updates", async () => {
124
+ let setCountFn: ((updater: (prev: number) => number) => void) | null =
125
+ null;
126
+
127
+ const testFiber = createTestResource(() => {
128
+ const [count, setCount] = tapState(10);
129
+
130
+ tapEffect(() => {
131
+ setCountFn = setCount;
132
+ });
133
+
134
+ return count;
135
+ });
136
+
137
+ // Initial render
138
+ renderTest(testFiber, undefined);
139
+ expect(getCommittedState(testFiber)).toBe(10);
140
+
141
+ // Functional update
142
+ setCountFn!((prev) => prev * 2);
143
+
144
+ await waitForNextTick();
145
+ expect(getCommittedState(testFiber)).toBe(20);
146
+
147
+ // Another functional update
148
+ setCountFn!((prev) => prev + 5);
149
+
150
+ await waitForNextTick();
151
+ expect(getCommittedState(testFiber)).toBe(25);
152
+ });
153
+ });
154
+
155
+ describe("Multiple States", () => {
156
+ it("should handle multiple state hooks independently", () => {
157
+ const testFiber = createTestResource(() => {
158
+ const [count1, setCount1] = tapState(1);
159
+ const [count2, setCount2] = tapState(2);
160
+ const [text, setText] = tapState("hello");
161
+
162
+ return {
163
+ count1,
164
+ count2,
165
+ text,
166
+ setters: { setCount1, setCount2, setText },
167
+ };
168
+ });
169
+
170
+ const result = renderTest(testFiber, undefined);
171
+ expect(result).toMatchObject({
172
+ count1: 1,
173
+ count2: 2,
174
+ text: "hello",
175
+ });
176
+ });
177
+
178
+ it("should update multiple states independently", async () => {
179
+ let setters: any = null;
180
+
181
+ const testFiber = createTestResource(() => {
182
+ const [a, setA] = tapState("a");
183
+ const [b, setB] = tapState("b");
184
+ const [c, setC] = tapState("c");
185
+
186
+ tapEffect(() => {
187
+ setters = { setA, setB, setC };
188
+ });
189
+
190
+ return { a, b, c };
191
+ });
192
+
193
+ // Initial render
194
+ renderTest(testFiber, undefined);
195
+ expect(getCommittedState(testFiber)).toEqual({ a: "a", b: "b", c: "c" });
196
+
197
+ // Update only B
198
+ setters.setB("B");
199
+ await waitForNextTick();
200
+ expect(getCommittedState(testFiber)).toEqual({ a: "a", b: "B", c: "c" });
201
+
202
+ // Update A and C
203
+ setters.setA("A");
204
+ setters.setC("C");
205
+ await waitForNextTick();
206
+ expect(getCommittedState(testFiber)).toEqual({ a: "A", b: "B", c: "C" });
207
+ });
208
+ });
209
+
210
+ describe("State Persistence", () => {
211
+ it("should persist state across prop changes", async () => {
212
+ let setCountFn: ((value: number) => void) | null = null;
213
+
214
+ const testFiber = createTestResource((props: { multiplier: number }) => {
215
+ const [count, setCount] = tapState(10);
216
+
217
+ tapEffect(() => {
218
+ setCountFn = setCount;
219
+ });
220
+
221
+ return {
222
+ count,
223
+ multiplied: count * props.multiplier,
224
+ };
225
+ });
226
+
227
+ // Initial render
228
+ const result1 = renderTest(testFiber, { multiplier: 2 });
229
+ expect(result1).toEqual({ count: 10, multiplied: 20 });
230
+
231
+ // Update state
232
+ setCountFn!(15);
233
+ await waitForNextTick();
234
+
235
+ // Re-render with different props
236
+ const result2 = renderTest(testFiber, { multiplier: 3 });
237
+ expect(result2).toEqual({ count: 15, multiplied: 45 });
238
+ });
239
+ });
240
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { tapEffect } from "../../hooks/tap-effect";
3
+ import { tapState } from "../../hooks/tap-state";
4
+ import { createTestResource, renderTest, unmountResource } from "../test-utils";
5
+ import {
6
+ renderResourceFiber,
7
+ commitResourceFiber,
8
+ } from "../../core/ResourceFiber";
9
+
10
+ describe("Errors - Effect Errors", () => {
11
+ it("should propagate errors from effects", () => {
12
+ const error = new Error("Effect error");
13
+
14
+ const resource = createTestResource(() => {
15
+ tapEffect(() => {
16
+ throw error;
17
+ });
18
+ return null;
19
+ });
20
+
21
+ expect(() => renderTest(resource, undefined)).toThrow(error);
22
+ });
23
+
24
+ it("should propagate errors from cleanup functions", () => {
25
+ const error = new Error("Cleanup error");
26
+ let dep = 0;
27
+
28
+ const resource = createTestResource(() => {
29
+ tapEffect(() => {
30
+ return () => {
31
+ if (dep > 0) {
32
+ throw error;
33
+ }
34
+ };
35
+ }, [dep]); // Cleanup will run when dep changes
36
+
37
+ return dep;
38
+ });
39
+
40
+ // First render and commit - establishes the effect
41
+ const ctx1 = renderResourceFiber(resource, undefined);
42
+ commitResourceFiber(resource, ctx1);
43
+
44
+ // Change dep to trigger cleanup on next render
45
+ dep = 1;
46
+
47
+ // Second render with different dep should trigger cleanup that throws
48
+ const ctx2 = renderResourceFiber(resource, undefined);
49
+ expect(() => commitResourceFiber(resource, ctx2)).toThrow(error);
50
+ });
51
+
52
+ it("should throw on invalid effect return value", () => {
53
+ const resource = createTestResource(() => {
54
+ tapEffect(() => {
55
+ return "not a function" as any; // Invalid return
56
+ });
57
+ return null;
58
+ });
59
+
60
+ expect(() => renderTest(resource, undefined)).toThrow(
61
+ "An effect function must either return a cleanup function or nothing",
62
+ );
63
+ });
64
+
65
+ it("should handle multiple effect errors", () => {
66
+ const error1 = new Error("First error");
67
+ const error2 = new Error("Second error");
68
+ const goodEffect = vi.fn();
69
+
70
+ const resource = createTestResource(() => {
71
+ tapEffect(() => {
72
+ throw error1;
73
+ });
74
+
75
+ tapEffect(goodEffect); // This won't run
76
+
77
+ tapEffect(() => {
78
+ throw error2;
79
+ });
80
+
81
+ return null;
82
+ });
83
+
84
+ // Should throw first error
85
+ expect(() => renderTest(resource, undefined)).toThrow(error1);
86
+ expect(goodEffect).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it("should continue cleanup on unmount despite errors", () => {
90
+ const cleanupError = new Error("Cleanup failed");
91
+ const cleanup1 = vi.fn(() => {
92
+ throw cleanupError;
93
+ });
94
+ const cleanup2 = vi.fn();
95
+ const cleanup3 = vi.fn();
96
+
97
+ const resource = createTestResource(() => {
98
+ tapEffect(() => cleanup1);
99
+ tapEffect(() => cleanup2);
100
+ tapEffect(() => cleanup3);
101
+ return null;
102
+ });
103
+
104
+ renderTest(resource, undefined);
105
+
106
+ // Unmount should throw the error but should still run all cleanups
107
+ expect(() => unmountResource(resource)).toThrow(cleanupError);
108
+ expect(cleanup1).toHaveBeenCalled();
109
+ expect(cleanup2).toHaveBeenCalled();
110
+ expect(cleanup3).toHaveBeenCalled();
111
+ });
112
+
113
+ it("should handle errors in effect with dependencies", () => {
114
+ const error = new Error("Dep effect error");
115
+ let shouldThrow = false;
116
+
117
+ const resource = createTestResource(() => {
118
+ const [dep, setDep] = tapState(0);
119
+
120
+ tapEffect(() => {
121
+ if (shouldThrow) {
122
+ throw error;
123
+ }
124
+ }, [dep]);
125
+
126
+ // Use effect to trigger state change
127
+ tapEffect(() => {
128
+ if (dep === 0) {
129
+ shouldThrow = true;
130
+ setDep(1); // Trigger effect re-run
131
+ }
132
+ }, [dep]);
133
+
134
+ return dep;
135
+ });
136
+
137
+ // The initial render will trigger setState which causes flushSync
138
+ // The flushed re-render will throw the error
139
+ expect(() => renderTest(resource, undefined)).toThrow(error);
140
+ });
141
+
142
+ it("should handle async errors in effects", async () => {
143
+ // Set up a promise to catch the async error
144
+ let asyncErrorPromise: Promise<void>;
145
+
146
+ const resource = createTestResource(() => {
147
+ tapEffect(() => {
148
+ // Async errors are not caught by the framework
149
+ asyncErrorPromise = new Promise((_, reject) => {
150
+ setTimeout(() => {
151
+ reject(new Error("Async error"));
152
+ }, 0);
153
+ });
154
+
155
+ // Catch the error to prevent unhandled rejection
156
+ asyncErrorPromise.catch(() => {
157
+ // Expected - async errors are not caught by the framework
158
+ });
159
+ });
160
+ return null;
161
+ });
162
+
163
+ // This won't throw synchronously
164
+ expect(() => renderTest(resource, undefined)).not.toThrow();
165
+
166
+ // Wait for the async error to be handled
167
+ await new Promise((resolve) => setTimeout(resolve, 10));
168
+ });
169
+
170
+ it("should properly clean up state after effect error", () => {
171
+ const error = new Error("Effect error");
172
+ let effectRan = false;
173
+
174
+ const resource = createTestResource(() => {
175
+ const [value] = tapState("initial");
176
+
177
+ tapEffect(() => {
178
+ effectRan = true;
179
+ throw error;
180
+ });
181
+
182
+ return value;
183
+ });
184
+
185
+ expect(() => renderTest(resource, undefined)).toThrow(error);
186
+ expect(effectRan).toBe(true);
187
+
188
+ // Resource should not have committed state since commit failed
189
+ // Since commit failed, we can't check the state through normal means
190
+ });
191
+
192
+ it("should handle errors in effect cleanup during re-render", () => {
193
+ const cleanupError = new Error("Cleanup during re-render");
194
+ let throwOnCleanup = false;
195
+
196
+ const resource = createTestResource(() => {
197
+ const [count, setCount] = tapState(0);
198
+
199
+ tapEffect(() => {
200
+ return () => {
201
+ if (throwOnCleanup) {
202
+ throw cleanupError;
203
+ }
204
+ };
205
+ }, [count]);
206
+
207
+ // Use effect to trigger state change
208
+ tapEffect(() => {
209
+ if (count === 0) {
210
+ throwOnCleanup = true;
211
+ setCount(1);
212
+ }
213
+ }, [count]);
214
+
215
+ return count;
216
+ });
217
+
218
+ // The initial render will trigger setState which causes flushSync
219
+ // During the flush, the cleanup will run and throw
220
+ expect(() => renderTest(resource, undefined)).toThrow(cleanupError);
221
+ });
222
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { tapEffect } from "../../hooks/tap-effect";
3
+ import { tapState } from "../../hooks/tap-state";
4
+ import { createTestResource, renderTest } from "../test-utils";
5
+ import {
6
+ renderResourceFiber,
7
+ commitResourceFiber,
8
+ unmountResourceFiber,
9
+ } from "../../core/ResourceFiber";
10
+
11
+ describe("Errors - Render Errors", () => {
12
+ it("should propagate errors during render", () => {
13
+ const error = new Error("Render error");
14
+
15
+ const resource = createTestResource(() => {
16
+ throw error;
17
+ });
18
+
19
+ expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
20
+ });
21
+
22
+ it("should throw when hooks are called outside render context", () => {
23
+ // Try to call hook outside of resource render
24
+ expect(() => {
25
+ tapState(0);
26
+ }).toThrow("No resource fiber available");
27
+
28
+ expect(() => {
29
+ tapEffect(() => {});
30
+ }).toThrow("No resource fiber available");
31
+ });
32
+
33
+ it("should handle errors in state initializers", () => {
34
+ const error = new Error("Initializer error");
35
+
36
+ const resource = createTestResource(() => {
37
+ const [value] = tapState(() => {
38
+ throw error;
39
+ });
40
+ return value;
41
+ });
42
+
43
+ expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
44
+ });
45
+
46
+ it("should detect render during render", () => {
47
+ const resource = createTestResource(() => {
48
+ const [count, setCount] = tapState(0);
49
+
50
+ // This violates the rules - no state updates during render
51
+ if (count < 5) {
52
+ expect(() => setCount(count + 1)).toThrow(
53
+ "Resource updated during render",
54
+ );
55
+ }
56
+
57
+ return count;
58
+ });
59
+
60
+ renderResourceFiber(resource, undefined);
61
+ });
62
+
63
+ it("should allow setState during commit (effects)", () => {
64
+ const resource = createTestResource(() => {
65
+ const [count, setCount] = tapState(0);
66
+
67
+ tapEffect(() => {
68
+ // setState during effects (commit phase) is allowed
69
+ if (count < 5) {
70
+ setCount(count + 1);
71
+ }
72
+ });
73
+
74
+ return count;
75
+ });
76
+
77
+ const ctx = renderResourceFiber(resource, undefined);
78
+ // This should not throw - setState in effects is allowed
79
+ expect(() => commitResourceFiber(resource, ctx)).not.toThrow();
80
+ unmountResourceFiber(resource);
81
+ });
82
+
83
+ it("should handle errors in hook order validation", () => {
84
+ let useStateFirst = true;
85
+
86
+ const resource = createTestResource(() => {
87
+ if (useStateFirst) {
88
+ tapState(1);
89
+ tapEffect(() => {});
90
+ } else {
91
+ tapEffect(() => {});
92
+ tapState(1);
93
+ }
94
+ return null;
95
+ });
96
+
97
+ renderResourceFiber(resource, undefined);
98
+
99
+ useStateFirst = false;
100
+
101
+ expect(() => renderResourceFiber(resource, undefined)).toThrow(
102
+ "Hook order changed between renders",
103
+ );
104
+ });
105
+
106
+ it("should maintain resource state after render error", () => {
107
+ let shouldThrow = false;
108
+
109
+ const resource = createTestResource(() => {
110
+ const [count, _setCount] = tapState(42);
111
+
112
+ if (shouldThrow) {
113
+ throw new Error("Render failed");
114
+ }
115
+
116
+ return count;
117
+ });
118
+
119
+ // First successful render
120
+ const result = renderTest(resource, undefined);
121
+ expect(result).toBe(42);
122
+
123
+ // Failed render
124
+ shouldThrow = true;
125
+ expect(() => renderTest(resource, undefined)).toThrow("Render failed");
126
+
127
+ // State should be unchanged after failed render
128
+ // The resource state is preserved
129
+ });
130
+
131
+ it("should handle complex error scenarios", () => {
132
+ let phase = "render";
133
+
134
+ const resource = createTestResource(() => {
135
+ if (phase === "hook-order") {
136
+ // Wrong hook order
137
+ tapEffect(() => {});
138
+ tapState(1);
139
+ } else {
140
+ tapState(1);
141
+ tapEffect(() => {
142
+ if (phase === "effect-error") {
143
+ throw new Error("Effect error");
144
+ }
145
+ });
146
+ }
147
+
148
+ if (phase === "render-error") {
149
+ throw new Error("Render error");
150
+ }
151
+
152
+ return phase;
153
+ });
154
+
155
+ // Successful render
156
+ renderTest(resource, undefined);
157
+
158
+ // Render error
159
+ phase = "render-error";
160
+ expect(() => renderTest(resource, undefined)).toThrow("Render error");
161
+
162
+ // Hook order error
163
+ phase = "hook-order";
164
+ expect(() => renderTest(resource, undefined)).toThrow("Hook order changed");
165
+
166
+ // Effect error
167
+ phase = "effect-error";
168
+ expect(() => renderTest(resource, undefined)).toThrow("Effect error");
169
+ });
170
+
171
+ it("should handle errors in nested hook calls", () => {
172
+ const useFeature = () => {
173
+ // This will fail if called outside render
174
+ const [value] = tapState("feature");
175
+ return value;
176
+ };
177
+
178
+ // Outside render context
179
+ expect(() => useFeature()).toThrow("No resource fiber available");
180
+
181
+ // Inside render context
182
+ const resource = createTestResource(() => {
183
+ const feature = useFeature(); // This works
184
+ return feature;
185
+ });
186
+
187
+ const result = renderTest(resource, undefined);
188
+ expect(result).toBe("feature");
189
+ });
190
+ });