@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 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react-shim/index.ts"],"mappings":";;;;;;cAuBa,QAAA,GAAY,YAAA,kBAAkB,OAAA;AAAA,cAG9B,UAAA,GAAc,OAAA,OAAc,UAAA,OAAiB,IAAA,sBAAU,MAAA,gCAAA,KAAA,CAAA,cAAA,CAAA,KAAA,CAAA,YAAA;AAAA,cAKvD,MAAA,GAAU,YAAA,WAC4C,QAAA,CAD1B,SAAA;AAAA,cAG5B,OAAA,GAAW,OAAA,OAAc,IAAS;AAAA,cAGlC,WAAA,GAAe,QAAA,OAAe,IAAS;AAAA,cAKvC,SAAA,GAAa,MAAA,OAAa,IAAU;AAAA,cAIpC,eAAA,GAAmB,MAAA,OAAa,IAAU;AAAA,cAG1C,cAAA,GAAkB,QAAa;AAAA,cAM/B,GAAA,GAAO,MAAW;AAAA,cAKlB,UAAA,GAAc,OAAY"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react-shim/index.ts"],"mappings":";;;;cAwBa,QAAA,GAAY,YAAkB;AAAA,cAG9B,UAAA,GAAc,OAAA,OAAc,UAAA,OAAiB,IAAA;AAAA,cAK7C,MAAA,GAAU,YAAkB;AAAA,cAG5B,OAAA,GAAW,OAAA,OAAc,IAAS;AAAA,cAGlC,WAAA,GAAe,QAAA,OAAe,IAAS;AAAA,cAKvC,SAAA,GAAa,MAAA,OAAa,IAAU;AAAA,cAMpC,eAAA,GAAmB,MAAA,OAAa,IAAU;AAAA,cAM1C,cAAA,GAAkB,QAAa;AAAA,cAS/B,GAAA,GAAO,MAAW;AAAA,cAOlB,UAAA,GAAc,OAAY"}
@@ -1,27 +1,27 @@
1
1
  import { peekResourceFiber } from "../core/helpers/execution-context.js";
2
- import { useReducer as useReducer$2 } from "../hooks/useReducer.js";
3
- import { useState as useState$2 } from "../hooks/useState.js";
4
- import { useRef as useRef$2 } from "../hooks/useRef.js";
5
- import { useMemo as useMemo$2 } from "../hooks/useMemo.js";
6
- import { useCallback as useCallback$1 } from "../hooks/useCallback.js";
7
- import { useEffect as useEffect$1 } from "../hooks/useEffect.js";
8
- import { useEffectEvent as useEffectEvent$1 } from "../hooks/useEffectEvent.js";
2
+ import { useReducer as useReducer$2 } from "../react-hooks/useReducer.js";
3
+ import { useState as useState$2 } from "../react-hooks/useState.js";
4
+ import { useRef as useRef$2 } from "../react-hooks/useRef.js";
5
+ import { useMemo as useMemo$2 } from "../react-hooks/useMemo.js";
6
+ import { useCallback as useCallback$2 } from "../react-hooks/useCallback.js";
7
+ import { useEffect as useEffect$2 } from "../react-hooks/useEffect.js";
8
+ import { useEffectEvent as useEffectEvent$2 } from "../react-hooks/useEffectEvent.js";
9
9
  import { isResourceContext, useResourceContext } from "../core/context.js";
10
- import * as React from "react";
11
- import react_default from "react";
10
+ import React, { default as react_default } from "react";
12
11
  export * from "react";
13
12
  //#region src/react-shim/index.ts
14
13
  const inTap = () => peekResourceFiber() !== null;
