@assistant-ui/tap 0.6.0 → 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.
Files changed (215) hide show
  1. package/README.md +9 -6
  2. package/dist/core/ResourceFiber.d.ts +5 -5
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +26 -18
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/createTapRoot.d.ts +9 -0
  7. package/dist/core/createTapRoot.d.ts.map +1 -0
  8. package/dist/core/createTapRoot.js +27 -0
  9. package/dist/core/createTapRoot.js.map +1 -0
  10. package/dist/core/helpers/commit.d.ts +1 -1
  11. package/dist/core/helpers/commit.d.ts.map +1 -1
  12. package/dist/core/helpers/commit.js +6 -1
  13. package/dist/core/helpers/commit.js.map +1 -1
  14. package/dist/core/helpers/execution-context.d.ts +4 -5
  15. package/dist/core/helpers/execution-context.d.ts.map +1 -1
  16. package/dist/core/helpers/execution-context.js +1 -7
  17. package/dist/core/helpers/execution-context.js.map +1 -1
  18. package/dist/core/helpers/root.d.ts +3 -2
  19. package/dist/core/helpers/root.d.ts.map +1 -1
  20. package/dist/core/helpers/root.js +19 -15
  21. package/dist/core/helpers/root.js.map +1 -1
  22. package/dist/core/react-dispatcher.d.ts.map +1 -1
  23. package/dist/core/react-dispatcher.js +17 -16
  24. package/dist/core/react-dispatcher.js.map +1 -1
  25. package/dist/core/resource.d.ts +2 -4
  26. package/dist/core/resource.d.ts.map +1 -1
  27. package/dist/core/resource.js +5 -10
  28. package/dist/core/resource.js.map +1 -1
  29. package/dist/core/scheduler.d.ts +2 -2
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +2 -2
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/types.d.ts +27 -25
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/hooks/useResource.d.ts +2 -2
  36. package/dist/hooks/useResource.d.ts.map +1 -1
  37. package/dist/hooks/useResource.js +14 -20
  38. package/dist/hooks/useResource.js.map +1 -1
  39. package/dist/hooks/useResources.d.ts +1 -1
  40. package/dist/hooks/useResources.d.ts.map +1 -1
  41. package/dist/hooks/useResources.js +18 -27
  42. package/dist/hooks/useResources.js.map +1 -1
  43. package/dist/hooks/useTapHost.d.ts +21 -0
  44. package/dist/hooks/useTapHost.d.ts.map +1 -0
  45. package/dist/hooks/useTapHost.js +30 -0
  46. package/dist/hooks/useTapHost.js.map +1 -0
  47. package/dist/hooks/useTapRoot.d.ts +18 -0
  48. package/dist/hooks/useTapRoot.d.ts.map +1 -0
  49. package/dist/hooks/useTapRoot.js +77 -0
  50. package/dist/hooks/useTapRoot.js.map +1 -0
  51. package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
  52. package/dist/hooks/utils/depsShallowEqual.js +5 -2
  53. package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
  54. package/dist/hooks/utils/useCell.d.ts +2 -2
  55. package/dist/hooks/utils/useCell.d.ts.map +1 -1
  56. package/dist/hooks/utils/useCell.js.map +1 -1
  57. package/dist/hooks/utils/useDevStrictMode.d.ts +5 -0
  58. package/dist/hooks/utils/useDevStrictMode.d.ts.map +1 -0
  59. package/dist/hooks/utils/useDevStrictMode.js +25 -0
  60. package/dist/hooks/utils/useDevStrictMode.js.map +1 -0
  61. package/dist/hooks/utils/useRenderMemo.d.ts +5 -0
  62. package/dist/hooks/utils/useRenderMemo.d.ts.map +1 -0
  63. package/dist/hooks/utils/useRenderMemo.js +25 -0
  64. package/dist/hooks/utils/useRenderMemo.js.map +1 -0
  65. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts +10 -0
  66. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts.map +1 -0
  67. package/dist/hooks/utils/useResourceFiberHostUtils.js +46 -0
  68. package/dist/hooks/utils/useResourceFiberHostUtils.js.map +1 -0
  69. package/dist/index.d.ts +7 -4
  70. package/dist/index.js +7 -4
  71. package/dist/{hooks → react-hooks}/index.d.ts +6 -6
  72. package/dist/{hooks → react-hooks}/index.js +5 -5
  73. package/dist/{hooks → react-hooks}/use.d.ts +1 -1
  74. package/dist/{hooks → react-hooks}/use.d.ts.map +1 -1
  75. package/dist/{hooks → react-hooks}/use.js +1 -1
  76. package/dist/react-hooks/use.js.map +1 -0
  77. package/dist/{hooks → react-hooks}/useCallback.d.ts +1 -1
  78. package/dist/react-hooks/useCallback.d.ts.map +1 -0
  79. package/dist/{hooks → react-hooks}/useCallback.js +1 -1
  80. package/dist/react-hooks/useCallback.js.map +1 -0
  81. package/dist/{hooks → react-hooks}/useEffect.d.ts +1 -1
  82. package/dist/react-hooks/useEffect.d.ts.map +1 -0
  83. package/dist/react-hooks/useEffect.js +35 -0
  84. package/dist/react-hooks/useEffect.js.map +1 -0
  85. package/dist/{hooks → react-hooks}/useEffectEvent.d.ts +1 -1
  86. package/dist/react-hooks/useEffectEvent.d.ts.map +1 -0
  87. package/dist/{hooks → react-hooks}/useEffectEvent.js +2 -2
  88. package/dist/react-hooks/useEffectEvent.js.map +1 -0
  89. package/dist/{hooks → react-hooks}/useMemo.d.ts +1 -1
  90. package/dist/react-hooks/useMemo.d.ts.map +1 -0
  91. package/dist/{hooks → react-hooks}/useMemo.js +3 -3
  92. package/dist/react-hooks/useMemo.js.map +1 -0
  93. package/dist/{hooks → react-hooks}/useMemoCache.d.ts +1 -1
  94. package/dist/react-hooks/useMemoCache.d.ts.map +1 -0
  95. package/dist/{hooks → react-hooks}/useMemoCache.js +1 -1
  96. package/dist/react-hooks/useMemoCache.js.map +1 -0
  97. package/dist/react-hooks/useReducer.d.ts +9 -0
  98. package/dist/react-hooks/useReducer.d.ts.map +1 -0
  99. package/dist/react-hooks/useReducer.js +120 -0
  100. package/dist/react-hooks/useReducer.js.map +1 -0
  101. package/dist/{hooks → react-hooks}/useRef.d.ts +1 -1
  102. package/dist/react-hooks/useRef.d.ts.map +1 -0
  103. package/dist/{hooks → react-hooks}/useRef.js +1 -1
  104. package/dist/react-hooks/useRef.js.map +1 -0
  105. package/dist/{hooks → react-hooks}/useState.d.ts +1 -1
  106. package/dist/react-hooks/useState.d.ts.map +1 -0
  107. package/dist/{hooks → react-hooks}/useState.js +3 -3
  108. package/dist/react-hooks/useState.js.map +1 -0
  109. package/dist/react-shim/index.d.ts +8 -10
  110. package/dist/react-shim/index.d.ts.map +1 -1
  111. package/dist/react-shim/index.js +19 -19
  112. package/dist/react-shim/index.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/__tests__/basic/resourceHandle.test.ts +32 -22
  115. package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
  116. package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
  117. package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
  118. package/src/__tests__/basic/tapState.basic.test.ts +11 -11
  119. package/src/__tests__/bench/hosts.bench.tsx +124 -0
  120. package/src/__tests__/bench/tree.bench.tsx +166 -0
  121. package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
  122. package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
  123. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
  124. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
  125. package/src/__tests__/parity/describeParity.tsx +217 -0
  126. package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
  127. package/src/__tests__/parity/parity.basics.test.tsx +281 -0
  128. package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
  129. package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
  130. package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
  131. package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
  132. package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
  133. package/src/__tests__/react/react-shim.test.tsx +1 -1
  134. package/src/__tests__/react/useResource.test.tsx +41 -26
  135. package/src/__tests__/react/useTapHost.test.tsx +233 -0
  136. package/src/__tests__/react-dispatcher.test.ts +4 -4
  137. package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
  138. package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
  139. package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
  140. package/src/__tests__/strictmode/strictmode.test.ts +39 -209
  141. package/src/__tests__/test-utils.ts +33 -23
  142. package/src/core/ResourceFiber.ts +43 -35
  143. package/src/core/createTapRoot.ts +45 -0
  144. package/src/core/helpers/commit.ts +12 -2
  145. package/src/core/helpers/execution-context.ts +4 -13
  146. package/src/core/helpers/root.ts +24 -12
  147. package/src/core/react-dispatcher.ts +14 -13
  148. package/src/core/resource.ts +5 -20
  149. package/src/core/scheduler.ts +1 -1
  150. package/src/core/types.ts +27 -21
  151. package/src/hooks/useResource.ts +18 -27
  152. package/src/hooks/useResources.ts +18 -42
  153. package/src/hooks/useTapHost.ts +60 -0
  154. package/src/hooks/useTapRoot.ts +135 -0
  155. package/src/hooks/utils/depsShallowEqual.ts +12 -2
  156. package/src/hooks/utils/useCell.ts +2 -2
  157. package/src/hooks/utils/useDevStrictMode.ts +34 -0
  158. package/src/hooks/utils/useRenderMemo.ts +27 -0
  159. package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
  160. package/src/index.ts +6 -3
  161. package/src/{hooks → react-hooks}/index.ts +4 -4
  162. package/src/react-hooks/useEffect.ts +58 -0
  163. package/src/{hooks → react-hooks}/useMemo.ts +1 -1
  164. package/src/react-hooks/useReducer.ts +254 -0
  165. package/src/{hooks → react-hooks}/useState.ts +2 -2
  166. package/src/react-shim/index.ts +24 -13
  167. package/dist/core/createResourceRoot.d.ts +0 -11
  168. package/dist/core/createResourceRoot.d.ts.map +0 -1
  169. package/dist/core/createResourceRoot.js +0 -31
  170. package/dist/core/createResourceRoot.js.map +0 -1
  171. package/dist/core/helpers/callResourceFn.d.ts +0 -1
  172. package/dist/core/helpers/callResourceFn.js +0 -19
  173. package/dist/core/helpers/callResourceFn.js.map +0 -1
  174. package/dist/hooks/use.js.map +0 -1
  175. package/dist/hooks/useCallback.d.ts.map +0 -1
  176. package/dist/hooks/useCallback.js.map +0 -1
  177. package/dist/hooks/useEffect.d.ts.map +0 -1
  178. package/dist/hooks/useEffect.js +0 -40
  179. package/dist/hooks/useEffect.js.map +0 -1
  180. package/dist/hooks/useEffectEvent.d.ts.map +0 -1
  181. package/dist/hooks/useEffectEvent.js.map +0 -1
  182. package/dist/hooks/useMemo.d.ts.map +0 -1
  183. package/dist/hooks/useMemo.js.map +0 -1
  184. package/dist/hooks/useMemoCache.d.ts.map +0 -1
  185. package/dist/hooks/useMemoCache.js.map +0 -1
  186. package/dist/hooks/useReducer.d.ts +0 -21
  187. package/dist/hooks/useReducer.d.ts.map +0 -1
  188. package/dist/hooks/useReducer.js +0 -81
  189. package/dist/hooks/useReducer.js.map +0 -1
  190. package/dist/hooks/useRef.d.ts.map +0 -1
  191. package/dist/hooks/useRef.js.map +0 -1
  192. package/dist/hooks/useResourceRoot.d.ts +0 -20
  193. package/dist/hooks/useResourceRoot.d.ts.map +0 -1
  194. package/dist/hooks/useResourceRoot.js +0 -77
  195. package/dist/hooks/useResourceRoot.js.map +0 -1
  196. package/dist/hooks/useState.d.ts.map +0 -1
  197. package/dist/hooks/useState.js.map +0 -1
  198. package/dist/react/hooks.d.ts +0 -25
  199. package/dist/react/hooks.d.ts.map +0 -1
  200. package/dist/react/hooks.js +0 -69
  201. package/dist/react/hooks.js.map +0 -1
  202. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
  203. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
  204. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
  205. package/src/core/createResourceRoot.ts +0 -53
  206. package/src/core/helpers/callResourceFn.ts +0 -21
  207. package/src/hooks/useEffect.ts +0 -72
  208. package/src/hooks/useReducer.ts +0 -160
  209. package/src/hooks/useResourceRoot.ts +0 -130
  210. package/src/react/hooks.ts +0 -112
  211. /package/src/{hooks → react-hooks}/use.ts +0 -0
  212. /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
  213. /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
  214. /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
  215. /package/src/{hooks → react-hooks}/useRef.ts +0 -0
