@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/dist/cli/serve.js +4 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/serve/bundler.d.ts +13 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +31 -36
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
- package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
- package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
- package/dist/serve/docs-ui/docs/index.json +349 -29
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
- package/dist/serve/docs-ui/docs/tutorial.md +701 -0
- package/dist/serve/docs-ui/llms.txt +3 -1
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +3 -0
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +20 -5
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/worker-runtime.d.ts +2 -2
- package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
- package/dist/serve/entries/worker-runtime.js +45 -10
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts +19 -0
- package/dist/serve/runtime/chip-config.d.ts.map +1 -0
- package/dist/serve/runtime/chip-config.js +19 -0
- package/dist/serve/runtime/chip-config.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +10 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -0
- package/dist/serve/runtime/chip-install.js +15 -0
- package/dist/serve/runtime/chip-install.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +17 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -0
- package/dist/serve/runtime/chip.js +257 -0
- package/dist/serve/runtime/chip.js.map +1 -0
- package/dist/serve/runtime/manifest.d.ts +22 -0
- package/dist/serve/runtime/manifest.d.ts.map +1 -0
- package/dist/serve/runtime/manifest.js +20 -0
- package/dist/serve/runtime/manifest.js.map +1 -0
- package/dist/serve/runtime/status.d.ts +49 -0
- package/dist/serve/runtime/status.d.ts.map +1 -0
- package/dist/serve/runtime/status.js +164 -0
- package/dist/serve/runtime/status.js.map +1 -0
- package/dist/serve/runtime/worker-connection.d.ts +4 -0
- package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
- package/dist/serve/runtime/worker-connection.js +11 -0
- package/dist/serve/runtime/worker-connection.js.map +1 -0
- package/dist/serve/runtime/worker-generation.d.ts +14 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +56 -0
- package/dist/serve/runtime/worker-generation.js.map +1 -0
- package/dist/serve/runtime/worker-replacement.d.ts +18 -0
- package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
- package/dist/serve/runtime/worker-replacement.js +29 -0
- package/dist/serve/runtime/worker-replacement.js.map +1 -0
- package/dist/serve/standalone-assets.d.ts +2 -2
- package/dist/serve/standalone-assets.d.ts.map +1 -1
- package/dist/serve/standalone-assets.js +1 -1
- package/dist/serve/standalone-assets.js.map +1 -1
- package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
- package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
- package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
- package/dist/serve/studio-ui/index.html +1 -1
- package/dist/serve/vite-ai-config.d.ts +25 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +45 -0
- package/dist/serve/vite-ai-config.js.map +1 -0
- package/dist/serve/vite-plugin.d.ts +17 -36
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +38 -57
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts +8 -0
- package/dist/serve/vite-rules-source.d.ts.map +1 -0
- package/dist/serve/vite-rules-source.js +23 -0
- package/dist/serve/vite-rules-source.js.map +1 -0
- package/dist/serve/vite-worker-runtime.d.ts +20 -0
- package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
- package/dist/serve/vite-worker-runtime.js +25 -0
- package/dist/serve/vite-worker-runtime.js.map +1 -0
- package/dist/serve/worker/client/connection.d.ts +7 -2
- package/dist/serve/worker/client/connection.d.ts.map +1 -1
- package/dist/serve/worker/client/connection.js +13 -16
- package/dist/serve/worker/client/connection.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +7 -0
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +28 -0
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client/runtime-control.d.ts +8 -0
- package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
- package/dist/serve/worker/client/runtime-control.js +11 -0
- package/dist/serve/worker/client/runtime-control.js.map +1 -0
- package/dist/serve/worker/entry.js +44 -2
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/host/studio.js +1 -1
- package/dist/serve/worker/host/studio.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +1 -1
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +15 -1
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/retirement.d.ts +21 -0
- package/dist/serve/worker/retirement.d.ts.map +1 -0
- package/dist/serve/worker/retirement.js +97 -0
- package/dist/serve/worker/retirement.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +5 -6
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +519 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +49 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +556 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +135 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/runtime/chip-config.ts +30 -0
- package/src/serve/runtime/chip-install.ts +27 -0
- package/src/serve/runtime/chip.ts +282 -0
- package/src/serve/runtime/manifest.ts +37 -0
- package/src/serve/runtime/status.ts +216 -0
- package/src/serve/runtime/worker-connection.ts +14 -0
- package/src/serve/runtime/worker-generation.ts +75 -0
- package/src/serve/runtime/worker-replacement.ts +42 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-ai-config.ts +77 -0
- package/src/serve/vite-plugin.ts +1075 -0
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +51 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +227 -0
- package/src/serve/worker/client/core.ts +384 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/runtime-control.ts +22 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +223 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1124 -0
- package/src/serve/worker/retirement.ts +125 -0
- package/src/serve/worker/serve-init.ts +605 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +18 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharedWorker host — Firestore write ops (single writes + batch + transaction).
|
|
3
|
+
*
|
|
4
|
+
* `setDoc`/`updateDoc`/`deleteDoc`/`addDoc`, the all-or-nothing `batchCommit`,
|
|
5
|
+
* and the full-fidelity `txnCommit` (read-set validation for multi-tab
|
|
6
|
+
* correctness). Owns the write-payload preparation helpers: sentinel
|
|
7
|
+
* reconstruction + marker rehydration (`prepareWriteData`), the batch-write
|
|
8
|
+
* applier, and the transaction read-set canonicalizer.
|
|
9
|
+
*
|
|
10
|
+
* Routed here by the host dispatcher (host/dispatch.ts) with the op's resolved
|
|
11
|
+
* Firestore handle (`db`). Never imports the dispatcher.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
doc as pyricDoc,
|
|
16
|
+
collection as pyricCollection,
|
|
17
|
+
setDoc,
|
|
18
|
+
updateDoc,
|
|
19
|
+
deleteDoc,
|
|
20
|
+
addDoc,
|
|
21
|
+
runTransaction,
|
|
22
|
+
writeBatch,
|
|
23
|
+
serverTimestamp,
|
|
24
|
+
increment,
|
|
25
|
+
arrayUnion,
|
|
26
|
+
arrayRemove,
|
|
27
|
+
deleteField,
|
|
28
|
+
type Firestore,
|
|
29
|
+
type DocumentReference,
|
|
30
|
+
type SetOptions,
|
|
31
|
+
} from 'pyric/firestore';
|
|
32
|
+
import { rehydrateDocValue } from 'pyric/firestore/internal/value-codec';
|
|
33
|
+
|
|
34
|
+
import type { OpMessage, WriteDescriptor, TxnReadEntry, SentinelMarker } from '../protocol.js';
|
|
35
|
+
import { serializeDocData, isSentinelMarker } from '../protocol.js';
|
|
36
|
+
import { type HostCtx, type PortLike, post, ok, fail, bestEffortFlush } from '../host-context.js';
|
|
37
|
+
|
|
38
|
+
// ─── Sentinel resolution ──────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk a write payload and replace every `SentinelMarker` with the
|
|
42
|
+
* corresponding pyric/firestore FieldValue sentinel object.
|
|
43
|
+
*
|
|
44
|
+
* WHY: FieldValue class instances don't survive structured clone — they
|
|
45
|
+
* arrive as plain objects and lose their prototype, breaking sandbox
|
|
46
|
+
* sentinel detection. The client sends `{ __sentinel: 'serverTimestamp' }`
|
|
47
|
+
* etc. and we reconstruct the real FieldValue here before the sandbox sees
|
|
48
|
+
* the data.
|
|
49
|
+
*
|
|
50
|
+
* The sandbox's sentinel-capture code (sentinel-capture.ts) recognizes
|
|
51
|
+
* FieldValue objects by their internal `__type` property, which the
|
|
52
|
+
* factories produce correctly.
|
|
53
|
+
*/
|
|
54
|
+
function resolveSentinels(value: unknown): unknown {
|
|
55
|
+
if (isSentinelMarker(value)) {
|
|
56
|
+
return resolveSentinel(value);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map(resolveSentinels);
|
|
60
|
+
}
|
|
61
|
+
if (value !== null && typeof value === 'object') {
|
|
62
|
+
// Leave CLASS instances intact (Timestamp/Bytes/LatLng rehydrated by
|
|
63
|
+
// prepareWriteData below, or FieldValue objects): walking their entries
|
|
64
|
+
// into a plain object would strip the prototype the sandbox keys on.
|
|
65
|
+
// Sentinel markers only ever live in plain JSON containers.
|
|
66
|
+
const proto = Object.getPrototypeOf(value);
|
|
67
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
const obj = value as Record<string, unknown>;
|
|
71
|
+
const out: Record<string, unknown> = {};
|
|
72
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
73
|
+
out[k] = resolveSentinels(v);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Prepare an incoming WRITE payload for the sandbox: rehydrate marker-shaped
|
|
82
|
+
* scalars into REAL wrapper instances, then rebuild FieldValue sentinels.
|
|
83
|
+
*
|
|
84
|
+
* WHY REHYDRATE WRITES (spike gap 4): over the JSON relay legs a Node-side
|
|
85
|
+
* `Timestamp`/`Bytes`/`GeoPoint` arrives as its `toJSON()` marker
|
|
86
|
+
* (`{ type: 'firestore/timestamp/1.0', … }` or `{ __type: 'timestamp', … }`).
|
|
87
|
+
* Without rehydration the worker STORES the marker as a plain map — reads
|
|
88
|
+
* mask the bug (the read path rehydrates), but in-worker rules comparisons
|
|
89
|
+
* and `orderBy` over that field see a map, not a timestamp. `rehydrateDocValue`
|
|
90
|
+
* is the same canonical codec the read path / persistence uses, so the wire,
|
|
91
|
+
* store, and IDB formats stay one format.
|
|
92
|
+
*
|
|
93
|
+
* Order matters: rehydration first (it passes `__sentinel` markers through
|
|
94
|
+
* as plain objects, rehydrating any marker-shaped values nested inside
|
|
95
|
+
* arrayUnion/arrayRemove), then sentinel resolution (which now skips the
|
|
96
|
+
* freshly rehydrated class instances — see resolveSentinels).
|
|
97
|
+
*/
|
|
98
|
+
export function prepareWriteData(value: unknown): unknown {
|
|
99
|
+
return resolveSentinels(rehydrateDocValue(value));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveSentinel(marker: SentinelMarker): unknown {
|
|
103
|
+
switch (marker.__sentinel) {
|
|
104
|
+
case 'serverTimestamp': return serverTimestamp();
|
|
105
|
+
case 'increment': return increment(marker.n);
|
|
106
|
+
case 'arrayUnion': return arrayUnion(...marker.values);
|
|
107
|
+
case 'arrayRemove': return arrayRemove(...marker.values);
|
|
108
|
+
case 'deleteField': return deleteField();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Transaction read-set canonicalization helpers ─────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rebuild a JSON-flattened `Uint8Array` (an index-keyed `{ "0": n, … }`
|
|
116
|
+
* map) and encode it as base64url without padding — the exact form
|
|
117
|
+
* `pyric/rules`' `Bytes.toBase64()` emits. Backs the transaction read-set
|
|
118
|
+
* canonicalizer's handling of prototype-stripped `Bytes` clones.
|
|
119
|
+
*/
|
|
120
|
+
/** Timestamp/Duration stripped-clone key set (they share field names). */
|
|
121
|
+
const TS_CLONE_KEYS = ['typeName', 'seconds', 'nanos'] as const;
|
|
122
|
+
|
|
123
|
+
/** Does `o` carry EXACTLY these own enumerable keys (no more, no fewer)?
|
|
124
|
+
* Backs the transaction canonicalizer's strict clone-shape matching. */
|
|
125
|
+
function hasExactKeys(o: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
126
|
+
const own = Object.keys(o);
|
|
127
|
+
return own.length === keys.length && keys.every((k) => k in o);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function indexMapToBase64Url(data: unknown): string {
|
|
131
|
+
const map = (data ?? {}) as Record<string, number>;
|
|
132
|
+
const keys = Object.keys(map);
|
|
133
|
+
const bytes = new Uint8Array(keys.length);
|
|
134
|
+
for (let i = 0; i < keys.length; i++) bytes[i] = map[String(i)] ?? 0;
|
|
135
|
+
let binary = '';
|
|
136
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);
|
|
137
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Batch write helper ───────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
type BatchHandle = {
|
|
143
|
+
set(ref: DocumentReference, data: Record<string, unknown>, options?: SetOptions): void;
|
|
144
|
+
update(ref: DocumentReference, data: Record<string, unknown>): void;
|
|
145
|
+
delete(ref: DocumentReference): void;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function applyWriteToBatch(
|
|
149
|
+
db: Firestore,
|
|
150
|
+
batch: unknown,
|
|
151
|
+
w: WriteDescriptor,
|
|
152
|
+
): void {
|
|
153
|
+
const b = batch as BatchHandle;
|
|
154
|
+
const ref = pyricDoc(db, w.path);
|
|
155
|
+
if (w.method === 'set') {
|
|
156
|
+
const data = prepareWriteData(w.data) as Record<string, unknown>;
|
|
157
|
+
b.set(ref, data, w.options as SetOptions | undefined);
|
|
158
|
+
} else if (w.method === 'update') {
|
|
159
|
+
const data = prepareWriteData(w.data) as Record<string, unknown>;
|
|
160
|
+
b.update(ref, data);
|
|
161
|
+
} else if (w.method === 'delete') {
|
|
162
|
+
b.delete(ref);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Op handler ────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/** The write op methods routed to {@link handleFirestoreWriteOp}. */
|
|
169
|
+
const WRITE_METHODS = new Set<string>([
|
|
170
|
+
'setDoc',
|
|
171
|
+
'updateDoc',
|
|
172
|
+
'deleteDoc',
|
|
173
|
+
'addDoc',
|
|
174
|
+
'batchCommit',
|
|
175
|
+
'txnCommit',
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
export function isFirestoreWriteOp(method: OpMessage['method']): boolean {
|
|
179
|
+
return WRITE_METHODS.has(method);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function handleFirestoreWriteOp(
|
|
183
|
+
ctx: HostCtx,
|
|
184
|
+
port: PortLike,
|
|
185
|
+
msg: OpMessage,
|
|
186
|
+
db: Firestore,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
switch (msg.method) {
|
|
189
|
+
case 'setDoc': {
|
|
190
|
+
try {
|
|
191
|
+
const ref = pyricDoc(db, msg.path);
|
|
192
|
+
const data = prepareWriteData(msg.data) as Record<string, unknown>;
|
|
193
|
+
await setDoc(ref, data, msg.options as SetOptions | undefined);
|
|
194
|
+
await bestEffortFlush(ctx);
|
|
195
|
+
ok(port, msg.id, null);
|
|
196
|
+
} catch (e) { fail(port, msg.id, e); }
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'updateDoc': {
|
|
201
|
+
try {
|
|
202
|
+
const ref = pyricDoc(db, msg.path);
|
|
203
|
+
const data = prepareWriteData(msg.data) as Record<string, unknown>;
|
|
204
|
+
await updateDoc(ref, data);
|
|
205
|
+
await bestEffortFlush(ctx);
|
|
206
|
+
ok(port, msg.id, null);
|
|
207
|
+
} catch (e) { fail(port, msg.id, e); }
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'deleteDoc': {
|
|
212
|
+
try {
|
|
213
|
+
const ref = pyricDoc(db, msg.path);
|
|
214
|
+
await deleteDoc(ref);
|
|
215
|
+
await bestEffortFlush(ctx);
|
|
216
|
+
ok(port, msg.id, null);
|
|
217
|
+
} catch (e) { fail(port, msg.id, e); }
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'addDoc': {
|
|
222
|
+
try {
|
|
223
|
+
const coll = pyricCollection(db, msg.collectionPath);
|
|
224
|
+
const data = prepareWriteData(msg.data) as Record<string, unknown>;
|
|
225
|
+
const ref = await addDoc(coll, data);
|
|
226
|
+
await bestEffortFlush(ctx);
|
|
227
|
+
ok(port, msg.id, { id: ref.id, path: ref.path });
|
|
228
|
+
} catch (e) { fail(port, msg.id, e); }
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'batchCommit': {
|
|
233
|
+
/**
|
|
234
|
+
* WHY: writeBatch in pyric/firestore buffers set/update/delete calls
|
|
235
|
+
* and commits them as one unit through the sandbox backend. We
|
|
236
|
+
* reconstruct the batch here from the wire write descriptors.
|
|
237
|
+
* Atomicity guarantee: the sandbox backend applies all writes or none
|
|
238
|
+
* (per-collection lock semantics of LocalEnvironment.batch()).
|
|
239
|
+
*/
|
|
240
|
+
try {
|
|
241
|
+
const batch = writeBatch(db);
|
|
242
|
+
for (const w of msg.writes) {
|
|
243
|
+
applyWriteToBatch(db, batch, w);
|
|
244
|
+
}
|
|
245
|
+
await (batch as { commit(): Promise<void> }).commit();
|
|
246
|
+
await bestEffortFlush(ctx);
|
|
247
|
+
ok(port, msg.id, null);
|
|
248
|
+
} catch (e) { fail(port, msg.id, e); }
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'txnCommit': {
|
|
253
|
+
/**
|
|
254
|
+
* READ-SET VALIDATION FOR MULTI-TAB CORRECTNESS
|
|
255
|
+
* -----------------------------------------------
|
|
256
|
+
* The client sends `{ method: 'txnCommit', reads, writes }` after its
|
|
257
|
+
* `updateFn` callback has resolved. `reads` is the set of docs the
|
|
258
|
+
* client read during `updateFn` (each entry is `{ path, data }` where
|
|
259
|
+
* `data` is the serialized snapshot the client saw, or null if the doc
|
|
260
|
+
* was missing).
|
|
261
|
+
*
|
|
262
|
+
* WHY READ-SET VALIDATION IS NECESSARY
|
|
263
|
+
* -------------------------------------
|
|
264
|
+
* A transaction spans two messages: the client's `txn.get` RPC (sent
|
|
265
|
+
* during `updateFn`) and this `txnCommit` RPC. Between those messages
|
|
266
|
+
* the event loop is free — another tab's write to any of the read docs
|
|
267
|
+
* can land in that gap via a separate `setDoc` message. Without
|
|
268
|
+
* validation that would be a silent lost update: the client computed
|
|
269
|
+
* its writes using stale data, committed them, and the other tab's
|
|
270
|
+
* changes were silently overwritten.
|
|
271
|
+
*
|
|
272
|
+
* HOW WE VALIDATE (no per-doc version/updateTime in the sandbox)
|
|
273
|
+
* ---------------------------------------------------------------
|
|
274
|
+
* Inside a real `runTransaction(db, callback)`, the callback re-reads
|
|
275
|
+
* every doc from the read-set via `tx.get(ref)`, serializes its current
|
|
276
|
+
* data to the same JSON form the client produced (using `serializeDocData`
|
|
277
|
+
* on both sides), and compares the JSON strings. If any doc's current
|
|
278
|
+
* serialized form differs from what the client recorded, another tab
|
|
279
|
+
* wrote it between the client's read and this commit — we throw an abort
|
|
280
|
+
* sentinel so the sandbox rolls back the transaction, and we return
|
|
281
|
+
* `{ ok: false, error: { code: 'aborted' } }` on the wire. The client's
|
|
282
|
+
* retry loop then re-runs `updateFn` with fresh reads.
|
|
283
|
+
*
|
|
284
|
+
* SERIALIZED-FORM EQUALITY — VIA THE CANONICAL CODEC
|
|
285
|
+
* ---------------------------------------------------
|
|
286
|
+
* Both JSON strings are CANONICALIZED before comparison:
|
|
287
|
+
* `JSON.stringify(rehydrateDocValue(JSON.parse(json)))`. Raw string
|
|
288
|
+
* equality is NOT safe here even within one process, because the two
|
|
289
|
+
* read paths yield DIFFERENT wrapper classes for the same stored
|
|
290
|
+
* value: `getDoc` (what the client's read-set echoes) returns
|
|
291
|
+
* `firebase/firestore` classes whose `Timestamp.toJSON()` emits
|
|
292
|
+
* `{ seconds, nanoseconds, type }`, while the transaction's
|
|
293
|
+
* validation re-read comes through the admin-compat wrapper whose
|
|
294
|
+
* `Timestamp.toJSON()` emits `{ type, seconds, nanoseconds }` — same
|
|
295
|
+
* value, different key order, different string. Rehydrating both
|
|
296
|
+
* sides through the ONE shared codec
|
|
297
|
+
* (`pyric/firestore/internal/value-codec`)
|
|
298
|
+
* collapses every marker family into the same wrapper classes with
|
|
299
|
+
* deterministic `toJSON()` key order, so string equality ↔ value
|
|
300
|
+
* equality again — an unmodified doc can never phantom-abort (which
|
|
301
|
+
* would livelock the client's retry loop), while a real concurrent
|
|
302
|
+
* write still mismatches. Plain-map key order is preserved by both
|
|
303
|
+
* paths from the same stored object, so it stays comparable.
|
|
304
|
+
*
|
|
305
|
+
* NOTE ON tx.set/update/delete SIGNATURE
|
|
306
|
+
* ---------------------------------------
|
|
307
|
+
* The modular pyric/firestore Transaction (wrapping the admin-compat
|
|
308
|
+
* TransactionWrapper) takes `tx.set(docRef, data)` where `docRef` is a
|
|
309
|
+
* real `DocumentReference` — NOT a path string. We build refs via
|
|
310
|
+
* `pyricDoc(db, path)` for each write.
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
/** Sentinel thrown inside the transaction callback to signal an abort. */
|
|
314
|
+
const TXN_ABORT = Symbol('txn-abort');
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Canonicalize a serialized doc-data JSON string (see
|
|
318
|
+
* SERIALIZED-FORM EQUALITY above). Two normalization passes:
|
|
319
|
+
*
|
|
320
|
+
* 1. `rehydrateDocValue` collapses the marker families (`__type`
|
|
321
|
+
* persistence markers and `firebase/firestore` `toJSON()`
|
|
322
|
+
* markers) into the one set of wrapper classes, whose
|
|
323
|
+
* `toJSON()` re-emits a single deterministic form.
|
|
324
|
+
* 2. The stringify replacer additionally normalizes PROTOTYPE-
|
|
325
|
+
* STRIPPED wrapper clones — the sandbox transaction's
|
|
326
|
+
* capture-by-value `structuredClone` turns a stored rules
|
|
327
|
+
* wrapper into a plain `{ typeName, … }` object that neither
|
|
328
|
+
* marker family matches, so without this pass an unmodified
|
|
329
|
+
* typed doc would never compare equal to the client's getDoc
|
|
330
|
+
* echo (a guaranteed phantom abort → retry livelock).
|
|
331
|
+
*/
|
|
332
|
+
const canonicalDocJson = (json: string): string =>
|
|
333
|
+
JSON.stringify(rehydrateDocValue(JSON.parse(json)), (_key, v) => {
|
|
334
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return v;
|
|
335
|
+
const o = v as Record<string, unknown>;
|
|
336
|
+
if (typeof o.typeName !== 'string' || o.__type !== undefined) return v;
|
|
337
|
+
// Re-shape a stripped rules-wrapper clone into the wrapper's own
|
|
338
|
+
// canonical toJSON marker form (kept in sync with pyric/rules'
|
|
339
|
+
// simulator/wrappers/* instance fields + toJSON()) — but ONLY
|
|
340
|
+
// when the key set EXACTLY matches that wrapper's own-field
|
|
341
|
+
// shape. A looser match would silently DROP extra keys from
|
|
342
|
+
// user data that merely resembles a clone, collapsing two
|
|
343
|
+
// genuinely different docs into one canonical form and letting
|
|
344
|
+
// a concurrent write commit undetected (false equality). A
|
|
345
|
+
// near-miss map passes through unchanged — worst case is a
|
|
346
|
+
// spurious abort + retry, never a lost update.
|
|
347
|
+
switch (o.typeName) {
|
|
348
|
+
case 'timestamp':
|
|
349
|
+
return hasExactKeys(o, TS_CLONE_KEYS)
|
|
350
|
+
? { __type: 'timestamp', seconds: o.seconds, nanos: o.nanos }
|
|
351
|
+
: v;
|
|
352
|
+
case 'duration':
|
|
353
|
+
return hasExactKeys(o, TS_CLONE_KEYS)
|
|
354
|
+
? { __type: 'duration', seconds: o.seconds, nanos: o.nanos }
|
|
355
|
+
: v;
|
|
356
|
+
case 'latlng':
|
|
357
|
+
return hasExactKeys(o, ['typeName', 'lat', 'lng'])
|
|
358
|
+
? { __type: 'latlng', lat: o.lat, lng: o.lng }
|
|
359
|
+
: v;
|
|
360
|
+
case 'reference':
|
|
361
|
+
return hasExactKeys(o, ['typeName', 'path'])
|
|
362
|
+
? { __type: 'reference', path: o.path }
|
|
363
|
+
: v;
|
|
364
|
+
case 'path':
|
|
365
|
+
return hasExactKeys(o, ['typeName', 'segments', 'bindings'])
|
|
366
|
+
? { __type: 'path', segments: o.segments }
|
|
367
|
+
: v;
|
|
368
|
+
case 'bytes':
|
|
369
|
+
// The Uint8Array field serialized as an index-keyed map;
|
|
370
|
+
// rebuild and emit Bytes.toJSON()'s base64url form.
|
|
371
|
+
return hasExactKeys(o, ['typeName', 'data'])
|
|
372
|
+
? { __type: 'bytes', base64: indexMapToBase64Url(o.data) }
|
|
373
|
+
: v;
|
|
374
|
+
default:
|
|
375
|
+
return v;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await runTransaction(db, async (tx) => {
|
|
381
|
+
// ── Step 1: validate the read-set ──────────────────────────────
|
|
382
|
+
// Re-read each doc the client touched and compare its current
|
|
383
|
+
// serialized form against what the client recorded at read time.
|
|
384
|
+
const modularTx = tx as {
|
|
385
|
+
get(ref: DocumentReference): Promise<{ exists: boolean | (() => boolean); data(): Record<string, unknown> | undefined }>;
|
|
386
|
+
set(ref: DocumentReference, data: Record<string, unknown>, opts?: SetOptions): void;
|
|
387
|
+
update(ref: DocumentReference, data: Record<string, unknown>): void;
|
|
388
|
+
delete(ref: DocumentReference): void;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
for (const r of (msg as { reads: TxnReadEntry[]; writes: typeof msg.writes }).reads) {
|
|
392
|
+
const ref = pyricDoc(db, r.path);
|
|
393
|
+
const currentSnap = await modularTx.get(ref);
|
|
394
|
+
const existsBool = typeof currentSnap.exists === 'function'
|
|
395
|
+
? (currentSnap.exists as () => boolean)()
|
|
396
|
+
: currentSnap.exists as boolean;
|
|
397
|
+
|
|
398
|
+
// Compute the serialized form of the current doc state.
|
|
399
|
+
const currentSerialized = existsBool && currentSnap.data()
|
|
400
|
+
? serializeDocData(currentSnap.data() as Record<string, unknown>)
|
|
401
|
+
: null;
|
|
402
|
+
|
|
403
|
+
// Compare against what the client recorded:
|
|
404
|
+
// both null → ok (doc still doesn't exist)
|
|
405
|
+
// both present + same JSON → ok
|
|
406
|
+
// anything else → conflict
|
|
407
|
+
const clientHadNull = r.data === null;
|
|
408
|
+
const workerHasNull = currentSerialized === null;
|
|
409
|
+
|
|
410
|
+
if (clientHadNull !== workerHasNull) {
|
|
411
|
+
// Existence changed (created or deleted by another tab).
|
|
412
|
+
throw TXN_ABORT;
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
!clientHadNull &&
|
|
416
|
+
!workerHasNull &&
|
|
417
|
+
canonicalDocJson(r.data!.json) !== canonicalDocJson(currentSerialized!.json)
|
|
418
|
+
) {
|
|
419
|
+
// Data changed by another tab.
|
|
420
|
+
throw TXN_ABORT;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── Step 2: apply the queued writes ────────────────────────────
|
|
425
|
+
for (const w of msg.writes) {
|
|
426
|
+
const ref = pyricDoc(db, w.path);
|
|
427
|
+
if (w.method === 'set') {
|
|
428
|
+
const data = prepareWriteData(w.data) as Record<string, unknown>;
|
|
429
|
+
modularTx.set(ref, data, w.options as SetOptions | undefined);
|
|
430
|
+
} else if (w.method === 'update') {
|
|
431
|
+
const data = prepareWriteData(w.data) as Record<string, unknown>;
|
|
432
|
+
modularTx.update(ref, data);
|
|
433
|
+
} else if (w.method === 'delete') {
|
|
434
|
+
modularTx.delete(ref);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
await bestEffortFlush(ctx);
|
|
439
|
+
ok(port, msg.id, null);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (e === TXN_ABORT) {
|
|
442
|
+
// Read-set conflict — signal the client to retry its updateFn.
|
|
443
|
+
const abortErr = { code: 'aborted', message: 'Transaction read-set conflict: a concurrent write invalidated the read snapshot.' };
|
|
444
|
+
post(port, { t: 'res', id: msg.id, ok: false, error: abortErr });
|
|
445
|
+
} else {
|
|
446
|
+
fail(port, msg.id, e);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
default: {
|
|
453
|
+
fail(port, msg.id, new Error(`Unknown method: ${String((msg as { method: unknown }).method)}`));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|