@ic-reactor/mcp 0.0.0-stage → 0.0.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 (1041) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
  4. package/.github/actions/setup-rust-wasm/action.yml +25 -0
  5. package/.github/dependabot.yml +107 -0
  6. package/.github/workflows/auth-latest.yml +67 -0
  7. package/.github/workflows/b3pay-site.yml +49 -0
  8. package/.github/workflows/ci.yml +430 -0
  9. package/.github/workflows/codeql.yml +41 -0
  10. package/.github/workflows/dependency-review.yml +68 -0
  11. package/.github/workflows/docs-check.yml +98 -0
  12. package/.github/workflows/docs.yml +265 -0
  13. package/.github/workflows/e2e.yml +90 -0
  14. package/.github/workflows/examples-published.yml +97 -0
  15. package/.github/workflows/release.yml +326 -0
  16. package/.husky/pre-commit +12 -0
  17. package/.prettierignore +109 -0
  18. package/.prettierrc +6 -0
  19. package/AGENTS.md +103 -0
  20. package/CHANGELOG.md +3268 -0
  21. package/CLAUDE.md +93 -0
  22. package/CNAME +1 -0
  23. package/CODE_OF_CONDUCT.md +7 -0
  24. package/CONTRIBUTING.md +433 -0
  25. package/LICENSE +21 -0
  26. package/README.md +2 -2
  27. package/SECURITY.md +74 -0
  28. package/b3pay-module.json +194 -0
  29. package/docs/.remarkrc.json +10 -0
  30. package/docs/README.md +64 -0
  31. package/docs/astro.config.mjs +205 -0
  32. package/docs/favicon.svg +15 -0
  33. package/docs/package.json +39 -0
  34. package/docs/public/favicon.svg +15 -0
  35. package/docs/public/ic-reactor-social-card.png +0 -0
  36. package/docs/scripts/b3pay-static.mjs +22 -0
  37. package/docs/scripts/check-example-docs.mjs +214 -0
  38. package/docs/scripts/check-links.mjs +212 -0
  39. package/docs/scripts/fix-dist-links.js +125 -0
  40. package/docs/scripts/fix-frontmatter.js +71 -0
  41. package/docs/scripts/remove-redundant-h1.cjs +48 -0
  42. package/docs/scripts/rewrite-typedoc-links.js +110 -0
  43. package/docs/scripts/split-index.js +69 -0
  44. package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
  45. package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
  46. package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
  47. package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
  48. package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
  49. package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
  50. package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
  51. package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
  52. package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
  53. package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
  54. package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
  55. package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
  56. package/docs/src/components/LibraryCards.astro +172 -0
  57. package/docs/src/components/LibraryIcon.astro +28 -0
  58. package/docs/src/components/SiteTitle.astro +241 -0
  59. package/docs/src/content/docs/404.mdx +19 -0
  60. package/docs/src/content/docs/build/agent-tools.mdx +593 -0
  61. package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
  62. package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
  63. package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
  64. package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
  65. package/docs/src/content/docs/examples/index.mdx +38 -0
  66. package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
  67. package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
  68. package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
  69. package/docs/src/content/docs/guides/auth.mdx +1181 -0
  70. package/docs/src/content/docs/guides/client.mdx +551 -0
  71. package/docs/src/content/docs/guides/deploy.mdx +180 -0
  72. package/docs/src/content/docs/guides/errors.mdx +475 -0
  73. package/docs/src/content/docs/guides/getting-started.mdx +620 -0
  74. package/docs/src/content/docs/guides/reads.mdx +586 -0
  75. package/docs/src/content/docs/guides/ssr.mdx +529 -0
  76. package/docs/src/content/docs/guides/testing.mdx +401 -0
  77. package/docs/src/content/docs/guides/values.mdx +261 -0
  78. package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
  79. package/docs/src/content/docs/guides/writes.mdx +572 -0
  80. package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
  81. package/docs/src/content/docs/index.mdx +65 -0
  82. package/docs/src/content/docs/migrating-from-3.mdx +535 -0
  83. package/docs/src/content/docs/packages/core.mdx +186 -0
  84. package/docs/src/content/docs/packages/mcp.mdx +163 -0
  85. package/docs/src/content/docs/packages/react.mdx +207 -0
  86. package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
  87. package/docs/src/content/docs/versioning.mdx +104 -0
  88. package/docs/src/content.config.ts +7 -0
  89. package/docs/src/styles/custom.css +189 -0
  90. package/docs/tsconfig.typedoc.json +26 -0
  91. package/docs/typedoc-entry.ts +17 -0
  92. package/docs/typedoc.json +37 -0
  93. package/e2e/Cargo.lock +646 -0
  94. package/e2e/Cargo.toml +3 -0
  95. package/e2e/README.md +101 -0
  96. package/e2e/frontends/build.mjs +94 -0
  97. package/e2e/frontends/index.html +10 -0
  98. package/e2e/global-setup.ts +65 -0
  99. package/e2e/icp.yaml +38 -0
  100. package/e2e/package.json +40 -0
  101. package/e2e/setup.ts +6 -0
  102. package/e2e/src/actor/Cargo.toml +14 -0
  103. package/e2e/src/actor/hello_actor.did +10 -0
  104. package/e2e/src/actor/src/lib.rs +73 -0
  105. package/e2e/src/calls.test.ts +132 -0
  106. package/e2e/src/declarations/hello_actor.envelope.json +175 -0
  107. package/e2e/src/declarations/hello_actor.ts +15 -0
  108. package/e2e/src/errors.test.ts +107 -0
  109. package/e2e/src/frontend-headers.test.ts +253 -0
  110. package/e2e/src/identity.test.ts +61 -0
  111. package/e2e/src/react.test.tsx +59 -0
  112. package/e2e/src/replica.ts +70 -0
  113. package/e2e/src/vite-env.test.ts +108 -0
  114. package/e2e/test.sh +47 -0
  115. package/e2e/tsconfig.json +20 -0
  116. package/e2e/tsconfig.typecheck.json +9 -0
  117. package/e2e/vitest.config.ts +22 -0
  118. package/eslint.config.mjs +100 -0
  119. package/evals/PREREGISTRATION.md +1346 -0
  120. package/evals/README.md +756 -0
  121. package/evals/conditions/thin/condition.json +8 -0
  122. package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
  123. package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
  124. package/evals/conditions/thin/package.json +25 -0
  125. package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
  126. package/evals/conditions/thin-guide/condition.json +10 -0
  127. package/evals/conditions/thin-guide/docs/llms.txt +292 -0
  128. package/evals/conditions/v3/condition.json +9 -0
  129. package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
  130. package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
  131. package/evals/conditions/v3/docs/llms-full.txt +1053 -0
  132. package/evals/conditions/v3/package.json +26 -0
  133. package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
  134. package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
  135. package/evals/conditions/v4/PORTING.md +384 -0
  136. package/evals/conditions/v4/README.md +106 -0
  137. package/evals/conditions/v4/condition.json +5 -0
  138. package/evals/conditions/v4/docs/llms.txt +323 -0
  139. package/evals/conditions/v4/package.json +27 -0
  140. package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
  141. package/evals/conditions/v4-proto/condition.json +6 -0
  142. package/evals/conditions/v4-proto/docs/llms.txt +291 -0
  143. package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
  144. package/evals/conditions/v4-proto/lib/package.json +27 -0
  145. package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
  146. package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
  147. package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
  148. package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
  149. package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
  150. package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
  151. package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
  152. package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
  153. package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
  154. package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
  155. package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
  156. package/evals/conditions/v4-proto/package.json +26 -0
  157. package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
  158. package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
  159. package/evals/drive.mjs +1221 -0
  160. package/evals/gate.mjs +161 -0
  161. package/evals/harness/aggregate.mjs +534 -0
  162. package/evals/harness/aggregate.test.mjs +344 -0
  163. package/evals/harness/assemble.mjs +216 -0
  164. package/evals/harness/assemble.test.mjs +143 -0
  165. package/evals/harness/auth.mjs +91 -0
  166. package/evals/harness/check-docs.mjs +157 -0
  167. package/evals/harness/declarations/icrc1.did.d.ts +95 -0
  168. package/evals/harness/declarations/icrc1.did.js +140 -0
  169. package/evals/harness/drive-auth.test.mjs +584 -0
  170. package/evals/harness/drive.test.mjs +509 -0
  171. package/evals/harness/fake-auth.ts +67 -0
  172. package/evals/harness/fake-ledger.ts +178 -0
  173. package/evals/harness/gate-plan.mjs +104 -0
  174. package/evals/harness/gate-plan.test.mjs +243 -0
  175. package/evals/harness/icrc1.did +88 -0
  176. package/evals/harness/judge.mjs +63 -0
  177. package/evals/harness/judge.test.mjs +81 -0
  178. package/evals/harness/leak-scan.mjs +1373 -0
  179. package/evals/harness/leak-scan.test.mjs +1300 -0
  180. package/evals/harness/probes/amounts.ts +14 -0
  181. package/evals/harness/probes/node-tool.probe.ts +48 -0
  182. package/evals/harness/probes/probe.mjs +149 -0
  183. package/evals/harness/probes/react-wallet.probe.ts +96 -0
  184. package/evals/harness/public/support/auth.ts +38 -0
  185. package/evals/harness/public/support/fake-replica.d.ts +28 -0
  186. package/evals/harness/public/support/fake-replica.js +434 -0
  187. package/evals/harness/public/support/ledger.idl.d.ts +95 -0
  188. package/evals/harness/public/support/ledger.idl.js +140 -0
  189. package/evals/harness/public/support/world.ts +179 -0
  190. package/evals/harness/runs.mjs +139 -0
  191. package/evals/harness/sandbox.mjs +88 -0
  192. package/evals/harness/sandbox.test.mjs +134 -0
  193. package/evals/harness/ship.mjs +264 -0
  194. package/evals/harness/ship.test.mjs +552 -0
  195. package/evals/harness/world.ts +291 -0
  196. package/evals/package.json +26 -0
  197. package/evals/pnpm-lock.yaml +1537 -0
  198. package/evals/pnpm-workspace.yaml +12 -0
  199. package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
  200. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
  201. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
  202. package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
  203. package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
  204. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
  205. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
  206. package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
  207. package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
  208. package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
  209. package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
  210. package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
  211. package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
  212. package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
  213. package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
  214. package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
  215. package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
  216. package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
  217. package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
  218. package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
  219. package/evals/results/README.md +31 -0
  220. package/evals/runs-pilot.log +150 -0
  221. package/evals/runs-pilot2.log +150 -0
  222. package/evals/score.mjs +256 -0
  223. package/evals/setup.mjs +547 -0
  224. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
  225. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
  226. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
  227. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
  228. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
  229. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
  230. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
  231. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
  232. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
  233. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
  234. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
  235. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
  236. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
  237. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
  238. package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
  239. package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
  240. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  241. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
  242. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
  243. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
  244. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
  245. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
  246. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
  247. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
  248. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
  249. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
  250. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
  251. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
  252. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
  253. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
  254. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
  255. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
  256. package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
  257. package/evals/tasks/node-tool/prompt.md +55 -0
  258. package/evals/tasks/node-tool/prompt.minimal.md +30 -0
  259. package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
  260. package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
  261. package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
  262. package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
  263. package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
  264. package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
  265. package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
  266. package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
  267. package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
  268. package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
  269. package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
  270. package/evals/tasks/node-tool/starter/src/index.ts +7 -0
  271. package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
  272. package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
  273. package/evals/tasks/node-tool/task.json +63 -0
  274. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
  275. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
  276. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
  277. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
  278. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
  279. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
  280. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
  281. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
  282. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
  283. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
  284. package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
  285. package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
  286. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
  287. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
  288. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  289. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
  290. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
  291. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
  292. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
  293. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
  294. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
  295. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
  296. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
  297. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
  298. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
  299. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
  300. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
  301. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
  302. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
  303. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
  304. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
  305. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
  306. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
  307. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
  308. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
  309. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
  310. package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
  311. package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
  312. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
  313. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
  314. package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
  315. package/evals/tasks/react-wallet/prompt.md +88 -0
  316. package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
  317. package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
  318. package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
  319. package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
  320. package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
  321. package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
  322. package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
  323. package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
  324. package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
  325. package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
  326. package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
  327. package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
  328. package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
  329. package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
  330. package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
  331. package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
  332. package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
  333. package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
  334. package/evals/tasks/react-wallet/task.json +84 -0
  335. package/examples/canister-studio/.stackblitzrc +4 -0
  336. package/examples/canister-studio/README.md +183 -0
  337. package/examples/canister-studio/did/ckbtc_minter.did +806 -0
  338. package/examples/canister-studio/did/icp_ledger.did +586 -0
  339. package/examples/canister-studio/did/icrc1.did +88 -0
  340. package/examples/canister-studio/package.json +45 -0
  341. package/examples/canister-studio/scripts/smoke.ts +188 -0
  342. package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
  343. package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
  344. package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
  345. package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
  346. package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
  347. package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
  348. package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
  349. package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
  350. package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
  351. package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
  352. package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
  353. package/examples/canister-studio/src/calls/dedupe.ts +189 -0
  354. package/examples/canister-studio/src/calls/error-info.ts +168 -0
  355. package/examples/canister-studio/src/calls/history.test.ts +284 -0
  356. package/examples/canister-studio/src/calls/history.ts +287 -0
  357. package/examples/canister-studio/src/calls/kinds.ts +68 -0
  358. package/examples/canister-studio/src/calls/prepare.ts +32 -0
  359. package/examples/canister-studio/src/calls/query-options.ts +30 -0
  360. package/examples/canister-studio/src/calls/read.ts +47 -0
  361. package/examples/canister-studio/src/calls/readback.ts +100 -0
  362. package/examples/canister-studio/src/calls/record.ts +123 -0
  363. package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
  364. package/examples/canister-studio/src/calls/share.ts +46 -0
  365. package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
  366. package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
  367. package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
  368. package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
  369. package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
  370. package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
  371. package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
  372. package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
  373. package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
  374. package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
  375. package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
  376. package/examples/canister-studio/src/components/AppShell.tsx +29 -0
  377. package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
  378. package/examples/canister-studio/src/components/Badge.tsx +79 -0
  379. package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
  380. package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
  381. package/examples/canister-studio/src/components/Card.tsx +64 -0
  382. package/examples/canister-studio/src/components/Code.tsx +101 -0
  383. package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
  384. package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
  385. package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
  386. package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
  387. package/examples/canister-studio/src/components/MethodList.tsx +244 -0
  388. package/examples/canister-studio/src/components/Notice.tsx +36 -0
  389. package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
  390. package/examples/canister-studio/src/components/RouteError.tsx +42 -0
  391. package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
  392. package/examples/canister-studio/src/components/Spinner.tsx +33 -0
  393. package/examples/canister-studio/src/components/Tabs.tsx +96 -0
  394. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
  395. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
  396. package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
  397. package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
  398. package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
  399. package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
  400. package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
  401. package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
  402. package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
  403. package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
  404. package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
  405. package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
  406. package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
  407. package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
  408. package/examples/canister-studio/src/components/canister-names.ts +11 -0
  409. package/examples/canister-studio/src/components/download.ts +34 -0
  410. package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
  411. package/examples/canister-studio/src/components/helpers.test.ts +107 -0
  412. package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
  413. package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
  414. package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
  415. package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
  416. package/examples/canister-studio/src/components/home/featured.ts +64 -0
  417. package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
  418. package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
  419. package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
  420. package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
  421. package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
  422. package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
  423. package/examples/canister-studio/src/components/modes.ts +31 -0
  424. package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
  425. package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
  426. package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
  427. package/examples/canister-studio/src/components/playground/sample.ts +43 -0
  428. package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
  429. package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
  430. package/examples/canister-studio/src/components/provenance.ts +38 -0
  431. package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
  432. package/examples/canister-studio/src/components/use-task.ts +56 -0
  433. package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
  434. package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
  435. package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
  436. package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
  437. package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
  438. package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
  439. package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
  440. package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
  441. package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
  442. package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
  443. package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
  444. package/examples/canister-studio/src/forms/context.ts +62 -0
  445. package/examples/canister-studio/src/forms/convert.ts +131 -0
  446. package/examples/canister-studio/src/forms/edge.test.ts +252 -0
  447. package/examples/canister-studio/src/forms/engine.test.ts +358 -0
  448. package/examples/canister-studio/src/forms/engine.ts +1492 -0
  449. package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
  450. package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
  451. package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
  452. package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
  453. package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
  454. package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
  455. package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
  456. package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
  457. package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
  458. package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
  459. package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
  460. package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
  461. package/examples/canister-studio/src/forms/hints.ts +128 -0
  462. package/examples/canister-studio/src/forms/json.ts +119 -0
  463. package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
  464. package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
  465. package/examples/canister-studio/src/interface/browser.test.ts +23 -0
  466. package/examples/canister-studio/src/interface/browser.ts +153 -0
  467. package/examples/canister-studio/src/interface/cache.ts +68 -0
  468. package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
  469. package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
  470. package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
  471. package/examples/canister-studio/src/interface/compile.server.ts +262 -0
  472. package/examples/canister-studio/src/interface/compile.test.ts +108 -0
  473. package/examples/canister-studio/src/interface/compile.ts +305 -0
  474. package/examples/canister-studio/src/interface/docs.test.ts +75 -0
  475. package/examples/canister-studio/src/interface/docs.ts +100 -0
  476. package/examples/canister-studio/src/interface/functions.test.ts +42 -0
  477. package/examples/canister-studio/src/interface/functions.ts +200 -0
  478. package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
  479. package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
  480. package/examples/canister-studio/src/interface/live-loader.ts +364 -0
  481. package/examples/canister-studio/src/interface/load.test.ts +328 -0
  482. package/examples/canister-studio/src/interface/load.ts +249 -0
  483. package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
  484. package/examples/canister-studio/src/interface/read-candid.ts +233 -0
  485. package/examples/canister-studio/src/interface/server-result.ts +42 -0
  486. package/examples/canister-studio/src/interface/typed.ts +68 -0
  487. package/examples/canister-studio/src/interface/types.ts +121 -0
  488. package/examples/canister-studio/src/lib/format.test.ts +107 -0
  489. package/examples/canister-studio/src/lib/format.ts +139 -0
  490. package/examples/canister-studio/src/lib/hex.test.ts +43 -0
  491. package/examples/canister-studio/src/lib/hex.ts +33 -0
  492. package/examples/canister-studio/src/lib/ids.test.ts +70 -0
  493. package/examples/canister-studio/src/lib/ids.ts +93 -0
  494. package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
  495. package/examples/canister-studio/src/lib/reactor.ts +55 -0
  496. package/examples/canister-studio/src/lib/registry.test.ts +74 -0
  497. package/examples/canister-studio/src/lib/registry.ts +238 -0
  498. package/examples/canister-studio/src/lib/schema.ts +148 -0
  499. package/examples/canister-studio/src/routeTree.gen.ts +227 -0
  500. package/examples/canister-studio/src/router.test.ts +290 -0
  501. package/examples/canister-studio/src/router.tsx +127 -0
  502. package/examples/canister-studio/src/routes/__root.tsx +58 -0
  503. package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
  504. package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
  505. package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
  506. package/examples/canister-studio/src/routes/history.tsx +55 -0
  507. package/examples/canister-studio/src/routes/index.tsx +169 -0
  508. package/examples/canister-studio/src/routes/playground.tsx +286 -0
  509. package/examples/canister-studio/src/routes/safety.tsx +57 -0
  510. package/examples/canister-studio/src/routes/wallet.tsx +213 -0
  511. package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
  512. package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
  513. package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
  514. package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
  515. package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
  516. package/examples/canister-studio/src/safety/lab-run.ts +283 -0
  517. package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
  518. package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
  519. package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
  520. package/examples/canister-studio/src/safety/scenarios.ts +924 -0
  521. package/examples/canister-studio/src/styles.css +3453 -0
  522. package/examples/canister-studio/src/styles.test.ts +46 -0
  523. package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
  524. package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
  525. package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
  526. package/examples/canister-studio/src/values/Inspector.tsx +133 -0
  527. package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
  528. package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
  529. package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
  530. package/examples/canister-studio/src/values/candid-text.ts +479 -0
  531. package/examples/canister-studio/src/values/hints.ts +58 -0
  532. package/examples/canister-studio/src/values/json.test.ts +66 -0
  533. package/examples/canister-studio/src/values/json.ts +31 -0
  534. package/examples/canister-studio/src/values/leaves.tsx +309 -0
  535. package/examples/canister-studio/src/values/schema.test.ts +123 -0
  536. package/examples/canister-studio/src/values/schema.ts +78 -0
  537. package/examples/canister-studio/src/values/test-router.tsx +21 -0
  538. package/examples/canister-studio/src/values/tree.tsx +670 -0
  539. package/examples/canister-studio/src/values/use-sha256.ts +29 -0
  540. package/examples/canister-studio/tsconfig.json +20 -0
  541. package/examples/canister-studio/vite.config.ts +50 -0
  542. package/examples/canister-studio/vitest.config.ts +20 -0
  543. package/examples/icrc-ledger/.stackblitzrc +4 -0
  544. package/examples/icrc-ledger/README.md +100 -0
  545. package/examples/icrc-ledger/ckbtc_minter.did +12 -0
  546. package/examples/icrc-ledger/icp_ledger.did +147 -0
  547. package/examples/icrc-ledger/icrc1.did +88 -0
  548. package/examples/icrc-ledger/index.html +13 -0
  549. package/examples/icrc-ledger/package.json +40 -0
  550. package/examples/icrc-ledger/scripts/smoke.ts +151 -0
  551. package/examples/icrc-ledger/src/App.tsx +71 -0
  552. package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
  553. package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
  554. package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
  555. package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
  556. package/examples/icrc-ledger/src/blocks.test.ts +247 -0
  557. package/examples/icrc-ledger/src/blocks.ts +140 -0
  558. package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
  559. package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
  560. package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
  561. package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
  562. package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
  563. package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
  564. package/examples/icrc-ledger/src/format.test.ts +41 -0
  565. package/examples/icrc-ledger/src/format.ts +68 -0
  566. package/examples/icrc-ledger/src/ledger.ts +15 -0
  567. package/examples/icrc-ledger/src/main.tsx +10 -0
  568. package/examples/icrc-ledger/src/minter.ts +16 -0
  569. package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
  570. package/examples/icrc-ledger/src/sandbox.ts +281 -0
  571. package/examples/icrc-ledger/src/styles.css +391 -0
  572. package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
  573. package/examples/icrc-ledger/src/transfer-form.ts +97 -0
  574. package/examples/icrc-ledger/tsconfig.json +19 -0
  575. package/examples/icrc-ledger/vite.config.ts +44 -0
  576. package/examples/icrc-ledger/vitest.config.ts +12 -0
  577. package/examples/next-ssr/.stackblitzrc +4 -0
  578. package/examples/next-ssr/README.md +112 -0
  579. package/examples/next-ssr/icrc1.did +88 -0
  580. package/examples/next-ssr/next.config.ts +10 -0
  581. package/examples/next-ssr/package.json +41 -0
  582. package/examples/next-ssr/scripts/smoke.ts +169 -0
  583. package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
  584. package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
  585. package/examples/next-ssr/src/app/account/page.tsx +82 -0
  586. package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
  587. package/examples/next-ssr/src/app/globals.css +278 -0
  588. package/examples/next-ssr/src/app/layout.tsx +34 -0
  589. package/examples/next-ssr/src/app/page.tsx +46 -0
  590. package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
  591. package/examples/next-ssr/src/app/providers.test.tsx +73 -0
  592. package/examples/next-ssr/src/app/providers.tsx +60 -0
  593. package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
  594. package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
  595. package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
  596. package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
  597. package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
  598. package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
  599. package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
  600. package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
  601. package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
  602. package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
  603. package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
  604. package/examples/next-ssr/src/components/server-data.ts +10 -0
  605. package/examples/next-ssr/src/format.ts +46 -0
  606. package/examples/next-ssr/src/ledgers.ts +38 -0
  607. package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
  608. package/examples/next-ssr/src/server/balance-route.ts +157 -0
  609. package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
  610. package/examples/next-ssr/src/server/error-summary.ts +37 -0
  611. package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
  612. package/examples/next-ssr/src/server/parse-owner.ts +34 -0
  613. package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
  614. package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
  615. package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
  616. package/examples/next-ssr/src/server/read-balances.ts +78 -0
  617. package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
  618. package/examples/next-ssr/src/server/request-client.ts +24 -0
  619. package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
  620. package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
  621. package/examples/next-ssr/src/testing/setup.ts +10 -0
  622. package/examples/next-ssr/tsconfig.json +31 -0
  623. package/examples/next-ssr/vitest.config.ts +59 -0
  624. package/examples/node-agent-tool/.stackblitzrc +4 -0
  625. package/examples/node-agent-tool/README.md +165 -0
  626. package/examples/node-agent-tool/icp.yaml +11 -0
  627. package/examples/node-agent-tool/icrc1.did +88 -0
  628. package/examples/node-agent-tool/package.json +32 -0
  629. package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
  630. package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
  631. package/examples/node-agent-tool/src/cli.test.ts +64 -0
  632. package/examples/node-agent-tool/src/cli.ts +23 -0
  633. package/examples/node-agent-tool/src/command-line.ts +159 -0
  634. package/examples/node-agent-tool/src/commands/balance.ts +73 -0
  635. package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
  636. package/examples/node-agent-tool/src/commands/demo.ts +359 -0
  637. package/examples/node-agent-tool/src/commands/info.ts +69 -0
  638. package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
  639. package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
  640. package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
  641. package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
  642. package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
  643. package/examples/node-agent-tool/src/commands/watch.ts +135 -0
  644. package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
  645. package/examples/node-agent-tool/src/context.ts +20 -0
  646. package/examples/node-agent-tool/src/dispatch.ts +70 -0
  647. package/examples/node-agent-tool/src/failure.test.ts +59 -0
  648. package/examples/node-agent-tool/src/failure.ts +176 -0
  649. package/examples/node-agent-tool/src/identity.test.ts +242 -0
  650. package/examples/node-agent-tool/src/identity.ts +228 -0
  651. package/examples/node-agent-tool/src/input.ts +110 -0
  652. package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
  653. package/examples/node-agent-tool/src/ledgers.ts +62 -0
  654. package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
  655. package/examples/node-agent-tool/src/network.test.ts +128 -0
  656. package/examples/node-agent-tool/src/network.ts +101 -0
  657. package/examples/node-agent-tool/src/output.test.ts +208 -0
  658. package/examples/node-agent-tool/src/output.ts +163 -0
  659. package/examples/node-agent-tool/src/run.ts +73 -0
  660. package/examples/node-agent-tool/src/test-kit.ts +167 -0
  661. package/examples/node-agent-tool/tsconfig.json +18 -0
  662. package/examples/node-agent-tool/vitest.config.ts +8 -0
  663. package/examples/vite-wallet/.stackblitzrc +4 -0
  664. package/examples/vite-wallet/Cargo.lock +646 -0
  665. package/examples/vite-wallet/Cargo.toml +5 -0
  666. package/examples/vite-wallet/README.md +107 -0
  667. package/examples/vite-wallet/backend/Cargo.toml +12 -0
  668. package/examples/vite-wallet/backend/backend.did +37 -0
  669. package/examples/vite-wallet/backend/src/lib.rs +133 -0
  670. package/examples/vite-wallet/icp.yaml +44 -0
  671. package/examples/vite-wallet/index.html +13 -0
  672. package/examples/vite-wallet/ledger.did +54 -0
  673. package/examples/vite-wallet/package.json +46 -0
  674. package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
  675. package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
  676. package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
  677. package/examples/vite-wallet/scripts/faucet.ts +41 -0
  678. package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
  679. package/examples/vite-wallet/src/AddressBook.tsx +147 -0
  680. package/examples/vite-wallet/src/App.tsx +53 -0
  681. package/examples/vite-wallet/src/Balance.test.tsx +57 -0
  682. package/examples/vite-wallet/src/Balance.tsx +58 -0
  683. package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
  684. package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
  685. package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
  686. package/examples/vite-wallet/src/Profile.test.tsx +145 -0
  687. package/examples/vite-wallet/src/Profile.tsx +92 -0
  688. package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
  689. package/examples/vite-wallet/src/SendIcp.tsx +244 -0
  690. package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
  691. package/examples/vite-wallet/src/SendToContact.tsx +102 -0
  692. package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
  693. package/examples/vite-wallet/src/SignIn.tsx +120 -0
  694. package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
  695. package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
  696. package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
  697. package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
  698. package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
  699. package/examples/vite-wallet/src/auth/local-page.ts +20 -0
  700. package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
  701. package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
  702. package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
  703. package/examples/vite-wallet/src/canisters/backend.ts +58 -0
  704. package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
  705. package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
  706. package/examples/vite-wallet/src/client.ts +45 -0
  707. package/examples/vite-wallet/src/main.tsx +18 -0
  708. package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
  709. package/examples/vite-wallet/src/refusal.ts +17 -0
  710. package/examples/vite-wallet/src/styles.css +282 -0
  711. package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
  712. package/examples/vite-wallet/src/token.ts +50 -0
  713. package/examples/vite-wallet/src/transfer.test.ts +178 -0
  714. package/examples/vite-wallet/src/transfer.ts +110 -0
  715. package/examples/vite-wallet/src/use-canisters.ts +49 -0
  716. package/examples/vite-wallet/tsconfig.json +19 -0
  717. package/examples/vite-wallet/vite.config.ts +47 -0
  718. package/examples/vite-wallet/vitest.config.ts +17 -0
  719. package/package.json +9 -4
  720. package/packages/core/.size-limit.js +47 -0
  721. package/packages/core/LICENSE +21 -0
  722. package/packages/core/README.md +84 -0
  723. package/packages/core/llms.txt +610 -0
  724. package/packages/core/package.json +89 -0
  725. package/packages/core/src/call.ts +961 -0
  726. package/packages/core/src/canister.ts +259 -0
  727. package/packages/core/src/client.ts +2061 -0
  728. package/packages/core/src/errors.ts +1079 -0
  729. package/packages/core/src/hydration.ts +239 -0
  730. package/packages/core/src/index.ts +23 -0
  731. package/packages/core/src/keys.ts +108 -0
  732. package/packages/core/src/management.ts +138 -0
  733. package/packages/core/src/network.ts +1020 -0
  734. package/packages/core/src/options.ts +1000 -0
  735. package/packages/core/src/runtime.ts +50 -0
  736. package/packages/core/src/testing/fake-replica.ts +1166 -0
  737. package/packages/core/src/testing/index.ts +16 -0
  738. package/packages/core/src/testing/test-auth.ts +340 -0
  739. package/packages/core/src/testing/test-client.ts +559 -0
  740. package/packages/core/src/types.ts +541 -0
  741. package/packages/core/src/units.ts +299 -0
  742. package/packages/core/src/version.ts +4 -0
  743. package/packages/core/tests/canister-helpers.ts +187 -0
  744. package/packages/core/tests/canister-id.test.ts +158 -0
  745. package/packages/core/tests/canister-unwrap.test.ts +94 -0
  746. package/packages/core/tests/canister.test-d.ts +406 -0
  747. package/packages/core/tests/canister.test.ts +1921 -0
  748. package/packages/core/tests/client-account-change.test.ts +131 -0
  749. package/packages/core/tests/client-agents.test.ts +402 -0
  750. package/packages/core/tests/client-auth-network.test.ts +719 -0
  751. package/packages/core/tests/client-disposed.test.ts +403 -0
  752. package/packages/core/tests/client-helpers.ts +136 -0
  753. package/packages/core/tests/client-hydration.test.ts +166 -0
  754. package/packages/core/tests/client-pinned.test.ts +626 -0
  755. package/packages/core/tests/client-session.test.ts +348 -0
  756. package/packages/core/tests/client.test-d.ts +181 -0
  757. package/packages/core/tests/client.test.ts +1162 -0
  758. package/packages/core/tests/encode.test.ts +171 -0
  759. package/packages/core/tests/env-canister-id.test.ts +666 -0
  760. package/packages/core/tests/env-cookie-copies.test.ts +182 -0
  761. package/packages/core/tests/error-guard.test.ts +222 -0
  762. package/packages/core/tests/errors.test-d.ts +280 -0
  763. package/packages/core/tests/errors.test.ts +1359 -0
  764. package/packages/core/tests/fixtures/README.md +28 -0
  765. package/packages/core/tests/fixtures/accounts.did +21 -0
  766. package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
  767. package/packages/core/tests/fixtures/accounts.ts +26 -0
  768. package/packages/core/tests/fixtures/archive.did +31 -0
  769. package/packages/core/tests/fixtures/archive.envelope.json +259 -0
  770. package/packages/core/tests/fixtures/archive.ts +55 -0
  771. package/packages/core/tests/fixtures/icrc1.did +88 -0
  772. package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
  773. package/packages/core/tests/fixtures/icrc1.ts +78 -0
  774. package/packages/core/tests/fixtures/management.did +724 -0
  775. package/packages/core/tests/fixtures/management.envelope.json +4455 -0
  776. package/packages/core/tests/fixtures/management.ts +486 -0
  777. package/packages/core/tests/fixtures/shapes.did +33 -0
  778. package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
  779. package/packages/core/tests/fixtures/shapes.ts +56 -0
  780. package/packages/core/tests/fixtures/skippable.did +15 -0
  781. package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
  782. package/packages/core/tests/fixtures/skippable.ts +33 -0
  783. package/packages/core/tests/func.test.ts +154 -0
  784. package/packages/core/tests/identity-attributes.test.ts +214 -0
  785. package/packages/core/tests/keys.test.ts +289 -0
  786. package/packages/core/tests/management.test.ts +206 -0
  787. package/packages/core/tests/network-helpers.ts +121 -0
  788. package/packages/core/tests/network.test-d.ts +78 -0
  789. package/packages/core/tests/network.test.ts +1051 -0
  790. package/packages/core/tests/options.test.ts +1594 -0
  791. package/packages/core/tests/redirect-sign-in.test.ts +393 -0
  792. package/packages/core/tests/resend.test-d.ts +81 -0
  793. package/packages/core/tests/resend.test.ts +723 -0
  794. package/packages/core/tests/skip.test-d.ts +125 -0
  795. package/packages/core/tests/testing-entry.test.ts +73 -0
  796. package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
  797. package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
  798. package/packages/core/tests/testing-test-auth.test.ts +483 -0
  799. package/packages/core/tests/testing-test-client.test-d.ts +184 -0
  800. package/packages/core/tests/testing-test-client.test.ts +1272 -0
  801. package/packages/core/tests/time-realm.test.ts +63 -0
  802. package/packages/core/tests/time.test.ts +238 -0
  803. package/packages/core/tests/traps/README.md +39 -0
  804. package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
  805. package/packages/core/tests/traps/classification-table.test.ts +256 -0
  806. package/packages/core/tests/traps/err-arm.test.ts +144 -0
  807. package/packages/core/tests/traps/fetch-spy.ts +39 -0
  808. package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
  809. package/packages/core/tests/traps/ledger.ts +248 -0
  810. package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
  811. package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
  812. package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
  813. package/packages/core/tests/traps/update-as-query.test.ts +244 -0
  814. package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
  815. package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
  816. package/packages/core/tests/traps.test-d.ts +476 -0
  817. package/packages/core/tests/units.test-d.ts +118 -0
  818. package/packages/core/tests/units.test.ts +634 -0
  819. package/packages/core/tsconfig.json +19 -0
  820. package/packages/core/tsconfig.typecheck.json +16 -0
  821. package/packages/core/vitest.config.ts +13 -0
  822. package/packages/mcp/LICENSE +21 -0
  823. package/packages/mcp/README.md +189 -0
  824. package/packages/mcp/package.json +62 -0
  825. package/packages/mcp/src/candid/candid-text.test.ts +455 -0
  826. package/packages/mcp/src/candid/candid-text.ts +501 -0
  827. package/packages/mcp/src/candid/contract.test.ts +398 -0
  828. package/packages/mcp/src/candid/formats.ts +155 -0
  829. package/packages/mcp/src/candid/hex.ts +29 -0
  830. package/packages/mcp/src/candid/json-schema.test.ts +758 -0
  831. package/packages/mcp/src/candid/json-schema.ts +454 -0
  832. package/packages/mcp/src/candid/json.test.ts +1207 -0
  833. package/packages/mcp/src/candid/json.ts +1053 -0
  834. package/packages/mcp/src/candid/schema-walk.ts +189 -0
  835. package/packages/mcp/src/candid/type-text.test.ts +179 -0
  836. package/packages/mcp/src/candid/type-text.ts +238 -0
  837. package/packages/mcp/src/cli.ts +134 -0
  838. package/packages/mcp/src/identity.ts +188 -0
  839. package/packages/mcp/src/interface/cache.test.ts +48 -0
  840. package/packages/mcp/src/interface/cache.ts +68 -0
  841. package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
  842. package/packages/mcp/src/interface/cli-cache.ts +122 -0
  843. package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
  844. package/packages/mcp/src/interface/compile-worker.ts +290 -0
  845. package/packages/mcp/src/interface/compile.test.ts +155 -0
  846. package/packages/mcp/src/interface/compile.ts +281 -0
  847. package/packages/mcp/src/interface/docs.test.ts +105 -0
  848. package/packages/mcp/src/interface/docs.ts +86 -0
  849. package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
  850. package/packages/mcp/src/interface/hex.ts +8 -0
  851. package/packages/mcp/src/interface/ids.test.ts +71 -0
  852. package/packages/mcp/src/interface/ids.ts +87 -0
  853. package/packages/mcp/src/interface/loader.test.ts +804 -0
  854. package/packages/mcp/src/interface/loader.ts +519 -0
  855. package/packages/mcp/src/interface/read-candid.test.ts +300 -0
  856. package/packages/mcp/src/interface/read-candid.ts +259 -0
  857. package/packages/mcp/src/interface/types.ts +113 -0
  858. package/packages/mcp/src/network.ts +122 -0
  859. package/packages/mcp/src/options.ts +149 -0
  860. package/packages/mcp/src/server/approval.ts +159 -0
  861. package/packages/mcp/src/server/naming.ts +43 -0
  862. package/packages/mcp/src/server/results.ts +102 -0
  863. package/packages/mcp/src/server/server.ts +797 -0
  864. package/packages/mcp/src/server/tools.ts +116 -0
  865. package/packages/mcp/src/usage.ts +40 -0
  866. package/packages/mcp/tests/approval.test.ts +60 -0
  867. package/packages/mcp/tests/identity.test.ts +234 -0
  868. package/packages/mcp/tests/naming.test.ts +62 -0
  869. package/packages/mcp/tests/options.test.ts +140 -0
  870. package/packages/mcp/tests/server.test.ts +997 -0
  871. package/packages/mcp/tsconfig.json +19 -0
  872. package/packages/mcp/tsconfig.typecheck.json +10 -0
  873. package/packages/mcp/vitest.config.ts +10 -0
  874. package/packages/react/LICENSE +21 -0
  875. package/packages/react/README.md +328 -0
  876. package/packages/react/llms.txt +95 -0
  877. package/packages/react/package.json +95 -0
  878. package/packages/react/src/index.tsx +678 -0
  879. package/packages/react/tests/abandoned-render.test.tsx +816 -0
  880. package/packages/react/tests/auth.test.tsx +315 -0
  881. package/packages/react/tests/caller-keys.test.tsx +2334 -0
  882. package/packages/react/tests/client-time.test.ts +40 -0
  883. package/packages/react/tests/entry.test.ts +52 -0
  884. package/packages/react/tests/helpers.tsx +291 -0
  885. package/packages/react/tests/hydration.test.tsx +262 -0
  886. package/packages/react/tests/ownership.test.tsx +260 -0
  887. package/packages/react/tests/provider.server.test.tsx +169 -0
  888. package/packages/react/tests/query-client.test.tsx +107 -0
  889. package/packages/react/tests/strict-mode.test.tsx +288 -0
  890. package/packages/react/tests/suspense.test-d.tsx +357 -0
  891. package/packages/react/tests/two-query-cores.test.tsx +135 -0
  892. package/packages/react/tests/types.test-d.tsx +63 -0
  893. package/packages/react/tsconfig.json +23 -0
  894. package/packages/react/tsconfig.test.json +8 -0
  895. package/packages/react/tsconfig.typecheck.json +16 -0
  896. package/packages/react/vitest.config.ts +39 -0
  897. package/packages/vite-plugin/LICENSE +21 -0
  898. package/packages/vite-plugin/README.md +532 -0
  899. package/packages/vite-plugin/package.json +76 -0
  900. package/packages/vite-plugin/src/describe-value.test.ts +43 -0
  901. package/packages/vite-plugin/src/describe-value.ts +167 -0
  902. package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
  903. package/packages/vite-plugin/src/dev-environment.ts +510 -0
  904. package/packages/vite-plugin/src/env.ts +431 -0
  905. package/packages/vite-plugin/src/fetch.ts +348 -0
  906. package/packages/vite-plugin/src/file-identity.ts +106 -0
  907. package/packages/vite-plugin/src/generate.ts +691 -0
  908. package/packages/vite-plugin/src/index.test.ts +1243 -0
  909. package/packages/vite-plugin/src/index.ts +2565 -0
  910. package/packages/vite-plugin/src/interface.ts +339 -0
  911. package/packages/vite-plugin/src/local-host.ts +34 -0
  912. package/packages/vite-plugin/src/options.ts +641 -0
  913. package/packages/vite-plugin/src/principal-text.test.ts +87 -0
  914. package/packages/vite-plugin/src/principal-text.ts +98 -0
  915. package/packages/vite-plugin/tests/close.test.ts +141 -0
  916. package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
  917. package/packages/vite-plugin/tests/fake-ic.ts +214 -0
  918. package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
  919. package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
  920. package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
  921. package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
  922. package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
  923. package/packages/vite-plugin/tests/generation.test.ts +1036 -0
  924. package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
  925. package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
  926. package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
  927. package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
  928. package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
  929. package/packages/vite-plugin/tests/package.test.ts +62 -0
  930. package/packages/vite-plugin/tests/project.test.ts +2993 -0
  931. package/packages/vite-plugin/tests/support.ts +339 -0
  932. package/packages/vite-plugin/tests/timeout.test.ts +107 -0
  933. package/packages/vite-plugin/tsconfig.json +23 -0
  934. package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
  935. package/packages/vite-plugin/vitest.config.ts +9 -0
  936. package/pnpm-workspace.yaml +23 -0
  937. package/scripts/ai-context-files.js +72 -0
  938. package/scripts/assemble-docs-site.mjs +485 -0
  939. package/scripts/assemble-docs-site.test.mjs +568 -0
  940. package/scripts/build-examples.js +58 -0
  941. package/scripts/check-ai-context.js +963 -0
  942. package/scripts/check-ai-context.test.mjs +811 -0
  943. package/scripts/check-exports.mjs +767 -0
  944. package/scripts/check-exports.test.mjs +1136 -0
  945. package/scripts/check-snippets/ambient.d.ts +428 -0
  946. package/scripts/check-snippets/app/generated/backend.did +7 -0
  947. package/scripts/check-snippets/app/generated/backend.ts +15 -0
  948. package/scripts/check-snippets/app/generated/icrc1.did +88 -0
  949. package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
  950. package/scripts/check-snippets/app/globals.ts +26 -0
  951. package/scripts/check-snippets/docs/guides/App.tsx +21 -0
  952. package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
  953. package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
  954. package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
  955. package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
  956. package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
  957. package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
  958. package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
  959. package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
  960. package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
  961. package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
  962. package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
  963. package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
  964. package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
  965. package/scripts/check-snippets.mjs +993 -0
  966. package/scripts/check-snippets.test.mjs +338 -0
  967. package/scripts/check-stack.mjs +501 -0
  968. package/scripts/check-stack.test.mjs +800 -0
  969. package/scripts/clean-node-modules.js +67 -0
  970. package/scripts/example-projects.js +58 -0
  971. package/scripts/export-budget.mjs +145 -0
  972. package/scripts/faults.json +6749 -0
  973. package/scripts/lib/fault-workspace.mjs +210 -0
  974. package/scripts/lib/io.mjs +29 -0
  975. package/scripts/lib/io.test.mjs +38 -0
  976. package/scripts/lib/markdown-fences.mjs +56 -0
  977. package/scripts/lib/markdown-fences.test.mjs +84 -0
  978. package/scripts/lib/package-exports.mjs +54 -0
  979. package/scripts/lib/package-exports.test.mjs +66 -0
  980. package/scripts/lib/release-pr-steps.mjs +47 -0
  981. package/scripts/release-tag.mjs +95 -0
  982. package/scripts/release-tag.test.mjs +278 -0
  983. package/scripts/release-workflow.test.mjs +98 -0
  984. package/scripts/release.js +167 -0
  985. package/scripts/release.test.mjs +347 -0
  986. package/scripts/removed-v3-names.js +125 -0
  987. package/scripts/size-app/.size-limit.js +90 -0
  988. package/scripts/size-app/core.js +3 -0
  989. package/scripts/size-app/package.json +24 -0
  990. package/scripts/size-app/react.js +4 -0
  991. package/scripts/size-app.test.mjs +73 -0
  992. package/scripts/sync-ai-context-versions.js +102 -0
  993. package/scripts/sync-example-versions.js +151 -0
  994. package/scripts/test-examples-published.mjs +1585 -0
  995. package/scripts/test-examples-published.test.mjs +1586 -0
  996. package/scripts/traps/any-method-vars-not-a-list.json +5 -0
  997. package/scripts/traps/canister-method-unknown.json +5 -0
  998. package/scripts/traps/client-says-who-calls-once.json +5 -0
  999. package/scripts/traps/client-says-who-calls.json +5 -0
  1000. package/scripts/traps/err-only-on-canister-err.json +14 -0
  1001. package/scripts/traps/format-units-refuses-missing-value.json +5 -0
  1002. package/scripts/traps/format-units-refuses-number.json +5 -0
  1003. package/scripts/traps/format-units-unknown-option.json +5 -0
  1004. package/scripts/traps/guard-err-is-the-methods.json +5 -0
  1005. package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
  1006. package/scripts/traps/internals-not-exported.json +9 -0
  1007. package/scripts/traps/network-refuses-unknown-name.json +5 -0
  1008. package/scripts/traps/never-err-has-no-type.json +5 -0
  1009. package/scripts/traps/options-method-unknown.json +5 -0
  1010. package/scripts/traps/options-vars-wrong-type.json +5 -0
  1011. package/scripts/traps/parse-units-refuses-number.json +5 -0
  1012. package/scripts/traps/parse-units-unknown-option.json +5 -0
  1013. package/scripts/traps/principal-is-not-plain-text.json +14 -0
  1014. package/scripts/traps/query-options-refuses-oneway.json +5 -0
  1015. package/scripts/traps/query-options-refuses-update.json +5 -0
  1016. package/scripts/traps/refuse-next-unknown-option.json +5 -0
  1017. package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
  1018. package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
  1019. package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
  1020. package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
  1021. package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
  1022. package/scripts/traps/test-handler-method-unknown.json +5 -0
  1023. package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
  1024. package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
  1025. package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
  1026. package/scripts/typecheck-examples.js +122 -0
  1027. package/scripts/verify-faults.mjs +375 -0
  1028. package/scripts/verify-faults.test.mjs +402 -0
  1029. package/scripts/verify-package-artifacts.mjs +590 -0
  1030. package/scripts/verify-peer-floors.js +399 -0
  1031. package/scripts/verify-peer-floors.test.mjs +422 -0
  1032. package/scripts/verify-test-fails-rules.mjs +104 -0
  1033. package/scripts/verify-test-fails-rules.test.mjs +215 -0
  1034. package/scripts/verify-test-fails.js +371 -0
  1035. package/scripts/verify-traps.mjs +505 -0
  1036. package/scripts/verify-traps.test.mjs +521 -0
  1037. package/scripts/verify-typescript-floor.js +239 -0
  1038. package/skill-packages/ic-reactor/SKILL.md +103 -0
  1039. package/stack.json +8 -0
  1040. package/tsconfig.base.json +23 -0
  1041. package/tsconfig.json +8 -0