@@ -1,10 +1,9 @@
1
- import { isDevelopment } from "./env";
2
1
  import type { ResourceFiber } from "../types";
3
2
 
4
3
  let currentResourceFiber: ResourceFiber<any, any> | null = null;
5
4
 
6
- export function withResourceFiber<R, P>(
7
- fiber: ResourceFiber<R, P>,
5
+ export function withResourceFiber<R, A extends readonly unknown[]>(
6
+ fiber: ResourceFiber<R, A>,
8
7
  fn: () => void,
9
8
  ): void {
10
9
  fiber.currentIndex = 0;
@@ -27,21 +26,13 @@ export function withResourceFiber<R, P>(
27
26
  currentResourceFiber = previousContext;
28
27
  }
29
28
  }
30
- export function getCurrentResourceFiber(): ResourceFiber<unknown, unknown> {
29
+ export function getCurrentResourceFiber(): ResourceFiber<unknown> {
31
30
  if (!currentResourceFiber) {
32
31
  throw new Error("No resource fiber available");
33
32
  }
34
33
  return currentResourceFiber;
35
34
  }
36
35
 
37
- export function peekResourceFiber(): ResourceFiber<unknown, unknown> | null {
36
+ export function peekResourceFiber(): ResourceFiber<unknown> | null {
38
37
  return currentResourceFiber;
39
38
  }
40
-
41
- export function getDevStrictMode(enable: boolean) {
42
- if (!isDevelopment) return null;
43
- if (currentResourceFiber?.devStrictMode)
44
- return currentResourceFiber.isFirstRender ? "child" : "root";
45
-
46
- return enable ? "root" : null;
47
- }
@@ -1,4 +1,9 @@
1
- import type { Cell, ResourceFiber, ResourceFiberRoot } from "../types";
1
+ import type {
2
+ Cell,
3
+ ChangelogRecord,
4
+ ResourceFiber,
5
+ ResourceFiberRoot,
6
+ } from "../types";
2
7
 
3
8
  export const createResourceFiberRoot = (
4
9
  dispatchUpdate: (cb: () => boolean) => void,
@@ -8,19 +13,17 @@ export const createResourceFiberRoot = (
8
13
  committedVersion: 0,
9
14
  dispatchUpdate,
10
15
  changelog: [],
11
- dirtyCells: [],
16
+ dirtyCells: new Set(),
12
17
  };
13
18
  };
14
19
 
15
20
  export const commitRoot = (root: ResourceFiberRoot): void => {
16
21
  for (const cell of root.dirtyCells) {
17
- cell.dirty = false;
18
- cell.queue.clear();
19
22
  cell.current = cell.workInProgress;
20
23
  }
21
24
  root.committedVersion = root.version;
22
25
  root.changelog.length = 0;
23
- root.dirtyCells.length = 0;
26
+ root.dirtyCells.clear();
24
27
  };
25
28
 
26
29
  export const setRootVersion = (
@@ -31,11 +34,13 @@ export const setRootVersion = (
31
34
  root.version = version;
32
35
  if (rollback) {
33
36
  for (const cell of root.dirtyCells) {
34
- cell.dirty = false;
35
- cell.queue.clear();
37
+ if (cell.queue !== null) {
38
+ for (const record of cell.queue) record.queued = false;
39
+ cell.queue = null;
40
+ }
36
41
  cell.workInProgress = cell.current;
37
42
  }
38
- root.dirtyCells.length = 0;
43
+ root.dirtyCells.clear();
39
44
 
40
45
  if (version === root.committedVersion) {
41
46
  root.changelog.length = 0;
@@ -49,19 +54,26 @@ export const setRootVersion = (
49
54
  root.changelog.pop();
50
55
  }
51
56
 
52
- root.changelog.forEach((apply) => apply());
57
+ root.changelog.forEach(applyChangelogRecord);
53
58
  commitRoot(root);
54
59
  }
55
60
  }
56
61
  };
57
62
 
63
+ export const applyChangelogRecord = (record: ChangelogRecord): void => {
64
+ markCellDirty(record.fiber, record.cell);
65
+ if (!record.queued) {
66
+ record.queued = true;
67
+ (record.cell.queue ??= []).push(record);
68
+ }
69
+ };
70
+
58
71
  export const markCellDirty = (
59
72
  fiber: ResourceFiber<any, any>,
60
73
  cell: Cell & { type: "reducer" },
61
74
  ): void => {
62
- if (!cell.dirty) {
63
- cell.dirty = true;
75
+ if (!fiber.root.dirtyCells.has(cell)) {
64
76
  fiber.markDirty?.();
65
- fiber.root.dirtyCells.push(cell);
77
+ fiber.root.dirtyCells.add(cell);
66
78
  }
67
79
  };
@@ -1,13 +1,14 @@
1
- import * as React from "react";
2
- import { useState } from "../hooks/useState";
3
- import { useReducer } from "../hooks/useReducer";
4
- import { useRef } from "../hooks/useRef";
5
- import { useMemo } from "../hooks/useMemo";
6
- import { useCallback } from "../hooks/useCallback";
7
- import { useEffect } from "../hooks/useEffect";
8
- import { useEffectEvent } from "../hooks/useEffectEvent";
9
- import { use } from "../hooks/use";
10
- import { useMemoCache } from "../hooks/useMemoCache";
1
+ import React from "react";
2
+
3
+ import { useState } from "../react-hooks/useState";
4
+ import { useReducer } from "../react-hooks/useReducer";
5
+ import { useRef } from "../react-hooks/useRef";
6
+ import { useMemo } from "../react-hooks/useMemo";
7
+ import { useCallback } from "../react-hooks/useCallback";
8
+ import { useEffect } from "../react-hooks/useEffect";
9
+ import { useEffectEvent } from "../react-hooks/useEffectEvent";
10
+ import { use } from "../react-hooks/use";
11
+ import { useMemoCache } from "../react-hooks/useMemoCache";
11
12
 
12
13
  // The dispatcher React reads while a resource renders, so hooks imported from
13
14
  // "react" route to tap with no build step. Hooks tap has no equivalent for are
@@ -30,10 +31,10 @@ const tapDispatcher = {
30
31
 
31
32
  // React's live dispatcher slot differs by version: React 19 exposes it as `H` on
32
33
  // the client internals object; React 18 as `ReactCurrentDispatcher.current`.
34
+ const ReactRuntime = React as any;
33
35
  const internals =
34
- (React as any)
35
- .__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??
36
- (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
36
+ ReactRuntime.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??
37
+ ReactRuntime.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
37
38
 
38
39
  const slot: { current: unknown } | null =
39
40
  internals == null
@@ -1,22 +1,7 @@
1
- import type { ResourceElement } from "./types";
2
- import { fnSymbol } from "./helpers/callResourceFn";
1
+ import type { Resource, ResourceElement } from "./types";
3
2
 
4
- export function resource<R>(fn: () => R): () => ResourceElement<R, undefined>;
5
- export function resource<R, P>(
6
- fn: (props: P) => R,
7
- ): (props: P) => ResourceElement<R, P>;
8
- export function resource<R, P>(
9
- fn: (props?: P) => R,
10
- ): (props?: P) => ResourceElement<R, P | undefined>;
11
- export function resource<R, P = undefined>(fn: (props: P) => R) {
12
- const type = (props?: P) => {
13
- return {
14
- type,
15
- props: props!,
16
- } satisfies ResourceElement<R, P>;
17
- };
18
-
19
- type[fnSymbol] = fn;
20
-
21
- return type;
3
+ export function resource<R, A extends readonly unknown[]>(
4
+ hook: (...args: A) => R,
5
+ ): Resource<R, A> {
6
+ return (...args: A): ResourceElement<R, A> => ({ hook, args });
22
7
  }
@@ -92,7 +92,7 @@ const scheduleMacrotask = (() => {
92
92
  return () => setTimeout(flushScheduled, 0);
93
93
  })();
94
94
 
95
- export const flushResourcesSync = <T>(callback: () => T): T => {
95
+ export const flushTapSync = <T>(callback: () => T): T => {
96
96
  const prev = flushState;
97
97
  flushState = {
98
98
  schedulers: new Set([]),
package/src/core/types.ts CHANGED
@@ -1,13 +1,15 @@
1
- import type { useEffect } from "../hooks/useEffect";
2
-
3
- export type ResourceElement<R, P = any> = {
4
- readonly type: Resource<R, P>;
5
- readonly props: P;
1
+ export type ResourceElement<R, A extends readonly unknown[] = any[]> = {
2
+ readonly hook: (...args: A) => R;
3
+ readonly args: Readonly<A>;
6
4
  readonly key?: string | number;
7
5
  };
8
6
 
9
- export type Resource<R, P> = (props: P) => ResourceElement<R, P>;
10
- export type ContravariantResource<R, P> = (props: P) => ResourceElement<R>;
7
+ export type Resource<R, A extends readonly unknown[] = any[]> = (
8
+ ...args: A
9
+ ) => ResourceElement<R, A>;
10
+ export type ContravariantResource<R, A extends readonly unknown[] = any[]> = (
11
+ ...args: A
12
+ ) => ResourceElement<R>;
11
13
 
12
14
  export type ExtractResourceReturnType<T> =
13
15
  T extends ResourceElement<infer R, any>
@@ -16,10 +18,13 @@ export type ExtractResourceReturnType<T> =
16
18
  ? R
17
19
  : never;
18
20
 
19
- export interface ReducerQueueEntry {
21
+ export interface ChangelogRecord {
22
+ readonly fiber: ResourceFiber<any, any>;
23
+ readonly cell: Cell & { type: "reducer" };
20
24
  readonly action: any;
21
25
  hasEagerState: boolean;
22
26
  eagerState: any;
27
+ queued: boolean;
23
28
  }
24
29
 
25
30
  export type Cell =
@@ -27,48 +32,49 @@ export type Cell =
27
32
  readonly type: "reducer";
28
33
  readonly dispatch: (action: any) => void;
29
34
 
30
- readonly queue: Set<ReducerQueueEntry>;
31
- dirty: boolean;
35
+ queue: ChangelogRecord[] | null;
36
+ renderQueue: any[] | null;
37
+
32
38
  workInProgress: any;
33
39
  current: any;
34
40
  reducer: (state: any, action: any) => any;
35
41
  }
36
42
  | {
37
43
  readonly type: "effect";
38
- cleanup: useEffect.Destructor | undefined;
44
+ cleanup: (() => void) | undefined;
39
45
  deps: readonly unknown[] | null | undefined;
40
46
  };
41
47
 
42
48
  export interface EffectTask {
43
- readonly effect: useEffect.EffectCallback;
44
- readonly deps: readonly unknown[] | undefined;
45
- readonly cell: Cell & { type: "effect" };
49
+ readonly cleanup: () => void;
50
+ readonly setup: () => void;
46
51
  }
47
52
 
48
53
  export interface RenderResult {
49
- readonly output: any;
50
- readonly props: any;
51
- readonly effectTasks: (() => void)[];
54
+ value: any;
55
+ effectTasks: EffectTask[];
52
56
  }
53
57
 
54
58
  export interface ResourceFiberRoot {
55
59
  version: number;
56
60
  committedVersion: number;
57
- readonly changelog: (() => void)[];
61
+ readonly changelog: ChangelogRecord[];
58
62
 
59
63
  readonly dispatchUpdate: (callback: () => boolean) => void;
60
- readonly dirtyCells: (Cell & { type: "reducer" })[];
64
+ readonly dirtyCells: Set<Cell & { type: "reducer" }>;
61
65
  }
62
66
 
63
- export interface ResourceFiber<R, P> {
67
+ export interface ResourceFiber<R, A extends readonly unknown[] = any[]> {
64
68
  readonly root: ResourceFiberRoot;
65
- readonly type: Resource<R, P>;
69
+ readonly hook: (...args: A) => R;
66
70
  readonly markDirty: (() => void) | undefined;
67
71
  readonly devStrictMode: "root" | "child" | null;
68
72
 
69
73
  cells: Cell[];
70
74
  currentIndex: number;
71
75
 
76
+ renderPendingCells: Set<Cell & { type: "reducer" }> | null;
77
+
72
78
  renderContext: RenderResult | undefined; // set during render
73
79
 
74
80
  isMounted: boolean;
@@ -1,49 +1,40 @@
1
1
  import type { ExtractResourceReturnType, ResourceElement } from "../core/types";
2
- import { useEffect } from "./useEffect";
3
2
  import {
4
- createResourceFiber,
5
3
  unmountResourceFiber,
6
4
  renderResourceFiber,
7
5
  commitResourceFiber,
8
6
  } from "../core/ResourceFiber";
9
- import { useMemo } from "./useMemo";
10
- import { useRef } from "./useRef";
11
- import { getCurrentResourceFiber } from "../core/helpers/execution-context";
7
+ import { useResourceFiberHost } from "./utils/useResourceFiberHostUtils";
8
+ import { useEffect, useMemo } from "react";
9
+ import { useRenderMemo } from "./utils/useRenderMemo";
12
10
 
13
- export function useResource<E extends ResourceElement<any, any>>(
11
+ export function useResource<E extends ResourceElement<any, any[]>>(
14
12
  element: E,
15
13
  ): ExtractResourceReturnType<E>;
16
- export function useResource<E extends ResourceElement<any, any>>(
14
+ export function useResource<E extends ResourceElement<any, any[]>>(
17
15
  element: E,
18
- propsDeps: readonly unknown[],
16
+ argsDeps: readonly unknown[],
19
17
  ): ExtractResourceReturnType<E>;
20
- export function useResource<E extends ResourceElement<any, any>>(
18
+ export function useResource<E extends ResourceElement<any, any[]>>(
21
19
  element: E,
22
- propsDeps?: readonly unknown[],
20
+ argsDeps?: readonly unknown[],
23
21
  ): ExtractResourceReturnType<E> {
24
- const parentFiber = getCurrentResourceFiber();
25
- const versionRef = useRef(0);
22
+ const { version, createFiber } = useResourceFiberHost();
26
23
  const fiber = useMemo(() => {
27
- void element.key;
28
- return createResourceFiber(element.type, parentFiber.root, () => {
29
- versionRef.current++;
30
- parentFiber.markDirty?.();
31
- });
32
- }, [element.type, element.key, parentFiber]);
24
+ return createFiber(element.hook, element.key);
25
+ }, [element.hook, element.key, createFiber]);
33
26
 
34
- const result = propsDeps
35
- ? // oxlint-disable-next-line react/rules-of-hooks -- propsDeps presence is fixed per call site, so the conditional call order is stable
36
- useMemo(
37
- () => renderResourceFiber(fiber, element.props),
38
- // oxlint-disable-next-line react/exhaustive-deps -- props identity replaced by user-provided deps
39
- [fiber, ...propsDeps, versionRef.current],
40
- )
41
- : renderResourceFiber(fiber, element.props);
27
+ const identity = argsDeps ?? [element.args];
28
+
29
+ const result = useRenderMemo(
30
+ () => renderResourceFiber(fiber, element.args),
31
+ [fiber, version, ...identity],
32
+ );
42
33
 
43
34
  useEffect(() => () => unmountResourceFiber(fiber), [fiber]);
44
35
  useEffect(() => {
45
36
  commitResourceFiber(fiber, result);
46
37
  }, [fiber, result]);
47
38
 
48
- return result.output;
39
+ return result.value;
49
40
  }
@@ -4,41 +4,24 @@ import type {
4
4
  ResourceElement,
5
5
  ResourceFiber,
6
6
  } from "../core/types";
7
- import { useEffect } from "./useEffect";
8
- import { useMemo } from "./useMemo";
9
- import { useCallback } from "./useCallback";
10
7
  import {
11
- createResourceFiber,
12
8
  unmountResourceFiber,
13
9
  renderResourceFiber,
14
10
  commitResourceFiber,
15
11
  } from "../core/ResourceFiber";
16
- import { useRef } from "./useRef";
17
- import { getCurrentResourceFiber } from "../core/helpers/execution-context";
12
+ import { useResourceFiberHost } from "./utils/useResourceFiberHostUtils";
13
+ import { useCallback, useEffect, useMemo } from "react";
14
+ import { useRenderMemo } from "./utils/useRenderMemo";
18
15
 
19
16
  type FiberState = {
20
- fiber: ResourceFiber<unknown, unknown>;
21
- next:
22
- | RenderResult
23
- | [ResourceFiber<unknown, unknown>, RenderResult]
24
- | "delete";
17
+ fiber: ResourceFiber<unknown>;
18
+ next: RenderResult | [ResourceFiber<unknown>, RenderResult] | "delete";
25
19
  };
26
20
 
27
- export function useResources<E extends ResourceElement<any, any>>(
21
+ export function useResources<E extends ResourceElement<any, any[]>>(
28
22
  getElements: () => readonly E[],
29
23
  getElementsDeps?: readonly unknown[],
30
24
  ): ExtractResourceReturnType<E>[] {
31
- const versionRef = useRef(0);
32
- const version = versionRef.current;
33
-
34
- const parentFiber = useMemo(() => getCurrentResourceFiber(), []);
35
- const markDirty = useMemo(
36
- () => () => {
37
- versionRef.current++;
38
- parentFiber.markDirty?.();
39
- },
40
- [parentFiber],
41
- );
42
25
  const fibers = useMemo(() => new Map<string | number, FiberState>(), []);
43
26
 
44
27
  const getElementsMemo = getElementsDeps
@@ -48,7 +31,8 @@ export function useResources<E extends ResourceElement<any, any>>(
48
31
 
49
32
  // Process each element
50
33
 
51
- const res = useMemo(() => {
34
+ const { version, createFiber } = useResourceFiberHost();
35
+ const res = useRenderMemo(() => {
52
36
  void version;
53
37
 
54
38
  const elementsArray = getElementsMemo();
@@ -73,31 +57,23 @@ export function useResources<E extends ResourceElement<any, any>>(
73
57
 
74
58
  let state = fibers.get(elementKey);
75
59
  if (!state) {
76
- const fiber = createResourceFiber(
77
- element.type,
78
- parentFiber.root,
79
- markDirty,
80
- );
81
- const result = renderResourceFiber(fiber, element.props);
60
+ const fiber = createFiber(element.hook, element.key);
61
+ const result = renderResourceFiber(fiber, element.args);
82
62
  state = {
83
63
  fiber,
84
64
  next: result,
85
65
  };
86
66
  newCount++;
87
67
  fibers.set(elementKey, state);
88
- results.push(result.output);
89
- } else if (state.fiber.type !== element.type) {
90
- const fiber = createResourceFiber(
91
- element.type,
92
- parentFiber.root,
93
- markDirty,
94
- );
95
- const result = renderResourceFiber(fiber, element.props);
68
+ results.push(result.value);
69
+ } else if (state.fiber.hook !== element.hook) {
70
+ const fiber = createFiber(element.hook, element.key);
71
+ const result = renderResourceFiber(fiber, element.args);
96
72
  state.next = [fiber, result];
97
- results.push(result.output);
73
+ results.push(result.value);
98
74
  } else {
99
- state.next = renderResourceFiber(state.fiber, element.props);
100
- results.push(state.next.output);
75
+ state.next = renderResourceFiber(state.fiber, element.args);
76
+ results.push(state.next.value);
101
77
  }
102
78
  }
103
79
 
@@ -111,7 +87,7 @@ export function useResources<E extends ResourceElement<any, any>>(
111
87
  }
112
88
 
113
89
  return results;
114
- }, [getElementsMemo, version, parentFiber, markDirty, fibers]);
90
+ }, [getElementsMemo, fibers, createFiber, version]);
115
91
 
116
92
  // Cleanup on unmount
117
93
  useEffect(() => {
@@ -0,0 +1,60 @@
1
+ import {
2
+ unmountResourceFiber,
3
+ renderResourceFiber,
4
+ commitResourceFiber,
5
+ } from "../core/ResourceFiber";
6
+ import { useResourceFiberHost } from "./utils/useResourceFiberHostUtils";
7
+ import { useEffect, useMemo } from "react";
8
+
9
+ export namespace useTapHost {
10
+ export interface Result<R> {
11
+ /**
12
+ * The current render output of the host resource.
13
+ */
14
+ value: R;
15
+
16
+ /**
17
+ * Commits the host's pending render result. Pass to a deps-less
18
+ * useEffect in a descendant component to land the commit ahead of
19
+ * the descendants' own effects; the first instance to run wins. A
20
+ * plain callback (not a hook) so React Compiler can compile the
21
+ * consumer; its identity changes on every host render.
22
+ */
23
+ effects: () => void;
24
+ }
25
+ }
26
+
27
+ const useHostRender = <R>(render: () => R): R => render();
28
+
29
+ export const useTapHost = <R>(callback: () => R): useTapHost.Result<R> => {
30
+ const { createFiber } = useResourceFiberHost();
31
+ const fiber = useMemo(
32
+ () => createFiber(useHostRender<R>, undefined),
33
+ [createFiber],
34
+ );
35
+
36
+ const render = renderResourceFiber(fiber, [callback]);
37
+
38
+ useEffect(() => {
39
+ return () => {
40
+ unmountResourceFiber(fiber);
41
+ };
42
+ }, [fiber]);
43
+
44
+ let renderCommitted = false;
45
+ const effects = () => {
46
+ // !isMounted: StrictMode/Activity replays effects without a re-render
47
+ // after unmounting the fiber; the replay must recommit it.
48
+ if (renderCommitted && fiber.isMounted) return;
49
+ renderCommitted = true;
50
+
51
+ commitResourceFiber(fiber, render);
52
+ };
53
+
54
+ useEffect(effects);
55
+
56
+ return {
57
+ value: render.value as R,
58
+ effects,
59
+ };
60
+ };
@@ -0,0 +1,135 @@
1
+ import {
2
+ commitResourceFiber,
3
+ createResourceFiber,
4
+ renderResourceFiber,
5
+ unmountResourceFiber,
6
+ } from "../core/ResourceFiber";
7
+ import { UpdateScheduler } from "../core/scheduler";
8
+ import { isDevelopment } from "../core/helpers/env";
9
+ import {
10
+ commitRoot,
11
+ createResourceFiberRoot,
12
+ setRootVersion,
13
+ } from "../core/helpers/root";
14
+ import { useEffect, useEffectEvent, useMemo, useRef } from "react";
15
+ import { useDevStrictMode } from "./utils/useDevStrictMode";
16
+
17
+ export namespace useTapRoot {
18
+ export type Unsubscribe = () => void;
19
+
20
+ export interface Root<R> {
21
+ /**
22
+ * Get the current value of the root.
23
+ */
24
+ getValue(): R;
25
+
26
+ /**
27
+ * Subscribe to the root.
28
+ */
29
+ subscribe(listener: () => void): Unsubscribe;
30
+ }
31
+ }
32
+
33
+ const useHostRoot = <R>(render: () => R): R => render();
34
+
35
+ export const useTapRoot = <R>(render: () => R): useTapRoot.Root<R> => {
36
+ const scheduler = useMemo(
37
+ () => new UpdateScheduler(() => handleUpdate()),
38
+ [],
39
+ );
40
+ const queue = useMemo(() => [] as (() => boolean)[], []);
41
+
42
+ const getDevStrictMode = useDevStrictMode();
43
+ const fiber = useMemo(() => {
44
+ const root = createResourceFiberRoot((callback) => {
45
+ if (!scheduler.isDirty && !callback()) return;
46
+
47
+ setRootVersion(root, root.committedVersion + root.changelog.length);
48
+ queue.push(callback);
49
+ scheduler.markDirty();
50
+ });
51
+ return createResourceFiber(
52
+ useHostRoot<R>,
53
+ root,
54
+ undefined,
55
+ getDevStrictMode(),
56
+ );
57
+ }, [queue, scheduler, getDevStrictMode]);
58
+
59
+ const drainedCount = fiber.root.version - fiber.root.committedVersion;
60
+ const render2 = renderResourceFiber(fiber, [render]);
61
+
62
+ const isMountedRef = useRef(false);
63
+ const committedArgsRef = useRef([render] as const);
64
+ const valueRef = useRef<R>(render2.value);
65
+ const subscribers = useMemo(() => new Set<() => void>(), []);
66
+
67
+ const publish = (output: R) => {
68
+ if (scheduler.isDirty || valueRef.current === output) return;
69
+ valueRef.current = output;
70
+ subscribers.forEach((listener) => listener());
71
+ };
72
+
73
+ const handleUpdate = useEffectEvent(() => {
74
+ setRootVersion(fiber.root, fiber.root.committedVersion);
75
+
76
+ queue.forEach((callback) => {
77
+ if (isDevelopment && fiber.devStrictMode) {
78
+ callback();
79
+ }
80
+
81
+ callback();
82
+ });
83
+
84
+ setRootVersion(
85
+ fiber.root,
86
+ fiber.root.committedVersion + fiber.root.changelog.length,
87
+ );
88
+
89
+ if (isDevelopment && fiber.devStrictMode) {
90
+ void renderResourceFiber(fiber, committedArgsRef.current);
91
+ }
92
+
93
+ const render = renderResourceFiber(fiber, committedArgsRef.current);
94
+
95
+ if (scheduler.isDirty)
96
+ throw new Error("Scheduler is dirty, this should never happen");
97
+
98
+ commitRoot(fiber.root);
99
+ queue.length = 0;
100
+
101
+ if (isMountedRef.current) {
102
+ commitResourceFiber(fiber, render);
103
+ }
104
+
105
+ publish(render.value);
106
+ });
107
+
108
+ useEffect(() => {
109
+ isMountedRef.current = true;
110
+ return () => {
111
+ isMountedRef.current = false;
112
+ unmountResourceFiber(fiber);
113
+ };
114
+ }, [fiber]);
115
+
116
+ useEffect(() => {
117
+ committedArgsRef.current = [render];
118
+ commitRoot(fiber.root);
119
+ queue.splice(0, drainedCount);
120
+ commitResourceFiber(fiber, render2);
121
+
122
+ publish(render2.value);
123
+ });
124
+
125
+ return useMemo(
126
+ () => ({
127
+ getValue: () => valueRef.current,
128
+ subscribe: (listener: () => void) => {
129
+ subscribers.add(listener);
130
+ return () => subscribers.delete(listener);
131
+ },
132
+ }),
133
+ [subscribers],
134
+ );
135
+ };