@assistant-ui/tap 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +20 -17
  2. package/dist/core/ResourceFiber.d.ts +2 -2
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +11 -9
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/createResourceRoot.d.ts +6 -0
  7. package/dist/core/createResourceRoot.d.ts.map +1 -0
  8. package/dist/core/createResourceRoot.js +32 -0
  9. package/dist/core/createResourceRoot.js.map +1 -0
  10. package/dist/core/helpers/callResourceFn.d.ts.map +1 -0
  11. package/dist/core/helpers/callResourceFn.js.map +1 -0
  12. package/dist/core/helpers/commit.d.ts +4 -0
  13. package/dist/core/helpers/commit.d.ts.map +1 -0
  14. package/dist/core/{commit.js → helpers/commit.js} +2 -2
  15. package/dist/core/helpers/commit.js.map +1 -0
  16. package/dist/core/helpers/env.d.ts.map +1 -0
  17. package/dist/core/helpers/env.js +3 -0
  18. package/dist/core/helpers/env.js.map +1 -0
  19. package/dist/core/{execution-context.d.ts → helpers/execution-context.d.ts} +1 -1
  20. package/dist/core/helpers/execution-context.d.ts.map +1 -0
  21. package/dist/core/helpers/execution-context.js.map +1 -0
  22. package/dist/core/helpers/root.d.ts +8 -0
  23. package/dist/core/helpers/root.d.ts.map +1 -0
  24. package/dist/core/helpers/root.js +52 -0
  25. package/dist/core/helpers/root.js.map +1 -0
  26. package/dist/core/resource.js +1 -1
  27. package/dist/core/resource.js.map +1 -1
  28. package/dist/core/scheduler.d.ts.map +1 -1
  29. package/dist/core/scheduler.js +12 -1
  30. package/dist/core/scheduler.js.map +1 -1
  31. package/dist/core/types.d.ts +25 -7
  32. package/dist/core/types.d.ts.map +1 -1
  33. package/dist/hooks/tap-effect-event.d.ts.map +1 -1
  34. package/dist/hooks/tap-effect-event.js +3 -2
  35. package/dist/hooks/tap-effect-event.js.map +1 -1
  36. package/dist/hooks/tap-memo.d.ts.map +1 -1
  37. package/dist/hooks/tap-memo.js +16 -17
  38. package/dist/hooks/tap-memo.js.map +1 -1
  39. package/dist/hooks/tap-reducer.d.ts +7 -0
  40. package/dist/hooks/tap-reducer.d.ts.map +1 -0
  41. package/dist/hooks/tap-reducer.js +87 -0
  42. package/dist/hooks/tap-reducer.js.map +1 -0
  43. package/dist/hooks/tap-resource.js +9 -9
  44. package/dist/hooks/tap-resource.js.map +1 -1
  45. package/dist/hooks/tap-resources.d.ts.map +1 -1
  46. package/dist/hooks/tap-resources.js +11 -11
  47. package/dist/hooks/tap-resources.js.map +1 -1
  48. package/dist/hooks/tap-state.d.ts.map +1 -1
  49. package/dist/hooks/tap-state.js +6 -63
  50. package/dist/hooks/tap-state.js.map +1 -1
  51. package/dist/hooks/utils/tapHook.d.ts +1 -1
  52. package/dist/hooks/utils/tapHook.d.ts.map +1 -1
  53. package/dist/hooks/utils/tapHook.js +2 -2
  54. package/dist/hooks/utils/tapHook.js.map +1 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +3 -3
  58. package/dist/index.js.map +1 -1
  59. package/dist/react/use-resource.d.ts +1 -1
  60. package/dist/react/use-resource.d.ts.map +1 -1
  61. package/dist/react/use-resource.js +14 -8
  62. package/dist/react/use-resource.js.map +1 -1
  63. package/dist/{tapSubscribableResource.d.ts → tapResourceRoot.d.ts} +3 -3
  64. package/dist/tapResourceRoot.d.ts.map +1 -0
  65. package/dist/tapResourceRoot.js +80 -0
  66. package/dist/tapResourceRoot.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/__tests__/basic/resourceHandle.test.ts +17 -14
  69. package/src/__tests__/basic/tapReducer.basic.test.ts +200 -0
  70. package/src/__tests__/react/concurrent-mode.test.tsx +1 -4
  71. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +215 -2
  72. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +77 -0
  73. package/src/__tests__/strictmode/strictmode.test.ts +82 -21
  74. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +67 -110
  75. package/src/__tests__/test-utils.ts +5 -1
  76. package/src/core/ResourceFiber.ts +22 -10
  77. package/src/core/createResourceRoot.ts +53 -0
  78. package/src/core/{callResourceFn.ts → helpers/callResourceFn.ts} +1 -1
  79. package/src/core/{commit.ts → helpers/commit.ts} +3 -3
  80. package/src/core/helpers/env.ts +3 -0
  81. package/src/core/{execution-context.ts → helpers/execution-context.ts} +1 -1
  82. package/src/core/helpers/root.ts +67 -0
  83. package/src/core/resource.ts +1 -1
  84. package/src/core/scheduler.ts +13 -1
  85. package/src/core/types.ts +27 -7
  86. package/src/hooks/tap-effect-event.ts +3 -2
  87. package/src/hooks/tap-memo.ts +24 -19
  88. package/src/hooks/tap-reducer.ts +148 -0
  89. package/src/hooks/tap-resource.ts +9 -9
  90. package/src/hooks/tap-resources.ts +23 -10
  91. package/src/hooks/tap-state.ts +11 -88
  92. package/src/hooks/utils/tapHook.ts +3 -3
  93. package/src/index.ts +3 -3
  94. package/src/react/use-resource.ts +24 -11
  95. package/src/tapResourceRoot.ts +131 -0
  96. package/dist/core/callResourceFn.d.ts.map +0 -1
  97. package/dist/core/callResourceFn.js.map +0 -1
  98. package/dist/core/commit.d.ts +0 -4
  99. package/dist/core/commit.d.ts.map +0 -1
  100. package/dist/core/commit.js.map +0 -1
  101. package/dist/core/createResource.d.ts +0 -15
  102. package/dist/core/createResource.d.ts.map +0 -1
  103. package/dist/core/createResource.js +0 -101
  104. package/dist/core/createResource.js.map +0 -1
  105. package/dist/core/env.d.ts.map +0 -1
  106. package/dist/core/env.js +0 -4
  107. package/dist/core/env.js.map +0 -1
  108. package/dist/core/execution-context.d.ts.map +0 -1
  109. package/dist/core/execution-context.js.map +0 -1
  110. package/dist/hooks/tap-inline-resource.d.ts +0 -3
  111. package/dist/hooks/tap-inline-resource.d.ts.map +0 -1
  112. package/dist/hooks/tap-inline-resource.js +0 -5
  113. package/dist/hooks/tap-inline-resource.js.map +0 -1
  114. package/dist/tapSubscribableResource.d.ts.map +0 -1
  115. package/dist/tapSubscribableResource.js +0 -60
  116. package/dist/tapSubscribableResource.js.map +0 -1
  117. package/src/core/createResource.ts +0 -155
  118. package/src/core/env.ts +0 -4
  119. package/src/hooks/tap-inline-resource.ts +0 -8
  120. package/src/tapSubscribableResource.ts +0 -101
  121. /package/dist/core/{callResourceFn.d.ts → helpers/callResourceFn.d.ts} +0 -0
  122. /package/dist/core/{callResourceFn.js → helpers/callResourceFn.js} +0 -0
  123. /package/dist/core/{env.d.ts → helpers/env.d.ts} +0 -0
  124. /package/dist/core/{execution-context.js → helpers/execution-context.js} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { resource } from "../../core/resource";
