@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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
- package/.github/actions/setup-rust-wasm/action.yml +25 -0
- package/.github/dependabot.yml +107 -0
- package/.github/workflows/auth-latest.yml +67 -0
- package/.github/workflows/b3pay-site.yml +49 -0
- package/.github/workflows/ci.yml +430 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/dependency-review.yml +68 -0
- package/.github/workflows/docs-check.yml +98 -0
- package/.github/workflows/docs.yml +265 -0
- package/.github/workflows/e2e.yml +90 -0
- package/.github/workflows/examples-published.yml +97 -0
- package/.github/workflows/release.yml +326 -0
- package/.husky/pre-commit +12 -0
- package/.prettierignore +109 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +3268 -0
- package/CLAUDE.md +93 -0
- package/CNAME +1 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +433 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/SECURITY.md +74 -0
- package/b3pay-module.json +194 -0
- package/docs/.remarkrc.json +10 -0
- package/docs/README.md +64 -0
- package/docs/astro.config.mjs +205 -0
- package/docs/favicon.svg +15 -0
- package/docs/package.json +39 -0
- package/docs/public/favicon.svg +15 -0
- package/docs/public/ic-reactor-social-card.png +0 -0
- package/docs/scripts/b3pay-static.mjs +22 -0
- package/docs/scripts/check-example-docs.mjs +214 -0
- package/docs/scripts/check-links.mjs +212 -0
- package/docs/scripts/fix-dist-links.js +125 -0
- package/docs/scripts/fix-frontmatter.js +71 -0
- package/docs/scripts/remove-redundant-h1.cjs +48 -0
- package/docs/scripts/rewrite-typedoc-links.js +110 -0
- package/docs/scripts/split-index.js +69 -0
- package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
- package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
- package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
- package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
- package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
- package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
- package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
- package/docs/src/components/LibraryCards.astro +172 -0
- package/docs/src/components/LibraryIcon.astro +28 -0
- package/docs/src/components/SiteTitle.astro +241 -0
- package/docs/src/content/docs/404.mdx +19 -0
- package/docs/src/content/docs/build/agent-tools.mdx +593 -0
- package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
- package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
- package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
- package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
- package/docs/src/content/docs/examples/index.mdx +38 -0
- package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
- package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
- package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
- package/docs/src/content/docs/guides/auth.mdx +1181 -0
- package/docs/src/content/docs/guides/client.mdx +551 -0
- package/docs/src/content/docs/guides/deploy.mdx +180 -0
- package/docs/src/content/docs/guides/errors.mdx +475 -0
- package/docs/src/content/docs/guides/getting-started.mdx +620 -0
- package/docs/src/content/docs/guides/reads.mdx +586 -0
- package/docs/src/content/docs/guides/ssr.mdx +529 -0
- package/docs/src/content/docs/guides/testing.mdx +401 -0
- package/docs/src/content/docs/guides/values.mdx +261 -0
- package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
- package/docs/src/content/docs/guides/writes.mdx +572 -0
- package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
- package/docs/src/content/docs/index.mdx +65 -0
- package/docs/src/content/docs/migrating-from-3.mdx +535 -0
- package/docs/src/content/docs/packages/core.mdx +186 -0
- package/docs/src/content/docs/packages/mcp.mdx +163 -0
- package/docs/src/content/docs/packages/react.mdx +207 -0
- package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
- package/docs/src/content/docs/versioning.mdx +104 -0
- package/docs/src/content.config.ts +7 -0
- package/docs/src/styles/custom.css +189 -0
- package/docs/tsconfig.typedoc.json +26 -0
- package/docs/typedoc-entry.ts +17 -0
- package/docs/typedoc.json +37 -0
- package/e2e/Cargo.lock +646 -0
- package/e2e/Cargo.toml +3 -0
- package/e2e/README.md +101 -0
- package/e2e/frontends/build.mjs +94 -0
- package/e2e/frontends/index.html +10 -0
- package/e2e/global-setup.ts +65 -0
- package/e2e/icp.yaml +38 -0
- package/e2e/package.json +40 -0
- package/e2e/setup.ts +6 -0
- package/e2e/src/actor/Cargo.toml +14 -0
- package/e2e/src/actor/hello_actor.did +10 -0
- package/e2e/src/actor/src/lib.rs +73 -0
- package/e2e/src/calls.test.ts +132 -0
- package/e2e/src/declarations/hello_actor.envelope.json +175 -0
- package/e2e/src/declarations/hello_actor.ts +15 -0
- package/e2e/src/errors.test.ts +107 -0
- package/e2e/src/frontend-headers.test.ts +253 -0
- package/e2e/src/identity.test.ts +61 -0
- package/e2e/src/react.test.tsx +59 -0
- package/e2e/src/replica.ts +70 -0
- package/e2e/src/vite-env.test.ts +108 -0
- package/e2e/test.sh +47 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/tsconfig.typecheck.json +9 -0
- package/e2e/vitest.config.ts +22 -0
- package/eslint.config.mjs +100 -0
- package/evals/PREREGISTRATION.md +1346 -0
- package/evals/README.md +756 -0
- package/evals/conditions/thin/condition.json +8 -0
- package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
- package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
- package/evals/conditions/thin/package.json +25 -0
- package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
- package/evals/conditions/thin-guide/condition.json +10 -0
- package/evals/conditions/thin-guide/docs/llms.txt +292 -0
- package/evals/conditions/v3/condition.json +9 -0
- package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
- package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
- package/evals/conditions/v3/docs/llms-full.txt +1053 -0
- package/evals/conditions/v3/package.json +26 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
- package/evals/conditions/v4/PORTING.md +384 -0
- package/evals/conditions/v4/README.md +106 -0
- package/evals/conditions/v4/condition.json +5 -0
- package/evals/conditions/v4/docs/llms.txt +323 -0
- package/evals/conditions/v4/package.json +27 -0
- package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
- package/evals/conditions/v4-proto/condition.json +6 -0
- package/evals/conditions/v4-proto/docs/llms.txt +291 -0
- package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
- package/evals/conditions/v4-proto/lib/package.json +27 -0
- package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
- package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
- package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
- package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
- package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
- package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
- package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
- package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
- package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
- package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
- package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
- package/evals/conditions/v4-proto/package.json +26 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
- package/evals/drive.mjs +1221 -0
- package/evals/gate.mjs +161 -0
- package/evals/harness/aggregate.mjs +534 -0
- package/evals/harness/aggregate.test.mjs +344 -0
- package/evals/harness/assemble.mjs +216 -0
- package/evals/harness/assemble.test.mjs +143 -0
- package/evals/harness/auth.mjs +91 -0
- package/evals/harness/check-docs.mjs +157 -0
- package/evals/harness/declarations/icrc1.did.d.ts +95 -0
- package/evals/harness/declarations/icrc1.did.js +140 -0
- package/evals/harness/drive-auth.test.mjs +584 -0
- package/evals/harness/drive.test.mjs +509 -0
- package/evals/harness/fake-auth.ts +67 -0
- package/evals/harness/fake-ledger.ts +178 -0
- package/evals/harness/gate-plan.mjs +104 -0
- package/evals/harness/gate-plan.test.mjs +243 -0
- package/evals/harness/icrc1.did +88 -0
- package/evals/harness/judge.mjs +63 -0
- package/evals/harness/judge.test.mjs +81 -0
- package/evals/harness/leak-scan.mjs +1373 -0
- package/evals/harness/leak-scan.test.mjs +1300 -0
- package/evals/harness/probes/amounts.ts +14 -0
- package/evals/harness/probes/node-tool.probe.ts +48 -0
- package/evals/harness/probes/probe.mjs +149 -0
- package/evals/harness/probes/react-wallet.probe.ts +96 -0
- package/evals/harness/public/support/auth.ts +38 -0
- package/evals/harness/public/support/fake-replica.d.ts +28 -0
- package/evals/harness/public/support/fake-replica.js +434 -0
- package/evals/harness/public/support/ledger.idl.d.ts +95 -0
- package/evals/harness/public/support/ledger.idl.js +140 -0
- package/evals/harness/public/support/world.ts +179 -0
- package/evals/harness/runs.mjs +139 -0
- package/evals/harness/sandbox.mjs +88 -0
- package/evals/harness/sandbox.test.mjs +134 -0
- package/evals/harness/ship.mjs +264 -0
- package/evals/harness/ship.test.mjs +552 -0
- package/evals/harness/world.ts +291 -0
- package/evals/package.json +26 -0
- package/evals/pnpm-lock.yaml +1537 -0
- package/evals/pnpm-workspace.yaml +12 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
- package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
- package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
- package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
- package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
- package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
- package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
- package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
- package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
- package/evals/results/README.md +31 -0
- package/evals/runs-pilot.log +150 -0
- package/evals/runs-pilot2.log +150 -0
- package/evals/score.mjs +256 -0
- package/evals/setup.mjs +547 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
- package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
- package/evals/tasks/node-tool/prompt.md +55 -0
- package/evals/tasks/node-tool/prompt.minimal.md +30 -0
- package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
- package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
- package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
- package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
- package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
- package/evals/tasks/node-tool/starter/src/index.ts +7 -0
- package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
- package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
- package/evals/tasks/node-tool/task.json +63 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
- package/evals/tasks/react-wallet/prompt.md +88 -0
- package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
- package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
- package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
- package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
- package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
- package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
- package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
- package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
- package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
- package/evals/tasks/react-wallet/task.json +84 -0
- package/examples/canister-studio/.stackblitzrc +4 -0
- package/examples/canister-studio/README.md +183 -0
- package/examples/canister-studio/did/ckbtc_minter.did +806 -0
- package/examples/canister-studio/did/icp_ledger.did +586 -0
- package/examples/canister-studio/did/icrc1.did +88 -0
- package/examples/canister-studio/package.json +45 -0
- package/examples/canister-studio/scripts/smoke.ts +188 -0
- package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
- package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
- package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
- package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
- package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
- package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
- package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
- package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
- package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
- package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
- package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
- package/examples/canister-studio/src/calls/dedupe.ts +189 -0
- package/examples/canister-studio/src/calls/error-info.ts +168 -0
- package/examples/canister-studio/src/calls/history.test.ts +284 -0
- package/examples/canister-studio/src/calls/history.ts +287 -0
- package/examples/canister-studio/src/calls/kinds.ts +68 -0
- package/examples/canister-studio/src/calls/prepare.ts +32 -0
- package/examples/canister-studio/src/calls/query-options.ts +30 -0
- package/examples/canister-studio/src/calls/read.ts +47 -0
- package/examples/canister-studio/src/calls/readback.ts +100 -0
- package/examples/canister-studio/src/calls/record.ts +123 -0
- package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
- package/examples/canister-studio/src/calls/share.ts +46 -0
- package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
- package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
- package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
- package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
- package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
- package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
- package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
- package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
- package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
- package/examples/canister-studio/src/components/AppShell.tsx +29 -0
- package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
- package/examples/canister-studio/src/components/Badge.tsx +79 -0
- package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
- package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
- package/examples/canister-studio/src/components/Card.tsx +64 -0
- package/examples/canister-studio/src/components/Code.tsx +101 -0
- package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
- package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
- package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
- package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
- package/examples/canister-studio/src/components/MethodList.tsx +244 -0
- package/examples/canister-studio/src/components/Notice.tsx +36 -0
- package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
- package/examples/canister-studio/src/components/RouteError.tsx +42 -0
- package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
- package/examples/canister-studio/src/components/Spinner.tsx +33 -0
- package/examples/canister-studio/src/components/Tabs.tsx +96 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
- package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
- package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
- package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
- package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
- package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
- package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
- package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
- package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
- package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
- package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
- package/examples/canister-studio/src/components/canister-names.ts +11 -0
- package/examples/canister-studio/src/components/download.ts +34 -0
- package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
- package/examples/canister-studio/src/components/helpers.test.ts +107 -0
- package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
- package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
- package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
- package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
- package/examples/canister-studio/src/components/home/featured.ts +64 -0
- package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
- package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
- package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
- package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
- package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
- package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
- package/examples/canister-studio/src/components/modes.ts +31 -0
- package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
- package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
- package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
- package/examples/canister-studio/src/components/playground/sample.ts +43 -0
- package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
- package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
- package/examples/canister-studio/src/components/provenance.ts +38 -0
- package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
- package/examples/canister-studio/src/components/use-task.ts +56 -0
- package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
- package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
- package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
- package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
- package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
- package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
- package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
- package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
- package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
- package/examples/canister-studio/src/forms/context.ts +62 -0
- package/examples/canister-studio/src/forms/convert.ts +131 -0
- package/examples/canister-studio/src/forms/edge.test.ts +252 -0
- package/examples/canister-studio/src/forms/engine.test.ts +358 -0
- package/examples/canister-studio/src/forms/engine.ts +1492 -0
- package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
- package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
- package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
- package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
- package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
- package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
- package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
- package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
- package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
- package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
- package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
- package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
- package/examples/canister-studio/src/forms/hints.ts +128 -0
- package/examples/canister-studio/src/forms/json.ts +119 -0
- package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
- package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
- package/examples/canister-studio/src/interface/browser.test.ts +23 -0
- package/examples/canister-studio/src/interface/browser.ts +153 -0
- package/examples/canister-studio/src/interface/cache.ts +68 -0
- package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
- package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
- package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
- package/examples/canister-studio/src/interface/compile.server.ts +262 -0
- package/examples/canister-studio/src/interface/compile.test.ts +108 -0
- package/examples/canister-studio/src/interface/compile.ts +305 -0
- package/examples/canister-studio/src/interface/docs.test.ts +75 -0
- package/examples/canister-studio/src/interface/docs.ts +100 -0
- package/examples/canister-studio/src/interface/functions.test.ts +42 -0
- package/examples/canister-studio/src/interface/functions.ts +200 -0
- package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
- package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
- package/examples/canister-studio/src/interface/live-loader.ts +364 -0
- package/examples/canister-studio/src/interface/load.test.ts +328 -0
- package/examples/canister-studio/src/interface/load.ts +249 -0
- package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
- package/examples/canister-studio/src/interface/read-candid.ts +233 -0
- package/examples/canister-studio/src/interface/server-result.ts +42 -0
- package/examples/canister-studio/src/interface/typed.ts +68 -0
- package/examples/canister-studio/src/interface/types.ts +121 -0
- package/examples/canister-studio/src/lib/format.test.ts +107 -0
- package/examples/canister-studio/src/lib/format.ts +139 -0
- package/examples/canister-studio/src/lib/hex.test.ts +43 -0
- package/examples/canister-studio/src/lib/hex.ts +33 -0
- package/examples/canister-studio/src/lib/ids.test.ts +70 -0
- package/examples/canister-studio/src/lib/ids.ts +93 -0
- package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
- package/examples/canister-studio/src/lib/reactor.ts +55 -0
- package/examples/canister-studio/src/lib/registry.test.ts +74 -0
- package/examples/canister-studio/src/lib/registry.ts +238 -0
- package/examples/canister-studio/src/lib/schema.ts +148 -0
- package/examples/canister-studio/src/routeTree.gen.ts +227 -0
- package/examples/canister-studio/src/router.test.ts +290 -0
- package/examples/canister-studio/src/router.tsx +127 -0
- package/examples/canister-studio/src/routes/__root.tsx +58 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
- package/examples/canister-studio/src/routes/history.tsx +55 -0
- package/examples/canister-studio/src/routes/index.tsx +169 -0
- package/examples/canister-studio/src/routes/playground.tsx +286 -0
- package/examples/canister-studio/src/routes/safety.tsx +57 -0
- package/examples/canister-studio/src/routes/wallet.tsx +213 -0
- package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
- package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
- package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
- package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
- package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
- package/examples/canister-studio/src/safety/lab-run.ts +283 -0
- package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
- package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
- package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
- package/examples/canister-studio/src/safety/scenarios.ts +924 -0
- package/examples/canister-studio/src/styles.css +3453 -0
- package/examples/canister-studio/src/styles.test.ts +46 -0
- package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
- package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
- package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
- package/examples/canister-studio/src/values/Inspector.tsx +133 -0
- package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
- package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
- package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
- package/examples/canister-studio/src/values/candid-text.ts +479 -0
- package/examples/canister-studio/src/values/hints.ts +58 -0
- package/examples/canister-studio/src/values/json.test.ts +66 -0
- package/examples/canister-studio/src/values/json.ts +31 -0
- package/examples/canister-studio/src/values/leaves.tsx +309 -0
- package/examples/canister-studio/src/values/schema.test.ts +123 -0
- package/examples/canister-studio/src/values/schema.ts +78 -0
- package/examples/canister-studio/src/values/test-router.tsx +21 -0
- package/examples/canister-studio/src/values/tree.tsx +670 -0
- package/examples/canister-studio/src/values/use-sha256.ts +29 -0
- package/examples/canister-studio/tsconfig.json +20 -0
- package/examples/canister-studio/vite.config.ts +50 -0
- package/examples/canister-studio/vitest.config.ts +20 -0
- package/examples/icrc-ledger/.stackblitzrc +4 -0
- package/examples/icrc-ledger/README.md +100 -0
- package/examples/icrc-ledger/ckbtc_minter.did +12 -0
- package/examples/icrc-ledger/icp_ledger.did +147 -0
- package/examples/icrc-ledger/icrc1.did +88 -0
- package/examples/icrc-ledger/index.html +13 -0
- package/examples/icrc-ledger/package.json +40 -0
- package/examples/icrc-ledger/scripts/smoke.ts +151 -0
- package/examples/icrc-ledger/src/App.tsx +71 -0
- package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
- package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
- package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
- package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
- package/examples/icrc-ledger/src/blocks.test.ts +247 -0
- package/examples/icrc-ledger/src/blocks.ts +140 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
- package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
- package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
- package/examples/icrc-ledger/src/format.test.ts +41 -0
- package/examples/icrc-ledger/src/format.ts +68 -0
- package/examples/icrc-ledger/src/ledger.ts +15 -0
- package/examples/icrc-ledger/src/main.tsx +10 -0
- package/examples/icrc-ledger/src/minter.ts +16 -0
- package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
- package/examples/icrc-ledger/src/sandbox.ts +281 -0
- package/examples/icrc-ledger/src/styles.css +391 -0
- package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
- package/examples/icrc-ledger/src/transfer-form.ts +97 -0
- package/examples/icrc-ledger/tsconfig.json +19 -0
- package/examples/icrc-ledger/vite.config.ts +44 -0
- package/examples/icrc-ledger/vitest.config.ts +12 -0
- package/examples/next-ssr/.stackblitzrc +4 -0
- package/examples/next-ssr/README.md +112 -0
- package/examples/next-ssr/icrc1.did +88 -0
- package/examples/next-ssr/next.config.ts +10 -0
- package/examples/next-ssr/package.json +41 -0
- package/examples/next-ssr/scripts/smoke.ts +169 -0
- package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
- package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
- package/examples/next-ssr/src/app/account/page.tsx +82 -0
- package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
- package/examples/next-ssr/src/app/globals.css +278 -0
- package/examples/next-ssr/src/app/layout.tsx +34 -0
- package/examples/next-ssr/src/app/page.tsx +46 -0
- package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
- package/examples/next-ssr/src/app/providers.test.tsx +73 -0
- package/examples/next-ssr/src/app/providers.tsx +60 -0
- package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
- package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
- package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
- package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
- package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
- package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
- package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
- package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
- package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
- package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
- package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
- package/examples/next-ssr/src/components/server-data.ts +10 -0
- package/examples/next-ssr/src/format.ts +46 -0
- package/examples/next-ssr/src/ledgers.ts +38 -0
- package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
- package/examples/next-ssr/src/server/balance-route.ts +157 -0
- package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
- package/examples/next-ssr/src/server/error-summary.ts +37 -0
- package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
- package/examples/next-ssr/src/server/parse-owner.ts +34 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
- package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
- package/examples/next-ssr/src/server/read-balances.ts +78 -0
- package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
- package/examples/next-ssr/src/server/request-client.ts +24 -0
- package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
- package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
- package/examples/next-ssr/src/testing/setup.ts +10 -0
- package/examples/next-ssr/tsconfig.json +31 -0
- package/examples/next-ssr/vitest.config.ts +59 -0
- package/examples/node-agent-tool/.stackblitzrc +4 -0
- package/examples/node-agent-tool/README.md +165 -0
- package/examples/node-agent-tool/icp.yaml +11 -0
- package/examples/node-agent-tool/icrc1.did +88 -0
- package/examples/node-agent-tool/package.json +32 -0
- package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
- package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
- package/examples/node-agent-tool/src/cli.test.ts +64 -0
- package/examples/node-agent-tool/src/cli.ts +23 -0
- package/examples/node-agent-tool/src/command-line.ts +159 -0
- package/examples/node-agent-tool/src/commands/balance.ts +73 -0
- package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
- package/examples/node-agent-tool/src/commands/demo.ts +359 -0
- package/examples/node-agent-tool/src/commands/info.ts +69 -0
- package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
- package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
- package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
- package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
- package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
- package/examples/node-agent-tool/src/commands/watch.ts +135 -0
- package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
- package/examples/node-agent-tool/src/context.ts +20 -0
- package/examples/node-agent-tool/src/dispatch.ts +70 -0
- package/examples/node-agent-tool/src/failure.test.ts +59 -0
- package/examples/node-agent-tool/src/failure.ts +176 -0
- package/examples/node-agent-tool/src/identity.test.ts +242 -0
- package/examples/node-agent-tool/src/identity.ts +228 -0
- package/examples/node-agent-tool/src/input.ts +110 -0
- package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
- package/examples/node-agent-tool/src/ledgers.ts +62 -0
- package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
- package/examples/node-agent-tool/src/network.test.ts +128 -0
- package/examples/node-agent-tool/src/network.ts +101 -0
- package/examples/node-agent-tool/src/output.test.ts +208 -0
- package/examples/node-agent-tool/src/output.ts +163 -0
- package/examples/node-agent-tool/src/run.ts +73 -0
- package/examples/node-agent-tool/src/test-kit.ts +167 -0
- package/examples/node-agent-tool/tsconfig.json +18 -0
- package/examples/node-agent-tool/vitest.config.ts +8 -0
- package/examples/vite-wallet/.stackblitzrc +4 -0
- package/examples/vite-wallet/Cargo.lock +646 -0
- package/examples/vite-wallet/Cargo.toml +5 -0
- package/examples/vite-wallet/README.md +107 -0
- package/examples/vite-wallet/backend/Cargo.toml +12 -0
- package/examples/vite-wallet/backend/backend.did +37 -0
- package/examples/vite-wallet/backend/src/lib.rs +133 -0
- package/examples/vite-wallet/icp.yaml +44 -0
- package/examples/vite-wallet/index.html +13 -0
- package/examples/vite-wallet/ledger.did +54 -0
- package/examples/vite-wallet/package.json +46 -0
- package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
- package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
- package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
- package/examples/vite-wallet/scripts/faucet.ts +41 -0
- package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
- package/examples/vite-wallet/src/AddressBook.tsx +147 -0
- package/examples/vite-wallet/src/App.tsx +53 -0
- package/examples/vite-wallet/src/Balance.test.tsx +57 -0
- package/examples/vite-wallet/src/Balance.tsx +58 -0
- package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
- package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
- package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
- package/examples/vite-wallet/src/Profile.test.tsx +145 -0
- package/examples/vite-wallet/src/Profile.tsx +92 -0
- package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
- package/examples/vite-wallet/src/SendIcp.tsx +244 -0
- package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
- package/examples/vite-wallet/src/SendToContact.tsx +102 -0
- package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
- package/examples/vite-wallet/src/SignIn.tsx +120 -0
- package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
- package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
- package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
- package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
- package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
- package/examples/vite-wallet/src/auth/local-page.ts +20 -0
- package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
- package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
- package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
- package/examples/vite-wallet/src/canisters/backend.ts +58 -0
- package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
- package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
- package/examples/vite-wallet/src/client.ts +45 -0
- package/examples/vite-wallet/src/main.tsx +18 -0
- package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
- package/examples/vite-wallet/src/refusal.ts +17 -0
- package/examples/vite-wallet/src/styles.css +282 -0
- package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
- package/examples/vite-wallet/src/token.ts +50 -0
- package/examples/vite-wallet/src/transfer.test.ts +178 -0
- package/examples/vite-wallet/src/transfer.ts +110 -0
- package/examples/vite-wallet/src/use-canisters.ts +49 -0
- package/examples/vite-wallet/tsconfig.json +19 -0
- package/examples/vite-wallet/vite.config.ts +47 -0
- package/examples/vite-wallet/vitest.config.ts +17 -0
- package/package.json +9 -4
- package/packages/core/.size-limit.js +47 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +84 -0
- package/packages/core/llms.txt +610 -0
- package/packages/core/package.json +89 -0
- package/packages/core/src/call.ts +961 -0
- package/packages/core/src/canister.ts +259 -0
- package/packages/core/src/client.ts +2061 -0
- package/packages/core/src/errors.ts +1079 -0
- package/packages/core/src/hydration.ts +239 -0
- package/packages/core/src/index.ts +23 -0
- package/packages/core/src/keys.ts +108 -0
- package/packages/core/src/management.ts +138 -0
- package/packages/core/src/network.ts +1020 -0
- package/packages/core/src/options.ts +1000 -0
- package/packages/core/src/runtime.ts +50 -0
- package/packages/core/src/testing/fake-replica.ts +1166 -0
- package/packages/core/src/testing/index.ts +16 -0
- package/packages/core/src/testing/test-auth.ts +340 -0
- package/packages/core/src/testing/test-client.ts +559 -0
- package/packages/core/src/types.ts +541 -0
- package/packages/core/src/units.ts +299 -0
- package/packages/core/src/version.ts +4 -0
- package/packages/core/tests/canister-helpers.ts +187 -0
- package/packages/core/tests/canister-id.test.ts +158 -0
- package/packages/core/tests/canister-unwrap.test.ts +94 -0
- package/packages/core/tests/canister.test-d.ts +406 -0
- package/packages/core/tests/canister.test.ts +1921 -0
- package/packages/core/tests/client-account-change.test.ts +131 -0
- package/packages/core/tests/client-agents.test.ts +402 -0
- package/packages/core/tests/client-auth-network.test.ts +719 -0
- package/packages/core/tests/client-disposed.test.ts +403 -0
- package/packages/core/tests/client-helpers.ts +136 -0
- package/packages/core/tests/client-hydration.test.ts +166 -0
- package/packages/core/tests/client-pinned.test.ts +626 -0
- package/packages/core/tests/client-session.test.ts +348 -0
- package/packages/core/tests/client.test-d.ts +181 -0
- package/packages/core/tests/client.test.ts +1162 -0
- package/packages/core/tests/encode.test.ts +171 -0
- package/packages/core/tests/env-canister-id.test.ts +666 -0
- package/packages/core/tests/env-cookie-copies.test.ts +182 -0
- package/packages/core/tests/error-guard.test.ts +222 -0
- package/packages/core/tests/errors.test-d.ts +280 -0
- package/packages/core/tests/errors.test.ts +1359 -0
- package/packages/core/tests/fixtures/README.md +28 -0
- package/packages/core/tests/fixtures/accounts.did +21 -0
- package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
- package/packages/core/tests/fixtures/accounts.ts +26 -0
- package/packages/core/tests/fixtures/archive.did +31 -0
- package/packages/core/tests/fixtures/archive.envelope.json +259 -0
- package/packages/core/tests/fixtures/archive.ts +55 -0
- package/packages/core/tests/fixtures/icrc1.did +88 -0
- package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
- package/packages/core/tests/fixtures/icrc1.ts +78 -0
- package/packages/core/tests/fixtures/management.did +724 -0
- package/packages/core/tests/fixtures/management.envelope.json +4455 -0
- package/packages/core/tests/fixtures/management.ts +486 -0
- package/packages/core/tests/fixtures/shapes.did +33 -0
- package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
- package/packages/core/tests/fixtures/shapes.ts +56 -0
- package/packages/core/tests/fixtures/skippable.did +15 -0
- package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
- package/packages/core/tests/fixtures/skippable.ts +33 -0
- package/packages/core/tests/func.test.ts +154 -0
- package/packages/core/tests/identity-attributes.test.ts +214 -0
- package/packages/core/tests/keys.test.ts +289 -0
- package/packages/core/tests/management.test.ts +206 -0
- package/packages/core/tests/network-helpers.ts +121 -0
- package/packages/core/tests/network.test-d.ts +78 -0
- package/packages/core/tests/network.test.ts +1051 -0
- package/packages/core/tests/options.test.ts +1594 -0
- package/packages/core/tests/redirect-sign-in.test.ts +393 -0
- package/packages/core/tests/resend.test-d.ts +81 -0
- package/packages/core/tests/resend.test.ts +723 -0
- package/packages/core/tests/skip.test-d.ts +125 -0
- package/packages/core/tests/testing-entry.test.ts +73 -0
- package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
- package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
- package/packages/core/tests/testing-test-auth.test.ts +483 -0
- package/packages/core/tests/testing-test-client.test-d.ts +184 -0
- package/packages/core/tests/testing-test-client.test.ts +1272 -0
- package/packages/core/tests/time-realm.test.ts +63 -0
- package/packages/core/tests/time.test.ts +238 -0
- package/packages/core/tests/traps/README.md +39 -0
- package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
- package/packages/core/tests/traps/classification-table.test.ts +256 -0
- package/packages/core/tests/traps/err-arm.test.ts +144 -0
- package/packages/core/tests/traps/fetch-spy.ts +39 -0
- package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
- package/packages/core/tests/traps/ledger.ts +248 -0
- package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
- package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
- package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
- package/packages/core/tests/traps/update-as-query.test.ts +244 -0
- package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
- package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
- package/packages/core/tests/traps.test-d.ts +476 -0
- package/packages/core/tests/units.test-d.ts +118 -0
- package/packages/core/tests/units.test.ts +634 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/core/tsconfig.typecheck.json +16 -0
- package/packages/core/vitest.config.ts +13 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +189 -0
- package/packages/mcp/package.json +62 -0
- package/packages/mcp/src/candid/candid-text.test.ts +455 -0
- package/packages/mcp/src/candid/candid-text.ts +501 -0
- package/packages/mcp/src/candid/contract.test.ts +398 -0
- package/packages/mcp/src/candid/formats.ts +155 -0
- package/packages/mcp/src/candid/hex.ts +29 -0
- package/packages/mcp/src/candid/json-schema.test.ts +758 -0
- package/packages/mcp/src/candid/json-schema.ts +454 -0
- package/packages/mcp/src/candid/json.test.ts +1207 -0
- package/packages/mcp/src/candid/json.ts +1053 -0
- package/packages/mcp/src/candid/schema-walk.ts +189 -0
- package/packages/mcp/src/candid/type-text.test.ts +179 -0
- package/packages/mcp/src/candid/type-text.ts +238 -0
- package/packages/mcp/src/cli.ts +134 -0
- package/packages/mcp/src/identity.ts +188 -0
- package/packages/mcp/src/interface/cache.test.ts +48 -0
- package/packages/mcp/src/interface/cache.ts +68 -0
- package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
- package/packages/mcp/src/interface/cli-cache.ts +122 -0
- package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
- package/packages/mcp/src/interface/compile-worker.ts +290 -0
- package/packages/mcp/src/interface/compile.test.ts +155 -0
- package/packages/mcp/src/interface/compile.ts +281 -0
- package/packages/mcp/src/interface/docs.test.ts +105 -0
- package/packages/mcp/src/interface/docs.ts +86 -0
- package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
- package/packages/mcp/src/interface/hex.ts +8 -0
- package/packages/mcp/src/interface/ids.test.ts +71 -0
- package/packages/mcp/src/interface/ids.ts +87 -0
- package/packages/mcp/src/interface/loader.test.ts +804 -0
- package/packages/mcp/src/interface/loader.ts +519 -0
- package/packages/mcp/src/interface/read-candid.test.ts +300 -0
- package/packages/mcp/src/interface/read-candid.ts +259 -0
- package/packages/mcp/src/interface/types.ts +113 -0
- package/packages/mcp/src/network.ts +122 -0
- package/packages/mcp/src/options.ts +149 -0
- package/packages/mcp/src/server/approval.ts +159 -0
- package/packages/mcp/src/server/naming.ts +43 -0
- package/packages/mcp/src/server/results.ts +102 -0
- package/packages/mcp/src/server/server.ts +797 -0
- package/packages/mcp/src/server/tools.ts +116 -0
- package/packages/mcp/src/usage.ts +40 -0
- package/packages/mcp/tests/approval.test.ts +60 -0
- package/packages/mcp/tests/identity.test.ts +234 -0
- package/packages/mcp/tests/naming.test.ts +62 -0
- package/packages/mcp/tests/options.test.ts +140 -0
- package/packages/mcp/tests/server.test.ts +997 -0
- package/packages/mcp/tsconfig.json +19 -0
- package/packages/mcp/tsconfig.typecheck.json +10 -0
- package/packages/mcp/vitest.config.ts +10 -0
- package/packages/react/LICENSE +21 -0
- package/packages/react/README.md +328 -0
- package/packages/react/llms.txt +95 -0
- package/packages/react/package.json +95 -0
- package/packages/react/src/index.tsx +678 -0
- package/packages/react/tests/abandoned-render.test.tsx +816 -0
- package/packages/react/tests/auth.test.tsx +315 -0
- package/packages/react/tests/caller-keys.test.tsx +2334 -0
- package/packages/react/tests/client-time.test.ts +40 -0
- package/packages/react/tests/entry.test.ts +52 -0
- package/packages/react/tests/helpers.tsx +291 -0
- package/packages/react/tests/hydration.test.tsx +262 -0
- package/packages/react/tests/ownership.test.tsx +260 -0
- package/packages/react/tests/provider.server.test.tsx +169 -0
- package/packages/react/tests/query-client.test.tsx +107 -0
- package/packages/react/tests/strict-mode.test.tsx +288 -0
- package/packages/react/tests/suspense.test-d.tsx +357 -0
- package/packages/react/tests/two-query-cores.test.tsx +135 -0
- package/packages/react/tests/types.test-d.tsx +63 -0
- package/packages/react/tsconfig.json +23 -0
- package/packages/react/tsconfig.test.json +8 -0
- package/packages/react/tsconfig.typecheck.json +16 -0
- package/packages/react/vitest.config.ts +39 -0
- package/packages/vite-plugin/LICENSE +21 -0
- package/packages/vite-plugin/README.md +532 -0
- package/packages/vite-plugin/package.json +76 -0
- package/packages/vite-plugin/src/describe-value.test.ts +43 -0
- package/packages/vite-plugin/src/describe-value.ts +167 -0
- package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
- package/packages/vite-plugin/src/dev-environment.ts +510 -0
- package/packages/vite-plugin/src/env.ts +431 -0
- package/packages/vite-plugin/src/fetch.ts +348 -0
- package/packages/vite-plugin/src/file-identity.ts +106 -0
- package/packages/vite-plugin/src/generate.ts +691 -0
- package/packages/vite-plugin/src/index.test.ts +1243 -0
- package/packages/vite-plugin/src/index.ts +2565 -0
- package/packages/vite-plugin/src/interface.ts +339 -0
- package/packages/vite-plugin/src/local-host.ts +34 -0
- package/packages/vite-plugin/src/options.ts +641 -0
- package/packages/vite-plugin/src/principal-text.test.ts +87 -0
- package/packages/vite-plugin/src/principal-text.ts +98 -0
- package/packages/vite-plugin/tests/close.test.ts +141 -0
- package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
- package/packages/vite-plugin/tests/fake-ic.ts +214 -0
- package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
- package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
- package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
- package/packages/vite-plugin/tests/generation.test.ts +1036 -0
- package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
- package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
- package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
- package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
- package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
- package/packages/vite-plugin/tests/package.test.ts +62 -0
- package/packages/vite-plugin/tests/project.test.ts +2993 -0
- package/packages/vite-plugin/tests/support.ts +339 -0
- package/packages/vite-plugin/tests/timeout.test.ts +107 -0
- package/packages/vite-plugin/tsconfig.json +23 -0
- package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
- package/packages/vite-plugin/vitest.config.ts +9 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/ai-context-files.js +72 -0
- package/scripts/assemble-docs-site.mjs +485 -0
- package/scripts/assemble-docs-site.test.mjs +568 -0
- package/scripts/build-examples.js +58 -0
- package/scripts/check-ai-context.js +963 -0
- package/scripts/check-ai-context.test.mjs +811 -0
- package/scripts/check-exports.mjs +767 -0
- package/scripts/check-exports.test.mjs +1136 -0
- package/scripts/check-snippets/ambient.d.ts +428 -0
- package/scripts/check-snippets/app/generated/backend.did +7 -0
- package/scripts/check-snippets/app/generated/backend.ts +15 -0
- package/scripts/check-snippets/app/generated/icrc1.did +88 -0
- package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
- package/scripts/check-snippets/app/globals.ts +26 -0
- package/scripts/check-snippets/docs/guides/App.tsx +21 -0
- package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
- package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
- package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
- package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
- package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
- package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
- package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
- package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
- package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
- package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
- package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
- package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
- package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
- package/scripts/check-snippets.mjs +993 -0
- package/scripts/check-snippets.test.mjs +338 -0
- package/scripts/check-stack.mjs +501 -0
- package/scripts/check-stack.test.mjs +800 -0
- package/scripts/clean-node-modules.js +67 -0
- package/scripts/example-projects.js +58 -0
- package/scripts/export-budget.mjs +145 -0
- package/scripts/faults.json +6749 -0
- package/scripts/lib/fault-workspace.mjs +210 -0
- package/scripts/lib/io.mjs +29 -0
- package/scripts/lib/io.test.mjs +38 -0
- package/scripts/lib/markdown-fences.mjs +56 -0
- package/scripts/lib/markdown-fences.test.mjs +84 -0
- package/scripts/lib/package-exports.mjs +54 -0
- package/scripts/lib/package-exports.test.mjs +66 -0
- package/scripts/lib/release-pr-steps.mjs +47 -0
- package/scripts/release-tag.mjs +95 -0
- package/scripts/release-tag.test.mjs +278 -0
- package/scripts/release-workflow.test.mjs +98 -0
- package/scripts/release.js +167 -0
- package/scripts/release.test.mjs +347 -0
- package/scripts/removed-v3-names.js +125 -0
- package/scripts/size-app/.size-limit.js +90 -0
- package/scripts/size-app/core.js +3 -0
- package/scripts/size-app/package.json +24 -0
- package/scripts/size-app/react.js +4 -0
- package/scripts/size-app.test.mjs +73 -0
- package/scripts/sync-ai-context-versions.js +102 -0
- package/scripts/sync-example-versions.js +151 -0
- package/scripts/test-examples-published.mjs +1585 -0
- package/scripts/test-examples-published.test.mjs +1586 -0
- package/scripts/traps/any-method-vars-not-a-list.json +5 -0
- package/scripts/traps/canister-method-unknown.json +5 -0
- package/scripts/traps/client-says-who-calls-once.json +5 -0
- package/scripts/traps/client-says-who-calls.json +5 -0
- package/scripts/traps/err-only-on-canister-err.json +14 -0
- package/scripts/traps/format-units-refuses-missing-value.json +5 -0
- package/scripts/traps/format-units-refuses-number.json +5 -0
- package/scripts/traps/format-units-unknown-option.json +5 -0
- package/scripts/traps/guard-err-is-the-methods.json +5 -0
- package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
- package/scripts/traps/internals-not-exported.json +9 -0
- package/scripts/traps/network-refuses-unknown-name.json +5 -0
- package/scripts/traps/never-err-has-no-type.json +5 -0
- package/scripts/traps/options-method-unknown.json +5 -0
- package/scripts/traps/options-vars-wrong-type.json +5 -0
- package/scripts/traps/parse-units-refuses-number.json +5 -0
- package/scripts/traps/parse-units-unknown-option.json +5 -0
- package/scripts/traps/principal-is-not-plain-text.json +14 -0
- package/scripts/traps/query-options-refuses-oneway.json +5 -0
- package/scripts/traps/query-options-refuses-update.json +5 -0
- package/scripts/traps/refuse-next-unknown-option.json +5 -0
- package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
- package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
- package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
- package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
- package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
- package/scripts/traps/test-handler-method-unknown.json +5 -0
- package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
- package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
- package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
- package/scripts/typecheck-examples.js +122 -0
- package/scripts/verify-faults.mjs +375 -0
- package/scripts/verify-faults.test.mjs +402 -0
- package/scripts/verify-package-artifacts.mjs +590 -0
- package/scripts/verify-peer-floors.js +399 -0
- package/scripts/verify-peer-floors.test.mjs +422 -0
- package/scripts/verify-test-fails-rules.mjs +104 -0
- package/scripts/verify-test-fails-rules.test.mjs +215 -0
- package/scripts/verify-test-fails.js +371 -0
- package/scripts/verify-traps.mjs +505 -0
- package/scripts/verify-traps.test.mjs +521 -0
- package/scripts/verify-typescript-floor.js +239 -0
- package/skill-packages/ic-reactor/SKILL.md +103 -0
- package/stack.json +8 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,1594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `client.queryOptions` and `client.mutationOptions`, driven by real TanStack
|
|
3
|
+
* Query observers on the client's own `QueryClient`, against a fake replica.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
6
|
+
import { principal, type Principal } from "@meantjs/schema"
|
|
7
|
+
import { HttpErrorCode, ProtocolError } from "@icp-sdk/core/agent"
|
|
8
|
+
import { Ed25519KeyIdentity } from "@icp-sdk/core/identity"
|
|
9
|
+
import {
|
|
10
|
+
MutationObserver,
|
|
11
|
+
QueryClient,
|
|
12
|
+
QueryObserver,
|
|
13
|
+
dehydrate,
|
|
14
|
+
focusManager,
|
|
15
|
+
hydrate,
|
|
16
|
+
onlineManager,
|
|
17
|
+
skipToken,
|
|
18
|
+
type MutationObserverOptions,
|
|
19
|
+
type QueryKey,
|
|
20
|
+
} from "@tanstack/query-core"
|
|
21
|
+
import { classifyError, type ReactorError } from "../src/errors.js"
|
|
22
|
+
import { createClient, type Client } from "../src/index.js"
|
|
23
|
+
import type { CanisterMutationOptions } from "../src/types.js"
|
|
24
|
+
import { createTestAuth } from "../src/testing/test-auth.js"
|
|
25
|
+
import {
|
|
26
|
+
ANONYMOUS,
|
|
27
|
+
ARCHIVE,
|
|
28
|
+
FEE,
|
|
29
|
+
LEDGER,
|
|
30
|
+
SHAPES,
|
|
31
|
+
canisterRequests,
|
|
32
|
+
clientAs,
|
|
33
|
+
clientWithAuth,
|
|
34
|
+
deferred,
|
|
35
|
+
ledgerCanister,
|
|
36
|
+
replicaWith,
|
|
37
|
+
requestsFor,
|
|
38
|
+
serve,
|
|
39
|
+
sleep,
|
|
40
|
+
type HandWritten,
|
|
41
|
+
} from "./canister-helpers.js"
|
|
42
|
+
import { withIdentity } from "./client-helpers.js"
|
|
43
|
+
import * as icrc1 from "./fixtures/icrc1.js"
|
|
44
|
+
import * as shapes from "./fixtures/shapes.js"
|
|
45
|
+
import * as skippable from "./fixtures/skippable.js"
|
|
46
|
+
import { icEnvCookie, stubPage } from "./network-helpers.js"
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.unstubAllGlobals()
|
|
50
|
+
focusManager.setFocused(undefined)
|
|
51
|
+
onlineManager.setOnline(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const alice = Ed25519KeyIdentity.generate()
|
|
55
|
+
const ALICE = principal(alice.getPrincipal().toText())
|
|
56
|
+
const BOB = principal(Ed25519KeyIdentity.generate().getPrincipal().toText())
|
|
57
|
+
|
|
58
|
+
/** The canister a local redeploy moves a `{ name }` to, in the tests that redeploy. */
|
|
59
|
+
const REDEPLOYED = ARCHIVE
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Another canister of the shapes interface, reached by its id, in the test
|
|
63
|
+
* that writes to one canister and lists a `{ name }` in `invalidates`.
|
|
64
|
+
*/
|
|
65
|
+
const BY_ID = "r7inp-6aaaa-aaaaa-aaabq-cai"
|
|
66
|
+
|
|
67
|
+
/** A shapes canister whose `who` answers with the caller, held back for a caller while a gate is set. */
|
|
68
|
+
function whoCanister(holds: Map<string, Promise<void>>) {
|
|
69
|
+
return serve<shapes.Actor>(shapes.actor, {
|
|
70
|
+
who: async (_args, { caller }) => {
|
|
71
|
+
await holds.get(caller)
|
|
72
|
+
return principal(caller)
|
|
73
|
+
},
|
|
74
|
+
one: ([n]) => n + 1n,
|
|
75
|
+
address: ([seed], { caller }) => `${seed}:${caller}`,
|
|
76
|
+
bump: ([n]) => ({ tag: "ok", value: n + 1n }),
|
|
77
|
+
note: () => undefined,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("queryOptions", () => {
|
|
82
|
+
it("reads through the client's QueryClient, as the caller in the key", async () => {
|
|
83
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
84
|
+
const client = clientAs(replica, alice)
|
|
85
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
86
|
+
const options = client.queryOptions(canister, "who", undefined)
|
|
87
|
+
await expect(client.queryClient.fetchQuery(options)).resolves.toBe(ALICE)
|
|
88
|
+
expect(client.queryClient.getQueryData(options.queryKey)).toBe(ALICE)
|
|
89
|
+
expect(requestsFor(replica, "who")).toMatchObject([
|
|
90
|
+
{ endpoint: "query", caller: ALICE },
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("never shows one principal's data under another's key, across sign-in, a switch with a read in flight, and sign-out", async () => {
|
|
95
|
+
const holds = new Map<string, Promise<void>>()
|
|
96
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(holds) })
|
|
97
|
+
const auth = createTestAuth({ seed: 1, signedIn: false })
|
|
98
|
+
const client = clientWithAuth(replica, auth)
|
|
99
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
100
|
+
// The options a component would build on each render, without retries
|
|
101
|
+
// so that a refused read settles at once.
|
|
102
|
+
const options = () => ({
|
|
103
|
+
...client.queryOptions(canister, "who", undefined),
|
|
104
|
+
retry: false,
|
|
105
|
+
})
|
|
106
|
+
const observer = new QueryObserver(client.queryClient, options())
|
|
107
|
+
const shown: { caller: unknown; data: unknown }[] = []
|
|
108
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
109
|
+
shown.push({ caller: observer.options.queryKey[2], data: result.data })
|
|
110
|
+
})
|
|
111
|
+
// A component re-renders when the caller changes, and builds its options
|
|
112
|
+
// again: the read moves to the new caller's key.
|
|
113
|
+
const stop = client.subscribe(() => observer.setOptions(options()))
|
|
114
|
+
const shows = (who: string) =>
|
|
115
|
+
vi.waitFor(() => expect(observer.getCurrentResult().data).toBe(who))
|
|
116
|
+
|
|
117
|
+
await shows(ANONYMOUS)
|
|
118
|
+
await client.signIn()
|
|
119
|
+
const a = client.caller()
|
|
120
|
+
await shows(a)
|
|
121
|
+
const keyOfA: QueryKey = observer.options.queryKey
|
|
122
|
+
const optionsOfA = observer.options
|
|
123
|
+
|
|
124
|
+
// A's next read is held in flight, past the switch to B.
|
|
125
|
+
const held = deferred()
|
|
126
|
+
holds.set(a, held.promise)
|
|
127
|
+
void observer.refetch()
|
|
128
|
+
await vi.waitFor(() =>
|
|
129
|
+
expect(
|
|
130
|
+
requestsFor(replica, "who").filter((r) => r.caller === a)
|
|
131
|
+
).toHaveLength(2)
|
|
132
|
+
)
|
|
133
|
+
auth.switchTo(2)
|
|
134
|
+
const b = client.caller()
|
|
135
|
+
await shows(b)
|
|
136
|
+
held.resolve()
|
|
137
|
+
holds.delete(a)
|
|
138
|
+
await sleep(100)
|
|
139
|
+
expect(observer.getCurrentResult().data).toBe(b)
|
|
140
|
+
|
|
141
|
+
// A's reads left the cache when B became current, and A's slow answer
|
|
142
|
+
// did not bring them back.
|
|
143
|
+
expect(client.queryClient.getQueryData(keyOfA)).toBeUndefined()
|
|
144
|
+
|
|
145
|
+
// A read of A's key that starts now runs A's query function, made for A:
|
|
146
|
+
// it must not be sent as B, whose answer would land under A's key.
|
|
147
|
+
const sent = requestsFor(replica, "who").length
|
|
148
|
+
await client.queryClient
|
|
149
|
+
.fetchQuery({ ...optionsOfA, staleTime: 0 })
|
|
150
|
+
.catch((error: unknown) => error)
|
|
151
|
+
expect(requestsFor(replica, "who")).toHaveLength(sent)
|
|
152
|
+
expect(client.queryClient.getQueryData(keyOfA)).toBeUndefined()
|
|
153
|
+
|
|
154
|
+
// Back to A: A's key shows A's data and nobody else's.
|
|
155
|
+
auth.switchTo(1)
|
|
156
|
+
await shows(a)
|
|
157
|
+
await auth.signOut()
|
|
158
|
+
await shows(ANONYMOUS)
|
|
159
|
+
await sleep(100)
|
|
160
|
+
stop()
|
|
161
|
+
unsubscribe()
|
|
162
|
+
|
|
163
|
+
const callers = new Set(shown.map(({ caller }) => caller))
|
|
164
|
+
expect(callers).toEqual(new Set([ANONYMOUS, a, b]))
|
|
165
|
+
for (const { caller, data } of shown) {
|
|
166
|
+
if (data !== undefined) expect(data).toBe(caller)
|
|
167
|
+
}
|
|
168
|
+
// Every request went out as the principal of the key it was made for.
|
|
169
|
+
expect(
|
|
170
|
+
requestsFor(replica, "who").every((r) =>
|
|
171
|
+
[ANONYMOUS, a, b].includes(r.caller as string)
|
|
172
|
+
)
|
|
173
|
+
).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("refuses to run once the caller its key holds is no longer current, and sends nothing", async () => {
|
|
177
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
178
|
+
const auth = createTestAuth({ seed: 1 })
|
|
179
|
+
const client = clientWithAuth(replica, auth)
|
|
180
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
181
|
+
const options = client.queryOptions(canister, "who", undefined)
|
|
182
|
+
auth.switchTo(2)
|
|
183
|
+
await expect(
|
|
184
|
+
client.queryClient.fetchQuery({ ...options, retry: false })
|
|
185
|
+
).rejects.toMatchObject({
|
|
186
|
+
kind: "cancelled",
|
|
187
|
+
code: "caller_changed",
|
|
188
|
+
mayHaveExecuted: false,
|
|
189
|
+
})
|
|
190
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("does not re-send itself: TanStack's retry decides", async () => {
|
|
194
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
195
|
+
const client = clientAs(replica, alice)
|
|
196
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
197
|
+
replica.refuseNext(503)
|
|
198
|
+
await expect(
|
|
199
|
+
client.queryClient.fetchQuery({
|
|
200
|
+
...client.queryOptions(canister, "one", 1n),
|
|
201
|
+
retry: false,
|
|
202
|
+
})
|
|
203
|
+
).rejects.toMatchObject({ kind: "not_delivered", httpStatus: 503 })
|
|
204
|
+
expect(requestsFor(replica, "one")).toHaveLength(1)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("retries only a failure that proves the read was not delivered, and never on a server", () => {
|
|
208
|
+
const replica = replicaWith({})
|
|
209
|
+
const client = clientAs(replica, alice)
|
|
210
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
211
|
+
const { retry } = client.queryOptions(canister, "one", 1n)
|
|
212
|
+
const refused = classifyError(
|
|
213
|
+
ProtocolError.fromCode(new HttpErrorCode(503, "busy", [])),
|
|
214
|
+
{ method: "one", canisterId: SHAPES, mode: "query" }
|
|
215
|
+
)
|
|
216
|
+
const rejected = classifyError(
|
|
217
|
+
{ kind: "Reject", code: { rejectCode: 4 } },
|
|
218
|
+
{ method: "one", canisterId: SHAPES, mode: "query" }
|
|
219
|
+
)
|
|
220
|
+
// Node is a server: nothing is retried.
|
|
221
|
+
expect(retry(0, refused)).toBe(false)
|
|
222
|
+
vi.stubGlobal("window", {})
|
|
223
|
+
expect(retry(0, refused)).toBe(true)
|
|
224
|
+
expect(retry(3, refused)).toBe(false)
|
|
225
|
+
expect(retry(0, rejected)).toBe(false)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("builds a tagged key for arguments that do not encode, and rejects invalid_args when run, sending nothing", async () => {
|
|
229
|
+
const replica = replicaWith({ [LEDGER]: ledgerCanister(new Map()) })
|
|
230
|
+
const client = clientAs(replica, alice)
|
|
231
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
232
|
+
// Half-typed input, as a form holds it mid-edit.
|
|
233
|
+
const typing = { owner: "rrkah-" as Principal, subaccount: null }
|
|
234
|
+
let options: ReturnType<
|
|
235
|
+
typeof client.queryOptions<icrc1.Actor, "icrc1_balance_of">
|
|
236
|
+
>
|
|
237
|
+
expect(() => {
|
|
238
|
+
options = client.queryOptions(ledger, "icrc1_balance_of", typing)
|
|
239
|
+
}).not.toThrow()
|
|
240
|
+
expect(options!.queryKey.slice(4, 6)).toEqual([
|
|
241
|
+
"icrc1_balance_of",
|
|
242
|
+
"$invalid",
|
|
243
|
+
])
|
|
244
|
+
await expect(
|
|
245
|
+
client.queryClient.fetchQuery({ ...options!, retry: false })
|
|
246
|
+
).rejects.toMatchObject({
|
|
247
|
+
kind: "invalid_args",
|
|
248
|
+
issues: [{ code: "invalid_type", path: "$args[0].owner" }],
|
|
249
|
+
mayHaveExecuted: false,
|
|
250
|
+
})
|
|
251
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it("rejects canister_id_unresolved for an unresolved { name }, sending nothing", async () => {
|
|
255
|
+
const replica = replicaWith({})
|
|
256
|
+
const client = clientAs(replica, alice)
|
|
257
|
+
const byName = client.canister<shapes.Actor>(shapes.actor, {
|
|
258
|
+
name: "backend",
|
|
259
|
+
})
|
|
260
|
+
await expect(
|
|
261
|
+
client.queryClient.fetchQuery({
|
|
262
|
+
...client.queryOptions(byName, "one", 1n),
|
|
263
|
+
retry: false,
|
|
264
|
+
})
|
|
265
|
+
).rejects.toMatchObject({
|
|
266
|
+
kind: "invalid_args",
|
|
267
|
+
code: "canister_id_unresolved",
|
|
268
|
+
})
|
|
269
|
+
expect(replica.requests).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("skips a read with skipToken", () => {
|
|
273
|
+
const { client, canister } = (() => {
|
|
274
|
+
const c = clientAs(replicaWith({}), alice)
|
|
275
|
+
return {
|
|
276
|
+
client: c,
|
|
277
|
+
canister: c.canister<shapes.Actor>(shapes.actor, { id: SHAPES }),
|
|
278
|
+
}
|
|
279
|
+
})()
|
|
280
|
+
const options = client.queryOptions(canister, "one", skipToken)
|
|
281
|
+
expect(options.queryFn).toBe(skipToken)
|
|
282
|
+
expect(options.queryKey[options.queryKey.length - 1]).toBe("$skip")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("gives a query function for every value in place of the variables, and skipToken only for skipToken", async () => {
|
|
286
|
+
// The types drop SkipToken from the options' queryFn for variables that
|
|
287
|
+
// cannot be skipToken, so the builder must run each such read: an empty
|
|
288
|
+
// record, a record of opts alone, an opt and null, each null where it can
|
|
289
|
+
// be, and no arguments.
|
|
290
|
+
const replica = replicaWith({
|
|
291
|
+
[SHAPES]: serve<skippable.Actor>(skippable.actor, {
|
|
292
|
+
empty_record: () => 1n,
|
|
293
|
+
only_opts: ([{ limit }]) => limit ?? 2n,
|
|
294
|
+
maybe_nat: ([n]) => n ?? 3n,
|
|
295
|
+
nothing_at_all: () => 4n,
|
|
296
|
+
no_args: () => 5n,
|
|
297
|
+
anything: () => 6n,
|
|
298
|
+
}),
|
|
299
|
+
})
|
|
300
|
+
const client = clientAs(replica, alice)
|
|
301
|
+
const canister = client.canister<skippable.Actor>(skippable.actor, {
|
|
302
|
+
id: SHAPES,
|
|
303
|
+
})
|
|
304
|
+
const reads = [
|
|
305
|
+
[client.queryOptions(canister, "empty_record", {}), 1n],
|
|
306
|
+
[
|
|
307
|
+
client.queryOptions(canister, "only_opts", {
|
|
308
|
+
limit: null,
|
|
309
|
+
prefix: null,
|
|
310
|
+
}),
|
|
311
|
+
2n,
|
|
312
|
+
],
|
|
313
|
+
[client.queryOptions(canister, "maybe_nat", null), 3n],
|
|
314
|
+
[client.queryOptions(canister, "nothing_at_all", null), 4n],
|
|
315
|
+
[client.queryOptions(canister, "no_args"), 5n],
|
|
316
|
+
[client.queryOptions(canister, "no_args", undefined), 5n],
|
|
317
|
+
[client.queryOptions(canister, "anything", null), 6n],
|
|
318
|
+
] as const
|
|
319
|
+
for (const [options, expected] of reads) {
|
|
320
|
+
expect(typeof options.queryFn).toBe("function")
|
|
321
|
+
await expect(client.queryClient.fetchQuery(options)).resolves.toBe(
|
|
322
|
+
expected
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
for (const method of [
|
|
326
|
+
"empty_record",
|
|
327
|
+
"only_opts",
|
|
328
|
+
"maybe_nat",
|
|
329
|
+
"nothing_at_all",
|
|
330
|
+
"no_args",
|
|
331
|
+
"anything",
|
|
332
|
+
] as const) {
|
|
333
|
+
const options = client.queryOptions(canister, method, skipToken)
|
|
334
|
+
expect(options.queryFn).toBe(skipToken)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it("refuses another copy's skipToken, which it cannot tell from an argument, and still skips its own and passes a symbol to reserved", async () => {
|
|
339
|
+
const replica = replicaWith({
|
|
340
|
+
[SHAPES]: serve<skippable.Actor>(skippable.actor, {
|
|
341
|
+
anything: () => 6n,
|
|
342
|
+
}),
|
|
343
|
+
})
|
|
344
|
+
const client = clientAs(replica, alice)
|
|
345
|
+
const canister = client.canister<skippable.Actor>(skippable.actor, {
|
|
346
|
+
id: SHAPES,
|
|
347
|
+
})
|
|
348
|
+
// What a second copy of @tanstack/query-core exports: its own Symbol().
|
|
349
|
+
const foreign = Symbol() as never
|
|
350
|
+
const twoCopies =
|
|
351
|
+
/a symbol, not core's skipToken: two copies of @tanstack\/query-core are installed.*`npm ls @tanstack\/query-core` must show one copy/
|
|
352
|
+
expect(() => client.queryOptions(canister, "maybe_nat", foreign)).toThrow(
|
|
353
|
+
twoCopies
|
|
354
|
+
)
|
|
355
|
+
expect(() =>
|
|
356
|
+
client.queryOptions(canister, "empty_record", foreign)
|
|
357
|
+
).toThrow(TypeError)
|
|
358
|
+
expect(() => client.queryKey(canister, "maybe_nat", foreign)).toThrow(
|
|
359
|
+
twoCopies
|
|
360
|
+
)
|
|
361
|
+
expect(() => client.queryKey(canister, "no_args", foreign)).toThrow(
|
|
362
|
+
twoCopies
|
|
363
|
+
)
|
|
364
|
+
// Its own skipToken still skips.
|
|
365
|
+
expect(client.queryOptions(canister, "maybe_nat", skipToken).queryFn).toBe(
|
|
366
|
+
skipToken
|
|
367
|
+
)
|
|
368
|
+
const skipped = client.queryKey(canister, "maybe_nat", skipToken as never)
|
|
369
|
+
expect(skipped[skipped.length - 1]).toBe("$skip")
|
|
370
|
+
// reserved takes every value, a symbol included: it is encoded and read.
|
|
371
|
+
const options = client.queryOptions(canister, "anything", foreign)
|
|
372
|
+
expect(typeof options.queryFn).toBe("function")
|
|
373
|
+
await expect(client.queryClient.fetchQuery(options)).resolves.toBe(6n)
|
|
374
|
+
expect(() => client.queryKey(canister, "anything", foreign)).not.toThrow()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it("throws a TypeError for an update or oneway method, a certified composite query, or an unknown fourth argument", () => {
|
|
378
|
+
const client = clientAs(replicaWith({}), alice)
|
|
379
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
380
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
381
|
+
id: SHAPES,
|
|
382
|
+
certified: true,
|
|
383
|
+
})
|
|
384
|
+
// The generated Actor's update and oneway methods do not compile here
|
|
385
|
+
// (tests/traps.test-d.ts); typed by hand, without modes, they do, and the
|
|
386
|
+
// check at run time refuses them.
|
|
387
|
+
const handWritten = client.canister<HandWritten<shapes.Actor>>(
|
|
388
|
+
shapes.actor,
|
|
389
|
+
{ id: SHAPES }
|
|
390
|
+
)
|
|
391
|
+
expect(() => client.queryOptions(handWritten, "bump", 1n)).toThrow(
|
|
392
|
+
/bump is an update method/
|
|
393
|
+
)
|
|
394
|
+
expect(() => client.queryOptions(handWritten, "note", "x")).toThrow(
|
|
395
|
+
TypeError
|
|
396
|
+
)
|
|
397
|
+
expect(() =>
|
|
398
|
+
client.queryOptions(handWritten, "fire", "x", { update: "idempotent" })
|
|
399
|
+
).toThrow(/oneway/)
|
|
400
|
+
expect(() => client.queryOptions(certified, "composite", 1n)).toThrow(
|
|
401
|
+
/composite query/
|
|
402
|
+
)
|
|
403
|
+
expect(() =>
|
|
404
|
+
client.queryOptions(canister, "one", 1n, {
|
|
405
|
+
update: "always",
|
|
406
|
+
} as unknown as { update: "idempotent" })
|
|
407
|
+
).toThrow(TypeError)
|
|
408
|
+
expect(() => client.queryOptions(canister, "composite", 1n)).not.toThrow()
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it("throws a TypeError for a method without results, which leaves a read nothing to cache, and its direct call still resolves", async () => {
|
|
412
|
+
const replica = replicaWith({
|
|
413
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, {
|
|
414
|
+
nothing: () => undefined,
|
|
415
|
+
}),
|
|
416
|
+
})
|
|
417
|
+
const client = clientAs(replica, alice)
|
|
418
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
419
|
+
// Its call resolves undefined, which TanStack Query reports as a failed
|
|
420
|
+
// read with nothing cached: refused when the options are built instead.
|
|
421
|
+
expect(() => client.queryOptions(canister, "nothing")).toThrow(TypeError)
|
|
422
|
+
expect(() => client.queryOptions(canister, "nothing", skipToken)).toThrow(
|
|
423
|
+
/nothing has no results, and a method without results has nothing to cache\. Call it directly\./
|
|
424
|
+
)
|
|
425
|
+
// An update without results, opted in as a read, has nothing to cache
|
|
426
|
+
// either.
|
|
427
|
+
expect(() =>
|
|
428
|
+
client.queryOptions(canister, "note", "x", { update: "idempotent" })
|
|
429
|
+
).toThrow(
|
|
430
|
+
/note has no results.*Call it directly, or through mutationOptions/
|
|
431
|
+
)
|
|
432
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
433
|
+
await expect(canister.nothing()).resolves.toBeUndefined()
|
|
434
|
+
expect(requestsFor(replica, "nothing")).toMatchObject([
|
|
435
|
+
{ endpoint: "query" },
|
|
436
|
+
])
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
describe("an update read as a query, with { update: 'idempotent' }", () => {
|
|
441
|
+
/** A signed-in client in a page, mounted so focus and network events reach its queries. */
|
|
442
|
+
function mounted() {
|
|
443
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
444
|
+
const auth = createTestAuth({ seed: 1 })
|
|
445
|
+
const client = clientWithAuth(replica, auth)
|
|
446
|
+
client.queryClient.mount()
|
|
447
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
448
|
+
return { replica, auth, client, canister }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Plays out what makes TanStack refetch: losing and regaining focus and the network, and a remount. */
|
|
452
|
+
async function refocusReconnectRemount(
|
|
453
|
+
remount: () => () => void
|
|
454
|
+
): Promise<void> {
|
|
455
|
+
focusManager.setFocused(false)
|
|
456
|
+
focusManager.setFocused(true)
|
|
457
|
+
await sleep(100)
|
|
458
|
+
onlineManager.setOnline(false)
|
|
459
|
+
onlineManager.setOnline(true)
|
|
460
|
+
await sleep(100)
|
|
461
|
+
const unsubscribe = remount()
|
|
462
|
+
await sleep(100)
|
|
463
|
+
unsubscribe()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
it("is fetched once and never refetched on focus, reconnect or remount", async () => {
|
|
467
|
+
const { replica, client, canister } = mounted()
|
|
468
|
+
const build = () =>
|
|
469
|
+
client.queryOptions(canister, "address", "x", { update: "idempotent" })
|
|
470
|
+
expect(build()).toMatchObject({
|
|
471
|
+
staleTime: Infinity,
|
|
472
|
+
refetchOnMount: false,
|
|
473
|
+
refetchOnWindowFocus: false,
|
|
474
|
+
refetchOnReconnect: false,
|
|
475
|
+
})
|
|
476
|
+
const observer = new QueryObserver(client.queryClient, build())
|
|
477
|
+
const unsubscribe = observer.subscribe(() => {})
|
|
478
|
+
await vi.waitFor(() =>
|
|
479
|
+
expect(observer.getCurrentResult().data).toBe(`x:${client.caller()}`)
|
|
480
|
+
)
|
|
481
|
+
await refocusReconnectRemount(() =>
|
|
482
|
+
new QueryObserver(client.queryClient, build()).subscribe(() => {})
|
|
483
|
+
)
|
|
484
|
+
unsubscribe()
|
|
485
|
+
client.queryClient.unmount()
|
|
486
|
+
expect(requestsFor(replica, "address")).toMatchObject([
|
|
487
|
+
{ endpoint: "call" },
|
|
488
|
+
])
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it("is refetched on focus, reconnect and remount when it is a plain read (the harness can see a refetch)", async () => {
|
|
492
|
+
const { replica, client, canister } = mounted()
|
|
493
|
+
const build = () => client.queryOptions(canister, "one", 1n)
|
|
494
|
+
const observer = new QueryObserver(client.queryClient, build())
|
|
495
|
+
const unsubscribe = observer.subscribe(() => {})
|
|
496
|
+
await vi.waitFor(() => expect(observer.getCurrentResult().data).toBe(2n))
|
|
497
|
+
await refocusReconnectRemount(() =>
|
|
498
|
+
new QueryObserver(client.queryClient, build()).subscribe(() => {})
|
|
499
|
+
)
|
|
500
|
+
unsubscribe()
|
|
501
|
+
client.queryClient.unmount()
|
|
502
|
+
expect(requestsFor(replica, "one").length).toBeGreaterThanOrEqual(3)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it("is read again after a write to its canister, unless invalidates leaves it out", async () => {
|
|
506
|
+
const { replica, client, canister } = mounted()
|
|
507
|
+
const observer = new QueryObserver(
|
|
508
|
+
client.queryClient,
|
|
509
|
+
client.queryOptions(canister, "address", "x", { update: "idempotent" })
|
|
510
|
+
)
|
|
511
|
+
const unsubscribe = observer.subscribe(() => {})
|
|
512
|
+
await vi.waitFor(() =>
|
|
513
|
+
expect(observer.getCurrentResult().data).toBe(`x:${client.caller()}`)
|
|
514
|
+
)
|
|
515
|
+
// Q10's default: every read of the canister written to, this one too.
|
|
516
|
+
await new MutationObserver(
|
|
517
|
+
client.queryClient,
|
|
518
|
+
client.mutationOptions(canister, "bump")
|
|
519
|
+
).mutate(1n)
|
|
520
|
+
expect(requestsFor(replica, "address")).toHaveLength(2)
|
|
521
|
+
await new MutationObserver(
|
|
522
|
+
client.queryClient,
|
|
523
|
+
client.mutationOptions(canister, "bump", {
|
|
524
|
+
invalidates: [[canister, "one"]],
|
|
525
|
+
})
|
|
526
|
+
).mutate(1n)
|
|
527
|
+
expect(requestsFor(replica, "address")).toHaveLength(2)
|
|
528
|
+
unsubscribe()
|
|
529
|
+
client.queryClient.unmount()
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it("is refused before sending for a caller who is not signed in", async () => {
|
|
533
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
534
|
+
const client = clientAs(replica, "anonymous")
|
|
535
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
536
|
+
await expect(
|
|
537
|
+
client.queryClient.fetchQuery({
|
|
538
|
+
...client.queryOptions(canister, "address", "x", {
|
|
539
|
+
update: "idempotent",
|
|
540
|
+
}),
|
|
541
|
+
retry: false,
|
|
542
|
+
})
|
|
543
|
+
).rejects.toMatchObject({ kind: "unauthenticated" })
|
|
544
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it("retries only a failure that proves the update never got in", () => {
|
|
548
|
+
vi.stubGlobal("window", {})
|
|
549
|
+
const client = clientAs(replicaWith({}), alice)
|
|
550
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
551
|
+
const { retry } = client.queryOptions(canister, "address", "x", {
|
|
552
|
+
update: "idempotent",
|
|
553
|
+
})
|
|
554
|
+
const update = {
|
|
555
|
+
method: "address",
|
|
556
|
+
canisterId: SHAPES,
|
|
557
|
+
mode: "update",
|
|
558
|
+
accepted: false,
|
|
559
|
+
} as const
|
|
560
|
+
expect(
|
|
561
|
+
retry(
|
|
562
|
+
0,
|
|
563
|
+
classifyError(
|
|
564
|
+
ProtocolError.fromCode(new HttpErrorCode(429, "slow down", [])),
|
|
565
|
+
update
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
).toBe(true)
|
|
569
|
+
expect(
|
|
570
|
+
retry(
|
|
571
|
+
0,
|
|
572
|
+
classifyError(
|
|
573
|
+
ProtocolError.fromCode(new HttpErrorCode(503, "busy", [])),
|
|
574
|
+
update
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
).toBe(false)
|
|
578
|
+
expect(retry(0, classifyError(new TypeError("lost"), update))).toBe(false)
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe("mutationOptions", () => {
|
|
583
|
+
/** A ledger, a balance read observed on the client's QueryClient, and a transfer mutation. */
|
|
584
|
+
async function wallet(client?: Client) {
|
|
585
|
+
const balances = new Map<string, bigint>([[ALICE, 1_000_000n]])
|
|
586
|
+
const replica = replicaWith({ [LEDGER]: ledgerCanister(balances) })
|
|
587
|
+
const reader = clientAs(replica, alice)
|
|
588
|
+
const writer = client ?? reader
|
|
589
|
+
const ledger = reader.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
590
|
+
const balance = new QueryObserver(
|
|
591
|
+
reader.queryClient,
|
|
592
|
+
reader.queryOptions(ledger, "icrc1_balance_of", {
|
|
593
|
+
owner: ALICE,
|
|
594
|
+
subaccount: null,
|
|
595
|
+
})
|
|
596
|
+
)
|
|
597
|
+
const unsubscribe = balance.subscribe(() => {})
|
|
598
|
+
await vi.waitFor(() =>
|
|
599
|
+
expect(balance.getCurrentResult().data).toBe(1_000_000n)
|
|
600
|
+
)
|
|
601
|
+
return { replica, reader, writer, ledger, balance, unsubscribe }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const transfer = (amount: bigint): icrc1.TransferArg => ({
|
|
605
|
+
to: { owner: BOB, subaccount: null },
|
|
606
|
+
amount,
|
|
607
|
+
fee: null,
|
|
608
|
+
memo: null,
|
|
609
|
+
from_subaccount: null,
|
|
610
|
+
created_at_time: null,
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const reads = (replica: ReturnType<typeof replicaWith>) =>
|
|
614
|
+
requestsFor(replica, "icrc1_balance_of").length
|
|
615
|
+
|
|
616
|
+
it("returns retry: false and, called with no onMutate run, calls the method as the caller current then", async () => {
|
|
617
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
618
|
+
const auth = createTestAuth({ seed: 1, signedIn: false })
|
|
619
|
+
const client = clientWithAuth(replica, auth)
|
|
620
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
621
|
+
const options = client.mutationOptions(canister, "address")
|
|
622
|
+
expect(options.retry).toBe(false)
|
|
623
|
+
expect(options.mutationKey).toEqual([
|
|
624
|
+
"ic-reactor",
|
|
625
|
+
client.network,
|
|
626
|
+
SHAPES,
|
|
627
|
+
"address",
|
|
628
|
+
])
|
|
629
|
+
await expect(options.mutationFn("x")).rejects.toMatchObject({
|
|
630
|
+
kind: "unauthenticated",
|
|
631
|
+
})
|
|
632
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
633
|
+
await auth.signIn()
|
|
634
|
+
await expect(options.mutationFn("x")).resolves.toBe(`x:${client.caller()}`)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it("invalidates the canister's reads after a lost reply, and the re-read shows the debit", async () => {
|
|
638
|
+
const { replica, writer, ledger, balance, unsubscribe } = await wallet()
|
|
639
|
+
const mutation = new MutationObserver(
|
|
640
|
+
writer.queryClient,
|
|
641
|
+
writer.mutationOptions(ledger, "icrc1_transfer")
|
|
642
|
+
)
|
|
643
|
+
replica.dropNextReply()
|
|
644
|
+
await expect(mutation.mutate(transfer(100n))).rejects.toMatchObject({
|
|
645
|
+
kind: "outcome_unknown",
|
|
646
|
+
mayHaveExecuted: true,
|
|
647
|
+
})
|
|
648
|
+
// onSettled has invalidated and waited for the active read to refetch.
|
|
649
|
+
expect(balance.getCurrentResult().data).toBe(1_000_000n - 100n - FEE)
|
|
650
|
+
expect(reads(replica)).toBe(2)
|
|
651
|
+
expect(requestsFor(replica, "icrc1_transfer")).toHaveLength(1)
|
|
652
|
+
unsubscribe()
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it.each([
|
|
656
|
+
["a success", 100n],
|
|
657
|
+
["a canister_err", 5_000_000n],
|
|
658
|
+
])("invalidates the canister's reads after %s", async (_label, amount) => {
|
|
659
|
+
const { replica, writer, ledger, unsubscribe } = await wallet()
|
|
660
|
+
const mutation = new MutationObserver(
|
|
661
|
+
writer.queryClient,
|
|
662
|
+
writer.mutationOptions(ledger, "icrc1_transfer")
|
|
663
|
+
)
|
|
664
|
+
await mutation.mutate(transfer(amount)).catch(() => undefined)
|
|
665
|
+
expect(reads(replica)).toBe(2)
|
|
666
|
+
unsubscribe()
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
it("does not invalidate after invalid_args, not_delivered or unauthenticated", async () => {
|
|
670
|
+
const { replica, writer, reader, ledger, balance, unsubscribe } =
|
|
671
|
+
await wallet()
|
|
672
|
+
const key = balance.options.queryKey
|
|
673
|
+
const mutation = new MutationObserver(
|
|
674
|
+
writer.queryClient,
|
|
675
|
+
writer.mutationOptions(ledger, "icrc1_transfer")
|
|
676
|
+
)
|
|
677
|
+
await expect(
|
|
678
|
+
mutation.mutate({ ...transfer(1n), amount: -1n })
|
|
679
|
+
).rejects.toMatchObject({ kind: "invalid_args" })
|
|
680
|
+
replica.refuseNext(400)
|
|
681
|
+
await expect(mutation.mutate(transfer(1n))).rejects.toMatchObject({
|
|
682
|
+
kind: "not_delivered",
|
|
683
|
+
})
|
|
684
|
+
const signedOut = createClientSignedOut(replica)
|
|
685
|
+
const anonymousLedger = signedOut.canister<icrc1.Actor>(icrc1.actor, {
|
|
686
|
+
id: LEDGER,
|
|
687
|
+
})
|
|
688
|
+
// The anonymous client's own QueryClient holds a read of the ledger too.
|
|
689
|
+
const otherRead = signedOut.queryOptions(anonymousLedger, "icrc1_fee")
|
|
690
|
+
await signedOut.queryClient.fetchQuery(otherRead)
|
|
691
|
+
await expect(
|
|
692
|
+
new MutationObserver(
|
|
693
|
+
signedOut.queryClient,
|
|
694
|
+
signedOut.mutationOptions(anonymousLedger, "icrc1_transfer")
|
|
695
|
+
).mutate(transfer(1n))
|
|
696
|
+
).rejects.toMatchObject({ kind: "unauthenticated" })
|
|
697
|
+
expect(
|
|
698
|
+
signedOut.queryClient.getQueryState(otherRead.queryKey)?.isInvalidated
|
|
699
|
+
).toBe(false)
|
|
700
|
+
expect(reader.queryClient.getQueryState(key)?.isInvalidated).toBe(false)
|
|
701
|
+
expect(reads(replica)).toBe(1)
|
|
702
|
+
unsubscribe()
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
it("does not invalidate after a write cancelled before it was sent", async () => {
|
|
706
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(new Map()) })
|
|
707
|
+
const auth = createTestAuth({ seed: 1, signedIn: false })
|
|
708
|
+
// The identity of the write's caller arrives only after they signed out.
|
|
709
|
+
const asked = deferred()
|
|
710
|
+
const gate = deferred()
|
|
711
|
+
let slow = false
|
|
712
|
+
const slowAuth = withIdentity(auth, async () => {
|
|
713
|
+
const identity = await auth.getIdentity()
|
|
714
|
+
if (slow) {
|
|
715
|
+
asked.resolve()
|
|
716
|
+
await gate.promise
|
|
717
|
+
}
|
|
718
|
+
return identity
|
|
719
|
+
})
|
|
720
|
+
const client = clientWithAuth(replica, slowAuth)
|
|
721
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
722
|
+
// A read the write would invalidate, made before anyone signed in: the
|
|
723
|
+
// sign-out below keeps the anonymous reads.
|
|
724
|
+
const read = client.queryOptions(canister, "one", 1n)
|
|
725
|
+
await client.queryClient.fetchQuery(read)
|
|
726
|
+
await client.signIn()
|
|
727
|
+
|
|
728
|
+
slow = true
|
|
729
|
+
const write = new MutationObserver(
|
|
730
|
+
client.queryClient,
|
|
731
|
+
client.mutationOptions(canister, "address")
|
|
732
|
+
).mutate("x")
|
|
733
|
+
await asked.promise
|
|
734
|
+
await auth.signOut()
|
|
735
|
+
gate.resolve()
|
|
736
|
+
await expect(write).rejects.toMatchObject({
|
|
737
|
+
kind: "cancelled",
|
|
738
|
+
code: "caller_changed",
|
|
739
|
+
mayHaveExecuted: false,
|
|
740
|
+
})
|
|
741
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
742
|
+
expect(client.queryClient.getQueryState(read.queryKey)?.isInvalidated).toBe(
|
|
743
|
+
false
|
|
744
|
+
)
|
|
745
|
+
expect(requestsFor(replica, "one")).toHaveLength(1)
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
it("invalidates only what invalidates lists, and nothing for []", async () => {
|
|
749
|
+
const { replica, writer, ledger, unsubscribe } = await wallet()
|
|
750
|
+
const fee = writer.queryOptions(ledger, "icrc1_fee")
|
|
751
|
+
await writer.queryClient.fetchQuery(fee)
|
|
752
|
+
const only = new MutationObserver(
|
|
753
|
+
writer.queryClient,
|
|
754
|
+
writer.mutationOptions(ledger, "icrc1_transfer", {
|
|
755
|
+
invalidates: [[ledger, "icrc1_fee"]],
|
|
756
|
+
})
|
|
757
|
+
)
|
|
758
|
+
await only.mutate(transfer(1n))
|
|
759
|
+
expect(writer.queryClient.getQueryState(fee.queryKey)?.isInvalidated).toBe(
|
|
760
|
+
true
|
|
761
|
+
)
|
|
762
|
+
expect(reads(replica)).toBe(1)
|
|
763
|
+
const none = new MutationObserver(
|
|
764
|
+
writer.queryClient,
|
|
765
|
+
writer.mutationOptions(ledger, "icrc1_transfer", { invalidates: [] })
|
|
766
|
+
)
|
|
767
|
+
await none.mutate(transfer(1n))
|
|
768
|
+
expect(reads(replica)).toBe(1)
|
|
769
|
+
unsubscribe()
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it("invalidates the reads of every caller, certified or not", async () => {
|
|
773
|
+
const holds = new Map<string, Promise<void>>()
|
|
774
|
+
const replica = replicaWith({ [SHAPES]: whoCanister(holds) })
|
|
775
|
+
const auth = createTestAuth({ seed: 1, signedIn: false })
|
|
776
|
+
const client = clientWithAuth(replica, auth)
|
|
777
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
778
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
779
|
+
id: SHAPES,
|
|
780
|
+
certified: true,
|
|
781
|
+
})
|
|
782
|
+
// Reads by nobody signed in, which a sign-in keeps, and by A.
|
|
783
|
+
const anonymous = client.queryOptions(canister, "who", undefined)
|
|
784
|
+
const certifiedAnonymous = client.queryOptions(certified, "one", 1n)
|
|
785
|
+
await client.queryClient.fetchQuery(anonymous)
|
|
786
|
+
await client.queryClient.fetchQuery(certifiedAnonymous)
|
|
787
|
+
await client.signIn()
|
|
788
|
+
const asA = client.queryOptions(canister, "who", undefined)
|
|
789
|
+
const certifiedAsA = client.queryOptions(certified, "one", 1n)
|
|
790
|
+
await client.queryClient.fetchQuery(asA)
|
|
791
|
+
await client.queryClient.fetchQuery(certifiedAsA)
|
|
792
|
+
// And one of B's, as a persister restores it into the cache.
|
|
793
|
+
const b = createTestAuth({ seed: 2 }).getPrincipal()!.toText()
|
|
794
|
+
const asB = [...asA.queryKey.slice(0, 2), b, ...asA.queryKey.slice(3)]
|
|
795
|
+
client.queryClient.setQueryData(asB, b)
|
|
796
|
+
await client
|
|
797
|
+
.mutationOptions(canister, "note")
|
|
798
|
+
.onSettled(undefined, null, "x")
|
|
799
|
+
for (const queryKey of [
|
|
800
|
+
anonymous.queryKey,
|
|
801
|
+
certifiedAnonymous.queryKey,
|
|
802
|
+
asA.queryKey,
|
|
803
|
+
certifiedAsA.queryKey,
|
|
804
|
+
asB,
|
|
805
|
+
]) {
|
|
806
|
+
expect(client.queryClient.getQueryState(queryKey)?.isInvalidated).toBe(
|
|
807
|
+
true
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
// An app that nests its own QueryClientProvider inside the client's keeps
|
|
813
|
+
// its reads, and runs its writes, on its own QueryClient (#903).
|
|
814
|
+
it("invalidates the reads on the QueryClient the mutation ran on, and on the client's own", async () => {
|
|
815
|
+
const { replica, reader, ledger, balance, unsubscribe } = await wallet()
|
|
816
|
+
const app = new QueryClient()
|
|
817
|
+
const account = { owner: ALICE, subaccount: null }
|
|
818
|
+
const appBalance = new QueryObserver(
|
|
819
|
+
app,
|
|
820
|
+
reader.queryOptions(ledger, "icrc1_balance_of", account)
|
|
821
|
+
)
|
|
822
|
+
const unsubscribeApp = appBalance.subscribe(() => {})
|
|
823
|
+
await vi.waitFor(() =>
|
|
824
|
+
expect(appBalance.getCurrentResult().data).toBe(1_000_000n)
|
|
825
|
+
)
|
|
826
|
+
const mutation = new MutationObserver(
|
|
827
|
+
app,
|
|
828
|
+
reader.mutationOptions(ledger, "icrc1_transfer")
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
await mutation.mutate(transfer(100n))
|
|
832
|
+
|
|
833
|
+
// onSettled waited for both active reads to refetch. A TanStack Query
|
|
834
|
+
// older than 5.89 does not tell onSettled which QueryClient the run was
|
|
835
|
+
// on, so only the client's own reads are invalidated there.
|
|
836
|
+
const handsClient = await sharesRunContext()
|
|
837
|
+
expect(balance.getCurrentResult().data).toBe(1_000_000n - 100n - FEE)
|
|
838
|
+
expect(appBalance.getCurrentResult().data).toBe(
|
|
839
|
+
handsClient ? 1_000_000n - 100n - FEE : 1_000_000n
|
|
840
|
+
)
|
|
841
|
+
expect(reads(replica)).toBe(handsClient ? 4 : 3)
|
|
842
|
+
unsubscribeApp()
|
|
843
|
+
unsubscribe()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* A client on a local page, where the ic_env cookie is trusted, and a
|
|
848
|
+
* `{ name: "backend" }` canister of it. The cookie is read afresh every
|
|
849
|
+
* time, and `redeploy()` moves the name from SHAPES to REDEPLOYED, as a
|
|
850
|
+
* local redeploy rewrites the cookie. `bump` waits for `hold` at SHAPES
|
|
851
|
+
* and at BY_ID, a shapes canister the name never points to.
|
|
852
|
+
*/
|
|
853
|
+
function redeployable(hold?: Promise<void>) {
|
|
854
|
+
let deployed = SHAPES
|
|
855
|
+
stubPage("http://localhost:5173")
|
|
856
|
+
vi.stubGlobal("document", {
|
|
857
|
+
get cookie() {
|
|
858
|
+
return icEnvCookie({ "PUBLIC_CANISTER_ID:backend": deployed })
|
|
859
|
+
},
|
|
860
|
+
})
|
|
861
|
+
const backendAt = (wait?: Promise<void>) =>
|
|
862
|
+
serve<shapes.Actor>(shapes.actor, {
|
|
863
|
+
one: ([n]) => n + 1n,
|
|
864
|
+
bump: async ([n]) => {
|
|
865
|
+
await wait
|
|
866
|
+
return { tag: "ok", value: n + 1n }
|
|
867
|
+
},
|
|
868
|
+
})
|
|
869
|
+
const replica = replicaWith({
|
|
870
|
+
[SHAPES]: backendAt(hold),
|
|
871
|
+
[REDEPLOYED]: backendAt(),
|
|
872
|
+
[BY_ID]: backendAt(hold),
|
|
873
|
+
})
|
|
874
|
+
const client = clientAs(replica, alice)
|
|
875
|
+
const backend = client.canister<shapes.Actor>(shapes.actor, {
|
|
876
|
+
name: "backend",
|
|
877
|
+
})
|
|
878
|
+
const redeploy = () => {
|
|
879
|
+
deployed = REDEPLOYED
|
|
880
|
+
}
|
|
881
|
+
return { replica, client, backend, redeploy }
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
type BumpOptions = CanisterMutationOptions<bigint, bigint, string>
|
|
885
|
+
|
|
886
|
+
/** What an app hands `useMutation` or a `MutationObserver` for `bump`, with an onMutate of its own or not. */
|
|
887
|
+
type AppBumpOptions = MutationObserverOptions<
|
|
888
|
+
bigint,
|
|
889
|
+
ReactorError<string>,
|
|
890
|
+
bigint,
|
|
891
|
+
unknown
|
|
892
|
+
>
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Writes `bump` to the `{ name }` canister with the options `compose`
|
|
896
|
+
* returns, and moves the name to another canister while the update is in
|
|
897
|
+
* flight. The write must reach the first canister, whose cached reads it
|
|
898
|
+
* invalidates, and leave the reads of the new one alone.
|
|
899
|
+
*/
|
|
900
|
+
async function writeAcrossRedeploy(
|
|
901
|
+
compose: (options: BumpOptions) => AppBumpOptions
|
|
902
|
+
) {
|
|
903
|
+
const held = deferred()
|
|
904
|
+
const { replica, client, backend, redeploy } = redeployable(held.promise)
|
|
905
|
+
const before = client.queryOptions(backend, "one", 1n)
|
|
906
|
+
expect(before.queryKey[3]).toBe(SHAPES)
|
|
907
|
+
await client.queryClient.fetchQuery(before)
|
|
908
|
+
|
|
909
|
+
const write = new MutationObserver(
|
|
910
|
+
client.queryClient,
|
|
911
|
+
compose(client.mutationOptions(backend, "bump"))
|
|
912
|
+
).mutate(1n)
|
|
913
|
+
await vi.waitFor(() => expect(requestsFor(replica, "bump")).toHaveLength(1))
|
|
914
|
+
// The update is in flight at the first canister; a redeploy gives the
|
|
915
|
+
// name another one, and a read made now goes there.
|
|
916
|
+
redeploy()
|
|
917
|
+
const after = client.queryOptions(backend, "one", 1n)
|
|
918
|
+
expect(after.queryKey[3]).toBe(REDEPLOYED)
|
|
919
|
+
await client.queryClient.fetchQuery(after)
|
|
920
|
+
held.resolve()
|
|
921
|
+
await expect(write).resolves.toBe(2n)
|
|
922
|
+
|
|
923
|
+
expect(requestsFor(replica, "bump")).toMatchObject([{ canisterId: SHAPES }])
|
|
924
|
+
// The reads of the canister written to are stale; the other's are not.
|
|
925
|
+
expect(
|
|
926
|
+
client.queryClient.getQueryState(before.queryKey)?.isInvalidated
|
|
927
|
+
).toBe(true)
|
|
928
|
+
expect(
|
|
929
|
+
client.queryClient.getQueryState(after.queryKey)?.isInvalidated
|
|
930
|
+
).toBe(false)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
it.each([
|
|
934
|
+
["as given", (options: BumpOptions) => options],
|
|
935
|
+
[
|
|
936
|
+
"under an onMutate of the app's that spreads the given one's result into its own",
|
|
937
|
+
(options: BumpOptions) => ({
|
|
938
|
+
...options,
|
|
939
|
+
onMutate: (vars: bigint, context?: unknown) => ({
|
|
940
|
+
...options.onMutate(vars, context),
|
|
941
|
+
previous: "the app's own context",
|
|
942
|
+
}),
|
|
943
|
+
}),
|
|
944
|
+
],
|
|
945
|
+
])(
|
|
946
|
+
"invalidates the canister a { name } write went to when the cookie moves the name before the write settles, with the options %s",
|
|
947
|
+
(_label, compose) => writeAcrossRedeploy(compose)
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
it("keeps a { name } write and its invalidation on one canister when an onMutate of the app's replaces the given one", async ({
|
|
951
|
+
skip,
|
|
952
|
+
}) => {
|
|
953
|
+
if (!(await sharesRunContext())) {
|
|
954
|
+
skip(
|
|
955
|
+
"this TanStack Query does not pass mutationFn and onSettled the run's context, so each resolves the canister when it runs"
|
|
956
|
+
)
|
|
957
|
+
}
|
|
958
|
+
// TanStack's optimistic-update recipe, spread over the options: the
|
|
959
|
+
// app's onMutate returns its own context and never calls the given one.
|
|
960
|
+
await writeAcrossRedeploy((options) => ({
|
|
961
|
+
...options,
|
|
962
|
+
onMutate: () => ({ previous: "the app's own context" }),
|
|
963
|
+
}))
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it("invalidates the canister a { name } in invalidates named when the write started, though the cookie moves the name before the write settles", async () => {
|
|
967
|
+
const held = deferred()
|
|
968
|
+
const { replica, client, backend, redeploy } = redeployable(held.promise)
|
|
969
|
+
const writer = client.canister<shapes.Actor>(shapes.actor, { id: BY_ID })
|
|
970
|
+
const before = client.queryOptions(backend, "one", 1n)
|
|
971
|
+
expect(before.queryKey[3]).toBe(SHAPES)
|
|
972
|
+
await client.queryClient.fetchQuery(before)
|
|
973
|
+
|
|
974
|
+
// The write goes to a canister reached by its id, and names the reads it
|
|
975
|
+
// changes through the { name }, the only target that a redeploy moves.
|
|
976
|
+
const write = new MutationObserver(
|
|
977
|
+
client.queryClient,
|
|
978
|
+
client.mutationOptions(writer, "bump", { invalidates: [backend] })
|
|
979
|
+
).mutate(1n)
|
|
980
|
+
await vi.waitFor(() => expect(requestsFor(replica, "bump")).toHaveLength(1))
|
|
981
|
+
redeploy()
|
|
982
|
+
const after = client.queryOptions(backend, "one", 1n)
|
|
983
|
+
expect(after.queryKey[3]).toBe(REDEPLOYED)
|
|
984
|
+
await client.queryClient.fetchQuery(after)
|
|
985
|
+
held.resolve()
|
|
986
|
+
await expect(write).resolves.toBe(2n)
|
|
987
|
+
|
|
988
|
+
expect(requestsFor(replica, "bump")).toMatchObject([{ canisterId: BY_ID }])
|
|
989
|
+
// The listed name pointed at SHAPES when the write started: its reads
|
|
990
|
+
// there are stale, and the reads of where it points now are not.
|
|
991
|
+
expect(
|
|
992
|
+
client.queryClient.getQueryState(before.queryKey)?.isInvalidated
|
|
993
|
+
).toBe(true)
|
|
994
|
+
expect(
|
|
995
|
+
client.queryClient.getQueryState(after.queryKey)?.isInvalidated
|
|
996
|
+
).toBe(false)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
it("sends a { name } write that waited offline to the canister resolved when it started, whose reads it invalidates", async ({
|
|
1000
|
+
skip,
|
|
1001
|
+
}) => {
|
|
1002
|
+
if (!(await sharesRunContext())) {
|
|
1003
|
+
skip(
|
|
1004
|
+
"this TanStack Query does not pass mutationFn the run's context, so mutationFn resolves the canister when it starts"
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
const { replica, client, backend, redeploy } = redeployable()
|
|
1008
|
+
const read = client.queryOptions(backend, "one", 1n)
|
|
1009
|
+
await client.queryClient.fetchQuery(read)
|
|
1010
|
+
|
|
1011
|
+
// Offline, the write is paused after onMutate ran and before mutationFn
|
|
1012
|
+
// does; the name moves to another canister in between.
|
|
1013
|
+
onlineManager.setOnline(false)
|
|
1014
|
+
const observer = new MutationObserver(
|
|
1015
|
+
client.queryClient,
|
|
1016
|
+
client.mutationOptions(backend, "bump")
|
|
1017
|
+
)
|
|
1018
|
+
const write = observer.mutate(1n)
|
|
1019
|
+
await vi.waitFor(() => {
|
|
1020
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1021
|
+
expect(observer.getCurrentResult().context).toBeDefined()
|
|
1022
|
+
})
|
|
1023
|
+
redeploy()
|
|
1024
|
+
onlineManager.setOnline(true)
|
|
1025
|
+
void client.queryClient.resumePausedMutations()
|
|
1026
|
+
await expect(write).resolves.toBe(2n)
|
|
1027
|
+
|
|
1028
|
+
expect(requestsFor(replica, "bump")).toMatchObject([{ canisterId: SHAPES }])
|
|
1029
|
+
expect(client.queryClient.getQueryState(read.queryKey)?.isInvalidated).toBe(
|
|
1030
|
+
true
|
|
1031
|
+
)
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* A shapes canister whose `address` answers with its sender, held back
|
|
1036
|
+
* while `hold` is set, and a client on it signed in as seed 1.
|
|
1037
|
+
*/
|
|
1038
|
+
function addressBook() {
|
|
1039
|
+
let hold: Promise<void> | undefined
|
|
1040
|
+
const calls = { before: undefined as (() => void) | undefined }
|
|
1041
|
+
const replica = replicaWith({
|
|
1042
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, {
|
|
1043
|
+
address: async ([seed], { caller }) => {
|
|
1044
|
+
await hold
|
|
1045
|
+
calls.before?.()
|
|
1046
|
+
return `${seed}:${caller}`
|
|
1047
|
+
},
|
|
1048
|
+
}),
|
|
1049
|
+
})
|
|
1050
|
+
const auth = createTestAuth({ seed: 1 })
|
|
1051
|
+
const client = clientWithAuth(replica, auth)
|
|
1052
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1053
|
+
const holdNext = () => {
|
|
1054
|
+
const held = deferred()
|
|
1055
|
+
hold = held.promise
|
|
1056
|
+
return held
|
|
1057
|
+
}
|
|
1058
|
+
return { replica, auth, client, canister, holdNext, calls }
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const callerChanged = {
|
|
1062
|
+
kind: "cancelled",
|
|
1063
|
+
code: "caller_changed",
|
|
1064
|
+
mayHaveExecuted: false,
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
it("refuses a write paused offline when the account is switched before it is sent, and sends nothing", async () => {
|
|
1068
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1069
|
+
onlineManager.setOnline(false)
|
|
1070
|
+
const observer = new MutationObserver(
|
|
1071
|
+
client.queryClient,
|
|
1072
|
+
client.mutationOptions(canister, "address")
|
|
1073
|
+
)
|
|
1074
|
+
const write = observer.mutate("x")
|
|
1075
|
+
write.catch(() => undefined)
|
|
1076
|
+
await vi.waitFor(() =>
|
|
1077
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1078
|
+
)
|
|
1079
|
+
auth.switchTo(2)
|
|
1080
|
+
onlineManager.setOnline(true)
|
|
1081
|
+
void client.queryClient.resumePausedMutations()
|
|
1082
|
+
await expect(write).rejects.toMatchObject(callerChanged)
|
|
1083
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
it("sends a write paused offline as its caller when nobody switched meanwhile", async () => {
|
|
1087
|
+
const { client, canister } = addressBook()
|
|
1088
|
+
const clickedAs = client.caller()
|
|
1089
|
+
onlineManager.setOnline(false)
|
|
1090
|
+
const observer = new MutationObserver(
|
|
1091
|
+
client.queryClient,
|
|
1092
|
+
client.mutationOptions(canister, "address")
|
|
1093
|
+
)
|
|
1094
|
+
const write = observer.mutate("x")
|
|
1095
|
+
await vi.waitFor(() =>
|
|
1096
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1097
|
+
)
|
|
1098
|
+
onlineManager.setOnline(true)
|
|
1099
|
+
void client.queryClient.resumePausedMutations()
|
|
1100
|
+
await expect(write).resolves.toBe(`x:${clickedAs}`)
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it("refuses a write queued behind its scope when the account is switched before its turn, and sends nothing", async () => {
|
|
1104
|
+
const { replica, auth, client, canister, holdNext } = addressBook()
|
|
1105
|
+
const clickedAs = client.caller()
|
|
1106
|
+
const options = {
|
|
1107
|
+
...client.mutationOptions(canister, "address"),
|
|
1108
|
+
scope: { id: "address" },
|
|
1109
|
+
}
|
|
1110
|
+
const held = holdNext()
|
|
1111
|
+
const first = new MutationObserver(client.queryClient, options).mutate("a")
|
|
1112
|
+
await vi.waitFor(() =>
|
|
1113
|
+
expect(requestsFor(replica, "address")).toHaveLength(1)
|
|
1114
|
+
)
|
|
1115
|
+
const queued = new MutationObserver(client.queryClient, options)
|
|
1116
|
+
const second = queued.mutate("b")
|
|
1117
|
+
second.catch(() => undefined)
|
|
1118
|
+
await vi.waitFor(() =>
|
|
1119
|
+
expect(queued.getCurrentResult().isPaused).toBe(true)
|
|
1120
|
+
)
|
|
1121
|
+
auth.switchTo(2)
|
|
1122
|
+
held.resolve()
|
|
1123
|
+
await expect(first).resolves.toBe(`a:${clickedAs}`)
|
|
1124
|
+
await expect(second).rejects.toMatchObject(callerChanged)
|
|
1125
|
+
expect(requestsFor(replica, "address")).toHaveLength(1)
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
/** Pauses `observer`'s write offline, runs `between`, and comes back online. */
|
|
1129
|
+
async function pausedOffline(
|
|
1130
|
+
client: Client,
|
|
1131
|
+
observer: MutationObserver<unknown, unknown, string, unknown>,
|
|
1132
|
+
between: () => void
|
|
1133
|
+
) {
|
|
1134
|
+
onlineManager.setOnline(false)
|
|
1135
|
+
const write = observer.mutate("x")
|
|
1136
|
+
write.catch(() => undefined)
|
|
1137
|
+
await vi.waitFor(() =>
|
|
1138
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1139
|
+
)
|
|
1140
|
+
between()
|
|
1141
|
+
onlineManager.setOnline(true)
|
|
1142
|
+
void client.queryClient.resumePausedMutations()
|
|
1143
|
+
return write
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** TanStack's optimistic-update recipe: an onMutate of the app's that returns its own context and never calls the given one. */
|
|
1147
|
+
const replacingOnMutate = (client: Client, canister: unknown) =>
|
|
1148
|
+
new MutationObserver<unknown, unknown, string, unknown>(
|
|
1149
|
+
client.queryClient,
|
|
1150
|
+
{
|
|
1151
|
+
...client.mutationOptions(
|
|
1152
|
+
canister as Parameters<Client["mutationOptions"]>[0],
|
|
1153
|
+
"address" as never
|
|
1154
|
+
),
|
|
1155
|
+
onMutate: () => ({ previous: "the app's own context" }),
|
|
1156
|
+
} as never
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
it("refuses a write paused offline across an account switch when an onMutate of the app's replaces the given one", async () => {
|
|
1160
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1161
|
+
const write = pausedOffline(
|
|
1162
|
+
client,
|
|
1163
|
+
replacingOnMutate(client, canister),
|
|
1164
|
+
() => auth.switchTo(2)
|
|
1165
|
+
)
|
|
1166
|
+
await expect(write).rejects.toMatchObject(callerChanged)
|
|
1167
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it("sends a write paused offline as its caller when an onMutate of the app's replaces the given one", async () => {
|
|
1171
|
+
const { client, canister } = addressBook()
|
|
1172
|
+
const clickedAs = client.caller()
|
|
1173
|
+
const write = pausedOffline(
|
|
1174
|
+
client,
|
|
1175
|
+
replacingOnMutate(client, canister),
|
|
1176
|
+
() => undefined
|
|
1177
|
+
)
|
|
1178
|
+
await expect(write).resolves.toBe(`x:${clickedAs}`)
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
it("refuses the retry of a write when the account is switched after the first attempt failed, and sends it no more", async () => {
|
|
1182
|
+
const { replica, auth, client, canister, calls } = addressBook()
|
|
1183
|
+
calls.before = () => {
|
|
1184
|
+
calls.before = undefined
|
|
1185
|
+
auth.switchTo(2)
|
|
1186
|
+
throw new Error("trapped")
|
|
1187
|
+
}
|
|
1188
|
+
const write = new MutationObserver(client.queryClient, {
|
|
1189
|
+
...client.mutationOptions(canister, "address"),
|
|
1190
|
+
// An app's own retry, against the advice.
|
|
1191
|
+
retry: 1,
|
|
1192
|
+
retryDelay: 0,
|
|
1193
|
+
}).mutate("x")
|
|
1194
|
+
await expect(write).rejects.toMatchObject(callerChanged)
|
|
1195
|
+
expect(requestsFor(replica, "address")).toHaveLength(1)
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
it("refuses a write TanStack restored from a persister, whose caller is not known, and sends nothing", async () => {
|
|
1199
|
+
const { replica, client, canister } = addressBook()
|
|
1200
|
+
const options = client.mutationOptions(canister, "address")
|
|
1201
|
+
const observer = new MutationObserver(client.queryClient, options)
|
|
1202
|
+
onlineManager.setOnline(false)
|
|
1203
|
+
observer.mutate("x").catch(() => undefined)
|
|
1204
|
+
await vi.waitFor(() =>
|
|
1205
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1206
|
+
)
|
|
1207
|
+
// What a persister writes and, after a reload, reads back.
|
|
1208
|
+
const persisted = dehydrate(client.queryClient)
|
|
1209
|
+
client.queryClient.getMutationCache().clear()
|
|
1210
|
+
// A restored mutation takes its mutationFn from the defaults.
|
|
1211
|
+
client.queryClient.setMutationDefaults(options.mutationKey, {
|
|
1212
|
+
mutationFn: options.mutationFn as never,
|
|
1213
|
+
})
|
|
1214
|
+
hydrate(client.queryClient, persisted)
|
|
1215
|
+
onlineManager.setOnline(true)
|
|
1216
|
+
await client.queryClient.resumePausedMutations()
|
|
1217
|
+
const [restored] = client.queryClient.getMutationCache().getAll()
|
|
1218
|
+
await vi.waitFor(() =>
|
|
1219
|
+
expect(restored?.state.error).toMatchObject(callerChanged)
|
|
1220
|
+
)
|
|
1221
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
it("refuses a write whose mutationFn gets no run context (TanStack before 5.89, another QueryClient) unless the caller onMutate read for its options is the live one", async () => {
|
|
1225
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1226
|
+
const clickedAs = client.caller()
|
|
1227
|
+
const options = client.mutationOptions(canister, "address")
|
|
1228
|
+
// TanStack before 5.89 calls both with the argument alone.
|
|
1229
|
+
options.onMutate("x")
|
|
1230
|
+
auth.switchTo(2)
|
|
1231
|
+
await expect(options.mutationFn("x")).rejects.toMatchObject(callerChanged)
|
|
1232
|
+
// A run whose onMutate read nothing for its options (a later render
|
|
1233
|
+
// handed it new ones): its caller is not known.
|
|
1234
|
+
await expect(
|
|
1235
|
+
options.mutationFn.call({ ...options }, "x")
|
|
1236
|
+
).rejects.toMatchObject(callerChanged)
|
|
1237
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1238
|
+
auth.switchTo(1)
|
|
1239
|
+
options.onMutate("y")
|
|
1240
|
+
await expect(options.mutationFn("y")).resolves.toBe(`y:${clickedAs}`)
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
it("never sends a write with no run context as the caller of a run whose mutationFn never ran", async () => {
|
|
1244
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1245
|
+
const options = client.mutationOptions(canister, "address")
|
|
1246
|
+
// Two runs of "z" as TanStack before 5.89 makes them through two
|
|
1247
|
+
// observers, each with its own options object. The first, started as the first account, never gets
|
|
1248
|
+
// to mutationFn (its mutation was removed).
|
|
1249
|
+
const abandoned = { ...options }
|
|
1250
|
+
options.onMutate.call(abandoned, "z")
|
|
1251
|
+
auth.switchTo(2)
|
|
1252
|
+
const second = client.caller()
|
|
1253
|
+
const run = { ...options }
|
|
1254
|
+
options.onMutate.call(run, "z")
|
|
1255
|
+
// Back to the first account before the second account's run is sent.
|
|
1256
|
+
auth.switchTo(1)
|
|
1257
|
+
await expect(options.mutationFn.call(run, "z")).rejects.toMatchObject(
|
|
1258
|
+
callerChanged
|
|
1259
|
+
)
|
|
1260
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1261
|
+
auth.switchTo(2)
|
|
1262
|
+
await expect(options.mutationFn.call(run, "z")).resolves.toBe(`z:${second}`)
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
it("never sends a run on another QueryClient (no run context) as the caller of a run of the client's own cache with the same argument", async () => {
|
|
1266
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1267
|
+
const alice = client.caller()
|
|
1268
|
+
const options = client.mutationOptions(canister, "address")
|
|
1269
|
+
// Alice's write of "x", paused offline in the client's own cache.
|
|
1270
|
+
onlineManager.setOnline(false)
|
|
1271
|
+
const observer = new MutationObserver(client.queryClient, options)
|
|
1272
|
+
observer.mutate("x").catch(() => undefined)
|
|
1273
|
+
await vi.waitFor(() =>
|
|
1274
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1275
|
+
)
|
|
1276
|
+
onlineManager.setOnline(true)
|
|
1277
|
+
// Bob's write of "x" through the same options on another QueryClient,
|
|
1278
|
+
// as TanStack before 5.89 runs it: both functions called as methods of
|
|
1279
|
+
// its observer's options object, with the argument alone.
|
|
1280
|
+
auth.switchTo(2)
|
|
1281
|
+
const bobsRun = { ...options }
|
|
1282
|
+
options.onMutate.call(bobsRun, "x")
|
|
1283
|
+
// Alice is back before Bob's run is sent.
|
|
1284
|
+
auth.switchTo(1)
|
|
1285
|
+
expect(client.caller()).toBe(alice)
|
|
1286
|
+
await expect(options.mutationFn.call(bobsRun, "x")).rejects.toMatchObject(
|
|
1287
|
+
callerChanged
|
|
1288
|
+
)
|
|
1289
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
it("never sends a write with no run context as the caller of a settled run that shares its options (one observer, TanStack before 5.89)", async () => {
|
|
1293
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1294
|
+
const alice = client.caller()
|
|
1295
|
+
const options = client.mutationOptions(canister, "address")
|
|
1296
|
+
const observer = new MutationObserver(client.queryClient, options)
|
|
1297
|
+
// Alice's write, settled and still cached.
|
|
1298
|
+
await expect(observer.mutate("a")).resolves.toBe(`a:${alice}`)
|
|
1299
|
+
// Bob's write through the same observer, held back offline: TanStack
|
|
1300
|
+
// builds it with the same options object as Alice's.
|
|
1301
|
+
auth.switchTo(2)
|
|
1302
|
+
onlineManager.setOnline(false)
|
|
1303
|
+
observer.mutate("b").catch(() => undefined)
|
|
1304
|
+
await vi.waitFor(() =>
|
|
1305
|
+
expect(observer.getCurrentResult().isPaused).toBe(true)
|
|
1306
|
+
)
|
|
1307
|
+
const runs = client.queryClient.getMutationCache().getAll()
|
|
1308
|
+
expect(runs).toHaveLength(2)
|
|
1309
|
+
expect(runs[0]?.options).toBe(runs[1]?.options)
|
|
1310
|
+
onlineManager.setOnline(true)
|
|
1311
|
+
// Alice is back when Bob's run is sent as TanStack before 5.89 sends it:
|
|
1312
|
+
// mutationFn called on the shared options, with the argument alone.
|
|
1313
|
+
auth.switchTo(1)
|
|
1314
|
+
await expect(
|
|
1315
|
+
options.mutationFn.call(runs[1]?.options, "b")
|
|
1316
|
+
).rejects.toMatchObject(callerChanged)
|
|
1317
|
+
// Only Alice's write went out.
|
|
1318
|
+
expect(requestsFor(replica, "address")).toHaveLength(1)
|
|
1319
|
+
// With Bob current, his run goes out as him: Alice's settled run is not
|
|
1320
|
+
// one it may run for.
|
|
1321
|
+
auth.switchTo(2)
|
|
1322
|
+
const bob = client.caller()
|
|
1323
|
+
await expect(options.mutationFn.call(runs[1]?.options, "b")).resolves.toBe(
|
|
1324
|
+
`b:${bob}`
|
|
1325
|
+
)
|
|
1326
|
+
})
|
|
1327
|
+
|
|
1328
|
+
it("refuses a write with no run context whose observer's pending runs started as different callers (TanStack before 5.89)", async () => {
|
|
1329
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1330
|
+
const options = client.mutationOptions(canister, "address")
|
|
1331
|
+
const observer = new MutationObserver(client.queryClient, options)
|
|
1332
|
+
// Alice's write, then Bob's, both held back offline.
|
|
1333
|
+
onlineManager.setOnline(false)
|
|
1334
|
+
observer.mutate("a").catch(() => undefined)
|
|
1335
|
+
auth.switchTo(2)
|
|
1336
|
+
observer.mutate("b").catch(() => undefined)
|
|
1337
|
+
const runs = client.queryClient.getMutationCache().getAll()
|
|
1338
|
+
await vi.waitFor(() =>
|
|
1339
|
+
expect(runs.every((run) => run.state.isPaused)).toBe(true)
|
|
1340
|
+
)
|
|
1341
|
+
onlineManager.setOnline(true)
|
|
1342
|
+
// Called on the options both share, mutationFn cannot tell which of them
|
|
1343
|
+
// it runs for, so it sends neither, whoever is current.
|
|
1344
|
+
auth.switchTo(1)
|
|
1345
|
+
await expect(
|
|
1346
|
+
options.mutationFn.call(runs[0]?.options, "a")
|
|
1347
|
+
).rejects.toMatchObject(callerChanged)
|
|
1348
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
it("refuses writes with no run context whose options' pending runs started as different callers, until they settle (another QueryClient)", async () => {
|
|
1352
|
+
const { replica, auth, client, canister } = addressBook()
|
|
1353
|
+
const alice = client.caller()
|
|
1354
|
+
const options = client.mutationOptions(canister, "address")
|
|
1355
|
+
// Two runs of one observer on another QueryClient, as TanStack before
|
|
1356
|
+
// 5.89 runs them: every function called on the observer's one options
|
|
1357
|
+
// object, with the argument alone.
|
|
1358
|
+
const shared = { ...options }
|
|
1359
|
+
options.onMutate.call(shared, "a")
|
|
1360
|
+
auth.switchTo(2)
|
|
1361
|
+
options.onMutate.call(shared, "b")
|
|
1362
|
+
// Neither run can be told from the other, so neither is sent, whoever
|
|
1363
|
+
// is current.
|
|
1364
|
+
await expect(options.mutationFn.call(shared, "b")).rejects.toMatchObject(
|
|
1365
|
+
callerChanged
|
|
1366
|
+
)
|
|
1367
|
+
auth.switchTo(1)
|
|
1368
|
+
await expect(options.mutationFn.call(shared, "a")).rejects.toMatchObject(
|
|
1369
|
+
callerChanged
|
|
1370
|
+
)
|
|
1371
|
+
expect(requestsFor(replica, "address")).toEqual([])
|
|
1372
|
+
// Once both settled, a new run is sent as its caller.
|
|
1373
|
+
await options.onSettled.call(shared, undefined, null, "a", undefined)
|
|
1374
|
+
await options.onSettled.call(shared, undefined, null, "b", undefined)
|
|
1375
|
+
options.onMutate.call(shared, "c")
|
|
1376
|
+
await expect(options.mutationFn.call(shared, "c")).resolves.toBe(
|
|
1377
|
+
`c:${alice}`
|
|
1378
|
+
)
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
it("reads no caller, and builds no auth, for a mutation of the app's on the client's QueryClient", async () => {
|
|
1382
|
+
let built = 0
|
|
1383
|
+
// In a page, where the client builds its auth on first use.
|
|
1384
|
+
vi.stubGlobal("window", {
|
|
1385
|
+
location: { origin: "https://app.example.com", protocol: "https:" },
|
|
1386
|
+
})
|
|
1387
|
+
const client = createClient({
|
|
1388
|
+
network: "local",
|
|
1389
|
+
auth: () => {
|
|
1390
|
+
built += 1
|
|
1391
|
+
throw new Error("the auth could not be built")
|
|
1392
|
+
},
|
|
1393
|
+
})
|
|
1394
|
+
const unrelated = new MutationObserver(client.queryClient, {
|
|
1395
|
+
mutationFn: (n: number) => Promise.resolve(n + 1),
|
|
1396
|
+
})
|
|
1397
|
+
await expect(unrelated.mutate(1)).resolves.toBe(2)
|
|
1398
|
+
expect(built).toBe(0)
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
it("throws a TypeError for an unknown third argument or a canister of another client", () => {
|
|
1402
|
+
const client = clientAs(replicaWith({}), alice)
|
|
1403
|
+
const other = clientAs(replicaWith({}), alice)
|
|
1404
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1405
|
+
const foreign = other.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1406
|
+
expect(() =>
|
|
1407
|
+
client.mutationOptions(ledger, "icrc1_transfer", {
|
|
1408
|
+
retry: 3,
|
|
1409
|
+
} as unknown as { invalidates: [] })
|
|
1410
|
+
).toThrow(/never retried/)
|
|
1411
|
+
expect(() =>
|
|
1412
|
+
client.mutationOptions(ledger, "icrc1_transfer", {
|
|
1413
|
+
invalidates: [foreign],
|
|
1414
|
+
})
|
|
1415
|
+
).toThrow(/another client/)
|
|
1416
|
+
expect(() =>
|
|
1417
|
+
client.mutationOptions(ledger, "icrc1_transfer", {
|
|
1418
|
+
invalidates: [[ledger, "nope"]],
|
|
1419
|
+
})
|
|
1420
|
+
).toThrow(/has no method "nope"/)
|
|
1421
|
+
})
|
|
1422
|
+
})
|
|
1423
|
+
|
|
1424
|
+
describe("timeoutMs", () => {
|
|
1425
|
+
// #875: a bound on how long one call waits, for reads and mutations.
|
|
1426
|
+
const within = <T>(call: Promise<T>) =>
|
|
1427
|
+
Promise.race([call, sleep(2_000).then(() => "still waiting")])
|
|
1428
|
+
|
|
1429
|
+
/** A ledger whose transfers and balances never answer. */
|
|
1430
|
+
const held = () => {
|
|
1431
|
+
const fake = ledgerCanister(new Map([[ALICE as string, 1_000_000n]]))
|
|
1432
|
+
const replica = replicaWith({
|
|
1433
|
+
[LEDGER]: {
|
|
1434
|
+
query: (method, arg, context) =>
|
|
1435
|
+
method === "icrc1_balance_of"
|
|
1436
|
+
? new Promise(() => {})
|
|
1437
|
+
: fake.query!(method, arg, context),
|
|
1438
|
+
update: () => new Promise(() => {}),
|
|
1439
|
+
},
|
|
1440
|
+
})
|
|
1441
|
+
return { replica, client: clientAs(replica, alice) }
|
|
1442
|
+
}
|
|
1443
|
+
const transfer = (amount: bigint): icrc1.TransferArg => ({
|
|
1444
|
+
to: { owner: BOB, subaccount: null },
|
|
1445
|
+
amount,
|
|
1446
|
+
fee: null,
|
|
1447
|
+
memo: null,
|
|
1448
|
+
from_subaccount: null,
|
|
1449
|
+
created_at_time: 1_700_000_000_000_000_000n,
|
|
1450
|
+
})
|
|
1451
|
+
|
|
1452
|
+
it.each([
|
|
1453
|
+
["its own timeoutMs", {}, { timeoutMs: 50 }],
|
|
1454
|
+
["the canister's timeoutMs", { timeoutMs: 50 }, {}],
|
|
1455
|
+
[
|
|
1456
|
+
"its own timeoutMs over the canister's",
|
|
1457
|
+
{ timeoutMs: 600_000 },
|
|
1458
|
+
{ timeoutMs: 50 },
|
|
1459
|
+
],
|
|
1460
|
+
] as const)(
|
|
1461
|
+
"rejects a mutation whose request went out outcome_unknown with %s, and offers it to resendOf",
|
|
1462
|
+
async (_, target, options) => {
|
|
1463
|
+
const { replica, client } = held()
|
|
1464
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, {
|
|
1465
|
+
id: LEDGER,
|
|
1466
|
+
...target,
|
|
1467
|
+
})
|
|
1468
|
+
const mutation = new MutationObserver(
|
|
1469
|
+
client.queryClient,
|
|
1470
|
+
client.mutationOptions(ledger, "icrc1_transfer", options)
|
|
1471
|
+
)
|
|
1472
|
+
const error = await within(mutation.mutate(transfer(1n))).catch(
|
|
1473
|
+
(failure: unknown) => failure
|
|
1474
|
+
)
|
|
1475
|
+
expect(error).toMatchObject({
|
|
1476
|
+
kind: "outcome_unknown",
|
|
1477
|
+
mayHaveExecuted: true,
|
|
1478
|
+
code: "timeout",
|
|
1479
|
+
})
|
|
1480
|
+
expect(mutation.getCurrentResult().error).toBe(error)
|
|
1481
|
+
expect(
|
|
1482
|
+
client.resendOf(error, ledger, "icrc1_transfer", {
|
|
1483
|
+
dedupedBy: (arg) => arg.created_at_time,
|
|
1484
|
+
})
|
|
1485
|
+
).toBeDefined()
|
|
1486
|
+
expect(requestsFor(replica, "icrc1_transfer")).toHaveLength(1)
|
|
1487
|
+
}
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
it("keeps a mutation re-sending an offered write outcome_unknown when it times out before it goes out", async () => {
|
|
1491
|
+
const { replica, client } = held()
|
|
1492
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1493
|
+
const mutation = new MutationObserver(
|
|
1494
|
+
client.queryClient,
|
|
1495
|
+
client.mutationOptions(ledger, "icrc1_transfer", { timeoutMs: 50 })
|
|
1496
|
+
)
|
|
1497
|
+
const first = await within(mutation.mutate(transfer(1n))).catch(
|
|
1498
|
+
(failure: unknown) => failure
|
|
1499
|
+
)
|
|
1500
|
+
const dedupedBy = (arg: icrc1.TransferArg) => arg.created_at_time
|
|
1501
|
+
const offer = client.resendOf(first, ledger, "icrc1_transfer", {
|
|
1502
|
+
dedupedBy,
|
|
1503
|
+
})
|
|
1504
|
+
expect(offer).toBeDefined()
|
|
1505
|
+
replica.refuseNext(429)
|
|
1506
|
+
const again = await within(mutation.mutate(offer!.arg)).catch(
|
|
1507
|
+
(failure: unknown) => failure
|
|
1508
|
+
)
|
|
1509
|
+
expect(again).toMatchObject({
|
|
1510
|
+
kind: "outcome_unknown",
|
|
1511
|
+
mayHaveExecuted: true,
|
|
1512
|
+
code: "timeout",
|
|
1513
|
+
})
|
|
1514
|
+
expect(
|
|
1515
|
+
client.resendOf(again, ledger, "icrc1_transfer", { dedupedBy })
|
|
1516
|
+
).toBeDefined()
|
|
1517
|
+
expect(requestsFor(replica, "icrc1_transfer")).toHaveLength(2)
|
|
1518
|
+
})
|
|
1519
|
+
|
|
1520
|
+
it("rejects a read of a canister with timeoutMs cancelled, with nothing executed", async () => {
|
|
1521
|
+
const { replica, client } = held()
|
|
1522
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, {
|
|
1523
|
+
id: LEDGER,
|
|
1524
|
+
timeoutMs: 50,
|
|
1525
|
+
})
|
|
1526
|
+
await expect(
|
|
1527
|
+
within(
|
|
1528
|
+
client.queryClient.fetchQuery(
|
|
1529
|
+
client.queryOptions(ledger, "icrc1_balance_of", {
|
|
1530
|
+
owner: ALICE,
|
|
1531
|
+
subaccount: null,
|
|
1532
|
+
})
|
|
1533
|
+
)
|
|
1534
|
+
)
|
|
1535
|
+
).rejects.toMatchObject({
|
|
1536
|
+
kind: "cancelled",
|
|
1537
|
+
mayHaveExecuted: false,
|
|
1538
|
+
code: "timeout",
|
|
1539
|
+
})
|
|
1540
|
+
expect(requestsFor(replica, "icrc1_balance_of")).toHaveLength(1)
|
|
1541
|
+
})
|
|
1542
|
+
|
|
1543
|
+
it.each([0, 0.5, Number.NaN, 2 ** 31, "50"])(
|
|
1544
|
+
"throws a TypeError for a mutation's timeoutMs %s",
|
|
1545
|
+
(timeoutMs) => {
|
|
1546
|
+
const { client } = held()
|
|
1547
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1548
|
+
expect(() =>
|
|
1549
|
+
client.mutationOptions(ledger, "icrc1_transfer", {
|
|
1550
|
+
timeoutMs: timeoutMs as number,
|
|
1551
|
+
})
|
|
1552
|
+
).toThrow(/2147483647/)
|
|
1553
|
+
}
|
|
1554
|
+
)
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Whether the installed TanStack Query hands `onMutate`, `mutationFn` and
|
|
1559
|
+
* `onSettled` the same function context for one run (5.89 and later), through
|
|
1560
|
+
* which `mutationFn` finds the canister `onMutate` resolved, and `onSettled`
|
|
1561
|
+
* the one `mutationFn` wrote to. The peer-floor check runs these tests on an
|
|
1562
|
+
* older release, which passes `mutationFn` and `onSettled` none.
|
|
1563
|
+
*/
|
|
1564
|
+
async function sharesRunContext(): Promise<boolean> {
|
|
1565
|
+
const seen: unknown[] = []
|
|
1566
|
+
await new MutationObserver(new QueryClient(), {
|
|
1567
|
+
onMutate: (_vars: void, context?: unknown) => {
|
|
1568
|
+
seen.push(context)
|
|
1569
|
+
},
|
|
1570
|
+
mutationFn: (_vars: void, context?: unknown) => {
|
|
1571
|
+
seen.push(context)
|
|
1572
|
+
return Promise.resolve()
|
|
1573
|
+
},
|
|
1574
|
+
onSettled: (
|
|
1575
|
+
_data: unknown,
|
|
1576
|
+
_error: unknown,
|
|
1577
|
+
_vars: void,
|
|
1578
|
+
_onMutateResult: unknown,
|
|
1579
|
+
context?: unknown
|
|
1580
|
+
) => {
|
|
1581
|
+
seen.push(context)
|
|
1582
|
+
},
|
|
1583
|
+
}).mutate()
|
|
1584
|
+
return (
|
|
1585
|
+
seen.length === 3 &&
|
|
1586
|
+
seen[0] !== undefined &&
|
|
1587
|
+
seen.every((context) => context === seen[0])
|
|
1588
|
+
)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** A client in a page, on `replica`, with nobody signed in. */
|
|
1592
|
+
function createClientSignedOut(replica: ReturnType<typeof replicaWith>) {
|
|
1593
|
+
return clientWithAuth(replica, createTestAuth({ seed: 3, signedIn: false }))
|
|
1594
|
+
}
|