15
- const useState = (initialState) => inTap() ? useState$2(initialState) : React.useState(initialState);
16
- const useReducer = (reducer, initialArg, init) => inTap() ? useReducer$2(reducer, initialArg, init) : React.useReducer(reducer, initialArg, init);
17
- const useRef = (initialValue) => inTap() ? useRef$2(initialValue) : React.useRef(initialValue);
18
- const useMemo = (factory, deps) => inTap() ? useMemo$2(factory, deps) : React.useMemo(factory, deps);
19
- const useCallback = (callback, deps) => inTap() ? useCallback$1(callback, deps) : React.useCallback(callback, deps);
20
- const useEffect = (effect, deps) => inTap() ? useEffect$1(effect, deps) : React.useEffect(effect, deps);
21
- const useLayoutEffect = (effect, deps) => inTap() ? useEffect$1(effect, deps) : React.useLayoutEffect(effect, deps);
22
- const useEffectEvent = (callback) => inTap() ? useEffectEvent$1(callback) : React.useEffectEvent(callback);
23
- const use = (usable) => isResourceContext(usable) ? useResourceContext(usable) : React.use(usable);
24
- const useContext = (context) => isResourceContext(context) ? useResourceContext(context) : React.useContext(context);
14
+ const ReactRuntime = React;
15
+ const useState = (initialState) => inTap() ? useState$2(initialState) : ReactRuntime.useState(initialState);
16
+ const useReducer = (reducer, initialArg, init) => inTap() ? useReducer$2(reducer, initialArg, init) : ReactRuntime.useReducer(reducer, initialArg, init);
17
+ const useRef = (initialValue) => inTap() ? useRef$2(initialValue) : ReactRuntime.useRef(initialValue);
18
+ const useMemo = (factory, deps) => inTap() ? useMemo$2(factory, deps) : ReactRuntime.useMemo(factory, deps);
19
+ const useCallback = (callback, deps) => inTap() ? useCallback$2(callback, deps) : ReactRuntime.useCallback(callback, deps);
20
+ const useEffect = (effect, deps) => inTap() ? useEffect$2(effect, deps) : ReactRuntime.useEffect(effect, deps);
21
+ const useLayoutEffect = (effect, deps) => inTap() ? useEffect$2(effect, deps) : ReactRuntime.useLayoutEffect(effect, deps);
22
+ const useEffectEvent = (callback) => inTap() ? useEffectEvent$2(callback) : ReactRuntime.useEffectEvent(callback);
23
+ const use = (usable) => isResourceContext(usable) ? useResourceContext(usable) : ReactRuntime.use(usable);
24
+ const useContext = (context) => isResourceContext(context) ? useResourceContext(context) : ReactRuntime.useContext(context);
25
25
  //#endregion
26
26
  export { react_default as default, use, useCallback, useContext, useEffect, useEffectEvent, useLayoutEffect, useMemo, useReducer, useRef, useState };