@@ -0,0 +1,2334 @@
1
+ /**
2
+ * Query keys follow the caller React renders with (#813).
3
+ *
4
+ * A server render has no session: it prefetches and renders as the anonymous
5
+ * principal, and `useAuth()` honours that with an anonymous server snapshot.
6
+ * The keys `useClient()` builds honour it too: it follows the client's caller
7
+ * with `useSyncExternalStore` and the same anonymous server snapshot. An
8
+ * `AuthClient` reads a stored session synchronously, so for a visitor who
9
+ * signed in on an earlier visit the client's live caller is already the user
10
+ * while the page hydrates; keys built from it would miss the data the server
11
+ * dehydrated, render other markup than the server's HTML, and send every
12
+ * prefetched read again.
13
+ *
14
+ * React moves a hydrated component on to the live caller itself, with the
15
+ * synchronous update `useSyncExternalStore` makes after hydrating when the
16
+ * snapshot differs from the server's. Two consequences are accepted and
17
+ * pinned here: a Suspense boundary still dehydrated below a component that
18
+ * moves is rendered on the client (L1), and a read the hydrating render built
19
+ * may run once and be cancelled before anything is sent (L2).
20
+ *
21
+ * Every client here is a `createTestClient()` over an in-memory replica whose
22
+ * one canister answers with the caller it saw, so a test reads from its log
23
+ * who sent what, and from the page whose answer it shows.
24
+ */
25
+ import { c } from "@meantjs/schema"
26
+ import {
27
+ createClient,
28
+ isReactorError,
29
+ type Canister,
30
+ type Client,
31
+ } from "@ic-reactor/core"
32
+ import { createTestClient } from "@ic-reactor/core/testing"
33
+ import {
34
+ HydrationBoundary,
35
+ dehydrate,
36
+ useMutation,
37
+ useQuery,
38
+ useSuspenseQuery,
39
+ type DehydratedState,
40
+ } from "@tanstack/react-query"
41
+ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
42
+ import {
43
+ Component,
44
+ StrictMode,
45
+ Suspense,
46
+ lazy,
47
+ startTransition,
48
+ useEffect,
49
+ useLayoutEffect,
50
+ useMemo,
51
+ useRef,
52
+ useState,
53
+ version,
54
+ type ComponentType,
55
+ type ReactElement,
56
+ type ReactNode,
57
+ } from "react"
58
+ import { hydrateRoot, type Root } from "react-dom/client"
59
+ import { renderToString } from "react-dom/server"
60
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
61
+ import { ReactorProvider, useAuth, useClient } from "../src/index.js"
62
+ import { clientOf, macrotask, testAuth } from "./helpers.js"
63
+
64
+ const ANONYMOUS = "2vxsx-fae"
65
+ const CANISTER = "rdmx6-jaaaa-aaaaa-aaadq-cai"
66
+ const REACT_18 = Number(version.split(".")[0]) < 19
67
+ /** What an empty slot of React Compiler's cache holds (`react-compiler-runtime`'s `$empty`). */
68
+ const EMPTY = Symbol.for("react.memo_cache_sentinel")
69
+
70
+ /** One query that answers with who called it. */
71
+ const WHO = c.service({
72
+ whoami: c.func([], [c.text], "query"),
73
+ stamp: c.func([], [c.text], "update"),
74
+ })
75
+ type Who = { whoami: () => Promise<string>; stamp: () => Promise<string> }
76
+ /** The options of a `whoami` read, as a client builds them. */
77
+ type WhoamiOptions = ReturnType<typeof whoamiOptions>
78
+ const whoamiOptions = (client: Client, who: Canister<Who>) =>
79
+ client.queryOptions(who, "whoami")
80
+
81
+ /**
82
+ * A test client and its replica. Signed in as the identity of `seed` unless
83
+ * `signedIn` is `false`, as a server's client and a first visit are.
84
+ */
85
+ function replica(options: { seed?: number; signedIn?: boolean } = {}) {
86
+ const test = createTestClient({
87
+ identity: options.seed ?? 7,
88
+ signedIn: options.signedIn ?? true,
89
+ })
90
+ test.mock<Who>(WHO, CANISTER, {
91
+ whoami: ({ caller }) => `read by ${caller}`,
92
+ stamp: ({ caller }) => `written by ${caller}`,
93
+ })
94
+ tests.push(test)
95
+ return test
96
+ }
97
+ type Replica = ReturnType<typeof replica>
98
+
99
+ /** The callers of every `whoami` the replica answered, in order. */
100
+ const whoamiCallers = (test: Replica): (string | undefined)[] =>
101
+ test.requests
102
+ .filter(({ methodName }) => methodName === "whoami")
103
+ .map(({ caller }) => caller)
104
+
105
+ const tests: ReturnType<typeof createTestClient>[] = []
106
+ /** Clients a test made with `createClient`, disposed after it. */
107
+ const made: Client[] = []
108
+ let root: Root | undefined
109
+ let container: HTMLElement
110
+
111
+ beforeEach(() => {
112
+ container = document.createElement("div")
113
+ document.body.appendChild(container)
114
+ })
115
+
116
+ afterEach(() => {
117
+ act(() => root?.unmount())
118
+ root = undefined
119
+ container.remove()
120
+ for (const test of tests.splice(0)) test.client.dispose()
121
+ for (const client of made.splice(0)) client.dispose()
122
+ vi.restoreAllMocks()
123
+ })
124
+
125
+ /** A component that reads with `useClient()` alone: no `useAuth()`. */
126
+ function Reader() {
127
+ const client = useClient()
128
+ const who = client.canister<Who>(WHO, { id: CANISTER })
129
+ const read = useQuery(client.queryOptions(who, "whoami"))
130
+ return <main>{read.isError ? "ERROR" : (read.data ?? "loading")}</main>
131
+ }
132
+
133
+ /** The root's own `useAuth()`, which moves on to the session after hydrating. */
134
+ function Header() {
135
+ const { status } = useAuth()
136
+ return <header>{status}</header>
137
+ }
138
+
139
+ /** The page: a header at the root, and the reader below a Suspense boundary. */
140
+ function page(
141
+ client: Client,
142
+ state: DehydratedState,
143
+ Late: ComponentType
144
+ ): ReactElement {
145
+ return (
146
+ <ReactorProvider client={() => client}>
147
+ <Header />
148
+ <HydrationBoundary state={state}>
149
+ <Suspense fallback={<p>loading</p>}>
150
+ <Late />
151
+ </Suspense>
152
+ </HydrationBoundary>
153
+ </ReactorProvider>
154
+ )
155
+ }
156
+
157
+ /**
158
+ * What a server sends: it prefetches as nobody, dehydrates, and renders the
159
+ * page to HTML with that client. The state crosses as JSON, as it does in an
160
+ * app.
161
+ */
162
+ async function serverRender() {
163
+ const server = replica({ signedIn: false })
164
+ const who = server.client.canister<Who>(WHO, { id: CANISTER })
165
+ await server.client.queryClient.prefetchQuery(
166
+ server.client.queryOptions(who, "whoami")
167
+ )
168
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
169
+ const html = renderToString(page(server.client, JSON.parse(json), Reader))
170
+ return { server, json, html }
171
+ }
172
+
173
+ /**
174
+ * Hydrates `container` with `element`, returning what React reported: the
175
+ * messages of its recoverable errors, the errors themselves, and its console.
176
+ */
177
+ async function hydrate(element: ReactElement) {
178
+ const recoverable: string[] = []
179
+ const reported: unknown[] = []
180
+ const logged = vi.spyOn(console, "error").mockImplementation(() => {})
181
+ await act(async () => {
182
+ root = hydrateRoot(container, element, {
183
+ onRecoverableError: (error) => {
184
+ reported.push(error)
185
+ recoverable.push(String((error as Error).message))
186
+ },
187
+ })
188
+ })
189
+ await act(macrotask)
190
+ return { recoverable, reported, logged }
191
+ }
192
+
193
+ /** Every `innerHTML` of `container` that a commit left, in order. */
194
+ function watchCommits() {
195
+ const commits: string[] = []
196
+ const watcher = new MutationObserver(() => commits.push(container.innerHTML))
197
+ watcher.observe(container, {
198
+ subtree: true,
199
+ childList: true,
200
+ characterData: true,
201
+ })
202
+ return { commits, stop: () => watcher.disconnect() }
203
+ }
204
+
205
+ /** Every error a query of `client` went into, in order. */
206
+ function watchErrors(client: Client) {
207
+ const errors: unknown[] = []
208
+ const stop = client.queryClient.getQueryCache().subscribe((event) => {
209
+ if (event.type === "updated" && event.action.type === "error") {
210
+ errors.push(event.action.error)
211
+ }
212
+ })
213
+ return { errors, stop }
214
+ }
215
+
216
+ /** A lazy component whose chunk arrives when the test calls `deliver`. */
217
+ function lateChunk(Content: ComponentType) {
218
+ let deliver!: () => void
219
+ const Late = lazy(
220
+ () =>
221
+ new Promise<{ default: ComponentType }>((resolve) => {
222
+ deliver = () => resolve({ default: Content })
223
+ })
224
+ )
225
+ return { Late, deliver: () => deliver() }
226
+ }
227
+
228
+ /**
229
+ * The server's HTML of a boundary rewritten the way a stream sends it before
230
+ * its content: pending, with the fallback in its place. React waits for the
231
+ * stream while the document is still loading, which `loading()` makes it.
232
+ */
233
+ function streamed(html: string, content: string) {
234
+ const pending = html.replace(
235
+ `<!--$-->${content}<!--/$-->`,
236
+ '<!--$?--><template id="B:0"></template><p>fallback</p><!--/$-->'
237
+ )
238
+ expect(pending).not.toBe(html)
239
+ return pending
240
+ }
241
+ async function whileLoading(run: () => Promise<void>) {
242
+ Object.defineProperty(document, "readyState", {
243
+ configurable: true,
244
+ get: () => "loading",
245
+ })
246
+ try {
247
+ await run()
248
+ } finally {
249
+ delete (document as unknown as { readyState?: string }).readyState
250
+ }
251
+ }
252
+
253
+ describe("hydrating with a stored session", () => {
254
+ it.each([
255
+ ["", (page: ReactElement) => page],
256
+ [
257
+ " under StrictMode",
258
+ (page: ReactElement) => <StrictMode>{page}</StrictMode>,
259
+ ],
260
+ ])(
261
+ "renders the server's keys, sends nothing during hydration, then reads once as the user%s",
262
+ async (_, wrap) => {
263
+ const { server, json, html } = await serverRender()
264
+ expect(html).toContain(`read by ${ANONYMOUS}`)
265
+ expect(whoamiCallers(server)).toEqual([ANONYMOUS])
266
+ container.innerHTML = html
267
+ const served = container.querySelector("main")
268
+
269
+ const browser = replica({ seed: 7 })
270
+ const user = browser.auth.getPrincipal()?.toText()
271
+ expect(browser.client.caller()).toBe(user)
272
+ const commits = watchCommits()
273
+ const { recoverable, logged } = await hydrate(
274
+ wrap(page(browser.client, JSON.parse(json), Reader))
275
+ )
276
+
277
+ // The first render matched the server's HTML: React kept its nodes and
278
+ // reported nothing.
279
+ expect(recoverable).toEqual([])
280
+ expect(logged).not.toHaveBeenCalled()
281
+ // Right after hydration the reader renders the user's keys, and reads
282
+ // them once, as the user. The anonymous read the server made is not sent
283
+ // again, and its answer is never shown as the user's.
284
+ await waitFor(() =>
285
+ expect(container.querySelector("main")?.textContent).toBe(
286
+ `read by ${user}`
287
+ )
288
+ )
289
+ commits.stop()
290
+ expect(container.querySelector("main")).toBe(served)
291
+ expect(whoamiCallers(browser)).toEqual([user])
292
+ expect(container.querySelector("header")?.textContent).toBe("signed-in")
293
+ expect(commits.commits.filter((html) => html.includes("ERROR"))).toEqual(
294
+ []
295
+ )
296
+ }
297
+ )
298
+
299
+ it("hydrates a provider given a spread copy of the client as one given the client", async () => {
300
+ const { json, html } = await serverRender()
301
+ container.innerHTML = html
302
+ const served = container.querySelector("main")
303
+ const browser = replica({ seed: 7 })
304
+ const user = browser.auth.getPrincipal()?.toText()
305
+ // A structural copy, as a test's double of a client often is.
306
+ const copy: Client = { ...browser.client }
307
+
308
+ const { recoverable, logged } = await hydrate(
309
+ page(copy, JSON.parse(json), Reader)
310
+ )
311
+
312
+ expect(recoverable).toEqual([])
313
+ expect(logged).not.toHaveBeenCalled()
314
+ await waitFor(() =>
315
+ expect(container.querySelector("main")?.textContent).toBe(
316
+ `read by ${user}`
317
+ )
318
+ )
319
+ expect(container.querySelector("main")).toBe(served)
320
+ expect(whoamiCallers(browser)).toEqual([user])
321
+ })
322
+
323
+ it("hydrates a late boundary below no component that renders with the caller from the server's keys, then moves it to the user", async () => {
324
+ const { json, html } = await serverRender()
325
+ container.innerHTML = html
326
+ const served = container.querySelector("main")
327
+
328
+ const { Late, deliver } = lateChunk(Reader)
329
+ const browser = replica({ seed: 7 })
330
+ const user = browser.auth.getPrincipal()?.toText()
331
+ const { recoverable, logged } = await hydrate(
332
+ page(browser.client, JSON.parse(json), Late)
333
+ )
334
+ // The root hydrated and its header moved on to the session; the boundary
335
+ // is still the server's.
336
+ expect(container.querySelector("header")?.textContent).toBe("signed-in")
337
+ expect(container.querySelector("main")?.textContent).toBe(
338
+ `read by ${ANONYMOUS}`
339
+ )
340
+
341
+ await act(async () => deliver())
342
+ await act(macrotask)
343
+
344
+ expect(recoverable).toEqual([])
345
+ expect(logged).not.toHaveBeenCalled()
346
+ await waitFor(() =>
347
+ expect(container.querySelector("main")?.textContent).toBe(
348
+ `read by ${user}`
349
+ )
350
+ )
351
+ expect(container.querySelector("main")).toBe(served)
352
+ expect(whoamiCallers(browser)).toEqual([user])
353
+ })
354
+
355
+ it.each([
356
+ ["a read the server did not prefetch (isLoading)", false],
357
+ ["a stale read the server prefetched (isFetching)", true],
358
+ ] as const)(
359
+ "hydrates a component that renders the fetch state of %s, then loads the user's read",
360
+ async (_, prefetched) => {
361
+ function Status() {
362
+ const client = useClient()
363
+ const who = client.canister<Who>(WHO, { id: CANISTER })
364
+ const read = useQuery(client.queryOptions(who, "whoami"))
365
+ return (
366
+ <main>
367
+ {`${read.isLoading ? "loading" : "settled"}|${
368
+ read.isFetching ? "fetching" : "idle"
369
+ }|${read.fetchStatus}|${read.data ?? "none"}`}
370
+ </main>
371
+ )
372
+ }
373
+ const tree = (client: Client, state: DehydratedState) => (
374
+ <ReactorProvider client={() => client}>
375
+ <HydrationBoundary state={state}>
376
+ <Status />
377
+ </HydrationBoundary>
378
+ </ReactorProvider>
379
+ )
380
+ const server = replica({ signedIn: false })
381
+ if (prefetched) {
382
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
383
+ await server.client.queryClient.prefetchQuery(
384
+ server.client.queryOptions(serverWho, "whoami")
385
+ )
386
+ }
387
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
388
+ container.innerHTML = renderToString(
389
+ tree(server.client, JSON.parse(json))
390
+ )
391
+ const served = container.querySelector("main")
392
+
393
+ const browser = replica({ seed: 7 })
394
+ const user = browser.auth.getPrincipal()?.toText()
395
+ const { recoverable, logged } = await hydrate(
396
+ tree(browser.client, JSON.parse(json))
397
+ )
398
+
399
+ expect(recoverable).toEqual([])
400
+ expect(logged).not.toHaveBeenCalled()
401
+ await waitFor(() =>
402
+ expect(container.querySelector("main")?.textContent).toBe(
403
+ `settled|idle|idle|read by ${user}`
404
+ )
405
+ )
406
+ expect(container.querySelector("main")).toBe(served)
407
+ expect(whoamiCallers(browser)).toEqual([user])
408
+ }
409
+ )
410
+ })
411
+
412
+ describe("useClient()", () => {
413
+ it("renders a component that uses only it again on sign-in, switch and sign-out, with the new caller's keys", async () => {
414
+ const test = replica({ seed: 1, signedIn: false })
415
+ const renders: string[] = []
416
+ function Keys() {
417
+ const client = useClient()
418
+ const who = client.canister<Who>(WHO, { id: CANISTER })
419
+ const key = client.queryKey(who, "whoami")
420
+ renders.push(String(key[2]))
421
+ const read = useQuery(client.queryOptions(who, "whoami"))
422
+ return (
423
+ <p data-testid="keys" data-caller={String(key[2])}>
424
+ {read.data ?? "loading"}
425
+ </p>
426
+ )
427
+ }
428
+ render(
429
+ <ReactorProvider client={() => test.client}>
430
+ <Keys />
431
+ </ReactorProvider>
432
+ )
433
+ const shown = () => screen.getByTestId("keys")
434
+ await waitFor(() =>
435
+ expect(shown().textContent).toBe(`read by ${ANONYMOUS}`)
436
+ )
437
+
438
+ // Each change comes from the auth, not from this tree: nothing else
439
+ // renders the component again.
440
+ await act(async () => {
441
+ await test.auth.signIn(1)
442
+ })
443
+ const first = test.auth.getPrincipal()?.toText()
444
+ expect(shown().dataset.caller).toBe(first)
445
+ await waitFor(() => expect(shown().textContent).toBe(`read by ${first}`))
446
+
447
+ act(() => {
448
+ test.auth.switchTo(2)
449
+ })
450
+ const second = test.auth.getPrincipal()?.toText()
451
+ expect(second).not.toBe(first)
452
+ expect(shown().dataset.caller).toBe(second)
453
+ await waitFor(() => expect(shown().textContent).toBe(`read by ${second}`))
454
+
455
+ await act(async () => {
456
+ await test.auth.signOut()
457
+ })
458
+ expect(shown().dataset.caller).toBe(ANONYMOUS)
459
+ expect(shown().textContent).toBe(`read by ${ANONYMOUS}`)
460
+ await waitFor(() => expect(whoamiCallers(test)).toHaveLength(4))
461
+
462
+ // Every read was sent as the caller whose key it fills: the anonymous key
463
+ // read again on its return, as it is stale.
464
+ expect(whoamiCallers(test)).toEqual([ANONYMOUS, first, second, ANONYMOUS])
465
+ // The component rendered with each caller's keys, in turn.
466
+ const callers = renders.filter((key, at) => key !== renders[at - 1])
467
+ expect(callers).toEqual([ANONYMOUS, first, second, ANONYMOUS])
468
+ })
469
+
470
+ it("returns one object per caller: another after a sign-in, a switch, a sign-out and an expiry, the same across renders for one caller", async () => {
471
+ const test = replica({ seed: 1 })
472
+ const seen: Client[] = []
473
+ let renderAgain!: () => void
474
+ function Probe() {
475
+ const [, setTick] = useState(0)
476
+ renderAgain = () => setTick((tick) => tick + 1)
477
+ seen.push(useClient())
478
+ return <p data-testid="status">{useAuth().status}</p>
479
+ }
480
+ render(
481
+ <ReactorProvider client={() => test.client}>
482
+ <Probe />
483
+ </ReactorProvider>
484
+ )
485
+ /** What the last render got, once the tree rendered again for no change of caller. */
486
+ const settle = () => {
487
+ const before = seen.length
488
+ act(() => renderAgain())
489
+ expect(seen.length).toBeGreaterThan(before)
490
+ return seen[seen.length - 1] as Client
491
+ }
492
+
493
+ const first = settle()
494
+ const user = test.auth.getPrincipal()?.toText()
495
+ act(() => {
496
+ test.auth.switchTo(2)
497
+ })
498
+ const switched = settle()
499
+ const second = test.auth.getPrincipal()?.toText()
500
+ await act(async () => {
501
+ await test.auth.signOut()
502
+ })
503
+ const signedOut = settle()
504
+ await act(async () => {
505
+ await test.auth.signIn(3)
506
+ })
507
+ const signedIn = settle()
508
+ const third = test.auth.getPrincipal()?.toText()
509
+ // The live caller's view says what the client says.
510
+ expect(signedIn.authState()).toBe(test.client.authState())
511
+ act(() => {
512
+ test.auth.expire()
513
+ })
514
+ const expired = settle()
515
+
516
+ expect(screen.getByTestId("status").textContent).toBe("expired")
517
+ // Each render for one caller got that caller's object, and no other.
518
+ const byCaller = new Map<string, Set<Client>>()
519
+ for (const client of seen) {
520
+ const objects = byCaller.get(client.caller()) ?? new Set()
521
+ byCaller.set(client.caller(), objects.add(client))
522
+ }
523
+ expect([...byCaller.keys()]).toEqual([user, second, ANONYMOUS, third])
524
+ for (const objects of byCaller.values()) expect(objects.size).toBe(1)
525
+ // A new object at every change of caller, and the anonymous one again
526
+ // when the caller turns anonymous, however it does.
527
+ expect(new Set([first, switched, signedOut, signedIn]).size).toBe(4)
528
+ expect(expired).toBe(signedOut)
529
+ expect(
530
+ [first, switched, signedOut, signedIn].map((client) => client.caller())
531
+ ).toEqual([user, second, ANONYMOUS, third])
532
+ // Each is a view of the provider's client: its cache, canisters and
533
+ // sign-in are the client's own, and its state the live one.
534
+ for (const client of seen) {
535
+ expect(client).not.toBe(test.client)
536
+ expect(clientOf(client)).toBe(test.client)
537
+ expect(client.queryClient).toBe(test.client.queryClient)
538
+ expect(client.canister).toBe(test.client.canister)
539
+ expect(client.subscribe).toBe(test.client.subscribe)
540
+ expect(client.signOut).toBe(test.client.signOut)
541
+ }
542
+ expect(expired.authState()).toBe(test.client.authState())
543
+ expect(expired.authState().status).toBe("expired")
544
+ })
545
+
546
+ // #894: what a component memoizes on the client (useMemo, an effect's
547
+ // dependencies, React Compiler's cache) is built again when the caller
548
+ // changes, because the client it gets changes with it.
549
+ it("rebuilds what a component memoizes on the client after a switch: useMemo, an effect, and React Compiler's output", async () => {
550
+ const test = replica({ seed: 1 })
551
+ function Memoized() {
552
+ const client = useClient()
553
+ const who = client.canister<Who>(WHO, { id: CANISTER })
554
+ const options = useMemo(
555
+ () => client.queryOptions(who, "whoami"),
556
+ [client, who]
557
+ )
558
+ const read = useQuery(options)
559
+ return (
560
+ <p data-testid="memo">
561
+ {read.isError ? "ERROR" : (read.data ?? "loading")}
562
+ </p>
563
+ )
564
+ }
565
+ function Effect() {
566
+ const client = useClient()
567
+ const [shown, setShown] = useState("loading")
568
+ useEffect(() => {
569
+ const who = client.canister<Who>(WHO, { id: CANISTER })
570
+ let current = true
571
+ setShown("loading")
572
+ client.queryClient.fetchQuery(client.queryOptions(who, "whoami")).then(
573
+ (data) => current && setShown(data),
574
+ () => current && setShown("ERROR")
575
+ )
576
+ return () => {
577
+ current = false
578
+ }
579
+ }, [client])
580
+ return <p data-testid="effect">{shown}</p>
581
+ }
582
+ /**
583
+ * What React Compiler 1.0 emits for
584
+ * `useQuery(client.queryOptions(who, "whoami"))`, written by hand: its
585
+ * cache is the `useState` array of `react-compiler-runtime`'s `c()`
586
+ * (React 19's own `c` keeps the same array on the fiber), and a slot is
587
+ * computed again only when an input it read changed by identity.
588
+ */
589
+ function Compiled() {
590
+ const $ = useState(() => new Array<unknown>(5).fill(EMPTY))[0]
591
+ const client = useClient()
592
+ let who: Canister<Who>
593
+ if ($[0] !== client) {
594
+ who = client.canister<Who>(WHO, { id: CANISTER })
595
+ $[0] = client
596
+ $[1] = who
597
+ } else {
598
+ who = $[1] as Canister<Who>
599
+ }
600
+ let options: WhoamiOptions
601
+ if ($[2] !== client || $[3] !== who) {
602
+ options = whoamiOptions(client, who)
603
+ $[2] = client
604
+ $[3] = who
605
+ $[4] = options
606
+ } else {
607
+ options = $[4] as WhoamiOptions
608
+ }
609
+ const read = useQuery(options)
610
+ return (
611
+ <p data-testid="compiled">
612
+ {read.isError ? "ERROR" : (read.data ?? "loading")}
613
+ </p>
614
+ )
615
+ }
616
+ render(
617
+ <ReactorProvider client={() => test.client}>
618
+ <Memoized />
619
+ <Effect />
620
+ <Compiled />
621
+ </ReactorProvider>
622
+ )
623
+ const all = ["memo", "effect", "compiled"] as const
624
+ const shown = () => all.map((id) => screen.getByTestId(id).textContent)
625
+ const first = test.auth.getPrincipal()?.toText()
626
+ await waitFor(() =>
627
+ expect(shown()).toEqual(all.map(() => `read by ${first}`))
628
+ )
629
+
630
+ act(() => {
631
+ test.auth.switchTo(2)
632
+ })
633
+ const second = test.auth.getPrincipal()?.toText()
634
+ expect(second).not.toBe(first)
635
+ // Never the previous caller's data under the new caller.
636
+ for (const text of shown()) expect(text).not.toBe(`read by ${first}`)
637
+ await waitFor(() =>
638
+ expect(shown()).toEqual(all.map(() => `read by ${second}`))
639
+ )
640
+
641
+ // A refetch reads as the caller now, and is never refused.
642
+ await act(() => test.client.queryClient.invalidateQueries())
643
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
644
+ expect(shown()).toEqual(all.map(() => `read by ${second}`))
645
+ expect(new Set(whoamiCallers(test).slice(-2))).toEqual(new Set([second]))
646
+ })
647
+
648
+ // #894: the client a render got writes as that render's caller or not at
649
+ // all, so a press handled before a switch reaches the render is refused
650
+ // instead of signed by the new caller, with no guard of the app's.
651
+ it("refuses a write pressed in the batch of a switch, before the render shows the new caller, and sends nothing", async () => {
652
+ const test = replica({ seed: 1 })
653
+ const failures: unknown[] = []
654
+ function Stamp() {
655
+ const client = useClient()
656
+ const who = client.canister<Who>(WHO, { id: CANISTER })
657
+ const stamp = useMutation({
658
+ ...client.mutationOptions(who, "stamp"),
659
+ onError: (error) => failures.push(error),
660
+ })
661
+ return (
662
+ <button data-status={stamp.status} onClick={() => stamp.mutate()}>
663
+ {stamp.data ?? `Stamp as ${client.caller()}`}
664
+ </button>
665
+ )
666
+ }
667
+ render(
668
+ <ReactorProvider client={() => test.client}>
669
+ <Stamp />
670
+ </ReactorProvider>
671
+ )
672
+ const first = test.auth.getPrincipal()?.toText()
673
+ const button = screen.getByRole("button", { name: `Stamp as ${first}` })
674
+ const stamps = () =>
675
+ test.requests.filter(
676
+ ({ methodName, endpoint }) =>
677
+ methodName === "stamp" && endpoint === "call"
678
+ )
679
+
680
+ act(() => {
681
+ test.auth.switchTo(2)
682
+ fireEvent.click(button)
683
+ })
684
+ const second = test.auth.getPrincipal()?.toText()
685
+ await screen.findByRole("button", { name: `Stamp as ${second}` })
686
+ await waitFor(() => expect(failures).toHaveLength(1))
687
+ expect(failures[0]).toMatchObject({
688
+ kind: "cancelled",
689
+ code: "caller_changed",
690
+ mayHaveExecuted: false,
691
+ })
692
+ expect(stamps()).toEqual([])
693
+ // The refusal reaches the options the press used (their onError); the
694
+ // mutation the new render shows starts over for the new caller, idle:
695
+ // the key holds the caller, so TanStack detached it from the stale run.
696
+ expect(
697
+ screen.getByRole("button", { name: `Stamp as ${second}` }).dataset.status
698
+ ).toBe("idle")
699
+
700
+ // The render that shows the new caller writes as them.
701
+ fireEvent.click(screen.getByRole("button", { name: `Stamp as ${second}` }))
702
+ await screen.findByRole("button", { name: `written by ${second}` })
703
+ expect(stamps().map(({ caller }) => caller)).toEqual([second])
704
+ })
705
+
706
+ // #894: a handler that signs in and then writes presses with the options
707
+ // of the last render React committed. The sign-in's update renders and
708
+ // commits before the handler goes on, so the write goes out as the user;
709
+ // when that render cannot commit yet (it suspends on the user's data), the
710
+ // press still has the anonymous caller's options and is refused, sending
711
+ // nothing. A direct call signs as the caller current either way.
712
+ it.each([
713
+ ["commits before the handler goes on", false, "sent as the user"],
714
+ ["suspends on the user's data", true, "refused"],
715
+ ])(
716
+ "decides a write pressed right after awaiting a sign-in by whether the signed-in render %s: %s",
717
+ async (_, suspends, outcome) => {
718
+ const test = replica({ seed: 1, signedIn: false })
719
+ const failures: unknown[] = []
720
+ let direct: Promise<string> | undefined
721
+ function Read({ client }: { client: Client }) {
722
+ const who = client.canister<Who>(WHO, { id: CANISTER })
723
+ return (
724
+ <p>{useSuspenseQuery(client.queryOptions(who, "whoami")).data}</p>
725
+ )
726
+ }
727
+ function SignInAndStamp() {
728
+ const client = useClient()
729
+ const who = client.canister<Who>(WHO, { id: CANISTER })
730
+ const stamp = useMutation({
731
+ ...client.mutationOptions(who, "stamp"),
732
+ onError: (error) => failures.push(error),
733
+ })
734
+ const signInAndStamp = async () => {
735
+ await client.signIn(1)
736
+ stamp.mutate()
737
+ }
738
+ const signInAndCall = async () => {
739
+ await client.signIn(1)
740
+ direct = who.stamp()
741
+ }
742
+ return (
743
+ <>
744
+ {suspends && <Read client={client} />}
745
+ <button onClick={() => void signInAndStamp()}>
746
+ sign in, mutate
747
+ </button>
748
+ <button onClick={() => void signInAndCall()}>sign in, call</button>
749
+ <output>{stamp.data ?? stamp.status}</output>
750
+ </>
751
+ )
752
+ }
753
+ render(
754
+ <ReactorProvider client={() => test.client}>
755
+ <Suspense fallback={<p>loading</p>}>
756
+ <SignInAndStamp />
757
+ </Suspense>
758
+ </ReactorProvider>
759
+ )
760
+ await screen.findByRole("button", { name: "sign in, mutate" })
761
+ const stamps = () =>
762
+ test.requests
763
+ .filter(
764
+ ({ methodName, endpoint }) =>
765
+ methodName === "stamp" && endpoint === "call"
766
+ )
767
+ .map(({ caller }) => caller)
768
+
769
+ fireEvent.click(screen.getByRole("button", { name: "sign in, mutate" }))
770
+ for (let tick = 0; tick < 5; tick++) await act(macrotask)
771
+ const user = test.auth.getPrincipal()?.toText()
772
+ if (outcome === "refused") {
773
+ await waitFor(() => expect(failures).toHaveLength(1))
774
+ expect(failures[0]).toMatchObject({
775
+ kind: "cancelled",
776
+ code: "caller_changed",
777
+ mayHaveExecuted: false,
778
+ })
779
+ expect(stamps()).toEqual([])
780
+ } else {
781
+ await screen.findByText(`written by ${user}`)
782
+ expect(failures).toEqual([])
783
+ expect(stamps()).toEqual([user])
784
+ }
785
+
786
+ // A direct call made after the sign-in signs as the user either way.
787
+ await act(async () => {
788
+ await test.client.signOut()
789
+ })
790
+ await screen.findByRole("button", { name: "sign in, call" })
791
+ fireEvent.click(screen.getByRole("button", { name: "sign in, call" }))
792
+ for (let tick = 0; tick < 5; tick++) await act(macrotask)
793
+ await expect(direct).resolves.toBe(`written by ${user}`)
794
+ expect(stamps()[stamps().length - 1]).toBe(user)
795
+ }
796
+ )
797
+
798
+ it("returns a view per caller after a hydration too: the server's caller for the hydrating render, the user's after it", async () => {
799
+ const { json, html } = await serverRender()
800
+ container.innerHTML = html
801
+ const browser = replica({ seed: 7 })
802
+ const user = browser.auth.getPrincipal()?.toText()
803
+ const seen: Client[] = []
804
+ function Probe() {
805
+ const client = useClient()
806
+ seen.push(client)
807
+ return <Reader />
808
+ }
809
+
810
+ await hydrate(page(browser.client, JSON.parse(json), Probe))
811
+
812
+ expect(seen).toHaveLength(2)
813
+ const [hydrating, after] = seen as [Client, Client]
814
+ // The hydrating render's view: the server's caller, the client's own
815
+ // cache and canisters.
816
+ expect(hydrating).not.toBe(browser.client)
817
+ expect(hydrating.caller()).toBe(ANONYMOUS)
818
+ expect(hydrating.authState()).toEqual({
819
+ status: "anonymous",
820
+ principal: ANONYMOUS,
821
+ })
822
+ expect(hydrating.queryClient).toBe(browser.client.queryClient)
823
+ expect(hydrating.canister).toBe(browser.client.canister)
824
+ expect(hydrating.subscribe).toBe(browser.client.subscribe)
825
+ // Then the user's view: another object, over the same client.
826
+ expect(after).not.toBe(hydrating)
827
+ expect(clientOf(after)).toBe(browser.client)
828
+ expect(after.caller()).toBe(user)
829
+ expect(after.authState()).toBe(browser.client.authState())
830
+ })
831
+
832
+ it("reads as nobody when the caller turns anonymous before the hydrating render's effects", async () => {
833
+ function Nav() {
834
+ const client = useClient()
835
+ const who = client.canister<Who>(WHO, { id: CANISTER })
836
+ const read = useQuery(client.queryOptions(who, "whoami"))
837
+ return <main>{read.isError ? "ERROR" : (read.data ?? "loading")}</main>
838
+ }
839
+ let expire!: () => void
840
+ function Expire() {
841
+ useLayoutEffect(() => expire(), [])
842
+ return null
843
+ }
844
+ const tree = (client: Client) => (
845
+ <ReactorProvider client={() => client}>
846
+ <Nav />
847
+ <Expire />
848
+ </ReactorProvider>
849
+ )
850
+ const server = replica({ signedIn: false })
851
+ expire = () => {}
852
+ container.innerHTML = renderToString(tree(server.client))
853
+ const browser = replica({ seed: 7 })
854
+ expire = () => browser.auth.expire()
855
+
856
+ const { recoverable } = await hydrate(tree(browser.client))
857
+
858
+ expect(recoverable).toEqual([])
859
+ await waitFor(() =>
860
+ expect(container.querySelector("main")?.textContent).toBe(
861
+ `read by ${ANONYMOUS}`
862
+ )
863
+ )
864
+ expect(whoamiCallers(browser)).toEqual([ANONYMOUS])
865
+ })
866
+
867
+ // N2, documented misuse: the guides say to read useClient() in render, and
868
+ // pin what keeping its hydrating value does, so that a change to it shows.
869
+ it("leaves a value kept from the hydrating render (state, ref, module variable) the anonymous view for good", async () => {
870
+ let module: Client | undefined
871
+ const kept: Client[] = []
872
+ function Keeper() {
873
+ const client = useClient()
874
+ const [state] = useState(client)
875
+ const ref = useRef(client)
876
+ module ??= client
877
+ kept.splice(0, 3, state, ref.current, module)
878
+ const who = state.canister<Who>(WHO, { id: CANISTER })
879
+ const read = useQuery(state.queryOptions(who, "whoami"))
880
+ return (
881
+ <main data-live={client.caller()} data-kept={state.caller()}>
882
+ {read.isError ? "ERROR" : (read.data ?? "loading")}
883
+ </main>
884
+ )
885
+ }
886
+ const tree = (client: Client, state: DehydratedState) => (
887
+ <ReactorProvider client={() => client}>
888
+ <HydrationBoundary state={state}>
889
+ <Keeper />
890
+ </HydrationBoundary>
891
+ </ReactorProvider>
892
+ )
893
+ const server = replica({ signedIn: false })
894
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
895
+ await server.client.queryClient.prefetchQuery(
896
+ server.client.queryOptions(serverWho, "whoami")
897
+ )
898
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
899
+ container.innerHTML = renderToString(tree(server.client, JSON.parse(json)))
900
+ module = undefined
901
+ const browser = replica({ seed: 7 })
902
+ const user = browser.auth.getPrincipal()?.toText()
903
+
904
+ const { recoverable } = await hydrate(
905
+ tree(browser.client, JSON.parse(json))
906
+ )
907
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
908
+
909
+ expect(recoverable).toEqual([])
910
+ const shown = () => container.querySelector("main")
911
+ // The component rendered again with the client, for the user, but what
912
+ // it kept is the hydrating render's view, and shows the server's data.
913
+ expect(shown()?.dataset.live).toBe(user)
914
+ expect(shown()?.dataset.kept).toBe(ANONYMOUS)
915
+ expect(shown()?.textContent).toBe(`read by ${ANONYMOUS}`)
916
+ expect(kept).toHaveLength(3)
917
+ const who = browser.client.canister<Who>(WHO, { id: CANISTER })
918
+ for (const value of kept) {
919
+ expect(value).not.toBe(browser.client)
920
+ expect(value.caller()).toBe(ANONYMOUS)
921
+ expect(value.queryKey(who, "whoami")).toContain(ANONYMOUS)
922
+ }
923
+ // A read it starts while the user is signed in is cancelled, and sends
924
+ // nothing as anyone.
925
+ await act(() => browser.client.queryClient.invalidateQueries())
926
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
927
+ expect(shown()?.textContent).toBe(`read by ${ANONYMOUS}`)
928
+ expect(whoamiCallers(browser)).toEqual([])
929
+ })
930
+
931
+ it.each([false, true])(
932
+ "hands a nested ReactorProvider given it the client itself, which reads as the user after a signed-in reload and follows a sign-out and a sign-in (StrictMode: %s)",
933
+ async (strict) => {
934
+ const seen: Client[] = []
935
+ function Inner() {
936
+ const client = useClient()
937
+ seen.push(client)
938
+ const who = client.canister<Who>(WHO, { id: CANISTER })
939
+ const read = useQuery(client.queryOptions(who, "whoami"))
940
+ return (
941
+ <main data-caller={client.caller()}>
942
+ {read.isError ? "ERROR" : (read.data ?? "loading")}
943
+ </main>
944
+ )
945
+ }
946
+ // The hydrating render's useClient() is the anonymous view: the nested
947
+ // provider must hold the client it was made over, not the view.
948
+ function Outer() {
949
+ const client = useClient()
950
+ return (
951
+ <ReactorProvider client={() => client}>
952
+ <Inner />
953
+ </ReactorProvider>
954
+ )
955
+ }
956
+ const tree = (client: Client, state: DehydratedState) => {
957
+ const element = (
958
+ <ReactorProvider client={() => client}>
959
+ <HydrationBoundary state={state}>
960
+ <Outer />
961
+ </HydrationBoundary>
962
+ </ReactorProvider>
963
+ )
964
+ return strict ? <StrictMode>{element}</StrictMode> : element
965
+ }
966
+ const server = replica({ signedIn: false })
967
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
968
+ await server.client.queryClient.prefetchQuery(
969
+ server.client.queryOptions(serverWho, "whoami")
970
+ )
971
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
972
+ container.innerHTML = renderToString(
973
+ tree(server.client, JSON.parse(json))
974
+ )
975
+ const browser = replica({ seed: 7 })
976
+ const user = browser.auth.getPrincipal()?.toText()
977
+ const shown = () => container.querySelector("main")
978
+ const commits = watchCommits()
979
+ try {
980
+ const { recoverable } = await hydrate(
981
+ tree(browser.client, JSON.parse(json))
982
+ )
983
+ expect(recoverable).toEqual([])
984
+ await waitFor(() =>
985
+ expect(shown()?.textContent).toBe(`read by ${user}`)
986
+ )
987
+ expect(shown()?.dataset.caller).toBe(user)
988
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
989
+ // StrictMode mounts the reader twice, and each mount may read.
990
+ expect(new Set(whoamiCallers(browser))).toEqual(new Set([user]))
991
+ // The nested provider's tree, past its hydrating render, is on the
992
+ // outer provider's client itself: the user's view of it.
993
+ const last = seen[seen.length - 1] as Client
994
+ expect(clientOf(last)).toBe(browser.client)
995
+ expect(last.caller()).toBe(user)
996
+
997
+ await act(async () => {
998
+ await browser.auth.signOut()
999
+ })
1000
+ await waitFor(() =>
1001
+ expect(shown()?.textContent).toBe(`read by ${ANONYMOUS}`)
1002
+ )
1003
+ expect(shown()?.dataset.caller).toBe(ANONYMOUS)
1004
+
1005
+ await act(async () => {
1006
+ await browser.auth.signIn(3)
1007
+ })
1008
+ const next = browser.auth.getPrincipal()?.toText()
1009
+ expect(next).not.toBe(user)
1010
+ await waitFor(() =>
1011
+ expect(shown()?.textContent).toBe(`read by ${next}`)
1012
+ )
1013
+ expect(shown()?.dataset.caller).toBe(next)
1014
+ expect(
1015
+ commits.commits.filter((html) => html.includes("ERROR"))
1016
+ ).toEqual([])
1017
+ } finally {
1018
+ commits.stop()
1019
+ }
1020
+ }
1021
+ )
1022
+ })
1023
+
1024
+ /**
1025
+ * L1: on a signed-in reload, a Suspense boundary that is still dehydrated
1026
+ * below a component that renders with the caller is rendered on the client
1027
+ * when that component moves on to the session.
1028
+ */
1029
+ describe("a Suspense boundary still dehydrated below a component that renders with the caller", () => {
1030
+ /** The boundary's content: what the server rendered, and the lazy chunk. */
1031
+ function Content() {
1032
+ return <article>from the server</article>
1033
+ }
1034
+
1035
+ /** A nav bar that holds the client for an action, and shows whose keys it would build. */
1036
+ function ActionNav({ Late }: { Late: ComponentType }) {
1037
+ const client = useClient()
1038
+ return (
1039
+ <nav data-caller={client.caller()}>
1040
+ <button onClick={() => void client.signIn()}>sign in</button>
1041
+ <Suspense fallback={<p>fallback</p>}>
1042
+ <Late />
1043
+ </Suspense>
1044
+ </nav>
1045
+ )
1046
+ }
1047
+
1048
+ /** The same, following the session as a header does. */
1049
+ function StatusNav({ Late }: { Late: ComponentType }) {
1050
+ const { principal } = useAuth()
1051
+ return (
1052
+ <nav data-caller={principal}>
1053
+ <Suspense fallback={<p>fallback</p>}>
1054
+ <Late />
1055
+ </Suspense>
1056
+ </nav>
1057
+ )
1058
+ }
1059
+
1060
+ /** The same, reading with the client, and showing an error if its read fails. */
1061
+ function ReadingNav({ Late }: { Late: ComponentType }) {
1062
+ const client = useClient()
1063
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1064
+ const read = useQuery(client.queryOptions(who, "whoami"))
1065
+ return (
1066
+ <nav data-caller={client.caller()}>
1067
+ <b>{read.isError ? "ERROR" : (read.data ?? "loading")}</b>
1068
+ <Suspense fallback={<p>fallback</p>}>
1069
+ <Late />
1070
+ </Suspense>
1071
+ </nav>
1072
+ )
1073
+ }
1074
+
1075
+ const tree = (
1076
+ client: Client,
1077
+ state: DehydratedState,
1078
+ Nav: ComponentType<{ Late: ComponentType }>,
1079
+ Late: ComponentType
1080
+ ) => (
1081
+ <ReactorProvider client={() => client}>
1082
+ <HydrationBoundary state={state}>
1083
+ <Nav Late={Late} />
1084
+ </HydrationBoundary>
1085
+ </ReactorProvider>
1086
+ )
1087
+
1088
+ it.each([
1089
+ ["useClient()", ActionNav, false],
1090
+ ["useAuth()", StatusNav, false],
1091
+ ["useClient() and reads", ReadingNav, false],
1092
+ ["useClient() and reads, under StrictMode", ReadingNav, true],
1093
+ ] as const)(
1094
+ "is rendered on the client when the parent that calls %s moves on, with the user's data and nothing sent as anyone else",
1095
+ async (_, Nav, strict) => {
1096
+ const server = replica({ signedIn: false })
1097
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1098
+ await server.client.queryClient.prefetchQuery(
1099
+ server.client.queryOptions(serverWho, "whoami")
1100
+ )
1101
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
1102
+ const wrap = (element: ReactElement) =>
1103
+ strict ? <StrictMode>{element}</StrictMode> : element
1104
+ container.innerHTML = renderToString(
1105
+ wrap(tree(server.client, JSON.parse(json), Nav, Content))
1106
+ )
1107
+ const served = container.querySelector("article")
1108
+ expect(served?.textContent).toBe("from the server")
1109
+ // Stale, so that mounting the parent's read fetches it again.
1110
+ const state = JSON.parse(json) as DehydratedState
1111
+ for (const query of state.queries) query.state.dataUpdatedAt = 1
1112
+
1113
+ const { Late, deliver } = lateChunk(Content)
1114
+ const browser = replica({ seed: 7 })
1115
+ const user = browser.auth.getPrincipal()?.toText()
1116
+ const errors = watchErrors(browser.client)
1117
+ const commits = watchCommits()
1118
+ try {
1119
+ const { recoverable, logged } = await hydrate(
1120
+ wrap(tree(browser.client, state, Nav, Late))
1121
+ )
1122
+
1123
+ // The parent moved on to the session right after hydrating, and that
1124
+ // update reached the boundary while its chunk was still loading:
1125
+ // React rendered it on the client, so it shows its fallback. React 18
1126
+ // reports it; React 19 does not.
1127
+ expect(container.querySelector("nav")?.dataset.caller).toBe(user)
1128
+ expect(container.querySelector("p")?.textContent).toBe("fallback")
1129
+ expect(served?.isConnected).toBe(false)
1130
+ if (REACT_18) expect(recoverable.length).toBeGreaterThan(0)
1131
+ else expect(recoverable).toEqual([])
1132
+ expect(logged).not.toHaveBeenCalled()
1133
+
1134
+ await act(async () => deliver())
1135
+ await act(macrotask)
1136
+ expect(container.querySelector("article")?.textContent).toBe(
1137
+ "from the server"
1138
+ )
1139
+ if (Nav === ReadingNav) {
1140
+ await waitFor(() =>
1141
+ expect(container.querySelector("b")?.textContent).toBe(
1142
+ `read by ${user}`
1143
+ )
1144
+ )
1145
+ }
1146
+ // Its data is still the user's (D20): nothing was sent as anyone
1147
+ // else, and the cancelled anonymous read, if it ran, was never shown.
1148
+ expect(new Set(whoamiCallers(browser))).toEqual(
1149
+ new Set(Nav === ReadingNav ? [user] : [])
1150
+ )
1151
+ expect(
1152
+ commits.commits.filter((html) => html.includes("ERROR"))
1153
+ ).toEqual([])
1154
+ for (const error of errors.errors) {
1155
+ expect(error).toMatchObject({
1156
+ kind: "cancelled",
1157
+ code: "caller_changed",
1158
+ })
1159
+ }
1160
+ } finally {
1161
+ commits.stop()
1162
+ errors.stop()
1163
+ }
1164
+ }
1165
+ )
1166
+
1167
+ it.each([
1168
+ ["useClient()", ActionNav],
1169
+ ["useAuth()", StatusNav],
1170
+ ["useClient() and reads", ReadingNav],
1171
+ ] as const)(
1172
+ "renders a streamed boundary on the client when the parent that calls %s moves on, on React 18 too",
1173
+ async (_, Nav) => {
1174
+ const server = replica({ signedIn: false })
1175
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1176
+ await server.client.queryClient.prefetchQuery(
1177
+ server.client.queryOptions(serverWho, "whoami")
1178
+ )
1179
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
1180
+ container.innerHTML = streamed(
1181
+ renderToString(tree(server.client, JSON.parse(json), Nav, Content)),
1182
+ "<article>from the server</article>"
1183
+ )
1184
+ await whileLoading(async () => {
1185
+ const browser = replica({ seed: 7 })
1186
+ const user = browser.auth.getPrincipal()?.toText()
1187
+ const { recoverable, logged } = await hydrate(
1188
+ tree(browser.client, JSON.parse(json), Nav, Content)
1189
+ )
1190
+ await act(macrotask)
1191
+
1192
+ // The move is a synchronous update, and it reached the boundary
1193
+ // before its HTML arrived: React rendered it on the client, and React
1194
+ // 18 reports that.
1195
+ expect(container.querySelector("nav")?.dataset.caller).toBe(user)
1196
+ expect(container.querySelector("template")).toBeNull()
1197
+ expect(container.querySelector("article")?.textContent).toBe(
1198
+ "from the server"
1199
+ )
1200
+ if (REACT_18) expect(recoverable.length).toBeGreaterThan(0)
1201
+ else expect(recoverable).toEqual([])
1202
+ expect(logged).not.toHaveBeenCalled()
1203
+ })
1204
+ }
1205
+ )
1206
+
1207
+ it.each([
1208
+ ["whose stored session expired", "expired"],
1209
+ ["whose session is signed in elsewhere", "elsewhere"],
1210
+ ] as const)(
1211
+ "is rendered on the client when a useAuth() parent shows its status after hydrating, in a tab %s",
1212
+ async (_, kind) => {
1213
+ // The caller stays anonymous in such a tab, but the status differs from
1214
+ // the server's, so the parent moves on as on a signed-in reload.
1215
+ function StatusOnlyNav({ Late }: { Late: ComponentType }) {
1216
+ const { status } = useAuth()
1217
+ return (
1218
+ <nav data-status={status}>
1219
+ <Suspense fallback={<p>fallback</p>}>
1220
+ <Late />
1221
+ </Suspense>
1222
+ </nav>
1223
+ )
1224
+ }
1225
+ const empty: DehydratedState = { mutations: [], queries: [] }
1226
+ const { server, browser } = await steadyTab(kind)
1227
+ container.innerHTML = renderToString(
1228
+ tree(server, empty, StatusOnlyNav, Content)
1229
+ )
1230
+ const served = container.querySelector("article")
1231
+ expect(served?.textContent).toBe("from the server")
1232
+
1233
+ const { Late, deliver } = lateChunk(Content)
1234
+ const { recoverable, logged } = await hydrate(
1235
+ tree(browser, empty, StatusOnlyNav, Late)
1236
+ )
1237
+
1238
+ expect(container.querySelector("nav")?.dataset.status).toBe(
1239
+ ownStatus[kind]
1240
+ )
1241
+ expect(container.querySelector("p")?.textContent).toBe("fallback")
1242
+ expect(served?.isConnected).toBe(false)
1243
+ if (REACT_18) expect(recoverable.length).toBeGreaterThan(0)
1244
+ else expect(recoverable).toEqual([])
1245
+ expect(logged).not.toHaveBeenCalled()
1246
+
1247
+ await act(async () => deliver())
1248
+ await act(macrotask)
1249
+ expect(container.querySelector("article")?.textContent).toBe(
1250
+ "from the server"
1251
+ )
1252
+ }
1253
+ )
1254
+
1255
+ it("keeps the server's HTML when it is passed as children to the component that moves on", async () => {
1256
+ function Nav({ children }: { children: ReactNode }) {
1257
+ const client = useClient()
1258
+ return <nav data-caller={client.caller()}>{children}</nav>
1259
+ }
1260
+ const tree = (client: Client, Late: ComponentType) => (
1261
+ <ReactorProvider client={() => client}>
1262
+ <Nav>
1263
+ <Suspense fallback={<p>fallback</p>}>
1264
+ <Late />
1265
+ </Suspense>
1266
+ </Nav>
1267
+ </ReactorProvider>
1268
+ )
1269
+ const server = replica({ signedIn: false })
1270
+ container.innerHTML = renderToString(tree(server.client, Content))
1271
+ const served = container.querySelector("article")
1272
+ const { Late, deliver } = lateChunk(Content)
1273
+ const browser = replica({ seed: 7 })
1274
+
1275
+ const { recoverable } = await hydrate(tree(browser.client, Late))
1276
+
1277
+ expect(container.querySelector("nav")?.dataset.caller).toBe(
1278
+ browser.auth.getPrincipal()?.toText()
1279
+ )
1280
+ expect(container.querySelector("article")).toBe(served)
1281
+ expect(container.querySelector("p")).toBeNull()
1282
+ await act(async () => deliver())
1283
+ await act(macrotask)
1284
+ expect(container.querySelector("article")).toBe(served)
1285
+ expect(recoverable).toEqual([])
1286
+ })
1287
+
1288
+ it("keeps a streamed boundary pending when it is passed as children to the component that moves on", async () => {
1289
+ function Nav({ children }: { children: ReactNode }) {
1290
+ const client = useClient()
1291
+ return <nav data-caller={client.caller()}>{children}</nav>
1292
+ }
1293
+ const tree = (client: Client) => (
1294
+ <ReactorProvider client={() => client}>
1295
+ <Nav>
1296
+ <Suspense fallback={<p>fallback</p>}>
1297
+ <Content />
1298
+ </Suspense>
1299
+ </Nav>
1300
+ </ReactorProvider>
1301
+ )
1302
+ const server = replica({ signedIn: false })
1303
+ container.innerHTML = streamed(
1304
+ renderToString(tree(server.client)),
1305
+ "<article>from the server</article>"
1306
+ )
1307
+ await whileLoading(async () => {
1308
+ const browser = replica({ seed: 7 })
1309
+ const { recoverable } = await hydrate(tree(browser.client))
1310
+ await act(macrotask)
1311
+
1312
+ // The nav moved on to the session; the boundary still waits for the
1313
+ // stream.
1314
+ expect(container.querySelector("nav")?.dataset.caller).toBe(
1315
+ browser.auth.getPrincipal()?.toText()
1316
+ )
1317
+ expect(recoverable).toEqual([])
1318
+ expect(container.querySelector("template")).not.toBeNull()
1319
+ expect(container.querySelector("article")).toBeNull()
1320
+ })
1321
+ })
1322
+
1323
+ // The `em` assertion can only fail on React 18, which verify:peer-floors
1324
+ // runs: React 19 commits an unrelated transition even while another one is
1325
+ // held, so it passes there with or without a held transition, and
1326
+ // verify:faults (React 19 only) cannot verify it. Checked by hand on 18.0
1327
+ // with the first attempt's held settle transition put back.
1328
+ it("holds no transition: an unrelated one commits at once while the chunk loads", async () => {
1329
+ let bump!: () => void
1330
+ function Counter() {
1331
+ const [count, setCount] = useState(0)
1332
+ bump = () => startTransition(() => setCount(1))
1333
+ return <em>{count}</em>
1334
+ }
1335
+ const pageWith = (client: Client, Late: ComponentType) => (
1336
+ <ReactorProvider client={() => client}>
1337
+ <ActionNav Late={Late} />
1338
+ <Counter />
1339
+ </ReactorProvider>
1340
+ )
1341
+ const server = replica({ signedIn: false })
1342
+ container.innerHTML = renderToString(pageWith(server.client, Content))
1343
+ const { Late, deliver } = lateChunk(Content)
1344
+ const browser = replica({ seed: 7 })
1345
+ await hydrate(pageWith(browser.client, Late))
1346
+
1347
+ await act(async () => bump())
1348
+ await act(macrotask)
1349
+ expect(container.querySelector("em")?.textContent).toBe("1")
1350
+
1351
+ await act(async () => deliver())
1352
+ await act(macrotask)
1353
+ expect(container.querySelector("article")?.textContent).toBe(
1354
+ "from the server"
1355
+ )
1356
+ expect(container.querySelector("nav")?.dataset.caller).toBe(
1357
+ browser.auth.getPrincipal()?.toText()
1358
+ )
1359
+ })
1360
+
1361
+ it.each([
1362
+ ["with the server's data", true],
1363
+ ["without the server's data", false],
1364
+ ] as const)(
1365
+ "lets a late child that reads the parent's key with useSuspenseQuery settle as the user: %s",
1366
+ async (_, dehydrated) => {
1367
+ const seen: (string | undefined)[] = []
1368
+ function Nav({ Late }: { Late: ComponentType }) {
1369
+ const client = useClient()
1370
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1371
+ const read = useQuery(client.queryOptions(who, "whoami"))
1372
+ seen.push(read.data)
1373
+ return (
1374
+ <nav>
1375
+ <b>{read.isError ? "ERROR" : "nav"}</b>
1376
+ <Suspense fallback={<p>fallback</p>}>
1377
+ <Late />
1378
+ </Suspense>
1379
+ </nav>
1380
+ )
1381
+ }
1382
+ let clicks = 0
1383
+ function Child() {
1384
+ const client = useClient()
1385
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1386
+ const read = useSuspenseQuery(client.queryOptions(who, "whoami"))
1387
+ return <article onClick={() => clicks++}>{read.data}</article>
1388
+ }
1389
+ const server = replica({ signedIn: false })
1390
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1391
+ await server.client.queryClient.prefetchQuery(
1392
+ server.client.queryOptions(serverWho, "whoami")
1393
+ )
1394
+ // The server rendered the read either way; it sent its state or not.
1395
+ const json = JSON.stringify(
1396
+ dehydrated
1397
+ ? dehydrate(server.client.queryClient)
1398
+ : { mutations: [], queries: [] }
1399
+ )
1400
+ container.innerHTML = renderToString(
1401
+ tree(server.client, JSON.parse(json), Nav, Child)
1402
+ )
1403
+ const state = JSON.parse(json) as DehydratedState
1404
+ for (const query of state.queries) query.state.dataUpdatedAt = 1
1405
+ const { Late, deliver } = lateChunk(Child)
1406
+ const browser = replica({ seed: 7 })
1407
+ const user = browser.auth.getPrincipal()?.toText()
1408
+ const commits = watchCommits()
1409
+ try {
1410
+ await hydrate(tree(browser.client, state, Nav, Late))
1411
+ await act(async () => deliver())
1412
+ for (let tick = 0; tick < 5; tick++) await act(macrotask)
1413
+
1414
+ // Nothing waits for anything: the child renders as the user and its
1415
+ // handler runs.
1416
+ await waitFor(() =>
1417
+ expect(container.querySelector("article")?.textContent).toBe(
1418
+ `read by ${user}`
1419
+ )
1420
+ )
1421
+ act(() => container.querySelector("article")?.click())
1422
+ expect(clicks).toBe(1)
1423
+ expect(seen[seen.length - 1]).toBe(`read by ${user}`)
1424
+ const fetching = browser.client.queryClient
1425
+ .getQueryCache()
1426
+ .getAll()
1427
+ .filter((query) => query.state.fetchStatus !== "idle")
1428
+ expect(fetching).toEqual([])
1429
+ expect(
1430
+ commits.commits.filter((html) => html.includes("ERROR"))
1431
+ ).toEqual([])
1432
+ expect(new Set(whoamiCallers(browser))).toEqual(new Set([user]))
1433
+ } finally {
1434
+ commits.stop()
1435
+ }
1436
+ }
1437
+ )
1438
+ })
1439
+
1440
+ /**
1441
+ * L2: a read the hydrating render built for the anonymous caller may run once
1442
+ * while the user is current. It is cancelled before anything is sent, and the
1443
+ * component has moved on to the user's key by then.
1444
+ */
1445
+ describe("a read built for the caller a hydrating render shows", () => {
1446
+ it("is cancelled when an effect of the hydrating render runs it, and sent as the user once the effect runs again", async () => {
1447
+ function Prefetch() {
1448
+ const client = useClient()
1449
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1450
+ useEffect(() => {
1451
+ void client.queryClient.prefetchQuery(
1452
+ client.queryOptions(who, "whoami")
1453
+ )
1454
+ }, [client, who])
1455
+ return <main>page</main>
1456
+ }
1457
+ const tree = (client: Client) => (
1458
+ <ReactorProvider client={() => client}>
1459
+ <Prefetch />
1460
+ </ReactorProvider>
1461
+ )
1462
+ const server = replica({ signedIn: false })
1463
+ container.innerHTML = renderToString(tree(server.client))
1464
+ const browser = replica({ seed: 7 })
1465
+ const user = browser.auth.getPrincipal()?.toText()
1466
+ const errors = watchErrors(browser.client)
1467
+
1468
+ const { recoverable } = await hydrate(tree(browser.client))
1469
+ await waitFor(() => expect(whoamiCallers(browser)).toEqual([user]))
1470
+ errors.stop()
1471
+
1472
+ // A listener of the client's QueryCache sees it, as kind "cancelled",
1473
+ // and the README's filter drops it, so such a listener reports nothing.
1474
+ expect(recoverable).toEqual([])
1475
+ expect(errors.errors).toHaveLength(1)
1476
+ expect(errors.errors[0]).toMatchObject({
1477
+ kind: "cancelled",
1478
+ code: "caller_changed",
1479
+ })
1480
+ expect(
1481
+ errors.errors.filter(
1482
+ (error) => !(isReactorError(error) && error.kind === "cancelled")
1483
+ )
1484
+ ).toEqual([])
1485
+ })
1486
+
1487
+ it("is never shown as an error by a component that hydrated", async () => {
1488
+ // The parent's useSuspenseQuery read is stale, so mounting it fetches it
1489
+ // again while the user is current.
1490
+ function SuspenseReader() {
1491
+ const client = useClient()
1492
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1493
+ const read = useSuspenseQuery(client.queryOptions(who, "whoami"))
1494
+ return <main>{read.isError ? "ERROR" : read.data}</main>
1495
+ }
1496
+ const server = replica({ signedIn: false })
1497
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1498
+ await server.client.queryClient.prefetchQuery(
1499
+ server.client.queryOptions(serverWho, "whoami")
1500
+ )
1501
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
1502
+ container.innerHTML = renderToString(
1503
+ page(server.client, JSON.parse(json), SuspenseReader)
1504
+ )
1505
+ const served = container.querySelector("main")
1506
+ const state = JSON.parse(json) as DehydratedState
1507
+ for (const query of state.queries) query.state.dataUpdatedAt = 1
1508
+ const browser = replica({ seed: 7 })
1509
+ const user = browser.auth.getPrincipal()?.toText()
1510
+ const commits = watchCommits()
1511
+ const errors = watchErrors(browser.client)
1512
+
1513
+ const { recoverable, logged } = await hydrate(
1514
+ <StrictMode>{page(browser.client, state, SuspenseReader)}</StrictMode>
1515
+ )
1516
+ await waitFor(() =>
1517
+ expect(container.querySelector("main")?.textContent).toBe(
1518
+ `read by ${user}`
1519
+ )
1520
+ )
1521
+ commits.stop()
1522
+ errors.stop()
1523
+
1524
+ expect(recoverable).toEqual([])
1525
+ expect(logged).not.toHaveBeenCalled()
1526
+ expect(container.querySelector("main")).toBe(served)
1527
+ expect(commits.commits.filter((html) => html.includes("ERROR"))).toEqual([])
1528
+ expect(whoamiCallers(browser)).toEqual([user])
1529
+ for (const error of errors.errors) {
1530
+ expect(error).toMatchObject({ kind: "cancelled", code: "caller_changed" })
1531
+ }
1532
+ })
1533
+ })
1534
+
1535
+ /**
1536
+ * L2, later: a read the hydrating render built is cancelled while the user is
1537
+ * current, and a component that hydrated may still be watching its key when
1538
+ * that happens (React hides it, mounted, while the move suspends). The read
1539
+ * leaves the key as it found it, so once the caller is anonymous again (a
1540
+ * sign-out, a session that expires) the key shows the server's data and is
1541
+ * read again as nobody, never the cancelled read as an error.
1542
+ */
1543
+ describe("a key whose read the hydrating render built, once the caller is anonymous again", () => {
1544
+ /** A second query, so that a sibling can suspend on a key of its own. */
1545
+ const PAIR = c.service({
1546
+ whoami: c.func([], [c.text], "query"),
1547
+ other: c.func([], [c.text], "query"),
1548
+ })
1549
+ type Pair = { whoami: () => Promise<string>; other: () => Promise<string> }
1550
+ function pairReplica(options: { seed?: number; signedIn?: boolean } = {}) {
1551
+ const test = createTestClient({
1552
+ identity: options.seed ?? 7,
1553
+ signedIn: options.signedIn ?? true,
1554
+ })
1555
+ test.mock<Pair>(PAIR, CANISTER, {
1556
+ whoami: ({ caller }) => `read by ${caller}`,
1557
+ other: ({ caller }) => `other by ${caller}`,
1558
+ })
1559
+ tests.push(test)
1560
+ return test
1561
+ }
1562
+
1563
+ /** The server's state of every read it prefetched, made stale. */
1564
+ async function prefetched(
1565
+ server: Replica | ReturnType<typeof pairReplica>,
1566
+ methods: readonly ("whoami" | "other")[]
1567
+ ) {
1568
+ const who = server.client.canister<Pair>(PAIR, { id: CANISTER })
1569
+ for (const method of methods) {
1570
+ await server.client.queryClient.prefetchQuery(
1571
+ server.client.queryOptions(who, method)
1572
+ )
1573
+ }
1574
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
1575
+ const stale = JSON.parse(json) as DehydratedState
1576
+ for (const query of stale.queries) query.state.dataUpdatedAt = 1
1577
+ return { fresh: JSON.parse(json) as DehydratedState, stale }
1578
+ }
1579
+
1580
+ const leave = {
1581
+ "a sign-out": async (test: Replica) => {
1582
+ await act(async () => {
1583
+ await test.auth.signOut()
1584
+ })
1585
+ },
1586
+ "an expired session": async (test: Replica) => {
1587
+ await act(async () => {
1588
+ test.auth.expire()
1589
+ await macrotask()
1590
+ })
1591
+ },
1592
+ }
1593
+
1594
+ const anonymousWhoami = (test: Replica) =>
1595
+ test.client.queryClient
1596
+ .getQueryCache()
1597
+ .getAll()
1598
+ .find(
1599
+ (query) =>
1600
+ query.queryKey.includes(ANONYMOUS) &&
1601
+ query.queryKey.includes("whoami")
1602
+ )
1603
+
1604
+ it.each([
1605
+ ["a sign-out", false],
1606
+ ["a sign-out", true],
1607
+ ["an expired session", false],
1608
+ ["an expired session", true],
1609
+ ] as const)(
1610
+ "is shown by a useSuspenseQuery reader with its data after %s, never as an error (StrictMode: %s)",
1611
+ async (how, strict) => {
1612
+ function SuspenseReader() {
1613
+ const client = useClient()
1614
+ const who = client.canister<Pair>(PAIR, { id: CANISTER })
1615
+ const read = useSuspenseQuery(client.queryOptions(who, "whoami"))
1616
+ return <main>{read.isError ? "ERROR" : read.data}</main>
1617
+ }
1618
+ const wrap = (element: ReactElement) =>
1619
+ strict ? <StrictMode>{element}</StrictMode> : element
1620
+ const server = pairReplica({ signedIn: false })
1621
+ const { fresh, stale } = await prefetched(server, ["whoami"])
1622
+ container.innerHTML = renderToString(
1623
+ wrap(page(server.client, fresh, SuspenseReader))
1624
+ )
1625
+ const browser = pairReplica({ seed: 7 })
1626
+ const user = browser.auth.getPrincipal()?.toText()
1627
+ const commits = watchCommits()
1628
+ try {
1629
+ await hydrate(wrap(page(browser.client, stale, SuspenseReader)))
1630
+ await waitFor(() =>
1631
+ expect(container.querySelector("main")?.textContent).toBe(
1632
+ `read by ${user}`
1633
+ )
1634
+ )
1635
+ expect(whoamiCallers(browser)).toEqual([user])
1636
+
1637
+ await leave[how](browser)
1638
+ await waitFor(() =>
1639
+ expect(whoamiCallers(browser)).toEqual([user, ANONYMOUS])
1640
+ )
1641
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
1642
+
1643
+ expect(container.querySelector("main")?.textContent).toBe(
1644
+ `read by ${ANONYMOUS}`
1645
+ )
1646
+ expect(
1647
+ commits.commits.filter((html) => html.includes("ERROR"))
1648
+ ).toEqual([])
1649
+ expect(anonymousWhoami(browser)?.state).toMatchObject({
1650
+ status: "success",
1651
+ fetchStatus: "idle",
1652
+ })
1653
+ } finally {
1654
+ commits.stop()
1655
+ }
1656
+ }
1657
+ )
1658
+
1659
+ // The rows without data guard an invariant: a reader with no data of the
1660
+ // server's has nothing a cancelled read could leave behind.
1661
+ it.each([
1662
+ ["with the server's data", false, true],
1663
+ ["with the server's data", true, true],
1664
+ ["without data", false, false],
1665
+ ["without data", true, false],
1666
+ ] as const)(
1667
+ "is shown by a useQuery reader %s whose sibling's suspense read suspends the move, never as an error (StrictMode: %s)",
1668
+ async (_, strict, withData) => {
1669
+ function Plain() {
1670
+ const client = useClient()
1671
+ const who = client.canister<Pair>(PAIR, { id: CANISTER })
1672
+ const read = useQuery(client.queryOptions(who, "whoami"))
1673
+ return <main>{read.isError ? "ERROR" : (read.data ?? "loading")}</main>
1674
+ }
1675
+ function Sibling() {
1676
+ const client = useClient()
1677
+ const who = client.canister<Pair>(PAIR, { id: CANISTER })
1678
+ const read = useSuspenseQuery(client.queryOptions(who, "other"))
1679
+ return <aside>{read.data}</aside>
1680
+ }
1681
+ const tree = (client: Client, state: DehydratedState) => {
1682
+ const element = (
1683
+ <ReactorProvider client={() => client}>
1684
+ <HydrationBoundary state={state}>
1685
+ <Suspense fallback={<p>loading</p>}>
1686
+ <Plain />
1687
+ <Sibling />
1688
+ </Suspense>
1689
+ </HydrationBoundary>
1690
+ </ReactorProvider>
1691
+ )
1692
+ return strict ? <StrictMode>{element}</StrictMode> : element
1693
+ }
1694
+ const server = pairReplica({ signedIn: false })
1695
+ const { fresh, stale } = await prefetched(
1696
+ server,
1697
+ withData ? ["whoami", "other"] : ["other"]
1698
+ )
1699
+ // Only the reader's data is stale. The sibling's is fresh, and the move
1700
+ // suspends on the user's key for it, which has nothing yet.
1701
+ const sibling = fresh.queries.find((q) => q.queryKey.includes("other"))
1702
+ for (const query of stale.queries) {
1703
+ if (query.queryKey.includes("other") && sibling !== undefined) {
1704
+ query.state.dataUpdatedAt = sibling.state.dataUpdatedAt
1705
+ }
1706
+ }
1707
+ container.innerHTML = renderToString(tree(server.client, fresh))
1708
+ const browser = pairReplica({ seed: 7 })
1709
+ const user = browser.auth.getPrincipal()?.toText()
1710
+ const commits = watchCommits()
1711
+ try {
1712
+ await hydrate(tree(browser.client, stale))
1713
+ await waitFor(() =>
1714
+ expect(container.querySelector("main")?.textContent).toBe(
1715
+ `read by ${user}`
1716
+ )
1717
+ )
1718
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
1719
+ // StrictMode mounts the reader twice, and each mount may read.
1720
+ expect(new Set(whoamiCallers(browser))).toEqual(new Set([user]))
1721
+ const sent = whoamiCallers(browser).length
1722
+
1723
+ await leave["a sign-out"](browser)
1724
+ await waitFor(() =>
1725
+ expect(whoamiCallers(browser).slice(sent)).toEqual([ANONYMOUS])
1726
+ )
1727
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
1728
+
1729
+ expect(container.querySelector("main")?.textContent).toBe(
1730
+ `read by ${ANONYMOUS}`
1731
+ )
1732
+ expect(
1733
+ commits.commits.filter((html) => html.includes("ERROR"))
1734
+ ).toEqual([])
1735
+ } finally {
1736
+ commits.stop()
1737
+ }
1738
+ }
1739
+ )
1740
+
1741
+ it("is shown with its data by a reader mounted after a sign-out, when an effect of the hydrating render fetched it", async () => {
1742
+ let showReader!: () => void
1743
+ function Prefetch() {
1744
+ const client = useClient()
1745
+ const who = client.canister<Pair>(PAIR, { id: CANISTER })
1746
+ const [shown, setShown] = useState(false)
1747
+ showReader = () => setShown(true)
1748
+ useEffect(() => {
1749
+ void client.queryClient.prefetchQuery(
1750
+ client.queryOptions(who, "whoami")
1751
+ )
1752
+ }, [client, who])
1753
+ return shown ? <Reader /> : <main>page</main>
1754
+ }
1755
+ const tree = (client: Client, state: DehydratedState) => (
1756
+ <ReactorProvider client={() => client}>
1757
+ <HydrationBoundary state={state}>
1758
+ <Prefetch />
1759
+ </HydrationBoundary>
1760
+ </ReactorProvider>
1761
+ )
1762
+ const server = pairReplica({ signedIn: false })
1763
+ const { fresh, stale } = await prefetched(server, ["whoami"])
1764
+ container.innerHTML = renderToString(tree(server.client, fresh))
1765
+ const browser = pairReplica({ seed: 7 })
1766
+ const user = browser.auth.getPrincipal()?.toText()
1767
+ await hydrate(tree(browser.client, stale))
1768
+ await waitFor(() => expect(whoamiCallers(browser)).toEqual([user]))
1769
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
1770
+
1771
+ expect(whoamiCallers(browser)).toEqual([user])
1772
+ await leave["a sign-out"](browser)
1773
+ const commits = watchCommits()
1774
+ try {
1775
+ act(() => showReader())
1776
+ await waitFor(() => expect(whoamiCallers(browser)).toContain(ANONYMOUS))
1777
+ for (let tick = 0; tick < 3; tick++) await act(macrotask)
1778
+ expect(container.querySelector("main")?.textContent).toBe(
1779
+ `read by ${ANONYMOUS}`
1780
+ )
1781
+ expect(commits.commits.filter((html) => html.includes("ERROR"))).toEqual(
1782
+ []
1783
+ )
1784
+ } finally {
1785
+ commits.stop()
1786
+ }
1787
+ })
1788
+
1789
+ it("hands an effect that awaits the hydrating render's fetch the data the key had, not a rejection", async () => {
1790
+ const settled: string[] = []
1791
+ function Fetch() {
1792
+ const client = useClient()
1793
+ const who = client.canister<Pair>(PAIR, { id: CANISTER })
1794
+ useEffect(() => {
1795
+ client.queryClient.fetchQuery(client.queryOptions(who, "whoami")).then(
1796
+ (data) => settled.push(`data ${data}`),
1797
+ (error: unknown) =>
1798
+ settled.push(`error ${(error as { code?: string }).code}`)
1799
+ )
1800
+ }, [client, who])
1801
+ return <main>page</main>
1802
+ }
1803
+ const tree = (client: Client, state: DehydratedState) => (
1804
+ <ReactorProvider client={() => client}>
1805
+ <HydrationBoundary state={state}>
1806
+ <Fetch />
1807
+ </HydrationBoundary>
1808
+ </ReactorProvider>
1809
+ )
1810
+ const server = pairReplica({ signedIn: false })
1811
+ const { fresh, stale } = await prefetched(server, ["whoami"])
1812
+ container.innerHTML = renderToString(tree(server.client, fresh))
1813
+ const browser = pairReplica({ seed: 7 })
1814
+ const user = browser.auth.getPrincipal()?.toText()
1815
+ const errors = watchErrors(browser.client)
1816
+ await hydrate(tree(browser.client, stale))
1817
+ await waitFor(() => expect(settled).toHaveLength(2))
1818
+ errors.stop()
1819
+
1820
+ // The view's fetch ran first and was cancelled before anything was sent;
1821
+ // the client's own, after the move, was sent as the user.
1822
+ expect(settled).toEqual([
1823
+ `data read by ${ANONYMOUS}`,
1824
+ `data read by ${user}`,
1825
+ ])
1826
+ expect(whoamiCallers(browser)).toEqual([user])
1827
+ expect(errors.errors).toEqual([])
1828
+ expect(anonymousWhoami(browser)?.state).toMatchObject({
1829
+ status: "success",
1830
+ fetchStatus: "idle",
1831
+ })
1832
+ })
1833
+ })
1834
+
1835
+ describe("a suspense read the server did not dehydrate", () => {
1836
+ it("is cancelled while the user is current, and its boundary renders on the client as the user, never showing an error", async () => {
1837
+ function Read() {
1838
+ const client = useClient()
1839
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1840
+ return (
1841
+ <main>{useSuspenseQuery(client.queryOptions(who, "whoami")).data}</main>
1842
+ )
1843
+ }
1844
+ class Catch extends Component<
1845
+ { children: ReactNode },
1846
+ { failed: boolean }
1847
+ > {
1848
+ state = { failed: false }
1849
+ static getDerivedStateFromError() {
1850
+ return { failed: true }
1851
+ }
1852
+ render() {
1853
+ return this.state.failed ? <b>ERROR</b> : this.props.children
1854
+ }
1855
+ }
1856
+ const tree = (client: Client) => (
1857
+ <ReactorProvider client={() => client}>
1858
+ <Catch>
1859
+ <Suspense fallback={<p>loading</p>}>
1860
+ <Read />
1861
+ </Suspense>
1862
+ </Catch>
1863
+ </ReactorProvider>
1864
+ )
1865
+ // The server read the data and rendered it, but sent no state for it.
1866
+ const server = replica({ signedIn: false })
1867
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1868
+ await server.client.queryClient.prefetchQuery(
1869
+ server.client.queryOptions(serverWho, "whoami")
1870
+ )
1871
+ container.innerHTML = renderToString(tree(server.client))
1872
+ expect(container.querySelector("main")?.textContent).toBe(
1873
+ `read by ${ANONYMOUS}`
1874
+ )
1875
+
1876
+ const browser = replica({ seed: 7 })
1877
+ const user = browser.auth.getPrincipal()?.toText()
1878
+ const commits = watchCommits()
1879
+ const { recoverable, reported } = await hydrate(tree(browser.client))
1880
+ await waitFor(() =>
1881
+ expect(container.querySelector("main")?.textContent).toBe(
1882
+ `read by ${user}`
1883
+ )
1884
+ )
1885
+ commits.stop()
1886
+
1887
+ // React reports that it rendered the boundary on the client, with the
1888
+ // cancelled read as the reason: the error itself on React 18, its
1889
+ // `cause` on React 19. An `onRecoverableError` that forwards errors
1890
+ // should ignore kind "cancelled".
1891
+ expect(recoverable.length).toBeGreaterThan(0)
1892
+ expect(
1893
+ reported.map((error) =>
1894
+ REACT_18 ? error : (error as { cause?: unknown }).cause
1895
+ )
1896
+ ).toContainEqual(
1897
+ expect.objectContaining({ kind: "cancelled", code: "caller_changed" })
1898
+ )
1899
+ expect(whoamiCallers(browser)).toEqual([user])
1900
+
1901
+ // Once nobody is current, the key the hydrating render read is read
1902
+ // again as nobody, and the cancelled read is not shown either.
1903
+ const after = watchCommits()
1904
+ await act(async () => {
1905
+ await browser.auth.signOut()
1906
+ })
1907
+ await waitFor(() =>
1908
+ expect(whoamiCallers(browser)).toEqual([user, ANONYMOUS])
1909
+ )
1910
+ await waitFor(() =>
1911
+ expect(container.querySelector("main")?.textContent).toBe(
1912
+ `read by ${ANONYMOUS}`
1913
+ )
1914
+ )
1915
+ after.stop()
1916
+ expect(
1917
+ [...commits.commits, ...after.commits].filter((html) =>
1918
+ html.includes("ERROR")
1919
+ )
1920
+ ).toEqual([])
1921
+ })
1922
+ })
1923
+
1924
+ /**
1925
+ * L3: the move to the session is a synchronous update, and a
1926
+ * `useSuspenseQuery` read with none of the user's data yet suspends it. With a
1927
+ * Suspense boundary above the read, the boundary shows its fallback until the
1928
+ * data arrives, and the page commits its other updates meanwhile. With none,
1929
+ * React 18 refuses a synchronous update that suspends and unmounts the root;
1930
+ * React 19 keeps the server's HTML but commits none of this page's updates
1931
+ * until the data arrives: a click, a state change from outside an event, and
1932
+ * a transition that renders the read again all wait (measured on 19.3: only a
1933
+ * transition that leaves the read alone commits). Accepted: React expects a
1934
+ * boundary above a component that suspends.
1935
+ */
1936
+ describe("a useSuspenseQuery read on a signed-in reload", () => {
1937
+ function SuspenseReader() {
1938
+ const client = useClient()
1939
+ const who = client.canister<Who>(WHO, { id: CANISTER })
1940
+ const read = useSuspenseQuery(client.queryOptions(who, "whoami"))
1941
+ return <main>{read.data}</main>
1942
+ }
1943
+ /** {@link Counter}'s setter, for an update from outside an event. */
1944
+ let setCount: ((update: (count: number) => number) => void) | undefined
1945
+ /**
1946
+ * A count, which a click on its first button adds one to and a click on its
1947
+ * second adds one to in a transition, above the read: each update of it
1948
+ * renders the read again.
1949
+ */
1950
+ function Counter({ boundary }: { boundary: boolean }) {
1951
+ const [count, set] = useState(0)
1952
+ setCount = set
1953
+ return (
1954
+ <>
1955
+ <nav>
1956
+ <button onClick={() => set((n) => n + 1)}>
1957
+ <em>{count}</em>
1958
+ </button>
1959
+ <button onClick={() => startTransition(() => set((n) => n + 1))}>
1960
+ later
1961
+ </button>
1962
+ </nav>
1963
+ {boundary ? (
1964
+ <Suspense fallback={<p>fallback</p>}>
1965
+ <SuspenseReader />
1966
+ </Suspense>
1967
+ ) : (
1968
+ <SuspenseReader />
1969
+ )}
1970
+ </>
1971
+ )
1972
+ }
1973
+ const tree = (client: Client, state: DehydratedState, boundary: boolean) => (
1974
+ <ReactorProvider client={() => client}>
1975
+ <HydrationBoundary state={state}>
1976
+ <header>top</header>
1977
+ <Counter boundary={boundary} />
1978
+ </HydrationBoundary>
1979
+ </ReactorProvider>
1980
+ )
1981
+ const count = () => container.querySelector("em")?.textContent
1982
+ /**
1983
+ * The server's page, with its read prefetched and dehydrated as nobody, and
1984
+ * the browser's replica, whose reads wait until the test calls `release`.
1985
+ */
1986
+ async function served(boundary: boolean) {
1987
+ const server = replica({ signedIn: false })
1988
+ const serverWho = server.client.canister<Who>(WHO, { id: CANISTER })
1989
+ await server.client.queryClient.prefetchQuery(
1990
+ server.client.queryOptions(serverWho, "whoami")
1991
+ )
1992
+ const json = JSON.stringify(dehydrate(server.client.queryClient))
1993
+ container.innerHTML = renderToString(
1994
+ tree(server.client, JSON.parse(json), boundary)
1995
+ )
1996
+ expect(container.querySelector("main")?.textContent).toBe(
1997
+ `read by ${ANONYMOUS}`
1998
+ )
1999
+ expect(count()).toBe("0")
2000
+ const browser = replica({ seed: 7 })
2001
+ let release!: () => void
2002
+ const held = new Promise<void>((resolve) => (release = resolve))
2003
+ browser.mock<Who>(WHO, CANISTER, {
2004
+ whoami: async ({ caller }) => {
2005
+ await held
2006
+ return `read by ${caller}`
2007
+ },
2008
+ })
2009
+ return {
2010
+ state: JSON.parse(json) as DehydratedState,
2011
+ main: container.querySelector("main"),
2012
+ browser,
2013
+ user: browser.auth.getPrincipal()?.toText(),
2014
+ release,
2015
+ }
2016
+ }
2017
+ /**
2018
+ * Adds one to the count three times, as a click, a transition and a state
2019
+ * change from outside an event, and returns the count shown after each.
2020
+ */
2021
+ async function update() {
2022
+ const [click, later] = container.querySelectorAll("button")
2023
+ const shown: (string | undefined)[] = []
2024
+ for (const run of [
2025
+ () => click.click(),
2026
+ () => later.click(),
2027
+ () => setCount?.((n) => n + 1),
2028
+ ]) {
2029
+ await act(async () => run())
2030
+ await act(macrotask)
2031
+ shown.push(count())
2032
+ }
2033
+ return shown
2034
+ }
2035
+
2036
+ it("shows the fallback of a Suspense boundary above it until the user's data arrives, then that data, and commits the page's updates meanwhile", async () => {
2037
+ const { state, main, browser, user, release } = await served(true)
2038
+ const commits = watchCommits()
2039
+
2040
+ const { recoverable, logged } = await hydrate(
2041
+ tree(browser.client, state, true)
2042
+ )
2043
+ expect(container.querySelector("p")?.textContent).toBe("fallback")
2044
+ expect(await update()).toEqual(["1", "2", "3"])
2045
+ expect(container.querySelector("p")?.textContent).toBe("fallback")
2046
+ release()
2047
+ await waitFor(() =>
2048
+ expect(container.querySelector("main")?.textContent).toBe(
2049
+ `read by ${user}`
2050
+ )
2051
+ )
2052
+ commits.stop()
2053
+
2054
+ expect(recoverable).toEqual([])
2055
+ expect(logged).not.toHaveBeenCalled()
2056
+ // The move hid the server's content behind the fallback, then showed the
2057
+ // user's data in the same node.
2058
+ expect(commits.commits[0]).toContain("<p>fallback</p>")
2059
+ expect(commits.commits[0]).toContain(`read by ${ANONYMOUS}`)
2060
+ expect(commits.commits[commits.commits.length - 1]).not.toContain(
2061
+ "fallback"
2062
+ )
2063
+ expect(container.querySelector("header")?.textContent).toBe("top")
2064
+ expect(count()).toBe("3")
2065
+ expect(container.querySelector("main")).toBe(main)
2066
+ expect(whoamiCallers(browser)).toEqual([user])
2067
+ })
2068
+
2069
+ it(
2070
+ REACT_18
2071
+ ? "fails to render with no Suspense boundary above it on React 18: React unmounts the root"
2072
+ : "keeps the server's HTML with no Suspense boundary above it on React 19, and commits none of the page's updates until the user's data arrives",
2073
+ async () => {
2074
+ const { state, main, browser, user, release } = await served(false)
2075
+ const commits = watchCommits()
2076
+ const recoverable: unknown[] = []
2077
+ const logged = vi.spyOn(console, "error").mockImplementation(() => {})
2078
+ let thrown: unknown
2079
+ try {
2080
+ await act(async () => {
2081
+ root = hydrateRoot(container, tree(browser.client, state, false), {
2082
+ onRecoverableError: (error) => recoverable.push(error),
2083
+ })
2084
+ })
2085
+ await act(macrotask)
2086
+ } catch (error) {
2087
+ thrown = error
2088
+ }
2089
+
2090
+ if (REACT_18) {
2091
+ release()
2092
+ commits.stop()
2093
+ expect(String((thrown as Error | undefined)?.message)).toMatch(
2094
+ /suspended while responding to synchronous input/
2095
+ )
2096
+ expect(container.innerHTML).toBe("")
2097
+ expect(commits.commits).toEqual([""])
2098
+ return
2099
+ }
2100
+ expect(thrown).toBeUndefined()
2101
+ // The page looks hydrated, but the move is still pending: a click and
2102
+ // a state change render with it, and a transition renders the read
2103
+ // again, so the read suspends each one.
2104
+ expect(await update()).toEqual(["0", "0", "0"])
2105
+ expect(container.querySelector("main")?.textContent).toBe(
2106
+ `read by ${ANONYMOUS}`
2107
+ )
2108
+ expect(commits.commits).toEqual([])
2109
+ release()
2110
+ await waitFor(() => {
2111
+ expect(container.querySelector("main")?.textContent).toBe(
2112
+ `read by ${user}`
2113
+ )
2114
+ expect(count()).toBe("3")
2115
+ })
2116
+ commits.stop()
2117
+ expect(recoverable).toEqual([])
2118
+ expect(logged).not.toHaveBeenCalled()
2119
+ expect(container.querySelector("main")).toBe(main)
2120
+ expect(whoamiCallers(browser)).toEqual([user])
2121
+ }
2122
+ )
2123
+ })
2124
+
2125
+ /**
2126
+ * Tabs whose caller is the one a hydrating render shows, or one that never
2127
+ * changes: `useClient()` then has nothing to move on to after hydrating. A
2128
+ * stored session that has ended (an `AuthClient` reports `expired` at once)
2129
+ * or that another origin holds (`signed-in-elsewhere`) leaves the caller
2130
+ * anonymous, like no session at all; a client built with `identity` keeps its
2131
+ * caller for good.
2132
+ */
2133
+ const steadyTabs = [
2134
+ ["without a session", "anonymous"],
2135
+ ["whose stored session expired", "expired"],
2136
+ ["whose session is signed in elsewhere", "elsewhere"],
2137
+ ["of a client built with identity", "identity"],
2138
+ ] as const
2139
+
2140
+ /** The client of one of {@link steadyTabs}, and the server's for its page. */
2141
+ async function steadyTab(kind: (typeof steadyTabs)[number][1]) {
2142
+ if (kind === "identity") {
2143
+ const identity = await testAuth({ identity: 7 }).getIdentity()
2144
+ const server = createClient({ network: "ic", identity })
2145
+ const browser = createClient({ network: "ic", identity })
2146
+ made.push(server, browser)
2147
+ return { server, browser, auth: undefined }
2148
+ }
2149
+ const server = replica({ signedIn: false }).client
2150
+ const test = replica({ seed: 7, signedIn: kind !== "anonymous" })
2151
+ if (kind === "expired") test.auth.expire()
2152
+ if (kind === "elsewhere") test.auth.elsewhere()
2153
+ return { server, browser: test.client, auth: test.auth }
2154
+ }
2155
+
2156
+ describe.each(steadyTabs)("a tab %s", (_, kind) => {
2157
+ it("hydrates a component that calls useClient() with one render, and leaves a streamed boundary below it pending", async () => {
2158
+ let renders = 0
2159
+ function Content() {
2160
+ return <article>content</article>
2161
+ }
2162
+ function Nav() {
2163
+ renders++
2164
+ const client = useClient()
2165
+ return (
2166
+ <nav data-caller={client.caller()}>
2167
+ <button onClick={() => void client.signIn()}>sign in</button>
2168
+ <Suspense fallback={<p>fallback</p>}>
2169
+ <Content />
2170
+ </Suspense>
2171
+ </nav>
2172
+ )
2173
+ }
2174
+ const tree = (client: Client) => (
2175
+ <ReactorProvider client={() => client}>
2176
+ <Nav />
2177
+ </ReactorProvider>
2178
+ )
2179
+ const { server, browser, auth } = await steadyTab(kind)
2180
+ container.innerHTML = streamed(
2181
+ renderToString(tree(server)),
2182
+ "<article>content</article>"
2183
+ )
2184
+ await whileLoading(async () => {
2185
+ renders = 0
2186
+ const { recoverable, logged } = await hydrate(tree(browser))
2187
+ await act(macrotask)
2188
+
2189
+ expect(renders).toBe(1)
2190
+ expect(recoverable).toEqual([])
2191
+ expect(logged).not.toHaveBeenCalled()
2192
+ expect(container.querySelector("template")).not.toBeNull()
2193
+ expect(container.querySelector("article")).toBeNull()
2194
+ expect(container.querySelector("p")?.textContent).toBe("fallback")
2195
+ expect(container.querySelector("nav")?.dataset.caller).toBe(
2196
+ browser.caller()
2197
+ )
2198
+
2199
+ // A change of status that leaves the caller as it is renders nothing
2200
+ // either: the client the component holds builds the same keys.
2201
+ if (auth !== undefined && kind !== "anonymous") {
2202
+ const before = browser.authState()
2203
+ act(() => (kind === "expired" ? auth.elsewhere() : auth.expire()))
2204
+ expect(browser.authState().status).not.toBe(before.status)
2205
+ expect(browser.authState().principal).toBe(before.principal)
2206
+ expect(renders).toBe(1)
2207
+ }
2208
+ })
2209
+ })
2210
+ })
2211
+
2212
+ /** What `useAuth()` shows in each of {@link steadyTabs} once it hydrated. */
2213
+ const ownStatus = {
2214
+ anonymous: "anonymous",
2215
+ expired: "expired",
2216
+ elsewhere: "signed-in-elsewhere",
2217
+ identity: "signed-in",
2218
+ } as const
2219
+
2220
+ describe.each(steadyTabs)("a hydrated useAuth() in a tab %s", (_, kind) => {
2221
+ it("shows the server's state while hydrating, then the tab's own, rendering again only for a status that differs", async () => {
2222
+ const statuses: string[] = []
2223
+ const auths: unknown[] = []
2224
+ function Status() {
2225
+ const auth = useAuth()
2226
+ statuses.push(auth.status)
2227
+ auths.push(auth)
2228
+ return <b>{auth.status}</b>
2229
+ }
2230
+ const tree = (client: Client) => (
2231
+ <ReactorProvider client={() => client}>
2232
+ <Status />
2233
+ </ReactorProvider>
2234
+ )
2235
+ const { server, browser } = await steadyTab(kind)
2236
+ container.innerHTML = renderToString(tree(server))
2237
+ expect(container.querySelector("b")?.textContent).toBe("anonymous")
2238
+ statuses.length = auths.length = 0
2239
+
2240
+ const { recoverable, logged } = await hydrate(tree(browser))
2241
+ await act(macrotask)
2242
+
2243
+ expect(recoverable).toEqual([])
2244
+ expect(logged).not.toHaveBeenCalled()
2245
+ expect(container.querySelector("b")?.textContent).toBe(ownStatus[kind])
2246
+ expect(statuses).toEqual(
2247
+ kind === "anonymous" ? ["anonymous"] : ["anonymous", ownStatus[kind]]
2248
+ )
2249
+ expect(new Set(auths).size).toBe(statuses.length)
2250
+ })
2251
+ })
2252
+
2253
+ describe("an anonymous tab", () => {
2254
+ it("hydrates with one render of each component that calls useClient() or useAuth(), and one useAuth() object", async () => {
2255
+ const server = replica({ signedIn: false })
2256
+ const renders = { client: 0, auth: 0 }
2257
+ const auths: unknown[] = []
2258
+ function ClientUser() {
2259
+ renders.client++
2260
+ return <i>{useClient().caller()}</i>
2261
+ }
2262
+ function AuthUser() {
2263
+ renders.auth++
2264
+ const auth = useAuth()
2265
+ auths.push(auth)
2266
+ return <b>{auth.status}</b>
2267
+ }
2268
+ const tree = (client: Client) => (
2269
+ <ReactorProvider client={() => client}>
2270
+ <ClientUser />
2271
+ <AuthUser />
2272
+ </ReactorProvider>
2273
+ )
2274
+ container.innerHTML = renderToString(tree(server.client))
2275
+ renders.client = renders.auth = 0
2276
+ auths.length = 0
2277
+
2278
+ const browser = replica({ signedIn: false })
2279
+ const { recoverable } = await hydrate(tree(browser.client))
2280
+ await act(macrotask)
2281
+
2282
+ expect(recoverable).toEqual([])
2283
+ expect(renders).toEqual({ client: 1, auth: 1 })
2284
+ expect(auths).toHaveLength(1)
2285
+ // A render the session did not cause keeps the object too.
2286
+ act(() => root?.render(tree(browser.client)))
2287
+ expect(renders.auth).toBe(2)
2288
+ expect(new Set(auths).size).toBe(1)
2289
+ // It still follows the session from then on.
2290
+ await act(async () => {
2291
+ await browser.auth.signIn(7)
2292
+ })
2293
+ expect(container.querySelector("b")?.textContent).toBe("signed-in")
2294
+ expect(container.querySelector("i")?.textContent).toBe(
2295
+ browser.auth.getPrincipal()?.toText()
2296
+ )
2297
+ })
2298
+
2299
+ it("leaves a streamed boundary below a useAuth() parent pending", async () => {
2300
+ let renders = 0
2301
+ function Nav() {
2302
+ renders++
2303
+ const { status } = useAuth()
2304
+ return (
2305
+ <nav data-status={status}>
2306
+ <Suspense fallback={<p>fallback</p>}>
2307
+ <article>content</article>
2308
+ </Suspense>
2309
+ </nav>
2310
+ )
2311
+ }
2312
+ const tree = (client: Client) => (
2313
+ <ReactorProvider client={() => client}>
2314
+ <Nav />
2315
+ </ReactorProvider>
2316
+ )
2317
+ const server = replica({ signedIn: false })
2318
+ container.innerHTML = streamed(
2319
+ renderToString(tree(server.client)),
2320
+ "<article>content</article>"
2321
+ )
2322
+ await whileLoading(async () => {
2323
+ renders = 0
2324
+ const browser = replica({ signedIn: false })
2325
+ const { recoverable } = await hydrate(tree(browser.client))
2326
+ await act(macrotask)
2327
+
2328
+ expect(renders).toBe(1)
2329
+ expect(recoverable).toEqual([])
2330
+ expect(container.querySelector("template")).not.toBeNull()
2331
+ expect(container.querySelector("article")).toBeNull()
2332
+ })
2333
+ })
2334
+ })