3
- import { isDevelopment } from "../../core/env";
3
+ import { isDevelopment } from "../../core/helpers/env";
4
4
  import { tapRef } from "../../hooks/tap-ref";
5
5
  import { tapState } from "../../hooks/tap-state";
6
6
  import { tapEffect } from "../../hooks/tap-effect";
7
+ import { tapMemo } from "../../hooks/tap-memo";
7
8
  import { tapResource } from "../../hooks/tap-resource";
8
- import { createResource } from "../../core/createResource";
9
+ import { createResourceRoot } from "../../core/createResourceRoot";
9
10
  import { withKey } from "../../core/withKey";
10
11
 
11
12
  describe("Strict Mode", () => {
@@ -13,6 +14,68 @@ describe("Strict Mode", () => {
13
14
  expect(isDevelopment).toBe(true);
14
15
  });
15
16
 
17
+ it("should persist tapMemo cache across strict mode double render", () => {
18
+ const events: string[] = [];
19
+ let outerCount = 0;
20
+ let memoCount = 0;
21
+
22
+ const TestResource = resource(() => {
23
+ const idx = outerCount++;
24
+ events.push(`outer-${idx}`);
25
+
26
+ tapMemo(() => {
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
+ // tapMemo 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 tapMemo factory and use the first result", () => {
53
+ const events: string[] = [];
54
+ let memoCallCount = 0;
55
+
56
+ const TestResource = resource(() => {
57
+ const memoValue = tapMemo(() => {
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
+
16
79
  it("should double-render on first render", () => {
17
80
  let renderCount = 0;
18
81
 
@@ -21,8 +84,9 @@ describe("Strict Mode", () => {
21
84
  return { renderCount };
22
85
  });
23
86
 
24
- const handle = createResource(TestResource(), { devStrictMode: true });
25
- const output = handle.getValue();
87
+ const root = createResourceRoot();
88
+ const sub = root.render(TestResource());
89
+ const output = sub.getValue();
26
90
 
27
91
  expect(renderCount).toBe(2);
28
92
  expect(output.renderCount).toBe(2);
@@ -47,7 +111,8 @@ describe("Strict Mode", () => {
47
111
  expect(ref.current).toBe(4);
48
112
  });
49
113
 
50
- createResource(TestResource(), { devStrictMode: true });
114
+ const root = createResourceRoot();
115
+ root.render(TestResource());
51
116
 
52
117
  expect(renderCount).toBe(4);
53
118
  });
@@ -86,7 +151,8 @@ describe("Strict Mode", () => {
86
151
  }, [count]);
87
152
  });
88
153
 
89
- createResource(TestResource(), { devStrictMode: true });
154
+ const root = createResourceRoot();
155
+ root.render(TestResource());
90
156
 
91
157
  expect(events).toEqual([
92
158
  "mount-1",
@@ -113,8 +179,9 @@ describe("Strict Mode", () => {
113
179
  return tapResource(TestChildResource());
114
180
  });
115
181
 
116
- const handle = createResource(TestResource(), { devStrictMode: true });
117
- const output = handle.getValue();
182
+ const root = createResourceRoot();
183
+ const sub = root.render(TestResource());
184
+ const output = sub.getValue();
118
185
 
119
186
  expect(renderCount).toBe(2);
120
187
  expect(output.renderCount).toBe(2);
@@ -134,10 +201,8 @@ describe("Strict Mode", () => {
134
201
  });
135
202
  });
136
203
 
137
- createResource(TestResource(), {
138
- mount: true,
139
- devStrictMode: true,
140
- });
204
+ const root = createResourceRoot();
205
+ root.render(TestResource());
141
206
 
142
207
  expect(events).toEqual([
143
208
  "render-0",
@@ -189,11 +254,9 @@ describe("Strict Mode", () => {
189
254
  return tapResource(withKey(id, TestChildResource()));
190
255
  });
191
256
 
192
- const handle = createResource(TestResource(), {
193
- mount: true,
194
- devStrictMode: true,
195
- });
196
- const output = handle.getValue();
257
+ const root = createResourceRoot();
258
+ const sub = root.render(TestResource());
259
+ const output = sub.getValue();
197
260
 
198
261
  expect(renderCount).toBe(4);
199
262
  expect(fnCount).toBe(4);
@@ -238,10 +301,8 @@ describe("Strict Mode", () => {
238
301
  return tapResource(withKey(id, TestChildResource()));
239
302
  });
240
303
 
241
- createResource(TestResource(), {
242
- mount: true,
243
- devStrictMode: true,
244
- });
304
+ const root = createResourceRoot();
305
+ root.render(TestResource());
245
306
 
246
307
  expect(events).toEqual([
247
308
  "outer-render-0",
@@ -7,52 +7,12 @@ import { describe, it, expect } from "vitest";
7
7
  import { resource } from "../../core/resource";
8
8
  import { tapState } from "../../hooks/tap-state";
9
9
  import { tapEffect } from "../../hooks/tap-effect";
10
- import { createResource } from "../../core/createResource";
10
+ import { createResourceRoot } from "../../core/createResourceRoot";
11
11
  import { flushResourcesSync } from "../../core/scheduler";
12
12
 
13
13
  describe("Tap Strict Mode - Rerender Sources", () => {
14
- describe("DEBUG: Callback invocation count", () => {
15
- it("should show how many times the dispatchUpdate callback is invoked", () => {
16
- let updaterInvocations = 0;
17
- const events: string[] = [];
18
-
19
- const TestResource = resource(() => {
20
- const [count, setCount] = tapState(0);
21
- events.push(`render count=${count}`);
22
-
23
- return {
24
- count,
25
- increment: () => {
26
- events.push("setState called");
27
- setCount((prevCount) => {
28
- updaterInvocations++;
29
- events.push(
30
- `updater invocation #${updaterInvocations} with prevCount=${prevCount}`,
31
- );
32
- return prevCount + 1;
33
- });
34
- },
35
- };
36
- });
37
-
38
- const handle = createResource(TestResource(), { devStrictMode: true });
39
-
40
- events.length = 0;
41
- updaterInvocations = 0;
42
-
43
- flushResourcesSync(() => {
44
- handle.getValue().increment();
45
- });
46
-
47
- console.log("Updater invocations:", updaterInvocations);
48
- console.log("Events:", events);
49
- console.log(
50
- "Expected: updater called twice (React behavior), actual:",
51
- updaterInvocations,
52
- );
53
- });
54
-
55
- it.skip("should use the same return value logic as React when updater returns different values", () => {
14
+ describe("Callback invocation count", () => {
15
+ it("should use the first return value when updater returns different values", () => {
56
16
  const events: string[] = [];
57
17
  let updaterCallCount = 0;
58
18
 
@@ -80,28 +40,26 @@ describe("Tap Strict Mode - Rerender Sources", () => {
80
40
  return { count };
81
41
  });
82
42
 
83
- createResource(TestResource(), {
84
- devStrictMode: true,
85
- mount: true,
86
- });
87
-
88
- console.log("Tap updater call count:", updaterCallCount);
89
- console.log("Tap events:", events);
43
+ const root = createResourceRoot();
44
+ root.render(TestResource());
90
45
 
91
- // React behavior: updater called 4 times, uses LAST return value (200)
92
- // Expected tap behavior: Same as React
46
+ // Tap behavior: updater called 4 times, uses FIRST return value per dispatch
47
+ // Effect #1 dispatch: updater(0) 100 (kept)
48
+ // Effect #1 cleanup, Effect #2 mount
49
+ // Effect #2 dispatch: updater(0) → 200 (kept... but wait, prev=100 from effect #1)
50
+ // Updater double-invoke happens per-dispatch (matching React ordering)
93
51
  expect(updaterCallCount).toBe(4);
94
52
  expect(events).toEqual([
95
53
  "render count=0",
96
54
  "render count=0",
97
55
  "effect mount",
98
- "updater call #1 with prev=0", // Effect #1: returns 100
56
+ "updater call #1 with prev=0",
99
57
  "effect cleanup",
100
58
  "effect mount",
101
- "updater call #2 with prev=0", // Effect #2: returns 200
102
- "updater call #3 with prev=100", // Strict mode double: returns 200
103
- "updater call #4 with prev=100", // Strict mode double again: returns 200
104
- "render count=200", // Uses LAST return value
59
+ "updater call #2 with prev=0",
60
+ "updater call #3 with prev=100",
61
+ "updater call #4 with prev=100",
62
+ "render count=200",
105
63
  "render count=200",
106
64
  ]);
107
65
  });
@@ -117,7 +75,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
117
75
  return { count };
118
76
  });
119
77
 
120
- createResource(TestResource(), { devStrictMode: true });
78
+ const root = createResourceRoot();
79
+ root.render(TestResource());
121
80
 
122
81
  expect(events).toEqual(["render count=0", "render count=0"]);
123
82
  });
@@ -144,7 +103,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
144
103
  return { count };
145
104
  });
146
105
 
147
- createResource(TestResource(), { devStrictMode: true, mount: true });
106
+ const root = createResourceRoot();
107
+ root.render(TestResource());
148
108
 
149
109
  expect(events).toEqual([
150
110
  "render count=0",
@@ -177,7 +137,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
177
137
  };
178
138
  });
179
139
 
180
- const handle = createResource(TestResource(), { devStrictMode: true });
140
+ const root = createResourceRoot();
141
+ const sub = root.render(TestResource());
181
142
 
182
143
  // Initial render is double
183
144
  expect(events).toEqual(["render count=0", "render count=0"]);
@@ -186,7 +147,7 @@ describe("Tap Strict Mode - Rerender Sources", () => {
186
147
 
187
148
  // Call the method inside flushResourcesSync (like clicking a button)
188
149
  flushResourcesSync(() => {
189
- handle.getValue().increment();
150
+ sub.getValue().increment();
190
151
  });
191
152
 
192
153
  // flushResourcesSync setState should ALSO double-render (matching React 19)
@@ -209,19 +170,20 @@ describe("Tap Strict Mode - Rerender Sources", () => {
209
170
  };
210
171
  });
211
172
 
212
- const handle = createResource(TestResource(), { devStrictMode: true });
173
+ const root = createResourceRoot();
174
+ const sub = root.render(TestResource());
213
175
 
214
176
  events.length = 0; // Clear initial renders
215
177
 
216
178
  // Multiple flushResourcesSync calls (like multiple button clicks)
217
179
  flushResourcesSync(() => {
218
- handle.getValue().increment();
180
+ sub.getValue().increment();
219
181
  });
220
182
  flushResourcesSync(() => {
221
- handle.getValue().increment();
183
+ sub.getValue().increment();
222
184
  });
223
185
  flushResourcesSync(() => {
224
- handle.getValue().increment();
186
+ sub.getValue().increment();
225
187
  });
226
188
 
227
189
  // Each call should cause double render
@@ -240,7 +202,7 @@ describe("Tap Strict Mode - Rerender Sources", () => {
240
202
  });
241
203
 
242
204
  describe("Source 4: setState in setTimeout", () => {
243
- it.skip("should double-render AND double-call setTimeout callback", async () => {
205
+ it("should double-render AND double-call setTimeout callback", async () => {
244
206
  const events: string[] = [];
245
207
 
246
208
  const TestResource = resource(() => {
@@ -259,7 +221,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
259
221
  return { count };
260
222
  });
261
223
 
262
- createResource(TestResource(), { devStrictMode: true, mount: true });
224
+ const root = createResourceRoot();
225
+ root.render(TestResource());
263
226
 
264
227
  // Wait for setTimeout
265
228
  await new Promise((resolve) => setTimeout(resolve, 50));
@@ -296,7 +259,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
296
259
  return { count };
297
260
  });
298
261
 
299
- createResource(TestResource(), { devStrictMode: true, mount: true });
262
+ const root = createResourceRoot();
263
+ root.render(TestResource());
300
264
 
301
265
  // Wait for promise
302
266
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -331,12 +295,13 @@ describe("Tap Strict Mode - Rerender Sources", () => {
331
295
  };
332
296
  });
333
297
 
334
- const handle = createResource(TestResource(), { devStrictMode: true });
298
+ const root = createResourceRoot();
299
+ const sub = root.render(TestResource());
335
300
 
336
301
  events.length = 0; // Clear initial renders
337
302
 
338
303
  flushResourcesSync(() => {
339
- handle.getValue().updateBoth();
304
+ sub.getValue().updateBoth();
340
305
  });
341
306
 
342
307
  // Both setState calls batched, but render is DOUBLED
@@ -365,7 +330,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
365
330
  return {};
366
331
  });
367
332
 
368
- createResource(TestResource(), { devStrictMode: true, mount: true });
333
+ const root = createResourceRoot();
334
+ root.render(TestResource());
369
335
 
370
336
  // Initial double-render, then batched setState causes another double-render
371
337
  expect(events).toEqual([
@@ -391,7 +357,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
391
357
  };
392
358
  });
393
359
 
394
- createResource(TestResource(), { devStrictMode: true });
360
+ const root = createResourceRoot();
361
+ root.render(TestResource());
395
362
 
396
363
  // Resource renders should be doubled
397
364
  expect(events).toEqual(["render count=0", "render count=0"]);
@@ -418,12 +385,13 @@ describe("Tap Strict Mode - Rerender Sources", () => {
418
385
  };
419
386
  });
420
387
 
421
- const handle = createResource(TestResource(), { devStrictMode: true });
388
+ const root = createResourceRoot();
389
+ const sub = root.render(TestResource());
422
390
 
423
391
  events.length = 0; // Clear initial renders
424
392
 
425
393
  flushResourcesSync(() => {
426
- handle.getValue().increment();
394
+ sub.getValue().increment();
427
395
  });
428
396
 
429
397
  // React behavior: Updater function is called TWICE in strict mode
@@ -438,7 +406,7 @@ describe("Tap Strict Mode - Rerender Sources", () => {
438
406
  });
439
407
 
440
408
  describe("Source 9: Complex effect patterns", () => {
441
- it.skip("should handle effect with dependencies and setState", () => {
409
+ it("should handle effect with dependencies and setState", () => {
442
410
  const events: string[] = [];
443
411
 
444
412
  const TestResource = resource(() => {
@@ -460,33 +428,26 @@ describe("Tap Strict Mode - Rerender Sources", () => {
460
428
  };
461
429
  });
462
430
 
463
- const handle = createResource(TestResource(), {
464
- devStrictMode: true,
465
- mount: true,
466
- });
431
+ const root = createResourceRoot();
432
+ const sub = root.render(TestResource());
467
433
 
468
- // React behavior: When effect calls setState during strict mode,
469
- // it triggers additional render cycles
434
+ // setDoubled(0*2) = setDoubled(0) is a no-op, so no extra render
470
435
  expect(events).toEqual([
471
436
  "render count=0 doubled=0",
472
437
  "render count=0 doubled=0",
473
438
  "effect count=0",
474
439
  "cleanup count=0",
475
440
  "effect count=0",
476
- "render count=0 doubled=0",
477
- "render count=0 doubled=0",
478
- "cleanup count=0",
479
- "effect count=0",
480
441
  ]);
481
442
 
482
443
  events.length = 0;
483
444
 
484
445
  // Trigger increment via flushResourcesSync
485
446
  flushResourcesSync(() => {
486
- handle.getValue().increment();
447
+ sub.getValue().increment();
487
448
  });
488
449
 
489
- // Should double-render with new count, effect updates doubled
450
+ // Double-render with new count, effect sets doubled=2, triggers another double-render
490
451
  expect(events).toEqual([
491
452
  "render count=1 doubled=0",
492
453
  "render count=1 doubled=0",
@@ -494,8 +455,6 @@ describe("Tap Strict Mode - Rerender Sources", () => {
494
455
  "effect count=1",
495
456
  "render count=1 doubled=2",
496
457
  "render count=1 doubled=2",
497
- "cleanup count=1",
498
- "effect count=1",
499
458
  ]);
500
459
  });
501
460
  });
@@ -517,7 +476,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
517
476
  return { value };
518
477
  });
519
478
 
520
- createResource(TestResource(), { devStrictMode: true });
479
+ const root = createResourceRoot();
480
+ root.render(TestResource());
521
481
 
522
482
  // tapState initializer should be called twice, first value kept
523
483
  expect(events).toEqual([
@@ -544,17 +504,19 @@ describe("Tap Strict Mode - Rerender Sources", () => {
544
504
  });
545
505
 
546
506
  // Create first instance
547
- const handle1 = createResource(TestResource(), { devStrictMode: true });
507
+ const root1 = createResourceRoot();
508
+ root1.render(TestResource());
548
509
 
549
510
  expect(events).toEqual(["render count=0", "render count=0"]);
550
511
 
551
512
  events.length = 0;
552
513
 
553
514
  // Unmount
554
- handle1.unmount();
515
+ root1.unmount();
555
516
 
556
517
  // Create second instance
557
- const handle2 = createResource(TestResource(), { devStrictMode: true });
518
+ const root2 = createResourceRoot();
519
+ const sub2 = root2.render(TestResource());
558
520
 
559
521
  // Should still double-render
560
522
  expect(events).toEqual(["render count=0", "render count=0"]);
@@ -563,7 +525,7 @@ describe("Tap Strict Mode - Rerender Sources", () => {
563
525
 
564
526
  // Method calls via flushResourcesSync should still double-render
565
527
  flushResourcesSync(() => {
566
- handle2.getValue().increment();
528
+ sub2.getValue().increment();
567
529
  });
568
530
 
569
531
  expect(events).toEqual(["render count=1", "render count=1"]);
@@ -600,10 +562,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
600
562
  return { count };
601
563
  });
602
564
 
603
- createResource(TestResource(), {
604
- devStrictMode: true,
605
- mount: true,
606
- });
565
+ const root = createResourceRoot();
566
+ root.render(TestResource());
607
567
 
608
568
  // Expected: setState(1) from effect #1 should be applied
609
569
  // even though effect #1 was cleaned up
@@ -649,10 +609,8 @@ describe("Tap Strict Mode - Rerender Sources", () => {
649
609
  return { count };
650
610
  });
651
611
 
652
- createResource(TestResource(), {
653
- devStrictMode: true,
654
- mount: true,
655
- });
612
+ const root = createResourceRoot();
613
+ root.render(TestResource());
656
614
 
657
615
  // Expected: Only setState(2) should be applied (last one wins)
658
616
  expect(events).toEqual([
@@ -668,7 +626,7 @@ describe("Tap Strict Mode - Rerender Sources", () => {
668
626
  ]);
669
627
  });
670
628
 
671
- it.skip("should handle updater functions from both effect mounts", () => {
629
+ it("should handle updater functions from both effect mounts", () => {
672
630
  const events: string[] = [];
673
631
  let effectRunCount = 0;
674
632
 
@@ -696,14 +654,13 @@ describe("Tap Strict Mode - Rerender Sources", () => {
696
654
  return { count };
697
655
  });
698
656
 
699
- createResource(TestResource(), {
700
- devStrictMode: true,
701
- mount: true,
702
- });
657
+ const root = createResourceRoot();
658
+ root.render(TestResource());
703
659
 
704
- // React behavior: Both updaters are queued and executed
705
- // Effect #1: updater(0) => 0 + 1 = 1
706
- // Effect #2: updater(0) => 0 + 2 = 2, but then runs TWICE more with prev=1
660
+ // Tap behavior: Both updaters are queued and executed, first value kept per dispatch
661
+ // Updater double-invoke happens per-dispatch (matching React ordering)
662
+ // Effect #1: updater(0) => 0 + 1 = 1 (kept)
663
+ // Effect #2: updater(0) => 0 + 2 = 2... but prev=1 from effect #1
707
664
  // Final: 3
708
665
  expect(events).toEqual([
709
666
  "render count=0",
@@ -1,3 +1,4 @@
1
+ import { createResourceFiberRoot } from "../core/helpers/root";
1
2
  import { resource } from "../core/resource";
2
3
  import {
3
4
  createResourceFiber,
@@ -30,7 +31,10 @@ export function createTestResource<R, P>(fn: (props: P) => R) {
30
31
  }
31
32
  };
32
33
 
33
- const fiber = createResourceFiber(resource(fn), rerenderCallback);
34
+ const fiber = createResourceFiber(
35
+ resource(fn),
36
+ createResourceFiberRoot(rerenderCallback),
37
+ );
34
38
  return fiber;
35
39
  }
36
40
 
@@ -1,17 +1,27 @@
1
- import { ResourceFiber, RenderResult, Resource } from "./types";
2
- import { commitRender, cleanupAllEffects } from "./commit";
3
- import { getDevStrictMode, withResourceFiber } from "./execution-context";
4
- import { callResourceFn } from "./callResourceFn";
5
- import { isDevelopment } from "./env";
1
+ import {
2
+ ResourceFiber,
3
+ RenderResult,
4
+ Resource,
5
+ ResourceFiberRoot,
6
+ } from "./types";
7
+ import { commitAllEffects, cleanupAllEffects } from "./helpers/commit";
8
+ import {
9
+ getDevStrictMode,
10
+ withResourceFiber,
11
+ } from "./helpers/execution-context";
12
+ import { callResourceFn } from "./helpers/callResourceFn";
13
+ import { isDevelopment } from "./helpers/env";
6
14
 
7
15
  export function createResourceFiber<R, P>(
8
16
  type: Resource<R, P>,
9
- dispatchUpdate: (callback: () => boolean) => void,
17
+ root: ResourceFiberRoot,
18
+ markDirty: (() => void) | undefined = undefined,
10
19
  strictMode: "root" | "child" | null = getDevStrictMode(false),
11
20
  ): ResourceFiber<R, P> {
12
21
  return {
13
22
  type,
14
- dispatchUpdate,
23
+ root,
24
+ markDirty,
15
25
  devStrictMode: strictMode,
16
26
  cells: [],
17
27
  currentIndex: 0,
@@ -35,7 +45,7 @@ export function renderResourceFiber<R, P>(
35
45
  props: P,
36
46
  ): RenderResult {
37
47
  const result = {
38
- commitTasks: [],
48
+ effectTasks: [],
39
49
  props,
40
50
  output: undefined as R | undefined,
41
51
  };
@@ -59,10 +69,12 @@ export function commitResourceFiber<R, P>(
59
69
  fiber.isMounted = true;
60
70
 
61
71
  if (isDevelopment && fiber.isNeverMounted && fiber.devStrictMode === "root") {
62
- commitRender(result);
72
+ fiber.isNeverMounted = false;
73
+
74
+ commitAllEffects(result);
63
75
  cleanupAllEffects(fiber);
64
76
  }
65
77
 
66
78
  fiber.isNeverMounted = false;
67
- commitRender(result);
79
+ commitAllEffects(result);
68
80
  }
@@ -0,0 +1,53 @@
1
+ import { ResourceElement } from "./types";
2
+ import {
3
+ createResourceFiber,
4
+ unmountResourceFiber,
5
+ renderResourceFiber,
6
+ commitResourceFiber,
7
+ } from "./ResourceFiber";
8
+ import { tapResourceRoot } from "../tapResourceRoot";
9
+ import { resource } from "./resource";
10
+ import { isDevelopment } from "./helpers/env";
11
+ import { flushResourcesSync, UpdateScheduler } from "./scheduler";
12
+ import { createResourceFiberRoot } from "./helpers/root";
13
+
14
+ const SubscribableResource = resource(tapResourceRoot);
15
+
16
+ export const createResourceRoot = () => {
17
+ const fiber = createResourceFiber<
18
+ tapResourceRoot.SubscribableResource<any>,
19
+ ResourceElement<any>
20
+ >(
21
+ SubscribableResource,
22
+ createResourceFiberRoot((callback) => {
23
+ new UpdateScheduler(() => {
24
+ if (callback()) {
25
+ throw new Error(
26
+ "Unexpected rerender of createResourceRoot outer fiber",
27
+ );
28
+ }
29
+ return false;
30
+ }).markDirty();
31
+ }),
32
+ undefined,
33
+ isDevelopment ? "root" : null,
34
+ );
35
+
36
+ return {
37
+ render: <R, P>(element: ResourceElement<R, P>) => {
38
+ // In strict mode, render twice to detect side effects
39
+ if (isDevelopment && fiber.devStrictMode === "root") {
40
+ void renderResourceFiber(fiber, element);
41
+ }
42
+
43
+ const render = renderResourceFiber(fiber, element);
44
+
45
+ flushResourcesSync(() => commitResourceFiber(fiber, render));
46
+
47
+ return render.output;
48
+ },
49
+ unmount: () => {
50
+ unmountResourceFiber(fiber);
51
+ },
52
+ };
53
+ };