27
27
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["hooks.useState","hooks.useReducer","hooks.useRef","hooks.useMemo","hooks.useCallback","hooks.useEffect","hooks.useEffectEvent"],"sources":["../../src/react-shim/index.ts"],"sourcesContent":["/* oxlint-disable react/rules-of-hooks -- this module deliberately routes hook calls between tap and React at runtime */\n/* oxlint-disable react/exhaustive-deps -- dependency arrays are forwarded verbatim from the caller */\n// Runtime drop-in for \"react\": forward everything from react, then override the\n// hooks that have a tap equivalent so they route to tap inside a resource render\n// and to React otherwise. Alias `react` to this module (build `output.paths` /\n// vitest resolver) in code that can run inside a tap resource.\n//\n// This subpath ships no type declarations: the build reverts the aliased\n// specifier back to `\"react\"` in emitted `.d.ts`, so consumer types resolve to\n// React's own. The source-level TS2498 from the `export *` below is suppressed.\nimport * as React from \"react\";\nimport { peekResourceFiber } from \"../core/helpers/execution-context\";\nimport * as hooks from \"../hooks\";\nimport { useResourceContext, isResourceContext } from \"../core/context\";\n\n// @ts-expect-error -- @types/react uses `export =`; this is valid at runtime.\nexport * from \"react\";\nexport { default } from \"react\";\n\nconst inTap = () => peekResourceFiber() !== null;\n\n// --- hooks with a tap equivalent: override the star-exported react hooks ---\n\nexport const useState = (initialState?: any) =>\n inTap() ? hooks.useState(initialState) : React.useState(initialState);\n\nexport const useReducer = (reducer: any, initialArg: any, init?: any) =>\n inTap()\n ? hooks.useReducer(reducer, initialArg, init)\n : React.useReducer(reducer, initialArg, init);\n\nexport const useRef = (initialValue?: any) =>\n inTap() ? hooks.useRef(initialValue) : React.useRef(initialValue);\n\nexport const useMemo = (factory: any, deps: any) =>\n inTap() ? hooks.useMemo(factory, deps) : React.useMemo(factory, deps);\n\nexport const useCallback = (callback: any, deps: any) =>\n inTap()\n ? hooks.useCallback(callback, deps)\n : React.useCallback(callback, deps);\n\nexport const useEffect = (effect: any, deps?: any) =>\n inTap() ? hooks.useEffect(effect, deps) : React.useEffect(effect, deps);\n\n// tap has a single effect primitive; layout effects collapse onto it\nexport const useLayoutEffect = (effect: any, deps?: any) =>\n inTap() ? hooks.useEffect(effect, deps) : React.useLayoutEffect(effect, deps);\n\nexport const useEffectEvent = (callback: any) =>\n inTap() ? hooks.useEffectEvent(callback) : React.useEffectEvent(callback);\n\n// `use(usable)` reads tap resource context when handed a tap context (routed by\n// its brand, not by ambient render state), and falls back to React's `use`\n// (promises / React context) for everything else.\nexport const use = (usable: any) =>\n isResourceContext(usable) ? useResourceContext(usable) : React.use(usable);\n\n// `useContext(context)` reads tap resource context when handed a tap context\n// (routed by its brand), and falls back to React's `useContext` otherwise.\nexport const useContext = (context: any) =>\n isResourceContext(context)\n ? useResourceContext(context)\n : React.useContext(context);\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAM,cAAc,kBAAkB,MAAM;AAI5C,MAAa,YAAY,iBACvB,MAAM,IAAIA,WAAe,YAAY,IAAI,MAAM,SAAS,YAAY;AAEtE,MAAa,cAAc,SAAc,YAAiB,SACxD,MAAM,IACFC,aAAiB,SAAS,YAAY,IAAI,IAC1C,MAAM,WAAW,SAAS,YAAY,IAAI;AAEhD,MAAa,UAAU,iBACrB,MAAM,IAAIC,SAAa,YAAY,IAAI,MAAM,OAAO,YAAY;AAElE,MAAa,WAAW,SAAc,SACpC,MAAM,IAAIC,UAAc,SAAS,IAAI,IAAI,MAAM,QAAQ,SAAS,IAAI;AAEtE,MAAa,eAAe,UAAe,SACzC,MAAM,IACFC,cAAkB,UAAU,IAAI,IAChC,MAAM,YAAY,UAAU,IAAI;AAEtC,MAAa,aAAa,QAAa,SACrC,MAAM,IAAIC,YAAgB,QAAQ,IAAI,IAAI,MAAM,UAAU,QAAQ,IAAI;AAGxE,MAAa,mBAAmB,QAAa,SAC3C,MAAM,IAAIA,YAAgB,QAAQ,IAAI,IAAI,MAAM,gBAAgB,QAAQ,IAAI;AAE9E,MAAa,kBAAkB,aAC7B,MAAM,IAAIC,iBAAqB,QAAQ,IAAI,MAAM,eAAe,QAAQ;AAK1E,MAAa,OAAO,WAClB,kBAAkB,MAAM,IAAI,mBAAmB,MAAM,IAAI,MAAM,IAAI,MAAM;AAI3E,MAAa,cAAc,YACzB,kBAAkB,OAAO,IACrB,mBAAmB,OAAO,IAC1B,MAAM,WAAW,OAAO"}
1
+ {"version":3,"file":"index.js","names":["hooks.useState","hooks.useReducer","hooks.useRef","hooks.useMemo","hooks.useCallback","hooks.useEffect","hooks.useEffectEvent"],"sources":["../../src/react-shim/index.ts"],"sourcesContent":["/* oxlint-disable react/rules-of-hooks -- this module deliberately routes hook calls between tap and React at runtime */\n/* oxlint-disable react/exhaustive-deps -- dependency arrays are forwarded verbatim from the caller */\n// Runtime drop-in for \"react\": forward everything from react, then override the\n// hooks that have a tap equivalent so they route to tap inside a resource render\n// and to React otherwise. Alias `react` to this module (build `output.paths` /\n// vitest resolver) in code that can run inside a tap resource.\n//\n// This subpath ships no type declarations: the build reverts the aliased\n// specifier back to `\"react\"` in emitted `.d.ts`, so consumer types resolve to\n// React's own. The source-level TS2498 from the `export *` below is suppressed.\nimport React from \"react\";\nimport { peekResourceFiber } from \"../core/helpers/execution-context\";\nimport * as hooks from \"../react-hooks\";\nimport { useResourceContext, isResourceContext } from \"../core/context\";\n\n// @ts-expect-error -- @types/react uses `export =`; this is valid at runtime.\nexport * from \"react\";\nexport { default } from \"react\";\n\nconst inTap = () => peekResourceFiber() !== null;\nconst ReactRuntime = React as any;\n\n// --- hooks with a tap equivalent: override the star-exported react hooks ---\n\nexport const useState = (initialState?: any) =>\n inTap() ? hooks.useState(initialState) : ReactRuntime.useState(initialState);\n\nexport const useReducer = (reducer: any, initialArg: any, init?: any) =>\n inTap()\n ? hooks.useReducer(reducer, initialArg, init)\n : ReactRuntime.useReducer(reducer, initialArg, init);\n\nexport const useRef = (initialValue?: any) =>\n inTap() ? hooks.useRef(initialValue) : ReactRuntime.useRef(initialValue);\n\nexport const useMemo = (factory: any, deps: any) =>\n inTap() ? hooks.useMemo(factory, deps) : ReactRuntime.useMemo(factory, deps);\n\nexport const useCallback = (callback: any, deps: any) =>\n inTap()\n ? hooks.useCallback(callback, deps)\n : ReactRuntime.useCallback(callback, deps);\n\nexport const useEffect = (effect: any, deps?: any) =>\n inTap()\n ? hooks.useEffect(effect, deps)\n : ReactRuntime.useEffect(effect, deps);\n\n// tap has a single effect primitive; layout effects collapse onto it\nexport const useLayoutEffect = (effect: any, deps?: any) =>\n inTap()\n ? hooks.useEffect(effect, deps)\n : ReactRuntime.useLayoutEffect(effect, deps);\n\n// The non-tap fallback requires a React version that provides useEffectEvent.\nexport const useEffectEvent = (callback: any) =>\n inTap()\n ? hooks.useEffectEvent(callback)\n : ReactRuntime.useEffectEvent(callback);\n\n// `use(usable)` reads tap resource context when handed a tap context (routed by\n// its brand, not by ambient render state), and falls back to React's `use`\n// (promises / React context) for everything else. The non-tap fallback requires\n// React 19.\nexport const use = (usable: any) =>\n isResourceContext(usable)\n ? useResourceContext(usable)\n : ReactRuntime.use(usable);\n\n// `useContext(context)` reads tap resource context when handed a tap context\n// (routed by its brand), and falls back to React's `useContext` otherwise.\nexport const useContext = (context: any) =>\n isResourceContext(context)\n ? useResourceContext(context)\n : ReactRuntime.useContext(context);\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAM,cAAc,kBAAkB,MAAM;AAC5C,MAAM,eAAe;AAIrB,MAAa,YAAY,iBACvB,MAAM,IAAIA,WAAe,YAAY,IAAI,aAAa,SAAS,YAAY;AAE7E,MAAa,cAAc,SAAc,YAAiB,SACxD,MAAM,IACFC,aAAiB,SAAS,YAAY,IAAI,IAC1C,aAAa,WAAW,SAAS,YAAY,IAAI;AAEvD,MAAa,UAAU,iBACrB,MAAM,IAAIC,SAAa,YAAY,IAAI,aAAa,OAAO,YAAY;AAEzE,MAAa,WAAW,SAAc,SACpC,MAAM,IAAIC,UAAc,SAAS,IAAI,IAAI,aAAa,QAAQ,SAAS,IAAI;AAE7E,MAAa,eAAe,UAAe,SACzC,MAAM,IACFC,cAAkB,UAAU,IAAI,IAChC,aAAa,YAAY,UAAU,IAAI;AAE7C,MAAa,aAAa,QAAa,SACrC,MAAM,IACFC,YAAgB,QAAQ,IAAI,IAC5B,aAAa,UAAU,QAAQ,IAAI;AAGzC,MAAa,mBAAmB,QAAa,SAC3C,MAAM,IACFA,YAAgB,QAAQ,IAAI,IAC5B,aAAa,gBAAgB,QAAQ,IAAI;AAG/C,MAAa,kBAAkB,aAC7B,MAAM,IACFC,iBAAqB,QAAQ,IAC7B,aAAa,eAAe,QAAQ;AAM1C,MAAa,OAAO,WAClB,kBAAkB,MAAM,IACpB,mBAAmB,MAAM,IACzB,aAAa,IAAI,MAAM;AAI7B,MAAa,cAAc,YACzB,kBAAkB,OAAO,IACrB,mBAAmB,OAAO,IAC1B,aAAa,WAAW,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/tap",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Reactive state management inspired by React hooks",
5
5
  "keywords": [
6
6
  "state-management",
@@ -1,50 +1,60 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { createResourceRoot } from "../../core/createResourceRoot";
3
- import { resource } from "../../core/resource";
2
+ import { createTapRoot } from "../../core/createTapRoot";
3
+ import { flushTapSync } from "../../core/scheduler";
4
+ import { useState } from "../../react-hooks/useState";
4
5
 
5
6
  describe("ResourceHandle - Basic Usage", () => {
6
7
  it("should create a resource handle with const API", () => {
7
- const TestResource = resource(function TestResource(props: number) {
8
+ const useTestResource = (props: number) => {
8
9
  return {
9
10
  value: props * 2,
10
11
  propsUsed: props,
11
12
  };
13
+ };
14
+
15
+ const sub = createTapRoot(function Root() {
16
+ return useTestResource(5);
12
17
  });
13
- const root = createResourceRoot();
14
- const sub = root.render(TestResource(5));
15
18
 
16
- // The subscribable provides getValue and subscribe
19
+ // The root provides getValue, subscribe, and unmount
17
20
  expect(typeof sub.getValue).toBe("function");
18
21
  expect(typeof sub.subscribe).toBe("function");
19
- expect(typeof root.render).toBe("function");
22
+ expect(typeof sub.unmount).toBe("function");
20
23
 
21
24
  // Initial state
22
25
  expect(sub.getValue().value).toBe(10);
23
26
  expect(sub.getValue().propsUsed).toBe(5);
24
27
  });
25
28
 
26
- it("should allow updating props", () => {
27
- const TestResource = resource(function TestResource(props: {
28
- multiplier: number;
29
- }) {
30
- return { result: 10 * props.multiplier };
29
+ it("should re-render and notify on internal state change", () => {
30
+ const useTestResource = () => {
31
+ const [count, setCount] = useState(0);
32
+ return { count, increment: () => setCount((c) => c + 1) };
33
+ };
34
+
35
+ const sub = createTapRoot(function Root() {
36
+ return useTestResource();
31
37
  });
32
- const root = createResourceRoot();
33
- const sub = root.render(TestResource({ multiplier: 2 }));
34
38
 
35
- // Initial state
36
- expect(sub.getValue().result).toBe(20);
39
+ expect(sub.getValue().count).toBe(0);
37
40
 
38
- // Can call render to update props
39
- expect(() => root.render(TestResource({ multiplier: 3 }))).not.toThrow();
41
+ const listener = vi.fn();
42
+ sub.subscribe(listener);
43
+
44
+ flushTapSync(() => sub.getValue().increment());
45
+
46
+ expect(sub.getValue().count).toBe(1);
47
+ expect(listener).toHaveBeenCalled();
40
48
  });
41
49
 
42
50
  it("should support subscribing and unsubscribing", () => {
43
- const TestResource = resource(function TestResource() {
44
- return { timestamp: Date.now() };
51
+ const useTestResource = () => {
52
+ return { timestamp: 0 };
53
+ };
54
+
55
+ const sub = createTapRoot(function Root() {
56
+ return useTestResource();
45
57
  });
46
- const root = createResourceRoot();
47
- const sub = root.render(TestResource());
48
58
 
49
59
  const subscriber1 = vi.fn();
50
60
  const subscriber2 = vi.fn();
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi, afterEach } from "vitest";
2
- import { useEffect } from "../../hooks/useEffect";
3
- import { useState } from "../../hooks/useState";
2
+ import { useEffect } from "../../react-hooks/useEffect";
3
+ import { useState } from "../../react-hooks/useState";
4
4
  import {
5
5
  createTestResource,
6
6
  renderTest,
@@ -31,7 +31,7 @@ describe("useEffect - Basic Functionality", () => {
31
31
  const manager = new TestResourceManager(testFiber);
32
32
 
33
33
  // Mount and render
34
- manager.renderAndMount(undefined);
34
+ manager.renderAndMount();
35
35
 
36
36
  // Effect should run after commit
37
37
  expect(executionOrder).toEqual(["render", "effect"]);
@@ -49,7 +49,7 @@ describe("useEffect - Basic Functionality", () => {
49
49
  });
50
50
 
51
51
  const manager = new TestResourceManager(testFiber);
52
- manager.renderAndMount(undefined);
52
+ manager.renderAndMount();
53
53
 
54
54
  // Effect should be called, but not cleanup
55
55
  expect(effect).toHaveBeenCalledTimes(1);
@@ -80,7 +80,7 @@ describe("useEffect - Basic Functionality", () => {
80
80
  });
81
81
 
82
82
  const manager = new TestResourceManager(testFiber);
83
- manager.renderAndMount(undefined);
83
+ manager.renderAndMount();
84
84
  manager.cleanup();
85
85
 
86
86
  // Cleanup should run in reverse order (LIFO)
@@ -111,7 +111,7 @@ describe("useEffect - Basic Functionality", () => {
111
111
  return null;
112
112
  });
113
113
 
114
- renderTest(testFiber, undefined);
114
+ renderTest(testFiber);
115
115
  expect(executionOrder).toEqual(["effect1", "effect2", "effect3"]);
116
116
  });
117
117
 
@@ -173,7 +173,7 @@ describe("useEffect - Basic Functionality", () => {
173
173
  });
174
174
 
175
175
  // Initial render
176
- renderTest(testFiber, undefined);
176
+ renderTest(testFiber);
177
177
  expect(effect).toHaveBeenCalledTimes(1);
178
178
 
179
179
  // Trigger re-render
@@ -234,7 +234,7 @@ describe("useEffect - Basic Functionality", () => {
234
234
  const manager = new TestResourceManager(testFiber);
235
235
 
236
236
  // Initial render
237
- manager.renderAndMount(undefined);
237
+ manager.renderAndMount();
238
238
  // Without mount tracking, the effect runs immediately during commit
239
239
  // This triggers setState which causes a synchronous re-render
240
240
  expect(events).toEqual([
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, afterEach } from "vitest";
2
- import { useReducer } from "../../hooks/useReducer";
3
- import { useEffect } from "../../hooks/useEffect";
2
+ import { useReducer } from "../../react-hooks/useReducer";
3
+ import { useEffect } from "../../react-hooks/useEffect";
4
4
  import {
5
5
  createTestResource,
6
6
  renderTest,
@@ -23,7 +23,7 @@ describe("useReducer - Basic Functionality", () => {
23
23
  return count;
24
24
  });
25
25
 
26
- const result = renderTest(testFiber, undefined);
26
+ const result = renderTest(testFiber);
27
27
  expect(result).toBe(0);
28
28
  });
29
29
 
@@ -39,12 +39,12 @@ describe("useReducer - Basic Functionality", () => {
39
39
  return count;
40
40
  });
41
41
 
42
- const result = renderTest(testFiber, undefined);
42
+ const result = renderTest(testFiber);
43
43
  expect(result).toBe(20);
44
44
  expect(initCalled).toBe(1);
45
45
 
46
46
  // Re-render should not call init again
47
- renderTest(testFiber, undefined);
47
+ renderTest(testFiber);
48
48
  expect(initCalled).toBe(1);
49
49
  });
50
50
  });
@@ -73,7 +73,7 @@ describe("useReducer - Basic Functionality", () => {
73
73
  return count;
74
74
  });
75
75
 
76
- renderTest(testFiber, undefined);
76
+ renderTest(testFiber);
77
77
  expect(getCommittedOutput(testFiber)).toBe(0);
78
78
 
79
79
  dispatchFn!({ type: "increment" });
@@ -91,7 +91,9 @@ describe("useReducer - Basic Functionality", () => {
91
91
  });
92
92
 
93
93
  describe("Same-state bailout", () => {
94
- it("should not re-render when reducer returns same state (Object.is)", async () => {
94
+ it("re-renders once when the reducer returns the same state, like React", async () => {
95
+ // React computes user reducers during render (no eager dispatch-time
96
+ // bailout), so a same-state dispatch still renders once.
95
97
  let renderCount = 0;
96
98
  const reducer = (state: number, action: number) =>
97
99
  action === 0 ? state : state + action;
@@ -109,13 +111,13 @@ describe("useReducer - Basic Functionality", () => {
109
111
  return count;
110
112
  });
111
113
 
112
- renderTest(testFiber, undefined);
114
+ renderTest(testFiber);
113
115
  expect(renderCount).toBe(1);
114
116
 
115
117
  // Dispatch action that returns same state
116
118
  dispatchFn!(0);
117
119
  await waitForNextTick();
118
- expect(renderCount).toBe(1);
120
+ expect(renderCount).toBe(2);
119
121
  });
120
122
  });
121
123
 
@@ -136,7 +138,7 @@ describe("useReducer - Basic Functionality", () => {
136
138
  return count;
137
139
  });
138
140
 
139
- renderTest(testFiber, undefined);
141
+ renderTest(testFiber);
140
142
  expect(getCommittedOutput(testFiber)).toBe(0);
141
143
 
142
144
  // Dispatch with multiplier=1
@@ -146,7 +148,7 @@ describe("useReducer - Basic Functionality", () => {
146
148
 
147
149
  // Change multiplier and dispatch
148
150
  multiplier = 10;
149
- renderTest(testFiber, undefined); // re-render to update reducer
151
+ renderTest(testFiber); // re-render to update reducer
150
152
  dispatchFn!(5);
151
153
  await waitForNextTick();
152
154
  expect(getCommittedOutput(testFiber)).toBe(55); // 5 + 5*10
@@ -168,7 +170,7 @@ describe("useReducer - Basic Functionality", () => {
168
170
  return count;
169
171
  });
170
172
 
171
- renderTest(testFiber, undefined);
173
+ renderTest(testFiber);
172
174
  expect(getCommittedOutput(testFiber)).toBe(0);
173
175
 
174
176
  // Multiple dispatches
@@ -191,8 +193,8 @@ describe("useReducer - Basic Functionality", () => {
191
193
  return count;
192
194
  });
193
195
 
194
- renderTest(testFiber, undefined);
195
- renderTest(testFiber, undefined);
196
+ renderTest(testFiber);
197
+ renderTest(testFiber);
196
198
 
197
199
  expect(dispatches[0]).toBe(dispatches[1]);
198
200
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, afterEach } from "vitest";
2
2
  import { useResources } from "../../hooks/useResources";
3
- import { useState } from "../../hooks/useState";
3
+ import { useState } from "../../react-hooks/useState";
4
4
  import { resource } from "../../core/resource";
5
5
  import { withKey } from "../../core/withKey";
6
6
  import {
@@ -13,25 +13,24 @@ import {
13
13
  const SimpleCounter = resource(createCounterResource());
14
14
 
15
15
  // Stateful counter that tracks its own count
16
- const StatefulCounter = resource(function StatefulCounter(props: {
17
- initial: number;
18
- }) {
16
+ const useStatefulCounter = (props: { initial: number }) => {
19
17
  const [count] = useState(props.initial);
20
18
  return { count };
21
- });
19
+ };
20
+
21
+ const StatefulCounter = resource(useStatefulCounter);
22
22
 
23
23
  // Display component for testing type changes
24
- const Display = resource(function Display(props: { text: string }) {
24
+ const useDisplay = (props: { text: string }) => {
25
25
  return { type: "display", text: props.text };
26
- });
26
+ };
27
+
28
+ const Display = resource(useDisplay);
27
29
 
28
30
  // Counter with render tracking for testing instance preservation
29
31
  const renderCounts = new Map<string, number>();
30
32
  const instances = new Map<string, object>();
31
- const TrackingCounter = resource(function TrackingCounter(props: {
32
- value: number;
33
- id: string;
34
- }) {
33
+ const useTrackingCounter = (props: { value: number; id: string }) => {
35
34
  const currentCount = (renderCounts.get(props.id) || 0) + 1;
36
35
  renderCounts.set(props.id, currentCount);
37
36
 
@@ -45,7 +44,9 @@ const TrackingCounter = resource(function TrackingCounter(props: {
45
44
  renderCount: currentCount,
46
45
  instance: instances.get(props.id),
47
46
  };
48
- });
47
+ };
48
+
49
+ const TrackingCounter = resource(useTrackingCounter);
49
50
 
50
51
  describe("useResources - Basic Functionality", () => {
51
52
  afterEach(() => {
@@ -67,15 +68,17 @@ describe("useResources - Basic Functionality", () => {
67
68
  return results;
68
69
  });
69
70
 
70
- const result = renderTest(testFiber, undefined);
71
+ const result = renderTest(testFiber);
71
72
  expect(result).toEqual([{ count: 10 }, { count: 20 }, { count: 30 }]);
72
73
  });
73
74
 
74
75
  it("should work with resource constructor syntax", () => {
75
- const Counter = resource(function Counter(props: { value: number }) {
76
+ const useCounter = (props: { value: number }) => {
76
77
  const [count] = useState(props.value);
77
78
  return { count, double: count * 2 };
78
- });
79
+ };
80
+
81
+ const Counter = resource(useCounter);
79
82
 
80
83
  const items = [
81
84
  { key: "first", value: 5 },
@@ -95,7 +98,7 @@ describe("useResources - Basic Functionality", () => {
95
98
  return results;
96
99
  });
97
100
 
98
- const result = renderTest(testFiber, undefined);
101
+ const result = renderTest(testFiber);
99
102
  expect(result).toEqual([
100
103
  { count: 5, double: 10 },
101
104
  { count: 10, double: 20 },
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, afterEach } from "vitest";
2
- import { useState } from "../../hooks/useState";
3
- import { useEffect } from "../../hooks/useEffect";
2
+ import { useState } from "../../react-hooks/useState";
3
+ import { useEffect } from "../../react-hooks/useEffect";
4
4
  import {
5
5
  createTestResource,
6
6
  renderTest,
@@ -21,7 +21,7 @@ describe("useState - Basic Functionality", () => {
21
21
  return count;
22
22
  });
23
23
 
24
- const result = renderTest(testFiber, undefined);
24
+ const result = renderTest(testFiber);
25
25
  expect(result).toBe(42);
26
26
  });
27
27
 
@@ -37,12 +37,12 @@ describe("useState - Basic Functionality", () => {
37
37
  });
38
38
 
39
39
  // First render
40
- const result = renderTest(testFiber, undefined);
40
+ const result = renderTest(testFiber);
41
41
  expect(result).toBe(100);
42
42
  expect(initCalled).toBe(1);
43
43
 
44
44
  // Re-render should not call initializer again
45
- renderTest(testFiber, undefined);
45
+ renderTest(testFiber);
46
46
  expect(initCalled).toBe(1);
47
47
  });
48
48
 
@@ -52,7 +52,7 @@ describe("useState - Basic Functionality", () => {
52
52
  return value;
53
53
  });
54
54
 
55
- const result = renderTest(testFiber, undefined);
55
+ const result = renderTest(testFiber);
56
56
  expect(result).toBeUndefined();
57
57
  });
58
58
  });
@@ -75,7 +75,7 @@ describe("useState - Basic Functionality", () => {
75
75
  });
76
76
 
77
77
  // Initial render
78
- const result1 = renderTest(testFiber, undefined);
78
+ const result1 = renderTest(testFiber);
79
79
  expect(result1).toEqual({ count: 0, renderCount: 1 });
80
80
 
81
81
  // Update state
@@ -107,7 +107,7 @@ describe("useState - Basic Functionality", () => {
107
107
  });
108
108
 
109
109
  // Initial render
110
- renderTest(testFiber, undefined);
110
+ renderTest(testFiber);
111
111
  expect(renderCount).toBe(1);
112
112
 
113
113
  // Set same value
@@ -135,7 +135,7 @@ describe("useState - Basic Functionality", () => {
135
135
  });
136
136
 
137
137
  // Initial render
138
- renderTest(testFiber, undefined);
138
+ renderTest(testFiber);
139
139
  expect(getCommittedOutput(testFiber)).toBe(10);
140
140
 
141
141
  // Functional update
@@ -167,7 +167,7 @@ describe("useState - Basic Functionality", () => {
167
167
  };
168
168
  });
169
169
 
170
- const result = renderTest(testFiber, undefined);
170
+ const result = renderTest(testFiber);
171
171
  expect(result).toMatchObject({
172
172
  count1: 1,
173
173
  count2: 2,
@@ -191,7 +191,7 @@ describe("useState - Basic Functionality", () => {
191
191
  });
192
192
 
193
193
  // Initial render
194
- renderTest(testFiber, undefined);
194
+ renderTest(testFiber);
195
195
  expect(getCommittedOutput(testFiber)).toEqual({ a: "a", b: "b", c: "c" });
196
196
 
197
197
  // Update only B
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Host-type benchmark: mount+unmount and update cost of a body with N
3
+ * hooks under each host. Runs against the BUILT package (dist), which the
4
+ * vitest config externalizes so tap and React both run as plain Node
5
+ * modules, outside vitest's module evaluator. Build first, then:
6
+ *
7
+ * pnpm build
8
+ * pnpm exec vitest bench --run --project prod src/__tests__/bench/hosts.bench.tsx
9
+ *
10
+ * Not part of the test suite; vitest only picks this up in bench mode.
11
+ */
12
+ /* oxlint-disable react/rules-of-hooks -- fixed-count hook loop, benchmark only */
13
+ import { bench, describe } from "vitest";
14
+ import { createElement, useState } from "react";
15
+ import { createRoot } from "react-dom/client";
16
+ import { flushSync } from "react-dom";
17
+ import {
18
+ createTapRoot,
19
+ flushTapSync,
20
+ resource,
21
+ useResource,
22
+ useTapRoot,
23
+ } from "@assistant-ui/tap";
24
+
25
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false;
26
+
27
+ const N = 10_000;
28
+
29
+ type Api = { sum: number; bump: () => void };
30
+
31
+ const useManyHooks = (): Api => {
32
+ let sum = 0;
33
+ let lastSet!: (updater: (v: number) => number) => void;
34
+ for (let i = 0; i < N; i++) {
35
+ const [v, setV] = useState(0);
36
+ sum += v;
37
+ lastSet = setV;
38
+ }
39
+ return { sum, bump: () => lastSet((v) => v + 1) };
40
+ };
41
+
42
+ type Host = {
43
+ api: () => Api;
44
+ flush: (fn: () => void) => void;
45
+ unmount: () => void;
46
+ };
47
+
48
+ const mountReactHost = (useProbe: () => () => Api): Host => {
49
+ let api!: () => Api;
50
+ function Probe() {
51
+ api = useProbe();
52
+ return null;
53
+ }
54
+ const root = createRoot(document.createElement("div"));
55
+ flushSync(() => root.render(createElement(Probe)));
56
+ return {
57
+ api: () => api(),
58
+ flush: (fn) => flushSync(fn),
59
+ unmount: () => flushSync(() => root.unmount()),
60
+ };
61
+ };
62
+
63
+ const ManyHooks = resource(useManyHooks);
64
+
65
+ const HOSTS: Record<string, () => Host> = {
66
+ react: () =>
67
+ mountReactHost(() => {
68
+ const value = useManyHooks();
69
+ return () => value;
70
+ }),
71
+ bridge: () =>
72
+ mountReactHost(() => {
73
+ const value = useResource(ManyHooks());
74
+ return () => value;
75
+ }),
76
+ tapRoot: () => {
77
+ const host = mountReactHost(() => {
78
+ const root = useTapRoot(function Root() {
79
+ return useManyHooks();
80
+ });
81
+ return () => root.getValue();
82
+ });
83
+ return { ...host, flush: (fn) => flushTapSync(fn) };
84
+ },
85
+ createTapRoot: () => {
86
+ const root = createTapRoot(function Root() {
87
+ return useManyHooks();
88
+ });
89
+ return {
90
+ api: () => root.getValue(),
91
+ flush: (fn) => flushTapSync(fn),
92
+ unmount: () => root.unmount(),
93
+ };
94
+ },
95
+ };
96
+
97
+ describe(`mount+unmount, ${N} useState hooks`, () => {
98
+ for (const [name, make] of Object.entries(HOSTS)) {
99
+ bench(name, () => {
100
+ const host = make();
101
+ host.unmount();
102
+ });
103
+ }
104
+ });
105
+
106
+ describe(`update (one dispatch, full re-render), ${N} useState hooks`, () => {
107
+ for (const [name, make] of Object.entries(HOSTS)) {
108
+ let host: Host;
109
+ bench(
110
+ name,
111
+ () => {
112
+ host.flush(() => host.api().bump());
113
+ },
114
+ {
115
+ setup: () => {
116
+ host = make();
117
+ },
118
+ teardown: () => {
119
+ host.unmount();
120
+ },
121
+ },
122
+ );
123
+ }
124
+ });