@assistant-ui/tap 0.3.4 → 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 -67
  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,56 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createResource } from "../../core/createResource";
3
+ import { resource } from "../../core/resource";
4
+
5
+ describe("ResourceHandle - Basic Usage", () => {
6
+ it("should create a resource handle with const API", () => {
7
+ const TestResource = resource((props: number) => {
8
+ return {
9
+ value: props * 2,
10
+ propsUsed: props,
11
+ };
12
+ });
13
+ const handle = createResource(TestResource(5));
14
+
15
+ // The handle provides a const API
16
+ expect(typeof handle.getState).toBe("function");
17
+ expect(typeof handle.subscribe).toBe("function");
18
+ expect(typeof handle.render).toBe("function");
19
+
20
+ // Initial state
21
+ expect(handle.getState().value).toBe(10);
22
+ expect(handle.getState().propsUsed).toBe(5);
23
+ });
24
+
25
+ it("should allow updating props", () => {
26
+ const TestResource = resource((props: { multiplier: number }) => {
27
+ return { result: 10 * props.multiplier };
28
+ });
29
+ const handle = createResource(TestResource({ multiplier: 2 }));
30
+
31
+ // Initial state
32
+ expect(handle.getState().result).toBe(20);
33
+
34
+ // Can call render to update props
35
+ expect(() => handle.render(TestResource({ multiplier: 3 }))).not.toThrow();
36
+ });
37
+
38
+ it("should support subscribing and unsubscribing", () => {
39
+ const TestResource = resource(() => ({ timestamp: Date.now() }));
40
+ const handle = createResource(TestResource());
41
+
42
+ const subscriber1 = vi.fn();
43
+ const subscriber2 = vi.fn();
44
+
45
+ // Can subscribe multiple callbacks
46
+ const unsub1 = handle.subscribe(subscriber1);
47
+ const unsub2 = handle.subscribe(subscriber2);
48
+
49
+ // Can unsubscribe individually
50
+ expect(typeof unsub1).toBe("function");
51
+ expect(typeof unsub2).toBe("function");
52
+
53
+ unsub1();
54
+ unsub2();
55
+ });
56
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { tapEffect } from "../../hooks/tap-effect";
3
+ import { tapState } from "../../hooks/tap-state";
4
+ import {
5
+ createTestResource,
6
+ renderTest,
7
+ cleanupAllResources,
8
+ TestResourceManager,
9
+ } from "../test-utils";
10
+
11
+ describe("tapEffect - Basic Functionality", () => {
12
+ afterEach(() => {
13
+ cleanupAllResources();
14
+ });
15
+
16
+ describe("Effect Lifecycle", () => {
17
+ it("should run effect after mount and commit", () => {
18
+ const executionOrder: string[] = [];
19
+
20
+ const testFiber = createTestResource(() => {
21
+ executionOrder.push("render");
22
+
23
+ tapEffect(() => {
24
+ executionOrder.push("effect");
25
+ });
26
+
27
+ return null;
28
+ });
29
+
30
+ // Use TestResourceManager for fine-grained control
31
+ const manager = new TestResourceManager(testFiber);
32
+
33
+ // Mount and render
34
+ manager.renderAndMount(undefined);
35
+
36
+ // Effect should run after commit
37
+ expect(executionOrder).toEqual(["render", "effect"]);
38
+
39
+ manager.cleanup();
40
+ });
41
+
42
+ it("should call cleanup function on unmount", () => {
43
+ const cleanup = vi.fn();
44
+ const effect = vi.fn(() => cleanup);
45
+
46
+ const testFiber = createTestResource(() => {
47
+ tapEffect(effect);
48
+ return null;
49
+ });
50
+
51
+ const manager = new TestResourceManager(testFiber);
52
+ manager.renderAndMount(undefined);
53
+
54
+ // Effect should be called, but not cleanup
55
+ expect(effect).toHaveBeenCalledTimes(1);
56
+ expect(cleanup).not.toHaveBeenCalled();
57
+
58
+ // Cleanup should be called on unmount
59
+ manager.cleanup();
60
+ expect(cleanup).toHaveBeenCalledTimes(1);
61
+ });
62
+
63
+ it("should cleanup effects in reverse order", () => {
64
+ const cleanupOrder: string[] = [];
65
+
66
+ const testFiber = createTestResource(() => {
67
+ tapEffect(() => {
68
+ return () => cleanupOrder.push("first");
69
+ });
70
+
71
+ tapEffect(() => {
72
+ return () => cleanupOrder.push("second");
73
+ });
74
+
75
+ tapEffect(() => {
76
+ return () => cleanupOrder.push("third");
77
+ });
78
+
79
+ return null;
80
+ });
81
+
82
+ const manager = new TestResourceManager(testFiber);
83
+ manager.renderAndMount(undefined);
84
+ manager.cleanup();
85
+
86
+ // Cleanup should run in reverse order (LIFO)
87
+ expect(cleanupOrder).toEqual(["third", "second", "first"]);
88
+ });
89
+ });
90
+
91
+ describe("Multiple Effects", () => {
92
+ it("should execute multiple effects in registration order", () => {
93
+ const executionOrder: string[] = [];
94
+ const effects = [
95
+ () => {
96
+ executionOrder.push("effect1");
97
+ },
98
+ () => {
99
+ executionOrder.push("effect2");
100
+ },
101
+ () => {
102
+ executionOrder.push("effect3");
103
+ },
104
+ ];
105
+
106
+ const testFiber = createTestResource(() => {
107
+ effects.forEach((fn) => tapEffect(fn));
108
+ return null;
109
+ });
110
+
111
+ renderTest(testFiber, undefined);
112
+ expect(executionOrder).toEqual(["effect1", "effect2", "effect3"]);
113
+ });
114
+
115
+ it("should handle mixed effects with and without dependencies", () => {
116
+ const effectCalls = {
117
+ always: 0,
118
+ once: 0,
119
+ conditional: 0,
120
+ };
121
+
122
+ const testFiber = createTestResource((props: { value: number }) => {
123
+ // Effect without deps - runs on every render
124
+ tapEffect(() => {
125
+ effectCalls.always++;
126
+ });
127
+
128
+ // Effect with empty deps - runs only once
129
+ tapEffect(() => {
130
+ effectCalls.once++;
131
+ }, []);
132
+
133
+ // Effect with deps - runs when deps change
134
+ tapEffect(() => {
135
+ effectCalls.conditional++;
136
+ }, [props.value]);
137
+
138
+ return effectCalls;
139
+ });
140
+
141
+ // Initial render
142
+ renderTest(testFiber, { value: 1 });
143
+ expect(effectCalls).toEqual({ always: 1, once: 1, conditional: 1 });
144
+
145
+ // Re-render with same props
146
+ renderTest(testFiber, { value: 1 });
147
+ expect(effectCalls).toEqual({ always: 2, once: 1, conditional: 1 });
148
+
149
+ // Re-render with different props
150
+ renderTest(testFiber, { value: 2 });
151
+ expect(effectCalls).toEqual({ always: 3, once: 1, conditional: 2 });
152
+ });
153
+ });
154
+
155
+ describe("Effect Dependencies", () => {
156
+ it("should not re-run effect with empty dependency array", () => {
157
+ const effect = vi.fn();
158
+ let triggerRerender: (() => void) | null = null;
159
+
160
+ const testFiber = createTestResource(() => {
161
+ const [, setState] = tapState(0);
162
+
163
+ tapEffect(() => {
164
+ triggerRerender = () => setState((prev) => prev + 1);
165
+ });
166
+
167
+ tapEffect(effect, []);
168
+
169
+ return null;
170
+ });
171
+
172
+ // Initial render
173
+ renderTest(testFiber, undefined);
174
+ expect(effect).toHaveBeenCalledTimes(1);
175
+
176
+ // Trigger re-render
177
+ triggerRerender!();
178
+
179
+ // Effect with empty deps should not re-run
180
+ expect(effect).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ it("should re-run effect when dependencies change", () => {
184
+ const effect = vi.fn();
185
+
186
+ const testFiber = createTestResource((props: { dep: string }) => {
187
+ tapEffect(() => {
188
+ effect(props.dep);
189
+ }, [props.dep]);
190
+
191
+ return null;
192
+ });
193
+
194
+ // Initial render
195
+ renderTest(testFiber, { dep: "a" });
196
+ expect(effect).toHaveBeenCalledTimes(1);
197
+ expect(effect).toHaveBeenLastCalledWith("a");
198
+
199
+ // Re-render with same dependency
200
+ renderTest(testFiber, { dep: "a" });
201
+ expect(effect).toHaveBeenCalledTimes(1);
202
+
203
+ // Re-render with different dependency
204
+ renderTest(testFiber, { dep: "b" });
205
+ expect(effect).toHaveBeenCalledTimes(2);
206
+ expect(effect).toHaveBeenLastCalledWith("b");
207
+ });
208
+ });
209
+
210
+ describe("Effect Timing", () => {
211
+ it("should run effects after state updates are committed", () => {
212
+ const events: string[] = [];
213
+
214
+ const testFiber = createTestResource(() => {
215
+ const [count, setCount] = tapState(0);
216
+
217
+ events.push(`render: ${count}`);
218
+
219
+ tapEffect(() => {
220
+ events.push(`effect: ${count}`);
221
+
222
+ // Only update on first effect to avoid infinite loop
223
+ if (count === 0) {
224
+ setCount(1);
225
+ }
226
+ });
227
+
228
+ return count;
229
+ });
230
+
231
+ const manager = new TestResourceManager(testFiber);
232
+
233
+ // Initial render
234
+ manager.renderAndMount(undefined);
235
+ // Without mount tracking, the effect runs immediately during commit
236
+ // This triggers setState which causes a synchronous re-render
237
+ expect(events).toEqual([
238
+ "render: 0",
239
+ "effect: 0",
240
+ "render: 1",
241
+ "effect: 1",
242
+ ]);
243
+
244
+ manager.cleanup();
245
+ });
246
+ });
247
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { tapResources } from "../../hooks/tap-resources";
3
+ import { tapState } from "../../hooks/tap-state";
4
+ import { resource } from "../../core/resource";
5
+ import {
6
+ createTestResource,
7
+ renderTest,
8
+ cleanupAllResources,
9
+ createCounterResource,
10
+ } from "../test-utils";
11
+
12
+ // ============================================================================
13
+ // Test Resources
14
+ // ============================================================================
15
+
16
+ // Simple counter that just returns the value
17
+ const SimpleCounter = resource(createCounterResource());
18
+
19
+ // Stateful counter that tracks its own count
20
+ const StatefulCounter = resource((props: { initial: number }) => {
21
+ const [count] = tapState(props.initial);
22
+ return { count };
23
+ });
24
+
25
+ // Display component for testing type changes
26
+ const Display = resource((props: { text: string }) => {
27
+ return { type: "display", text: props.text };
28
+ });
29
+
30
+ // Counter with render tracking for testing instance preservation
31
+ const renderCounts = new Map<string, number>();
32
+ const instances = new Map<string, object>();
33
+ const TrackingCounter = resource((props: { value: number; id: string }) => {
34
+ const currentCount = (renderCounts.get(props.id) || 0) + 1;
35
+ renderCounts.set(props.id, currentCount);
36
+
37
+ if (!instances.has(props.id)) {
38
+ instances.set(props.id, { id: `fiber-${props.id}` });
39
+ }
40
+
41
+ return {
42
+ value: props.value,
43
+ id: props.id,
44
+ renderCount: currentCount,
45
+ instance: instances.get(props.id),
46
+ };
47
+ });
48
+
49
+ // ============================================================================
50
+ // Tests
51
+ // ============================================================================
52
+
53
+ describe("tapResources - Basic Functionality", () => {
54
+ afterEach(() => {
55
+ cleanupAllResources();
56
+ });
57
+
58
+ describe("Basic Rendering", () => {
59
+ it("should render multiple resources with keys", () => {
60
+ const testFiber = createTestResource(() => {
61
+ const results = tapResources(
62
+ { a: 10, b: 20, c: 30 },
63
+ (value) => SimpleCounter({ value }),
64
+ [],
65
+ );
66
+
67
+ return results;
68
+ });
69
+
70
+ const result = renderTest(testFiber, undefined);
71
+ expect(result).toEqual({
72
+ a: { count: 10 },
73
+ b: { count: 20 },
74
+ c: { count: 30 },
75
+ });
76
+ });
77
+
78
+ it("should work with resource constructor syntax", () => {
79
+ const Counter = resource((props: { value: number }) => {
80
+ const [count] = tapState(props.value);
81
+ return { count, double: count * 2 };
82
+ });
83
+
84
+ const testFiber = createTestResource(() => {
85
+ const items = {
86
+ first: { value: 5 },
87
+ second: { value: 10 },
88
+ third: { value: 15 },
89
+ };
90
+
91
+ const results = tapResources(
92
+ items,
93
+ (item) => Counter({ value: item.value }),
94
+ [],
95
+ );
96
+
97
+ return results;
98
+ });
99
+
100
+ const result = renderTest(testFiber, undefined);
101
+ expect(result).toEqual({
102
+ first: { count: 5, double: 10 },
103
+ second: { count: 10, double: 20 },
104
+ third: { count: 15, double: 30 },
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("Instance Preservation", () => {
110
+ it("should maintain resource instances when keys remain the same", () => {
111
+ const testFiber = createTestResource(
112
+ (props: { items: Record<string, { value: number; id: string }> }) => {
113
+ return tapResources(
114
+ props.items,
115
+ (item) => TrackingCounter({ value: item.value, id: item.id }),
116
+
117
+ [],
118
+ );
119
+ },
120
+ );
121
+
122
+ // Initial render
123
+ const result1 = renderTest(testFiber, {
124
+ items: { a: { value: 1, id: "a" }, b: { value: 2, id: "b" } },
125
+ });
126
+
127
+ // Verify initial state
128
+ expect(result1.a).toMatchObject({
129
+ id: "a",
130
+ value: 1,
131
+ renderCount: 1,
132
+ });
133
+ expect(result1.b).toMatchObject({
134
+ id: "b",
135
+ value: 2,
136
+ renderCount: 1,
137
+ });
138
+
139
+ // Re-render with same keys but different values
140
+ const result2 = renderTest(testFiber, {
141
+ items: { b: { value: 20, id: "b" }, a: { value: 10, id: "a" } },
142
+ });
143
+
144
+ // Verify instances are preserved
145
+ expect(result2.b).toMatchObject({
146
+ id: "b",
147
+ value: 20,
148
+ renderCount: 2,
149
+ });
150
+ expect(result2.a).toMatchObject({
151
+ id: "a",
152
+ value: 10,
153
+ renderCount: 2,
154
+ });
155
+ });
156
+ });
157
+
158
+ describe("Dynamic List Management", () => {
159
+ it("should handle adding and removing resources", () => {
160
+ const testFiber = createTestResource(
161
+ (props: { items: Record<string, number> }) => {
162
+ const results = tapResources(
163
+ props.items,
164
+ (value) => SimpleCounter({ value }),
165
+
166
+ [],
167
+ );
168
+ return results;
169
+ },
170
+ );
171
+
172
+ // Initial render with 3 items
173
+ const result1 = renderTest(testFiber, { items: { a: 0, b: 10, c: 20 } });
174
+ expect(result1).toEqual({
175
+ a: { count: 0 },
176
+ b: { count: 10 },
177
+ c: { count: 20 },
178
+ });
179
+
180
+ // Remove middle item
181
+ const result2 = renderTest(testFiber, { items: { a: 0, c: 10 } });
182
+ expect(result2).toEqual({
183
+ a: { count: 0 },
184
+ c: { count: 10 },
185
+ });
186
+
187
+ // Add new item
188
+ const result3 = renderTest(testFiber, { items: { a: 0, c: 10, d: 20 } });
189
+ expect(result3).toEqual({
190
+ a: { count: 0 },
191
+ c: { count: 10 },
192
+ d: { count: 20 },
193
+ });
194
+ });
195
+
196
+ it("should handle changing resource types for the same key", () => {
197
+ const testFiber = createTestResource((props: { useCounter: boolean }) => {
198
+ const results = tapResources(
199
+ { item: props.useCounter },
200
+ (useCounter) =>
201
+ useCounter
202
+ ? StatefulCounter({ initial: 42 })
203
+ : Display({ text: "Hello" }),
204
+ [],
205
+ );
206
+ return results;
207
+ });
208
+
209
+ // Start with Counter
210
+ const result1 = renderTest(testFiber, { useCounter: true });
211
+ expect(result1).toEqual({ item: { count: 42 } });
212
+
213
+ // Switch to Display
214
+ const result2 = renderTest(testFiber, { useCounter: false });
215
+ expect(result2).toEqual({ item: { type: "display", text: "Hello" } });
216
+
217
+ // Switch back to Counter (new instance)
218
+ const result3 = renderTest(testFiber, { useCounter: true });
219
+ expect(result3).toEqual({ item: { count: 42 } });
220
+ });
221
+ });
222
+ });