dotdo 0.0.1 → 0.1.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/LICENSE +1 -1
- package/README.md +446 -315
- package/cli/README.md +238 -0
- package/cli/agent.ts +72 -0
- package/cli/bin.js +44 -0
- package/cli/bin.ts +38 -0
- package/cli/build.ts +157 -0
- package/cli/commands/auth/login.ts +14 -0
- package/cli/commands/auth/logout.ts +6 -0
- package/cli/commands/auth/whoami.ts +16 -0
- package/cli/commands/deploy-multi.ts +245 -0
- package/cli/commands/dev/deploy.ts +100 -0
- package/cli/commands/dev/dev.ts +95 -0
- package/cli/commands/dev/logs.ts +91 -0
- package/cli/commands/dev-local.ts +88 -0
- package/cli/commands/do-ops.ts +314 -0
- package/cli/commands/index.ts +100 -0
- package/cli/commands/init.ts +247 -0
- package/cli/commands/introspect/emitter.ts +315 -0
- package/cli/commands/introspect/index.ts +193 -0
- package/cli/commands/link.ts +598 -0
- package/cli/commands/snippets.ts +415 -0
- package/cli/commands/tunnel.ts +239 -0
- package/cli/device-auth.ts +289 -0
- package/cli/fallback.ts +12 -0
- package/cli/index.ts +121 -0
- package/cli/main.ts +246 -0
- package/cli/mcp-stdio.ts +790 -0
- package/cli/package.json +62 -0
- package/cli/runtime/do-registry.ts +193 -0
- package/cli/runtime/embedded-db.ts +344 -0
- package/cli/runtime/index.ts +9 -0
- package/cli/runtime/miniflare-adapter.ts +162 -0
- package/cli/sandbox.ts +82 -0
- package/cli/src/args.ts +174 -0
- package/cli/src/auth.ts +55 -0
- package/cli/src/commands/call.ts +84 -0
- package/cli/src/commands/charge.ts +96 -0
- package/cli/src/commands/config.ts +115 -0
- package/cli/src/commands/email.ts +112 -0
- package/cli/src/commands/llm.ts +115 -0
- package/cli/src/commands/queue.ts +134 -0
- package/cli/src/commands/text.ts +86 -0
- package/cli/src/config.ts +185 -0
- package/cli/src/output.ts +246 -0
- package/cli/src/rpc.ts +192 -0
- package/cli/utils/config.ts +282 -0
- package/cli/utils/detect.ts +73 -0
- package/cli/utils/index.ts +15 -0
- package/cli/utils/logger.ts +232 -0
- package/dist/ai/index.js +19 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/template-literals.js +852 -0
- package/dist/ai/template-literals.js.map +1 -0
- package/dist/api/middleware/auth-federation.js +573 -0
- package/dist/api/middleware/auth-federation.js.map +1 -0
- package/dist/api/middleware/auth.js +545 -0
- package/dist/api/middleware/auth.js.map +1 -0
- package/dist/db/actions.js +212 -0
- package/dist/db/actions.js.map +1 -0
- package/dist/db/auth.js +506 -0
- package/dist/db/auth.js.map +1 -0
- package/dist/db/branches.js +65 -0
- package/dist/db/branches.js.map +1 -0
- package/dist/db/clickhouse.js +1074 -0
- package/dist/db/clickhouse.js.map +1 -0
- package/dist/db/dlq.js +39 -0
- package/dist/db/dlq.js.map +1 -0
- package/dist/db/events.js +28 -0
- package/dist/db/events.js.map +1 -0
- package/dist/db/exec.js +64 -0
- package/dist/db/exec.js.map +1 -0
- package/dist/db/files.js +85 -0
- package/dist/db/files.js.map +1 -0
- package/dist/db/flags.js +24 -0
- package/dist/db/flags.js.map +1 -0
- package/dist/db/git.js +116 -0
- package/dist/db/git.js.map +1 -0
- package/dist/db/iceberg/inverted-index.js +862 -0
- package/dist/db/iceberg/inverted-index.js.map +1 -0
- package/dist/db/iceberg/puffin.js +878 -0
- package/dist/db/iceberg/puffin.js.map +1 -0
- package/dist/db/iceberg/search-manifest.js +422 -0
- package/dist/db/iceberg/search-manifest.js.map +1 -0
- package/dist/db/iceberg/types.js +8 -0
- package/dist/db/iceberg/types.js.map +1 -0
- package/dist/db/index.js +121 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/integrations.js +368 -0
- package/dist/db/integrations.js.map +1 -0
- package/dist/db/json-indexes.js +332 -0
- package/dist/db/json-indexes.js.map +1 -0
- package/dist/db/linked-accounts.js +287 -0
- package/dist/db/linked-accounts.js.map +1 -0
- package/dist/db/nouns.js +183 -0
- package/dist/db/nouns.js.map +1 -0
- package/dist/db/objects.js +170 -0
- package/dist/db/objects.js.map +1 -0
- package/dist/db/primitives/dag-scheduler/index.js +869 -0
- package/dist/db/primitives/dag-scheduler/index.js.map +1 -0
- package/dist/db/primitives/exactly-once-context.js +237 -0
- package/dist/db/primitives/exactly-once-context.js.map +1 -0
- package/dist/db/primitives/index.js +62 -0
- package/dist/db/primitives/index.js.map +1 -0
- package/dist/db/primitives/keyed-router.js +145 -0
- package/dist/db/primitives/keyed-router.js.map +1 -0
- package/dist/db/primitives/observability.js +162 -0
- package/dist/db/primitives/observability.js.map +1 -0
- package/dist/db/primitives/schema-evolution.js +643 -0
- package/dist/db/primitives/schema-evolution.js.map +1 -0
- package/dist/db/primitives/stateful-operator/index.js +770 -0
- package/dist/db/primitives/stateful-operator/index.js.map +1 -0
- package/dist/db/primitives/temporal-store.js +306 -0
- package/dist/db/primitives/temporal-store.js.map +1 -0
- package/dist/db/primitives/typed-column-store.js +1229 -0
- package/dist/db/primitives/typed-column-store.js.map +1 -0
- package/dist/db/primitives/utils/duration.js +162 -0
- package/dist/db/primitives/utils/duration.js.map +1 -0
- package/dist/db/primitives/utils/murmur3.js +116 -0
- package/dist/db/primitives/utils/murmur3.js.map +1 -0
- package/dist/db/primitives/watermark-service.js +136 -0
- package/dist/db/primitives/watermark-service.js.map +1 -0
- package/dist/db/primitives/window-manager.js +764 -0
- package/dist/db/primitives/window-manager.js.map +1 -0
- package/dist/db/relationships.js +66 -0
- package/dist/db/relationships.js.map +1 -0
- package/dist/db/schema-minimal.js +61 -0
- package/dist/db/schema-minimal.js.map +1 -0
- package/dist/db/search.js +28 -0
- package/dist/db/search.js.map +1 -0
- package/dist/db/stores.js +1665 -0
- package/dist/db/stores.js.map +1 -0
- package/dist/db/things.js +297 -0
- package/dist/db/things.js.map +1 -0
- package/dist/db/vault.js +171 -0
- package/dist/db/vault.js.map +1 -0
- package/dist/db/verbs.js +102 -0
- package/dist/db/verbs.js.map +1 -0
- package/dist/do/base.js +48 -0
- package/dist/do/base.js.map +1 -0
- package/dist/do/tiny.js +31 -0
- package/dist/do/tiny.js.map +1 -0
- package/dist/lib/DOAuth.js +261 -0
- package/dist/lib/DOAuth.js.map +1 -0
- package/dist/lib/DODispatcher.js +72 -0
- package/dist/lib/DODispatcher.js.map +1 -0
- package/dist/lib/Modifier.js +189 -0
- package/dist/lib/Modifier.js.map +1 -0
- package/dist/lib/StateStorage.js +403 -0
- package/dist/lib/StateStorage.js.map +1 -0
- package/dist/lib/TypeRegistry.js +122 -0
- package/dist/lib/TypeRegistry.js.map +1 -0
- package/dist/lib/ai/gateway.js +247 -0
- package/dist/lib/ai/gateway.js.map +1 -0
- package/dist/lib/ai/tool-loop-agent.js +591 -0
- package/dist/lib/ai/tool-loop-agent.js.map +1 -0
- package/dist/lib/auto-wiring.js +439 -0
- package/dist/lib/auto-wiring.js.map +1 -0
- package/dist/lib/browse/browserbase.js +163 -0
- package/dist/lib/browse/browserbase.js.map +1 -0
- package/dist/lib/browse/cloudflare.js +144 -0
- package/dist/lib/browse/cloudflare.js.map +1 -0
- package/dist/lib/browse/index.js +62 -0
- package/dist/lib/browse/index.js.map +1 -0
- package/dist/lib/browse/types.js +13 -0
- package/dist/lib/browse/types.js.map +1 -0
- package/dist/lib/cache/index.js +37 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/visibility.js +638 -0
- package/dist/lib/cache/visibility.js.map +1 -0
- package/dist/lib/capabilities.js +268 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/channels/base.js +106 -0
- package/dist/lib/channels/base.js.map +1 -0
- package/dist/lib/channels/discord.js +94 -0
- package/dist/lib/channels/discord.js.map +1 -0
- package/dist/lib/channels/email.js +204 -0
- package/dist/lib/channels/email.js.map +1 -0
- package/dist/lib/channels/index.js +90 -0
- package/dist/lib/channels/index.js.map +1 -0
- package/dist/lib/channels/mdxui-chat.js +95 -0
- package/dist/lib/channels/mdxui-chat.js.map +1 -0
- package/dist/lib/channels/slack-blockkit.js +121 -0
- package/dist/lib/channels/slack-blockkit.js.map +1 -0
- package/dist/lib/channels/types.js +7 -0
- package/dist/lib/channels/types.js.map +1 -0
- package/dist/lib/cloudflare/ai.js +654 -0
- package/dist/lib/cloudflare/ai.js.map +1 -0
- package/dist/lib/cloudflare/index.js +88 -0
- package/dist/lib/cloudflare/index.js.map +1 -0
- package/dist/lib/cloudflare/kv.js +342 -0
- package/dist/lib/cloudflare/kv.js.map +1 -0
- package/dist/lib/cloudflare/queues.js +434 -0
- package/dist/lib/cloudflare/queues.js.map +1 -0
- package/dist/lib/cloudflare/r2.js +604 -0
- package/dist/lib/cloudflare/r2.js.map +1 -0
- package/dist/lib/cloudflare/vectorize.js +494 -0
- package/dist/lib/cloudflare/vectorize.js.map +1 -0
- package/dist/lib/cloudflare/workflows.js +569 -0
- package/dist/lib/cloudflare/workflows.js.map +1 -0
- package/dist/lib/colo/caching.js +196 -0
- package/dist/lib/colo/caching.js.map +1 -0
- package/dist/lib/colo/detection.js +194 -0
- package/dist/lib/colo/detection.js.map +1 -0
- package/dist/lib/colo/external-data.js +219 -0
- package/dist/lib/colo/external-data.js.map +1 -0
- package/dist/lib/colo/globe-data.js +179 -0
- package/dist/lib/colo/globe-data.js.map +1 -0
- package/dist/lib/colo/index.js +16 -0
- package/dist/lib/colo/index.js.map +1 -0
- package/dist/lib/decorators.js +37 -0
- package/dist/lib/decorators.js.map +1 -0
- package/dist/lib/discovery.js +81 -0
- package/dist/lib/discovery.js.map +1 -0
- package/dist/lib/executors/AgenticFunctionExecutor.js +619 -0
- package/dist/lib/executors/AgenticFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/BaseFunctionExecutor.js +328 -0
- package/dist/lib/executors/BaseFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/CascadeExecutor.js +418 -0
- package/dist/lib/executors/CascadeExecutor.js.map +1 -0
- package/dist/lib/executors/CodeFunctionExecutor.js +904 -0
- package/dist/lib/executors/CodeFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/GenerativeFunctionExecutor.js +904 -0
- package/dist/lib/executors/GenerativeFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/HumanFunctionExecutor.js +884 -0
- package/dist/lib/executors/HumanFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/ParallelStepExecutor.js +308 -0
- package/dist/lib/executors/ParallelStepExecutor.js.map +1 -0
- package/dist/lib/executors/types.js +12 -0
- package/dist/lib/executors/types.js.map +1 -0
- package/dist/lib/experiments.js +89 -0
- package/dist/lib/experiments.js.map +1 -0
- package/dist/lib/flags/store.js +262 -0
- package/dist/lib/flags/store.js.map +1 -0
- package/dist/lib/functions/FunctionComposition.js +467 -0
- package/dist/lib/functions/FunctionComposition.js.map +1 -0
- package/dist/lib/functions/FunctionMiddleware.js +457 -0
- package/dist/lib/functions/FunctionMiddleware.js.map +1 -0
- package/dist/lib/functions/FunctionRegistry.js +426 -0
- package/dist/lib/functions/FunctionRegistry.js.map +1 -0
- package/dist/lib/functions/createFunction.js +1048 -0
- package/dist/lib/functions/createFunction.js.map +1 -0
- package/dist/lib/humans/index.js +68 -0
- package/dist/lib/humans/index.js.map +1 -0
- package/dist/lib/humans/templates.js +117 -0
- package/dist/lib/humans/templates.js.map +1 -0
- package/dist/lib/identity.js +98 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/index.js +9 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logging/error-logger.js +163 -0
- package/dist/lib/logging/error-logger.js.map +1 -0
- package/dist/lib/logging/index.js +160 -0
- package/dist/lib/logging/index.js.map +1 -0
- package/dist/lib/mixins/bash.js +753 -0
- package/dist/lib/mixins/bash.js.map +1 -0
- package/dist/lib/mixins/fs.js +648 -0
- package/dist/lib/mixins/fs.js.map +1 -0
- package/dist/lib/mixins/git.js +1006 -0
- package/dist/lib/mixins/git.js.map +1 -0
- package/dist/lib/mixins/npm.js +662 -0
- package/dist/lib/mixins/npm.js.map +1 -0
- package/dist/lib/noun-id.js +278 -0
- package/dist/lib/noun-id.js.map +1 -0
- package/dist/lib/rate-limit/sliding-window.js +148 -0
- package/dist/lib/rate-limit/sliding-window.js.map +1 -0
- package/dist/lib/rate-limit.js +110 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/rpc/bindings.js +548 -0
- package/dist/lib/rpc/bindings.js.map +1 -0
- package/dist/lib/rpc/index.js +64 -0
- package/dist/lib/rpc/index.js.map +1 -0
- package/dist/lib/safe-stringify.js +223 -0
- package/dist/lib/safe-stringify.js.map +1 -0
- package/dist/lib/sandbox/miniflare-sandbox.js +1007 -0
- package/dist/lib/sandbox/miniflare-sandbox.js.map +1 -0
- package/dist/lib/sqids.js +110 -0
- package/dist/lib/sqids.js.map +1 -0
- package/dist/lib/sql/adapters/index.js +10 -0
- package/dist/lib/sql/adapters/index.js.map +1 -0
- package/dist/lib/sql/adapters/node-sql-parser.js +552 -0
- package/dist/lib/sql/adapters/node-sql-parser.js.map +1 -0
- package/dist/lib/sql/adapters/pgsql-parser.js +1190 -0
- package/dist/lib/sql/adapters/pgsql-parser.js.map +1 -0
- package/dist/lib/sql/index.js +277 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql/types.js +56 -0
- package/dist/lib/sql/types.js.map +1 -0
- package/dist/lib/type-classifier.js +126 -0
- package/dist/lib/type-classifier.js.map +1 -0
- package/dist/lib/utils/html.js +47 -0
- package/dist/lib/utils/html.js.map +1 -0
- package/dist/lib/validation.js +48 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/lib/vault/store.js +411 -0
- package/dist/lib/vault/store.js.map +1 -0
- package/dist/metrics/hunch.js +739 -0
- package/dist/metrics/hunch.js.map +1 -0
- package/dist/objects/API.js +302 -0
- package/dist/objects/API.js.map +1 -0
- package/dist/objects/Agent.js +179 -0
- package/dist/objects/Agent.js.map +1 -0
- package/dist/objects/AgenticFunctionExecutor.js +8 -0
- package/dist/objects/AgenticFunctionExecutor.js.map +1 -0
- package/dist/objects/App.js +83 -0
- package/dist/objects/App.js.map +1 -0
- package/dist/objects/Browser.js +884 -0
- package/dist/objects/Browser.js.map +1 -0
- package/dist/objects/Business.js +107 -0
- package/dist/objects/Business.js.map +1 -0
- package/dist/objects/CLI.js +221 -0
- package/dist/objects/CLI.js.map +1 -0
- package/dist/objects/CodeFunctionExecutor.js +8 -0
- package/dist/objects/CodeFunctionExecutor.js.map +1 -0
- package/dist/objects/Collection.js +161 -0
- package/dist/objects/Collection.js.map +1 -0
- package/dist/objects/DO.js +41 -0
- package/dist/objects/DO.js.map +1 -0
- package/dist/objects/DOBase.js +2309 -0
- package/dist/objects/DOBase.js.map +1 -0
- package/dist/objects/DOCache.js +153 -0
- package/dist/objects/DOCache.js.map +1 -0
- package/dist/objects/DOFull.js +1676 -0
- package/dist/objects/DOFull.js.map +1 -0
- package/dist/objects/DOTiny.js +207 -0
- package/dist/objects/DOTiny.js.map +1 -0
- package/dist/objects/Directory.js +199 -0
- package/dist/objects/Directory.js.map +1 -0
- package/dist/objects/Entity.js +413 -0
- package/dist/objects/Entity.js.map +1 -0
- package/dist/objects/Function.js +116 -0
- package/dist/objects/Function.js.map +1 -0
- package/dist/objects/Human.js +231 -0
- package/dist/objects/Human.js.map +1 -0
- package/dist/objects/HumanFunctionExecutor.js +8 -0
- package/dist/objects/HumanFunctionExecutor.js.map +1 -0
- package/dist/objects/IcebergMetadataDO.js +938 -0
- package/dist/objects/IcebergMetadataDO.js.map +1 -0
- package/dist/objects/IntegrationsDO.js +1174 -0
- package/dist/objects/IntegrationsDO.js.map +1 -0
- package/dist/objects/ObservabilityBroadcaster.js +149 -0
- package/dist/objects/ObservabilityBroadcaster.js.map +1 -0
- package/dist/objects/Package.js +154 -0
- package/dist/objects/Package.js.map +1 -0
- package/dist/objects/Product.js +193 -0
- package/dist/objects/Product.js.map +1 -0
- package/dist/objects/SDK.js +152 -0
- package/dist/objects/SDK.js.map +1 -0
- package/dist/objects/SaaS.js +235 -0
- package/dist/objects/SaaS.js.map +1 -0
- package/dist/objects/SandboxDO.js +759 -0
- package/dist/objects/SandboxDO.js.map +1 -0
- package/dist/objects/Service.js +337 -0
- package/dist/objects/Service.js.map +1 -0
- package/dist/objects/Site.js +80 -0
- package/dist/objects/Site.js.map +1 -0
- package/dist/objects/Startup.js +479 -0
- package/dist/objects/Startup.js.map +1 -0
- package/dist/objects/ThingsDO.js +170 -0
- package/dist/objects/ThingsDO.js.map +1 -0
- package/dist/objects/VectorShardDO.js +650 -0
- package/dist/objects/VectorShardDO.js.map +1 -0
- package/dist/objects/Worker.js +144 -0
- package/dist/objects/Worker.js.map +1 -0
- package/dist/objects/Workflow.js +196 -0
- package/dist/objects/Workflow.js.map +1 -0
- package/dist/objects/WorkflowFactory.js +313 -0
- package/dist/objects/WorkflowFactory.js.map +1 -0
- package/dist/objects/WorkflowRuntime.js +863 -0
- package/dist/objects/WorkflowRuntime.js.map +1 -0
- package/dist/objects/circuit-breaker-bulkhead.js +178 -0
- package/dist/objects/circuit-breaker-bulkhead.js.map +1 -0
- package/dist/objects/createFunction.js +934 -0
- package/dist/objects/createFunction.js.map +1 -0
- package/dist/objects/index.js +80 -0
- package/dist/objects/index.js.map +1 -0
- package/dist/objects/lifecycle/Branch.js +275 -0
- package/dist/objects/lifecycle/Branch.js.map +1 -0
- package/dist/objects/lifecycle/Clone.js +1499 -0
- package/dist/objects/lifecycle/Clone.js.map +1 -0
- package/dist/objects/lifecycle/Compact.js +237 -0
- package/dist/objects/lifecycle/Compact.js.map +1 -0
- package/dist/objects/lifecycle/Promote.js +476 -0
- package/dist/objects/lifecycle/Promote.js.map +1 -0
- package/dist/objects/lifecycle/Shard.js +560 -0
- package/dist/objects/lifecycle/Shard.js.map +1 -0
- package/dist/objects/lifecycle/index.js +15 -0
- package/dist/objects/lifecycle/index.js.map +1 -0
- package/dist/objects/lifecycle/types.js +33 -0
- package/dist/objects/lifecycle/types.js.map +1 -0
- package/dist/objects/mixins/infrastructure.js +171 -0
- package/dist/objects/mixins/infrastructure.js.map +1 -0
- package/dist/objects/modules/StoresModule.js +153 -0
- package/dist/objects/modules/StoresModule.js.map +1 -0
- package/dist/objects/persistence/checkpoint-manager.js +606 -0
- package/dist/objects/persistence/checkpoint-manager.js.map +1 -0
- package/dist/objects/persistence/index.js +72 -0
- package/dist/objects/persistence/index.js.map +1 -0
- package/dist/objects/persistence/migration-runner.js +562 -0
- package/dist/objects/persistence/migration-runner.js.map +1 -0
- package/dist/objects/persistence/replication-manager.js +501 -0
- package/dist/objects/persistence/replication-manager.js.map +1 -0
- package/dist/objects/persistence/tiered-storage-manager.js +595 -0
- package/dist/objects/persistence/tiered-storage-manager.js.map +1 -0
- package/dist/objects/persistence/types.js +14 -0
- package/dist/objects/persistence/types.js.map +1 -0
- package/dist/objects/persistence/wal-manager.js +653 -0
- package/dist/objects/persistence/wal-manager.js.map +1 -0
- package/dist/objects/presets/index.js +20 -0
- package/dist/objects/presets/index.js.map +1 -0
- package/dist/objects/presets/primitives.js +188 -0
- package/dist/objects/presets/primitives.js.map +1 -0
- package/dist/objects/primitives/alarm-adapter.js +141 -0
- package/dist/objects/primitives/alarm-adapter.js.map +1 -0
- package/dist/objects/primitives/index.js +337 -0
- package/dist/objects/primitives/index.js.map +1 -0
- package/dist/objects/primitives/storage-adapter.js +182 -0
- package/dist/objects/primitives/storage-adapter.js.map +1 -0
- package/dist/objects/primitives/with-primitives.js +102 -0
- package/dist/objects/primitives/with-primitives.js.map +1 -0
- package/dist/objects/services/StoreManager.js +227 -0
- package/dist/objects/services/StoreManager.js.map +1 -0
- package/dist/objects/services/index.js +13 -0
- package/dist/objects/services/index.js.map +1 -0
- package/dist/objects/transport/auth-layer.js +1451 -0
- package/dist/objects/transport/auth-layer.js.map +1 -0
- package/dist/objects/transport/capnweb-target.js +355 -0
- package/dist/objects/transport/capnweb-target.js.map +1 -0
- package/dist/objects/transport/chain.js +441 -0
- package/dist/objects/transport/chain.js.map +1 -0
- package/dist/objects/transport/handler.js +58 -0
- package/dist/objects/transport/handler.js.map +1 -0
- package/dist/objects/transport/index.js +53 -0
- package/dist/objects/transport/index.js.map +1 -0
- package/dist/objects/transport/mcp-server.js +691 -0
- package/dist/objects/transport/mcp-server.js.map +1 -0
- package/dist/objects/transport/rest-autowire.js +1508 -0
- package/dist/objects/transport/rest-autowire.js.map +1 -0
- package/dist/objects/transport/rest-router.js +440 -0
- package/dist/objects/transport/rest-router.js.map +1 -0
- package/dist/objects/transport/rpc-server.js +1539 -0
- package/dist/objects/transport/rpc-server.js.map +1 -0
- package/dist/objects/transport/shared.js +576 -0
- package/dist/objects/transport/shared.js.map +1 -0
- package/dist/objects/transport/sync-engine.js +291 -0
- package/dist/objects/transport/sync-engine.js.map +1 -0
- package/dist/objects/transport/types.js +8 -0
- package/dist/objects/transport/types.js.map +1 -0
- package/dist/sandbox/index.js +258 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/snippets/artifacts-config.js +241 -0
- package/dist/snippets/artifacts-config.js.map +1 -0
- package/dist/snippets/artifacts-ingest.js +832 -0
- package/dist/snippets/artifacts-ingest.js.map +1 -0
- package/dist/snippets/artifacts-serve.js +1035 -0
- package/dist/snippets/artifacts-serve.js.map +1 -0
- package/dist/snippets/artifacts-types.js +161 -0
- package/dist/snippets/artifacts-types.js.map +1 -0
- package/dist/snippets/cache-probe.js +376 -0
- package/dist/snippets/cache-probe.js.map +1 -0
- package/dist/snippets/cache.js +10 -0
- package/dist/snippets/cache.js.map +1 -0
- package/dist/snippets/events.js +469 -0
- package/dist/snippets/events.js.map +1 -0
- package/dist/snippets/index.js +7 -0
- package/dist/snippets/index.js.map +1 -0
- package/dist/snippets/proxy.js +495 -0
- package/dist/snippets/proxy.js.map +1 -0
- package/dist/snippets/search.js +1759 -0
- package/dist/snippets/search.js.map +1 -0
- package/dist/streams/index.js +30 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/observability.js +68 -0
- package/dist/streams/observability.js.map +1 -0
- package/dist/types/AI.js +92 -0
- package/dist/types/AI.js.map +1 -0
- package/dist/types/AIFunction.js +171 -0
- package/dist/types/AIFunction.js.map +1 -0
- package/dist/types/BrowseVerb.js +89 -0
- package/dist/types/BrowseVerb.js.map +1 -0
- package/dist/types/Browser.js +31 -0
- package/dist/types/Browser.js.map +1 -0
- package/dist/types/Chaos.js +15 -0
- package/dist/types/Chaos.js.map +1 -0
- package/dist/types/CloudflareBindings.js +109 -0
- package/dist/types/CloudflareBindings.js.map +1 -0
- package/dist/types/Collection.js +50 -0
- package/dist/types/Collection.js.map +1 -0
- package/dist/types/DO.js +2 -0
- package/dist/types/DO.js.map +1 -0
- package/dist/types/DOLocation.js +63 -0
- package/dist/types/DOLocation.js.map +1 -0
- package/dist/types/EventHandler.js +57 -0
- package/dist/types/EventHandler.js.map +1 -0
- package/dist/types/Experiment.js +33 -0
- package/dist/types/Experiment.js.map +1 -0
- package/dist/types/Flag.js +57 -0
- package/dist/types/Flag.js.map +1 -0
- package/dist/types/Lifecycle.js +13 -0
- package/dist/types/Lifecycle.js.map +1 -0
- package/dist/types/Location.js +169 -0
- package/dist/types/Location.js.map +1 -0
- package/dist/types/Noun.js +66 -0
- package/dist/types/Noun.js.map +1 -0
- package/dist/types/SessionEvent.js +194 -0
- package/dist/types/SessionEvent.js.map +1 -0
- package/dist/types/Thing.js +55 -0
- package/dist/types/Thing.js.map +1 -0
- package/dist/types/ThingDO.js +153 -0
- package/dist/types/ThingDO.js.map +1 -0
- package/dist/types/Things.js +2 -0
- package/dist/types/Things.js.map +1 -0
- package/dist/types/Verb.js +119 -0
- package/dist/types/Verb.js.map +1 -0
- package/dist/types/WorkflowContext.js +70 -0
- package/dist/types/WorkflowContext.js.map +1 -0
- package/dist/types/analytics-api.js +13 -0
- package/dist/types/analytics-api.js.map +1 -0
- package/dist/types/capabilities.js +135 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/drizzle.js +12 -0
- package/dist/types/drizzle.js.map +1 -0
- package/dist/types/event.js +201 -0
- package/dist/types/event.js.map +1 -0
- package/dist/types/fn.js +12 -0
- package/dist/types/fn.js.map +1 -0
- package/dist/types/iceberg.js +48 -0
- package/dist/types/iceberg.js.map +1 -0
- package/dist/types/ids.js +170 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.js +41 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/introspect.js +54 -0
- package/dist/types/introspect.js.map +1 -0
- package/dist/types/observability.js +124 -0
- package/dist/types/observability.js.map +1 -0
- package/dist/types/sync-protocol.js +175 -0
- package/dist/types/sync-protocol.js.map +1 -0
- package/dist/types/vector.js +13 -0
- package/dist/types/vector.js.map +1 -0
- package/dist/workflows/ScheduleManager.js +473 -0
- package/dist/workflows/ScheduleManager.js.map +1 -0
- package/dist/workflows/StepDOBridge.js +149 -0
- package/dist/workflows/StepDOBridge.js.map +1 -0
- package/dist/workflows/StepResultStorage.js +232 -0
- package/dist/workflows/StepResultStorage.js.map +1 -0
- package/dist/workflows/WaitForEventManager.js +461 -0
- package/dist/workflows/WaitForEventManager.js.map +1 -0
- package/dist/workflows/analyzer.js +332 -0
- package/dist/workflows/analyzer.js.map +1 -0
- package/dist/workflows/compat/activity-router.js +484 -0
- package/dist/workflows/compat/activity-router.js.map +1 -0
- package/dist/workflows/compat/backends/cloudflare-workflows.js +431 -0
- package/dist/workflows/compat/backends/cloudflare-workflows.js.map +1 -0
- package/dist/workflows/compat/backends/index.js +14 -0
- package/dist/workflows/compat/backends/index.js.map +1 -0
- package/dist/workflows/compat/errors/index.js +375 -0
- package/dist/workflows/compat/errors/index.js.map +1 -0
- package/dist/workflows/compat/index.js +79 -0
- package/dist/workflows/compat/index.js.map +1 -0
- package/dist/workflows/compat/inngest/index.js +989 -0
- package/dist/workflows/compat/inngest/index.js.map +1 -0
- package/dist/workflows/compat/qstash/index.js +1263 -0
- package/dist/workflows/compat/qstash/index.js.map +1 -0
- package/dist/workflows/compat/temporal/activities.js +739 -0
- package/dist/workflows/compat/temporal/activities.js.map +1 -0
- package/dist/workflows/compat/temporal/child-workflows.js +154 -0
- package/dist/workflows/compat/temporal/child-workflows.js.map +1 -0
- package/dist/workflows/compat/temporal/client.js +381 -0
- package/dist/workflows/compat/temporal/client.js.map +1 -0
- package/dist/workflows/compat/temporal/context.js +309 -0
- package/dist/workflows/compat/temporal/context.js.map +1 -0
- package/dist/workflows/compat/temporal/determinism.js +216 -0
- package/dist/workflows/compat/temporal/determinism.js.map +1 -0
- package/dist/workflows/compat/temporal/errors.js +128 -0
- package/dist/workflows/compat/temporal/errors.js.map +1 -0
- package/dist/workflows/compat/temporal/index.js +2464 -0
- package/dist/workflows/compat/temporal/index.js.map +1 -0
- package/dist/workflows/compat/temporal/saga.js +504 -0
- package/dist/workflows/compat/temporal/saga.js.map +1 -0
- package/dist/workflows/compat/temporal/signals.js +364 -0
- package/dist/workflows/compat/temporal/signals.js.map +1 -0
- package/dist/workflows/compat/temporal/storage.js +271 -0
- package/dist/workflows/compat/temporal/storage.js.map +1 -0
- package/dist/workflows/compat/temporal/timers.js +347 -0
- package/dist/workflows/compat/temporal/timers.js.map +1 -0
- package/dist/workflows/compat/temporal/types.js +7 -0
- package/dist/workflows/compat/temporal/types.js.map +1 -0
- package/dist/workflows/compat/temporal/unified-primitives.js +339 -0
- package/dist/workflows/compat/temporal/unified-primitives.js.map +1 -0
- package/dist/workflows/compat/trigger/index.js +468 -0
- package/dist/workflows/compat/trigger/index.js.map +1 -0
- package/dist/workflows/compat/utils/index.js +69 -0
- package/dist/workflows/compat/utils/index.js.map +1 -0
- package/dist/workflows/context/correlation-capability.js +266 -0
- package/dist/workflows/context/correlation-capability.js.map +1 -0
- package/dist/workflows/context/correlation.js +484 -0
- package/dist/workflows/context/correlation.js.map +1 -0
- package/dist/workflows/context/experiment.js +289 -0
- package/dist/workflows/context/experiment.js.map +1 -0
- package/dist/workflows/context/flag.js +244 -0
- package/dist/workflows/context/flag.js.map +1 -0
- package/dist/workflows/context/foundation.js +648 -0
- package/dist/workflows/context/foundation.js.map +1 -0
- package/dist/workflows/context/human-base.js +106 -0
- package/dist/workflows/context/human-base.js.map +1 -0
- package/dist/workflows/context/human.js +368 -0
- package/dist/workflows/context/human.js.map +1 -0
- package/dist/workflows/context/measure.js +354 -0
- package/dist/workflows/context/measure.js.map +1 -0
- package/dist/workflows/context/rate-limit.js +358 -0
- package/dist/workflows/context/rate-limit.js.map +1 -0
- package/dist/workflows/context/user.js +117 -0
- package/dist/workflows/context/user.js.map +1 -0
- package/dist/workflows/context/vault.js +360 -0
- package/dist/workflows/context/vault.js.map +1 -0
- package/dist/workflows/data/entity-events/entity-events.js +489 -0
- package/dist/workflows/data/entity-events/entity-events.js.map +1 -0
- package/dist/workflows/data/experiment/index.js +599 -0
- package/dist/workflows/data/experiment/index.js.map +1 -0
- package/dist/workflows/data/goal/context.js +558 -0
- package/dist/workflows/data/goal/context.js.map +1 -0
- package/dist/workflows/data/goal/index.js +32 -0
- package/dist/workflows/data/goal/index.js.map +1 -0
- package/dist/workflows/data/measure/index.js +840 -0
- package/dist/workflows/data/measure/index.js.map +1 -0
- package/dist/workflows/data/stream/index.js +1149 -0
- package/dist/workflows/data/stream/index.js.map +1 -0
- package/dist/workflows/data/track/context.js +883 -0
- package/dist/workflows/data/track/context.js.map +1 -0
- package/dist/workflows/data/track/index.js +15 -0
- package/dist/workflows/data/track/index.js.map +1 -0
- package/dist/workflows/data/view/context.js +864 -0
- package/dist/workflows/data/view/context.js.map +1 -0
- package/dist/workflows/domain.js +93 -0
- package/dist/workflows/domain.js.map +1 -0
- package/dist/workflows/flag.js +176 -0
- package/dist/workflows/flag.js.map +1 -0
- package/dist/workflows/flags.js +217 -0
- package/dist/workflows/flags.js.map +1 -0
- package/dist/workflows/hash.js +209 -0
- package/dist/workflows/hash.js.map +1 -0
- package/dist/workflows/index.js +50 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/on.js +378 -0
- package/dist/workflows/on.js.map +1 -0
- package/dist/workflows/pipeline-promise.js +481 -0
- package/dist/workflows/pipeline-promise.js.map +1 -0
- package/dist/workflows/pipeline-types.js +20 -0
- package/dist/workflows/pipeline-types.js.map +1 -0
- package/dist/workflows/proxy.js +76 -0
- package/dist/workflows/proxy.js.map +1 -0
- package/dist/workflows/runtime.js +310 -0
- package/dist/workflows/runtime.js.map +1 -0
- package/dist/workflows/schedule-builder.js +327 -0
- package/dist/workflows/schedule-builder.js.map +1 -0
- package/dist/workflows/visibility/index.js +146 -0
- package/dist/workflows/visibility/index.js.map +1 -0
- package/dist/workflows/visibility/query-parser.js +150 -0
- package/dist/workflows/visibility/query-parser.js.map +1 -0
- package/dist/workflows/visibility/store.js +223 -0
- package/dist/workflows/visibility/store.js.map +1 -0
- package/dist/workflows/visibility/types.js +30 -0
- package/dist/workflows/visibility/types.js.map +1 -0
- package/dist/workflows/workflow.js +53 -0
- package/dist/workflows/workflow.js.map +1 -0
- package/package.json +294 -46
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone Lifecycle Module
|
|
3
|
+
*
|
|
4
|
+
* Handles all clone operations for Durable Objects:
|
|
5
|
+
* - Atomic clone: All-or-nothing clone operation
|
|
6
|
+
* - Staged clone: Two-phase commit with prepare/commit
|
|
7
|
+
* - Eventual clone: Background async clone with eventual consistency
|
|
8
|
+
* - Resumable clone: Checkpoint-based clone that can be resumed
|
|
9
|
+
*/
|
|
10
|
+
import * as schema from '../../db';
|
|
11
|
+
// Storage prefixes
|
|
12
|
+
const STAGING_PREFIX = 'staging:';
|
|
13
|
+
const CHECKPOINT_PREFIX = 'checkpoint:';
|
|
14
|
+
const TWO_PC_PREFIX = '2pc:';
|
|
15
|
+
const DEFAULT_TOKEN_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
const DEFAULT_COORDINATOR_TIMEOUT = 30000; // 30 seconds
|
|
17
|
+
const DEFAULT_ACK_TIMEOUT = 10000; // 10 seconds
|
|
18
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
19
|
+
/**
|
|
20
|
+
* Clone lifecycle module implementing Strategy pattern.
|
|
21
|
+
*/
|
|
22
|
+
export class CloneModule {
|
|
23
|
+
ctx;
|
|
24
|
+
// In-memory state for clone operations
|
|
25
|
+
_conflictResolvers = new Map();
|
|
26
|
+
_completionCallbacks = new Map();
|
|
27
|
+
_errorCallbacks = new Map();
|
|
28
|
+
_resumableClones = new Map();
|
|
29
|
+
_cloneLocks = new Map();
|
|
30
|
+
_broadcastCallbacks = [];
|
|
31
|
+
initialize(context) {
|
|
32
|
+
this.ctx = context;
|
|
33
|
+
}
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// MAIN CLONE METHOD
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
/**
|
|
38
|
+
* Clone current state to a new DO with configurable modes.
|
|
39
|
+
*/
|
|
40
|
+
async clone(target, options, validColos) {
|
|
41
|
+
const { mode, includeHistory = false, includeState = true, shallow = false, transform, branch: targetBranch, version: targetVersion, colo, timeout = 30000, correlationId = crypto.randomUUID(), } = options;
|
|
42
|
+
// Validate mode first
|
|
43
|
+
const validModes = ['atomic', 'staged', 'eventual', 'resumable'];
|
|
44
|
+
if (!validModes.includes(mode)) {
|
|
45
|
+
throw new Error(`Invalid mode: '${mode}' is not a valid clone mode`);
|
|
46
|
+
}
|
|
47
|
+
// Handle eventual mode
|
|
48
|
+
if (mode === 'eventual') {
|
|
49
|
+
return this.initiateEventualClone(target, options);
|
|
50
|
+
}
|
|
51
|
+
// Handle staged mode (two-phase commit)
|
|
52
|
+
if (mode === 'staged') {
|
|
53
|
+
return this.prepareStagedClone(target, options);
|
|
54
|
+
}
|
|
55
|
+
// Handle resumable mode (checkpoint-based)
|
|
56
|
+
if (mode === 'resumable') {
|
|
57
|
+
return this.initiateResumableClone(target, options);
|
|
58
|
+
}
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
// === VALIDATION ===
|
|
61
|
+
// Validate options
|
|
62
|
+
if (typeof timeout !== 'number' || timeout < 0) {
|
|
63
|
+
throw new Error('Invalid timeout: must be a non-negative number');
|
|
64
|
+
}
|
|
65
|
+
// Validate colo code if provided
|
|
66
|
+
if (colo && !validColos.has(colo)) {
|
|
67
|
+
throw new Error(`Invalid colo: '${colo}' is not a valid location code`);
|
|
68
|
+
}
|
|
69
|
+
// Validate target namespace URL format
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(target);
|
|
72
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
73
|
+
throw new Error('Invalid namespace URL: must use http or https protocol');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (e.message?.includes('Invalid namespace URL')) {
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
81
|
+
}
|
|
82
|
+
// Prevent cloning to same namespace
|
|
83
|
+
if (target === this.ctx.ns) {
|
|
84
|
+
throw new Error('Cannot clone to same namespace');
|
|
85
|
+
}
|
|
86
|
+
// Validate branch if specified
|
|
87
|
+
if (targetBranch) {
|
|
88
|
+
const branches = await this.ctx.db.select().from(schema.branches);
|
|
89
|
+
const branchExists = branches.some((b) => b.name === targetBranch);
|
|
90
|
+
if (!branchExists) {
|
|
91
|
+
throw new Error(`Branch not found: '${targetBranch}'`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Validate version if specified
|
|
95
|
+
if (targetVersion !== undefined) {
|
|
96
|
+
if (targetVersion < 0 || !Number.isInteger(targetVersion)) {
|
|
97
|
+
throw new Error(`Invalid version: ${targetVersion}`);
|
|
98
|
+
}
|
|
99
|
+
const branches = await this.ctx.db.select().from(schema.branches);
|
|
100
|
+
const mainBranch = branches.find((b) => b.name === 'main' || b.name === null);
|
|
101
|
+
const allThings = await this.ctx.db.select().from(schema.things);
|
|
102
|
+
const maxVersion = Math.max(allThings.length, mainBranch?.head ?? 0);
|
|
103
|
+
if (targetVersion > maxVersion) {
|
|
104
|
+
throw new Error(`Version not found: ${targetVersion}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Get things to validate source has state
|
|
108
|
+
let things = await this.ctx.db.select().from(schema.things);
|
|
109
|
+
if (targetBranch) {
|
|
110
|
+
things = things.filter((t) => t.branch === targetBranch);
|
|
111
|
+
}
|
|
112
|
+
const nonDeletedThings = things.filter((t) => !t.deleted);
|
|
113
|
+
if (includeState && nonDeletedThings.length === 0) {
|
|
114
|
+
throw new Error('No state to clone: source is empty');
|
|
115
|
+
}
|
|
116
|
+
const relationships = await this.ctx.db.select().from(schema.relationships);
|
|
117
|
+
// Acquire exclusive lock using blockConcurrencyWhile
|
|
118
|
+
return this.ctx.ctx.blockConcurrencyWhile(async () => {
|
|
119
|
+
try {
|
|
120
|
+
await this.ctx.emitEvent('clone.started', {
|
|
121
|
+
target,
|
|
122
|
+
mode,
|
|
123
|
+
correlationId,
|
|
124
|
+
thingsCount: nonDeletedThings.length,
|
|
125
|
+
});
|
|
126
|
+
if (!this.ctx.env.DO) {
|
|
127
|
+
throw new Error('DO namespace not configured');
|
|
128
|
+
}
|
|
129
|
+
const targetUrl = new URL(target);
|
|
130
|
+
const doId = this.ctx.env.DO.idFromName(target);
|
|
131
|
+
const stub = this.ctx.env.DO.get(doId);
|
|
132
|
+
// Health check
|
|
133
|
+
try {
|
|
134
|
+
const healthResponse = await Promise.race([
|
|
135
|
+
stub.fetch(new Request(`${targetUrl.origin}/health`)),
|
|
136
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Health check timeout')), Math.min(timeout, 5000))),
|
|
137
|
+
]);
|
|
138
|
+
if (!healthResponse.ok && healthResponse.status !== 404) {
|
|
139
|
+
if (healthResponse.status === 409) {
|
|
140
|
+
throw new Error('Target already exists: conflict detected');
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Target health check failed: ${healthResponse.status}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
const errorMessage = e.message || String(e);
|
|
147
|
+
if (errorMessage.includes('Connection refused') ||
|
|
148
|
+
errorMessage.includes('unreachable') ||
|
|
149
|
+
errorMessage.includes('Health check')) {
|
|
150
|
+
throw new Error(`Target unreachable: health check failed - ${errorMessage}`);
|
|
151
|
+
}
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
// Get latest version of each thing
|
|
155
|
+
const latestVersions = new Map();
|
|
156
|
+
if (includeState) {
|
|
157
|
+
for (const thing of nonDeletedThings) {
|
|
158
|
+
const existing = latestVersions.get(thing.id);
|
|
159
|
+
if (!existing) {
|
|
160
|
+
latestVersions.set(thing.id, thing);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Prepare data for transfer
|
|
165
|
+
const thingsToClone = includeState
|
|
166
|
+
? Array.from(latestVersions.values()).map((t) => ({
|
|
167
|
+
id: t.id,
|
|
168
|
+
type: t.type,
|
|
169
|
+
branch: t.branch,
|
|
170
|
+
name: t.name,
|
|
171
|
+
data: t.data,
|
|
172
|
+
deleted: false,
|
|
173
|
+
}))
|
|
174
|
+
: [];
|
|
175
|
+
const relationshipsToClone = shallow
|
|
176
|
+
? []
|
|
177
|
+
: relationships.map((r) => ({
|
|
178
|
+
id: r.id,
|
|
179
|
+
verb: r.verb,
|
|
180
|
+
from: r.from,
|
|
181
|
+
to: r.to,
|
|
182
|
+
data: r.data,
|
|
183
|
+
createdAt: r.createdAt,
|
|
184
|
+
}));
|
|
185
|
+
let actionsToClone = [];
|
|
186
|
+
let eventsToClone = [];
|
|
187
|
+
if (includeHistory) {
|
|
188
|
+
const actions = await this.ctx.db.select().from(schema.actions);
|
|
189
|
+
actionsToClone = actions;
|
|
190
|
+
const events = await this.ctx.db.select().from(schema.events);
|
|
191
|
+
eventsToClone = events;
|
|
192
|
+
}
|
|
193
|
+
// Apply transform function if provided
|
|
194
|
+
let finalThingsToClone = thingsToClone;
|
|
195
|
+
let finalRelationshipsToClone = relationshipsToClone;
|
|
196
|
+
if (transform) {
|
|
197
|
+
try {
|
|
198
|
+
const stateForTransform = {
|
|
199
|
+
things: thingsToClone.map((t) => ({
|
|
200
|
+
id: t.id,
|
|
201
|
+
type: t.type,
|
|
202
|
+
branch: t.branch,
|
|
203
|
+
name: t.name,
|
|
204
|
+
data: t.data,
|
|
205
|
+
deleted: t.deleted,
|
|
206
|
+
})),
|
|
207
|
+
relationships: relationshipsToClone.map((r) => ({
|
|
208
|
+
id: r.id,
|
|
209
|
+
verb: r.verb,
|
|
210
|
+
from: r.from,
|
|
211
|
+
to: r.to,
|
|
212
|
+
data: r.data,
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
const transformedState = await Promise.resolve(transform(stateForTransform));
|
|
216
|
+
finalThingsToClone = transformedState.things.map((t) => ({
|
|
217
|
+
id: t.id,
|
|
218
|
+
type: t.type,
|
|
219
|
+
branch: t.branch,
|
|
220
|
+
name: t.name,
|
|
221
|
+
data: t.data,
|
|
222
|
+
deleted: t.deleted,
|
|
223
|
+
}));
|
|
224
|
+
finalRelationshipsToClone = (transformedState.relationships || []).map((r) => ({
|
|
225
|
+
id: r.id,
|
|
226
|
+
verb: r.verb,
|
|
227
|
+
from: r.from,
|
|
228
|
+
to: r.to,
|
|
229
|
+
data: r.data,
|
|
230
|
+
createdAt: new Date(),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
catch (transformError) {
|
|
234
|
+
throw new Error(`Transform error: ${transformError.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Step 1: Initialize target
|
|
238
|
+
const initResponse = await Promise.race([
|
|
239
|
+
stub.fetch(new Request(`${targetUrl.origin}/init`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
correlationId,
|
|
244
|
+
mode: 'atomic',
|
|
245
|
+
}),
|
|
246
|
+
})),
|
|
247
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Clone timeout after ${timeout}ms`)), timeout)),
|
|
248
|
+
]);
|
|
249
|
+
if (!initResponse.ok) {
|
|
250
|
+
throw new Error(`Init failed: ${initResponse.status} ${initResponse.statusText}`);
|
|
251
|
+
}
|
|
252
|
+
// Step 2: Transfer data to target
|
|
253
|
+
const response = await Promise.race([
|
|
254
|
+
stub.fetch(new Request(`${targetUrl.origin}/transfer`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
things: finalThingsToClone,
|
|
259
|
+
relationships: finalRelationshipsToClone,
|
|
260
|
+
actions: includeHistory ? actionsToClone : undefined,
|
|
261
|
+
events: includeHistory ? eventsToClone : undefined,
|
|
262
|
+
correlationId,
|
|
263
|
+
}),
|
|
264
|
+
})),
|
|
265
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Clone timeout after ${timeout}ms`)), timeout)),
|
|
266
|
+
]);
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new Error(`Transfer failed: ${response.status} ${response.statusText}`);
|
|
269
|
+
}
|
|
270
|
+
const duration = Date.now() - startTime;
|
|
271
|
+
await this.ctx.emitEvent('clone.completed', {
|
|
272
|
+
target,
|
|
273
|
+
doId: doId.toString(),
|
|
274
|
+
correlationId,
|
|
275
|
+
thingsCount: finalThingsToClone.length,
|
|
276
|
+
duration,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
ns: target,
|
|
281
|
+
doId: doId.toString(),
|
|
282
|
+
mode: 'atomic',
|
|
283
|
+
thingsCloned: finalThingsToClone.length,
|
|
284
|
+
relationshipsCloned: finalRelationshipsToClone.length,
|
|
285
|
+
duration,
|
|
286
|
+
historyIncluded: includeHistory,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const errorMessage = error.message || String(error);
|
|
291
|
+
await this.ctx.emitEvent('clone.failed', {
|
|
292
|
+
target,
|
|
293
|
+
error: errorMessage,
|
|
294
|
+
correlationId,
|
|
295
|
+
});
|
|
296
|
+
await this.ctx.emitEvent('clone.rollback', {
|
|
297
|
+
target,
|
|
298
|
+
reason: errorMessage,
|
|
299
|
+
correlationId,
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
306
|
+
// STAGED CLONE OPERATIONS (TWO-PHASE COMMIT)
|
|
307
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
308
|
+
/**
|
|
309
|
+
* Prepare a staged clone (Phase 1 of two-phase commit)
|
|
310
|
+
*/
|
|
311
|
+
async prepareStagedClone(target, options) {
|
|
312
|
+
const token = crypto.randomUUID();
|
|
313
|
+
const timeout = options.tokenTimeout ?? DEFAULT_TOKEN_TIMEOUT;
|
|
314
|
+
const expiresAt = new Date(Date.now() + timeout);
|
|
315
|
+
const stagingNs = `${target}-staging-${token.slice(0, 8)}`;
|
|
316
|
+
options.onPrepareProgress?.(0);
|
|
317
|
+
try {
|
|
318
|
+
new URL(target);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
322
|
+
}
|
|
323
|
+
if (options.validateTarget) {
|
|
324
|
+
const existingObjects = await this.ctx.db.select().from(schema.objects);
|
|
325
|
+
const occupied = existingObjects.some((obj) => obj.ns === target && obj.primary);
|
|
326
|
+
if (occupied) {
|
|
327
|
+
throw new Error(`Target namespace is occupied: ${target}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Check for concurrent staging
|
|
331
|
+
const existingStagings = await this.ctx.ctx.storage.list({ prefix: STAGING_PREFIX });
|
|
332
|
+
for (const [, value] of existingStagings) {
|
|
333
|
+
const staging = value;
|
|
334
|
+
if (staging.targetNs === target && staging.status === 'prepared') {
|
|
335
|
+
if (new Date(staging.expiresAt) > new Date()) {
|
|
336
|
+
throw new Error(`Target namespace is locked by pending clone operation`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
await this.ctx.emitEvent('clone.staging.started', { token, target });
|
|
341
|
+
// Get things to clone
|
|
342
|
+
const things = await this.ctx.db.select().from(schema.things);
|
|
343
|
+
const branchThings = things.filter((t) => !t.deleted && (t.branch === null || t.branch === this.ctx.currentBranch));
|
|
344
|
+
if (branchThings.length === 0) {
|
|
345
|
+
throw new Error('No state to clone: source is empty');
|
|
346
|
+
}
|
|
347
|
+
// Get latest version of each thing
|
|
348
|
+
const latestVersions = new Map();
|
|
349
|
+
for (const thing of branchThings) {
|
|
350
|
+
if (!latestVersions.has(thing.id)) {
|
|
351
|
+
latestVersions.set(thing.id, thing);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const thingsToClone = Array.from(latestVersions.values());
|
|
355
|
+
const totalItems = thingsToClone.length;
|
|
356
|
+
// Create checkpoints
|
|
357
|
+
const checkpoints = [];
|
|
358
|
+
const checkpointInterval = options.checkpointInterval ?? 0;
|
|
359
|
+
const maxCheckpoints = options.maxCheckpoints ?? 100;
|
|
360
|
+
let itemsProcessed = 0;
|
|
361
|
+
const clonedThingIds = [];
|
|
362
|
+
const clonedRelationshipIds = [];
|
|
363
|
+
for (const thing of thingsToClone) {
|
|
364
|
+
clonedThingIds.push(thing.id);
|
|
365
|
+
itemsProcessed++;
|
|
366
|
+
if (checkpointInterval > 0 && itemsProcessed % checkpointInterval === 0) {
|
|
367
|
+
const checkpoint = this.createStagedCheckpoint(token, checkpoints.length + 1, itemsProcessed, totalItems, clonedThingIds.slice(), clonedRelationshipIds.slice(), this.ctx.currentBranch, itemsProcessed, options.validationMode === 'strict');
|
|
368
|
+
checkpoints.push(checkpoint);
|
|
369
|
+
while (checkpoints.length > maxCheckpoints) {
|
|
370
|
+
checkpoints.shift();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const progress = Math.floor((itemsProcessed / totalItems) * 100);
|
|
374
|
+
options.onPrepareProgress?.(progress);
|
|
375
|
+
}
|
|
376
|
+
// Final checkpoint
|
|
377
|
+
if (checkpointInterval > 0 && itemsProcessed > 0 && itemsProcessed % checkpointInterval !== 0) {
|
|
378
|
+
const finalCheckpoint = this.createStagedCheckpoint(token, checkpoints.length + 1, itemsProcessed, totalItems, clonedThingIds.slice(), clonedRelationshipIds.slice(), this.ctx.currentBranch, itemsProcessed, options.validationMode === 'strict');
|
|
379
|
+
checkpoints.push(finalCheckpoint);
|
|
380
|
+
while (checkpoints.length > maxCheckpoints) {
|
|
381
|
+
checkpoints.shift();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const sizeBytes = JSON.stringify(thingsToClone).length;
|
|
385
|
+
const mappedThings = thingsToClone.map((t) => ({
|
|
386
|
+
id: t.id,
|
|
387
|
+
type: t.type,
|
|
388
|
+
branch: t.branch,
|
|
389
|
+
name: t.name,
|
|
390
|
+
data: t.data,
|
|
391
|
+
deleted: t.deleted ?? false,
|
|
392
|
+
}));
|
|
393
|
+
const stagingData = {
|
|
394
|
+
sourceNs: this.ctx.ns,
|
|
395
|
+
targetNs: target,
|
|
396
|
+
stagingNs,
|
|
397
|
+
things: mappedThings,
|
|
398
|
+
expiresAt: expiresAt.toISOString(),
|
|
399
|
+
status: 'prepared',
|
|
400
|
+
createdAt: new Date().toISOString(),
|
|
401
|
+
integrityHash: this.computeStagingIntegrityHash(mappedThings),
|
|
402
|
+
metadata: {
|
|
403
|
+
thingsCount: thingsToClone.length,
|
|
404
|
+
sizeBytes,
|
|
405
|
+
branch: this.ctx.currentBranch,
|
|
406
|
+
version: thingsToClone.length,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
await this.ctx.ctx.storage.put(`${STAGING_PREFIX}${token}`, stagingData);
|
|
410
|
+
for (const checkpoint of checkpoints) {
|
|
411
|
+
await this.ctx.ctx.storage.put(`${CHECKPOINT_PREFIX}${token}:${checkpoint.id}`, checkpoint);
|
|
412
|
+
}
|
|
413
|
+
// Store 2PC prepare decision
|
|
414
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}prepare:${token}`, {
|
|
415
|
+
phase: 'prepared',
|
|
416
|
+
target,
|
|
417
|
+
createdAt: new Date(),
|
|
418
|
+
expiresAt,
|
|
419
|
+
});
|
|
420
|
+
// Store 2PC config
|
|
421
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}config:${token}`, {
|
|
422
|
+
coordinatorTimeout: options.coordinatorTimeout ?? DEFAULT_COORDINATOR_TIMEOUT,
|
|
423
|
+
participantAckTimeout: options.participantAckTimeout ?? DEFAULT_ACK_TIMEOUT,
|
|
424
|
+
maxAckRetries: options.maxAckRetries ?? DEFAULT_MAX_RETRIES,
|
|
425
|
+
prepareTimeout: options.prepareTimeout,
|
|
426
|
+
commitTimeout: options.commitTimeout,
|
|
427
|
+
abortTimeout: options.abortTimeout,
|
|
428
|
+
});
|
|
429
|
+
// Initialize audit log
|
|
430
|
+
const auditLog = {
|
|
431
|
+
token,
|
|
432
|
+
sourceNs: this.ctx.ns,
|
|
433
|
+
targetNs: [target],
|
|
434
|
+
events: [
|
|
435
|
+
{
|
|
436
|
+
type: 'prepare',
|
|
437
|
+
status: 'completed',
|
|
438
|
+
timestamp: new Date(),
|
|
439
|
+
data: { thingsCount: thingsToClone.length, sizeBytes },
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
createdAt: new Date(),
|
|
443
|
+
};
|
|
444
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}audit:${token}`, auditLog);
|
|
445
|
+
// Initialize participant history
|
|
446
|
+
const participantHistory = {
|
|
447
|
+
target,
|
|
448
|
+
transitions: [{ from: 'initial', to: 'prepared', timestamp: new Date() }],
|
|
449
|
+
};
|
|
450
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}history:${token}:${target}`, participantHistory);
|
|
451
|
+
options.onPrepareProgress?.(100);
|
|
452
|
+
await this.ctx.emitEvent('clone.staging.completed', { token, target, thingsCount: thingsToClone.length });
|
|
453
|
+
await this.ctx.emitEvent('clone.prepared', { token, target, expiresAt });
|
|
454
|
+
const participantAck = {
|
|
455
|
+
target,
|
|
456
|
+
status: 'ready',
|
|
457
|
+
vote: 'yes',
|
|
458
|
+
timestamp: new Date(),
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
phase: 'prepared',
|
|
462
|
+
token,
|
|
463
|
+
expiresAt,
|
|
464
|
+
stagingNs,
|
|
465
|
+
metadata: {
|
|
466
|
+
thingsCount: thingsToClone.length,
|
|
467
|
+
sizeBytes,
|
|
468
|
+
branch: this.ctx.currentBranch,
|
|
469
|
+
version: thingsToClone.length,
|
|
470
|
+
},
|
|
471
|
+
participantAck,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Commit a staged clone (Phase 2 of two-phase commit)
|
|
476
|
+
*/
|
|
477
|
+
async commitClone(token, options) {
|
|
478
|
+
if (!token || token.trim() === '') {
|
|
479
|
+
throw new Error('Invalid token: token is empty');
|
|
480
|
+
}
|
|
481
|
+
const staging = (await this.ctx.ctx.storage.get(`${STAGING_PREFIX}${token}`));
|
|
482
|
+
if (!staging) {
|
|
483
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Invalid or not found' });
|
|
484
|
+
throw new Error('Invalid or not found: staging token');
|
|
485
|
+
}
|
|
486
|
+
if (staging.status === 'committed') {
|
|
487
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Already committed' });
|
|
488
|
+
throw new Error('Clone already committed');
|
|
489
|
+
}
|
|
490
|
+
if (staging.status === 'aborted') {
|
|
491
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Already aborted' });
|
|
492
|
+
throw new Error('Clone was aborted');
|
|
493
|
+
}
|
|
494
|
+
if (new Date(staging.expiresAt) < new Date()) {
|
|
495
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Token expired' });
|
|
496
|
+
throw new Error('Token expired');
|
|
497
|
+
}
|
|
498
|
+
// Verify integrity
|
|
499
|
+
const currentHash = this.computeStagingIntegrityHash(staging.things);
|
|
500
|
+
if (currentHash !== staging.integrityHash) {
|
|
501
|
+
await this.ctx.emitEvent('clone.staging.corrupted', { token, target: staging.targetNs });
|
|
502
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Integrity check failed' });
|
|
503
|
+
throw new Error('Staging data corrupted: integrity check failed');
|
|
504
|
+
}
|
|
505
|
+
// Validate checkpoints
|
|
506
|
+
const checkpointKeys = await this.ctx.ctx.storage.list({ prefix: `${CHECKPOINT_PREFIX}${token}:` });
|
|
507
|
+
for (const [, value] of checkpointKeys) {
|
|
508
|
+
const checkpoint = value;
|
|
509
|
+
const expectedChecksum = this.computeCheckpointChecksum(checkpoint.state);
|
|
510
|
+
if (checkpoint.checksum !== expectedChecksum) {
|
|
511
|
+
await this.ctx.emitEvent('clone.commit.failed', { token, reason: 'Checkpoint validation failed' });
|
|
512
|
+
throw new Error('Checkpoint validation failed: checksum mismatch');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
await this.ctx.emitEvent('clone.commit.started', { token, target: staging.targetNs });
|
|
516
|
+
// Persist coordinator decision
|
|
517
|
+
const commitDecision = {
|
|
518
|
+
decision: 'commit',
|
|
519
|
+
startedAt: new Date(),
|
|
520
|
+
completedAt: null,
|
|
521
|
+
};
|
|
522
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}decision:${token}`, commitDecision);
|
|
523
|
+
// Broadcast commit
|
|
524
|
+
for (const callback of this._broadcastCallbacks) {
|
|
525
|
+
callback('commit', staging.targetNs);
|
|
526
|
+
}
|
|
527
|
+
// Update audit log
|
|
528
|
+
const auditLog = (await this.ctx.ctx.storage.get(`${TWO_PC_PREFIX}audit:${token}`));
|
|
529
|
+
if (auditLog) {
|
|
530
|
+
auditLog.events.push({ type: 'commit', status: 'started', timestamp: new Date() }, { type: 'broadcast', status: 'completed', timestamp: new Date(), participant: staging.targetNs });
|
|
531
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}audit:${token}`, auditLog);
|
|
532
|
+
}
|
|
533
|
+
// Create target DO and transfer state
|
|
534
|
+
if (!this.ctx.env.DO) {
|
|
535
|
+
throw new Error('DO namespace not configured');
|
|
536
|
+
}
|
|
537
|
+
const doId = this.ctx.env.DO.idFromName(staging.targetNs);
|
|
538
|
+
const stub = this.ctx.env.DO.get(doId);
|
|
539
|
+
await stub.fetch(new Request(`https://${staging.targetNs}/init`, {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
body: JSON.stringify({ things: staging.things }),
|
|
542
|
+
}));
|
|
543
|
+
const committedAt = new Date();
|
|
544
|
+
commitDecision.completedAt = committedAt;
|
|
545
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}decision:${token}`, commitDecision);
|
|
546
|
+
// Store tombstone
|
|
547
|
+
const tombstone = { ...staging, things: [], status: 'committed' };
|
|
548
|
+
await this.ctx.ctx.storage.put(`${STAGING_PREFIX}${token}`, tombstone);
|
|
549
|
+
// Clean up checkpoints
|
|
550
|
+
for (const [key] of checkpointKeys) {
|
|
551
|
+
await this.ctx.ctx.storage.delete(key);
|
|
552
|
+
}
|
|
553
|
+
// Update participant history
|
|
554
|
+
const historyKey = `${TWO_PC_PREFIX}history:${token}:${staging.targetNs}`;
|
|
555
|
+
const history = (await this.ctx.ctx.storage.get(historyKey));
|
|
556
|
+
if (history) {
|
|
557
|
+
history.transitions.push({ from: 'prepared', to: 'committed', timestamp: committedAt });
|
|
558
|
+
await this.ctx.ctx.storage.put(historyKey, history);
|
|
559
|
+
}
|
|
560
|
+
// Update audit log
|
|
561
|
+
if (auditLog) {
|
|
562
|
+
auditLog.events.push({ type: 'commit', status: 'completed', timestamp: committedAt }, { type: 'participant_ack', status: 'completed', timestamp: committedAt, participant: staging.targetNs });
|
|
563
|
+
auditLog.outcome = 'committed';
|
|
564
|
+
await this.ctx.ctx.storage.put(`${TWO_PC_PREFIX}audit:${token}`, auditLog);
|
|
565
|
+
}
|
|
566
|
+
await this.ctx.emitEvent('clone.committed', {
|
|
567
|
+
token,
|
|
568
|
+
target: staging.targetNs,
|
|
569
|
+
result: { ns: staging.targetNs, doId: doId.toString(), mode: 'staged' },
|
|
570
|
+
});
|
|
571
|
+
const participantAcks = [
|
|
572
|
+
{ target: staging.targetNs, status: 'committed', timestamp: committedAt },
|
|
573
|
+
];
|
|
574
|
+
return {
|
|
575
|
+
phase: 'committed',
|
|
576
|
+
result: {
|
|
577
|
+
ns: staging.targetNs,
|
|
578
|
+
doId: doId.toString(),
|
|
579
|
+
mode: 'staged',
|
|
580
|
+
staged: { prepareId: token, committed: true },
|
|
581
|
+
},
|
|
582
|
+
committedAt,
|
|
583
|
+
participantAcks,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Abort a staged clone
|
|
588
|
+
*/
|
|
589
|
+
async abortClone(token, reason) {
|
|
590
|
+
const staging = (await this.ctx.ctx.storage.get(`${STAGING_PREFIX}${token}`));
|
|
591
|
+
const abortedAt = new Date();
|
|
592
|
+
if (staging) {
|
|
593
|
+
const tombstone = { ...staging, things: [], status: 'aborted' };
|
|
594
|
+
await this.ctx.ctx.storage.put(`${STAGING_PREFIX}${token}`, tombstone);
|
|
595
|
+
const checkpointKeys = await this.ctx.ctx.storage.list({ prefix: `${CHECKPOINT_PREFIX}${token}:` });
|
|
596
|
+
for (const [key] of checkpointKeys) {
|
|
597
|
+
await this.ctx.ctx.storage.delete(key);
|
|
598
|
+
}
|
|
599
|
+
await this.ctx.emitEvent('clone.aborted', { token, target: staging.targetNs, reason });
|
|
600
|
+
}
|
|
601
|
+
return { phase: 'aborted', token, reason, abortedAt };
|
|
602
|
+
}
|
|
603
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
604
|
+
// STAGED CLONE HELPERS
|
|
605
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
606
|
+
createStagedCheckpoint(cloneId, sequence, itemsProcessed, totalItems, clonedThingIds, clonedRelationshipIds, branch, lastVersion, validated) {
|
|
607
|
+
const state = { clonedThingIds, clonedRelationshipIds, branch, lastVersion };
|
|
608
|
+
const checksum = this.computeCheckpointChecksum(state);
|
|
609
|
+
return {
|
|
610
|
+
id: `cp-${cloneId.slice(0, 8)}-${sequence}`,
|
|
611
|
+
cloneId,
|
|
612
|
+
sequence,
|
|
613
|
+
itemsProcessed,
|
|
614
|
+
totalItems,
|
|
615
|
+
createdAt: new Date(),
|
|
616
|
+
checksum,
|
|
617
|
+
state,
|
|
618
|
+
validated,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
computeCheckpointChecksum(state) {
|
|
622
|
+
const content = JSON.stringify(state);
|
|
623
|
+
let hash = 0;
|
|
624
|
+
for (let i = 0; i < content.length; i++) {
|
|
625
|
+
const char = content.charCodeAt(i);
|
|
626
|
+
hash = ((hash << 5) - hash) + char;
|
|
627
|
+
hash = hash & hash;
|
|
628
|
+
}
|
|
629
|
+
return Math.abs(hash).toString(36);
|
|
630
|
+
}
|
|
631
|
+
computeStagingIntegrityHash(things) {
|
|
632
|
+
const content = JSON.stringify(things);
|
|
633
|
+
let hash = 0;
|
|
634
|
+
for (let i = 0; i < content.length; i++) {
|
|
635
|
+
const char = content.charCodeAt(i);
|
|
636
|
+
hash = ((hash << 5) - hash) + char;
|
|
637
|
+
hash = hash & hash;
|
|
638
|
+
}
|
|
639
|
+
return Math.abs(hash).toString(36);
|
|
640
|
+
}
|
|
641
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
642
|
+
// STAGING STATUS METHODS
|
|
643
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
644
|
+
async getStagingStatus(stagingNs) {
|
|
645
|
+
const allStagings = await this.ctx.ctx.storage.list({ prefix: STAGING_PREFIX });
|
|
646
|
+
for (const [key, value] of allStagings) {
|
|
647
|
+
const staging = value;
|
|
648
|
+
if (staging.stagingNs === stagingNs) {
|
|
649
|
+
if (staging.status === 'aborted' || staging.status === 'committed') {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const token = key.replace(STAGING_PREFIX, '');
|
|
653
|
+
return {
|
|
654
|
+
exists: true,
|
|
655
|
+
status: staging.status === 'prepared' ? 'ready' : staging.status,
|
|
656
|
+
token,
|
|
657
|
+
createdAt: new Date(staging.createdAt),
|
|
658
|
+
expiresAt: new Date(staging.expiresAt),
|
|
659
|
+
integrityHash: staging.integrityHash,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
async getCloneTokenStatus(token) {
|
|
666
|
+
const staging = (await this.ctx.ctx.storage.get(`${STAGING_PREFIX}${token}`));
|
|
667
|
+
if (!staging) {
|
|
668
|
+
return { valid: false, status: 'not_found' };
|
|
669
|
+
}
|
|
670
|
+
const expiresAt = new Date(staging.expiresAt);
|
|
671
|
+
if (expiresAt < new Date()) {
|
|
672
|
+
return { valid: false, status: 'expired', expiresAt };
|
|
673
|
+
}
|
|
674
|
+
return { valid: staging.status === 'prepared', status: staging.status, expiresAt };
|
|
675
|
+
}
|
|
676
|
+
async getCloneCheckpoints(token) {
|
|
677
|
+
const checkpointKeys = await this.ctx.ctx.storage.list({ prefix: `${CHECKPOINT_PREFIX}${token}:` });
|
|
678
|
+
const checkpoints = [];
|
|
679
|
+
for (const [, value] of checkpointKeys) {
|
|
680
|
+
checkpoints.push(value);
|
|
681
|
+
}
|
|
682
|
+
checkpoints.sort((a, b) => a.sequence - b.sequence);
|
|
683
|
+
return checkpoints;
|
|
684
|
+
}
|
|
685
|
+
async validateCheckpoint(checkpointId) {
|
|
686
|
+
const allCheckpoints = await this.ctx.ctx.storage.list({ prefix: CHECKPOINT_PREFIX });
|
|
687
|
+
for (const [, value] of allCheckpoints) {
|
|
688
|
+
const checkpoint = value;
|
|
689
|
+
if (checkpoint.id === checkpointId) {
|
|
690
|
+
const expectedChecksum = this.computeCheckpointChecksum(checkpoint.state);
|
|
691
|
+
if (checkpoint.checksum === expectedChecksum) {
|
|
692
|
+
return { valid: true };
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
return { valid: false, error: 'Checksum mismatch' };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return { valid: false, error: 'Checkpoint not found' };
|
|
700
|
+
}
|
|
701
|
+
async resumeCloneFromCheckpoint(checkpointId) {
|
|
702
|
+
const allCheckpoints = await this.ctx.ctx.storage.list({ prefix: CHECKPOINT_PREFIX });
|
|
703
|
+
let foundCheckpoint = null;
|
|
704
|
+
let originalToken = null;
|
|
705
|
+
for (const [key, value] of allCheckpoints) {
|
|
706
|
+
const checkpoint = value;
|
|
707
|
+
if (checkpoint.id === checkpointId) {
|
|
708
|
+
foundCheckpoint = checkpoint;
|
|
709
|
+
const parts = key.split(':');
|
|
710
|
+
if (parts.length >= 2) {
|
|
711
|
+
originalToken = parts[1] ?? null;
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (!foundCheckpoint || !originalToken) {
|
|
717
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
718
|
+
}
|
|
719
|
+
const staging = (await this.ctx.ctx.storage.get(`${STAGING_PREFIX}${originalToken}`));
|
|
720
|
+
if (!staging) {
|
|
721
|
+
throw new Error(`Original staging data not found for checkpoint`);
|
|
722
|
+
}
|
|
723
|
+
const newToken = crypto.randomUUID();
|
|
724
|
+
const expiresAt = new Date(Date.now() + DEFAULT_TOKEN_TIMEOUT);
|
|
725
|
+
const stagingNs = `${staging.targetNs}-staging-${newToken.slice(0, 8)}`;
|
|
726
|
+
const newStagingData = {
|
|
727
|
+
...staging,
|
|
728
|
+
stagingNs,
|
|
729
|
+
expiresAt: expiresAt.toISOString(),
|
|
730
|
+
status: 'prepared',
|
|
731
|
+
createdAt: new Date().toISOString(),
|
|
732
|
+
};
|
|
733
|
+
await this.ctx.ctx.storage.put(`${STAGING_PREFIX}${newToken}`, newStagingData);
|
|
734
|
+
const originalCheckpoints = await this.getCloneCheckpoints(originalToken);
|
|
735
|
+
for (const cp of originalCheckpoints) {
|
|
736
|
+
const newCheckpoint = { ...cp, cloneId: newToken };
|
|
737
|
+
await this.ctx.ctx.storage.put(`${CHECKPOINT_PREFIX}${newToken}:${cp.id}`, newCheckpoint);
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
phase: 'prepared',
|
|
741
|
+
token: newToken,
|
|
742
|
+
expiresAt,
|
|
743
|
+
stagingNs,
|
|
744
|
+
metadata: staging.metadata,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async gcStagingAreas() {
|
|
748
|
+
let cleaned = 0;
|
|
749
|
+
let checkpointsCleaned = 0;
|
|
750
|
+
const now = new Date();
|
|
751
|
+
const allStagings = await this.ctx.ctx.storage.list({ prefix: STAGING_PREFIX });
|
|
752
|
+
for (const [key, value] of allStagings) {
|
|
753
|
+
const staging = value;
|
|
754
|
+
const expiresAt = new Date(staging.expiresAt);
|
|
755
|
+
if (expiresAt < now) {
|
|
756
|
+
const token = key.replace(STAGING_PREFIX, '');
|
|
757
|
+
await this.ctx.emitEvent('clone.expired', { token, target: staging.targetNs });
|
|
758
|
+
await this.ctx.ctx.storage.delete(key);
|
|
759
|
+
cleaned++;
|
|
760
|
+
const checkpointKeys = await this.ctx.ctx.storage.list({ prefix: `${CHECKPOINT_PREFIX}${token}:` });
|
|
761
|
+
for (const [cpKey] of checkpointKeys) {
|
|
762
|
+
await this.ctx.ctx.storage.delete(cpKey);
|
|
763
|
+
checkpointsCleaned++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return { cleaned, checkpointsCleaned };
|
|
768
|
+
}
|
|
769
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
770
|
+
// EVENTUAL CLONE OPERATIONS
|
|
771
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
772
|
+
async initiateEventualClone(target, options) {
|
|
773
|
+
try {
|
|
774
|
+
new URL(target);
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
778
|
+
}
|
|
779
|
+
const id = crypto.randomUUID();
|
|
780
|
+
const things = await this.ctx.db.select().from(schema.things);
|
|
781
|
+
const cloneBranch = options?.branch || this.ctx.currentBranch;
|
|
782
|
+
const branchFilter = cloneBranch === 'main' ? null : cloneBranch;
|
|
783
|
+
const branchThings = things.filter((t) => t.branch === branchFilter && !t.deleted);
|
|
784
|
+
const totalItems = branchThings.length;
|
|
785
|
+
const eventualOptions = options;
|
|
786
|
+
const syncInterval = eventualOptions?.syncInterval ?? 5000;
|
|
787
|
+
const maxDivergence = eventualOptions?.maxDivergence ?? 100;
|
|
788
|
+
const conflictResolution = eventualOptions?.conflictResolution ?? 'last-write-wins';
|
|
789
|
+
const chunked = eventualOptions?.chunked ?? false;
|
|
790
|
+
const chunkSize = eventualOptions?.chunkSize ?? 1000;
|
|
791
|
+
const rateLimit = eventualOptions?.rateLimit ?? null;
|
|
792
|
+
const customResolver = eventualOptions?.conflictResolver;
|
|
793
|
+
if (customResolver) {
|
|
794
|
+
this._conflictResolvers.set(id, customResolver);
|
|
795
|
+
}
|
|
796
|
+
const state = {
|
|
797
|
+
id,
|
|
798
|
+
targetNs: target,
|
|
799
|
+
status: 'pending',
|
|
800
|
+
progress: 0,
|
|
801
|
+
phase: 'initial',
|
|
802
|
+
itemsSynced: 0,
|
|
803
|
+
totalItems,
|
|
804
|
+
itemsRemaining: totalItems,
|
|
805
|
+
lastSyncAt: null,
|
|
806
|
+
divergence: totalItems,
|
|
807
|
+
maxDivergence,
|
|
808
|
+
syncInterval,
|
|
809
|
+
errorCount: 0,
|
|
810
|
+
lastError: null,
|
|
811
|
+
conflictResolution: customResolver ? 'custom' : conflictResolution,
|
|
812
|
+
hasCustomResolver: !!customResolver,
|
|
813
|
+
chunked,
|
|
814
|
+
chunkSize,
|
|
815
|
+
rateLimit,
|
|
816
|
+
createdAt: new Date().toISOString(),
|
|
817
|
+
updatedAt: new Date().toISOString(),
|
|
818
|
+
lastSyncedVersion: 0,
|
|
819
|
+
};
|
|
820
|
+
await this.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
821
|
+
await this.ctx.emitEvent('clone.initiated', { id, target, mode: 'eventual' });
|
|
822
|
+
// Schedule initial sync
|
|
823
|
+
const currentAlarm = await this.ctx.ctx.storage.getAlarm();
|
|
824
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
825
|
+
await this.ctx.ctx.storage.setAlarm(Date.now() + 100);
|
|
826
|
+
}
|
|
827
|
+
return this.createEventualHandle(id, state);
|
|
828
|
+
}
|
|
829
|
+
createEventualHandle(id, initialState) {
|
|
830
|
+
const self = this;
|
|
831
|
+
let currentStatus = initialState.status;
|
|
832
|
+
const handle = {
|
|
833
|
+
id,
|
|
834
|
+
get status() {
|
|
835
|
+
return currentStatus;
|
|
836
|
+
},
|
|
837
|
+
async getProgress() {
|
|
838
|
+
const state = await self.getEventualCloneState(id);
|
|
839
|
+
if (state)
|
|
840
|
+
currentStatus = state.status;
|
|
841
|
+
return state?.progress ?? 0;
|
|
842
|
+
},
|
|
843
|
+
async getSyncStatus() {
|
|
844
|
+
const state = await self.getEventualCloneState(id);
|
|
845
|
+
if (state)
|
|
846
|
+
currentStatus = state.status;
|
|
847
|
+
return {
|
|
848
|
+
phase: state?.phase ?? 'initial',
|
|
849
|
+
itemsSynced: state?.itemsSynced ?? 0,
|
|
850
|
+
totalItems: state?.totalItems ?? 0,
|
|
851
|
+
lastSyncAt: state?.lastSyncAt ? new Date(state.lastSyncAt) : null,
|
|
852
|
+
divergence: state?.divergence ?? 0,
|
|
853
|
+
maxDivergence: state?.maxDivergence ?? 100,
|
|
854
|
+
syncInterval: state?.syncInterval ?? 5000,
|
|
855
|
+
errorCount: state?.errorCount ?? 0,
|
|
856
|
+
lastError: state?.lastError ? new Error(state.lastError) : null,
|
|
857
|
+
};
|
|
858
|
+
},
|
|
859
|
+
async pause() {
|
|
860
|
+
const state = await self.getEventualCloneState(id);
|
|
861
|
+
if (!state)
|
|
862
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
863
|
+
state.status = 'paused';
|
|
864
|
+
state.updatedAt = new Date().toISOString();
|
|
865
|
+
await self.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
866
|
+
currentStatus = 'paused';
|
|
867
|
+
await self.ctx.emitEvent('clone.paused', { id });
|
|
868
|
+
},
|
|
869
|
+
async resume() {
|
|
870
|
+
const state = await self.getEventualCloneState(id);
|
|
871
|
+
if (!state)
|
|
872
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
873
|
+
state.status = state.phase === 'delta' || state.phase === 'catchup' ? 'active' : 'syncing';
|
|
874
|
+
state.updatedAt = new Date().toISOString();
|
|
875
|
+
await self.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
876
|
+
currentStatus = state.status;
|
|
877
|
+
await self.ctx.emitEvent('clone.resumed', { id });
|
|
878
|
+
const currentAlarm = await self.ctx.ctx.storage.getAlarm();
|
|
879
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
880
|
+
await self.ctx.ctx.storage.setAlarm(Date.now() + 100);
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
async sync() {
|
|
884
|
+
return self.performEventualSync(id);
|
|
885
|
+
},
|
|
886
|
+
async cancel() {
|
|
887
|
+
const state = await self.getEventualCloneState(id);
|
|
888
|
+
if (!state)
|
|
889
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
890
|
+
state.status = 'cancelled';
|
|
891
|
+
state.updatedAt = new Date().toISOString();
|
|
892
|
+
await self.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
893
|
+
currentStatus = 'cancelled';
|
|
894
|
+
await self.ctx.emitEvent('clone.cancelled', { id });
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
return handle;
|
|
898
|
+
}
|
|
899
|
+
async getEventualCloneState(id) {
|
|
900
|
+
return (await this.ctx.ctx.storage.get(`eventual:${id}`));
|
|
901
|
+
}
|
|
902
|
+
async performEventualSync(id) {
|
|
903
|
+
const startTime = Date.now();
|
|
904
|
+
const state = await this.getEventualCloneState(id);
|
|
905
|
+
if (!state)
|
|
906
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
907
|
+
if (state.status === 'cancelled' || state.status === 'paused') {
|
|
908
|
+
return { itemsSynced: 0, duration: 0, conflicts: [] };
|
|
909
|
+
}
|
|
910
|
+
const conflicts = [];
|
|
911
|
+
let itemsSynced = 0;
|
|
912
|
+
try {
|
|
913
|
+
if (state.status === 'pending') {
|
|
914
|
+
state.status = 'syncing';
|
|
915
|
+
state.phase = 'bulk';
|
|
916
|
+
await this.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
917
|
+
await this.ctx.emitEvent('clone.syncing', { id, progress: state.progress });
|
|
918
|
+
}
|
|
919
|
+
const things = await this.ctx.db.select().from(schema.things);
|
|
920
|
+
const branchThings = things.filter((t) => (t.branch === null || t.branch === this.ctx.currentBranch) && !t.deleted);
|
|
921
|
+
const latestVersions = new Map();
|
|
922
|
+
for (const thing of branchThings) {
|
|
923
|
+
latestVersions.set(thing.id, thing);
|
|
924
|
+
}
|
|
925
|
+
let itemsToSync = [];
|
|
926
|
+
if (state.phase === 'bulk' || state.phase === 'initial') {
|
|
927
|
+
itemsToSync = Array.from(latestVersions.values());
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
itemsToSync = Array.from(latestVersions.values()).filter((_, idx) => idx >= state.lastSyncedVersion);
|
|
931
|
+
}
|
|
932
|
+
if (state.chunked && itemsToSync.length > state.chunkSize) {
|
|
933
|
+
itemsToSync = itemsToSync.slice(0, state.chunkSize);
|
|
934
|
+
}
|
|
935
|
+
if (state.rateLimit && itemsToSync.length > state.rateLimit) {
|
|
936
|
+
itemsToSync = itemsToSync.slice(0, state.rateLimit);
|
|
937
|
+
}
|
|
938
|
+
if (this.ctx.env.DO && itemsToSync.length > 0) {
|
|
939
|
+
const doId = this.ctx.env.DO.idFromName(state.targetNs);
|
|
940
|
+
const stub = this.ctx.env.DO.get(doId);
|
|
941
|
+
const response = await stub.fetch(new Request(`https://${state.targetNs}/sync`, {
|
|
942
|
+
method: 'POST',
|
|
943
|
+
body: JSON.stringify({
|
|
944
|
+
cloneId: id,
|
|
945
|
+
things: itemsToSync.map((t) => ({
|
|
946
|
+
id: t.id,
|
|
947
|
+
type: t.type,
|
|
948
|
+
branch: null,
|
|
949
|
+
name: t.name,
|
|
950
|
+
data: t.data,
|
|
951
|
+
deleted: false,
|
|
952
|
+
version: things.indexOf(t) + 1,
|
|
953
|
+
})),
|
|
954
|
+
}),
|
|
955
|
+
}));
|
|
956
|
+
if (response.ok) {
|
|
957
|
+
itemsSynced = itemsToSync.length;
|
|
958
|
+
try {
|
|
959
|
+
const responseData = (await response.json());
|
|
960
|
+
if (responseData.conflicts && Array.isArray(responseData.conflicts)) {
|
|
961
|
+
for (const conflict of responseData.conflicts) {
|
|
962
|
+
const resolution = state.hasCustomResolver ? 'custom' : state.conflictResolution;
|
|
963
|
+
const conflictInfo = {
|
|
964
|
+
thingId: conflict.thingId,
|
|
965
|
+
sourceVersion: conflict.sourceVersion,
|
|
966
|
+
targetVersion: conflict.targetVersion,
|
|
967
|
+
resolution,
|
|
968
|
+
resolvedAt: new Date(),
|
|
969
|
+
};
|
|
970
|
+
conflicts.push(conflictInfo);
|
|
971
|
+
await this.ctx.emitEvent('clone.conflict', { id, ...conflictInfo });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
// Response may not be JSON
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// Update state
|
|
981
|
+
state.itemsSynced += itemsSynced;
|
|
982
|
+
state.lastSyncedVersion += itemsSynced;
|
|
983
|
+
state.lastSyncAt = new Date().toISOString();
|
|
984
|
+
state.itemsRemaining = Math.max(0, state.totalItems - state.itemsSynced);
|
|
985
|
+
state.progress = state.totalItems > 0 ? Math.floor((state.itemsSynced / state.totalItems) * 100) : 100;
|
|
986
|
+
state.divergence = state.itemsRemaining;
|
|
987
|
+
state.errorCount = 0;
|
|
988
|
+
state.lastError = null;
|
|
989
|
+
state.updatedAt = new Date().toISOString();
|
|
990
|
+
await this.ctx.emitEvent('clone.progress', {
|
|
991
|
+
id,
|
|
992
|
+
progress: state.progress,
|
|
993
|
+
itemsSynced: state.itemsSynced,
|
|
994
|
+
totalItems: state.totalItems,
|
|
995
|
+
phase: state.phase,
|
|
996
|
+
});
|
|
997
|
+
if (state.progress >= 100) {
|
|
998
|
+
state.status = 'active';
|
|
999
|
+
state.phase = 'delta';
|
|
1000
|
+
await this.ctx.emitEvent('clone.active', { id, target: state.targetNs });
|
|
1001
|
+
}
|
|
1002
|
+
else if (state.itemsSynced > 0 && state.phase === 'bulk') {
|
|
1003
|
+
state.phase = state.progress >= 80 ? 'catchup' : 'bulk';
|
|
1004
|
+
}
|
|
1005
|
+
await this.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
1006
|
+
const duration = Date.now() - startTime;
|
|
1007
|
+
await this.ctx.emitEvent('clone.sync.completed', { id, itemsSynced, duration });
|
|
1008
|
+
return { itemsSynced, duration, conflicts };
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
state.errorCount++;
|
|
1012
|
+
state.lastError = error.message;
|
|
1013
|
+
state.updatedAt = new Date().toISOString();
|
|
1014
|
+
if (state.errorCount >= 10) {
|
|
1015
|
+
state.status = 'error';
|
|
1016
|
+
await this.ctx.emitEvent('clone.error', { id, error: state.lastError });
|
|
1017
|
+
}
|
|
1018
|
+
await this.ctx.ctx.storage.put(`eventual:${id}`, state);
|
|
1019
|
+
return { itemsSynced: 0, duration: Date.now() - startTime, conflicts: [] };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async handleEventualCloneAlarms() {
|
|
1023
|
+
const keys = await this.ctx.ctx.storage.list({ prefix: 'eventual:' });
|
|
1024
|
+
let nextAlarmTime = null;
|
|
1025
|
+
for (const [, value] of keys) {
|
|
1026
|
+
const state = value;
|
|
1027
|
+
if (state.status === 'cancelled' || state.status === 'paused' || state.status === 'error') {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const lastSync = state.lastSyncAt ? new Date(state.lastSyncAt).getTime() : 0;
|
|
1031
|
+
const now = Date.now();
|
|
1032
|
+
const nextSync = lastSync + state.syncInterval;
|
|
1033
|
+
const needsSync = now >= nextSync || state.divergence > state.maxDivergence;
|
|
1034
|
+
if (needsSync || state.status === 'pending') {
|
|
1035
|
+
await this.performEventualSync(state.id);
|
|
1036
|
+
const updatedState = await this.getEventualCloneState(state.id);
|
|
1037
|
+
if (updatedState &&
|
|
1038
|
+
updatedState.status !== 'active' &&
|
|
1039
|
+
updatedState.status !== 'cancelled' &&
|
|
1040
|
+
updatedState.status !== 'error') {
|
|
1041
|
+
const nextSyncTime = Date.now() + updatedState.syncInterval;
|
|
1042
|
+
if (!nextAlarmTime || nextSyncTime < nextAlarmTime) {
|
|
1043
|
+
nextAlarmTime = nextSyncTime;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
if (!nextAlarmTime || nextSync < nextAlarmTime) {
|
|
1049
|
+
nextAlarmTime = nextSync;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (nextAlarmTime) {
|
|
1054
|
+
await this.ctx.ctx.storage.setAlarm(nextAlarmTime);
|
|
1055
|
+
}
|
|
1056
|
+
await this.processResumableClones();
|
|
1057
|
+
}
|
|
1058
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1059
|
+
// RESUMABLE CLONE OPERATIONS
|
|
1060
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1061
|
+
async initiateResumableClone(target, options) {
|
|
1062
|
+
const batchSize = options?.batchSize || 100;
|
|
1063
|
+
const checkpointInterval = options?.checkpointInterval || 1;
|
|
1064
|
+
const maxRetries = options?.maxRetries || 3;
|
|
1065
|
+
const retryDelay = options?.retryDelay || 1000;
|
|
1066
|
+
const lockTimeout = options?.lockTimeout || 300000;
|
|
1067
|
+
const checkpointRetentionMs = options?.checkpointRetentionMs || 3600000;
|
|
1068
|
+
const compress = options?.compress || false;
|
|
1069
|
+
const maxBandwidth = options?.maxBandwidth;
|
|
1070
|
+
try {
|
|
1071
|
+
new URL(target);
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
1075
|
+
}
|
|
1076
|
+
// Check for existing lock
|
|
1077
|
+
const existingLock = this._cloneLocks.get(target) ||
|
|
1078
|
+
(await this.ctx.ctx.storage.get(`clone-lock:${target}`));
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
if (existingLock && !existingLock.isStale && new Date(existingLock.expiresAt).getTime() > now) {
|
|
1081
|
+
if (!options?.forceLock) {
|
|
1082
|
+
throw new Error(`Clone operation already in progress for target: ${target}`);
|
|
1083
|
+
}
|
|
1084
|
+
await this.releaseCloneLock(target, existingLock.cloneId);
|
|
1085
|
+
}
|
|
1086
|
+
let state;
|
|
1087
|
+
let cloneId;
|
|
1088
|
+
if (options?.resumeFrom) {
|
|
1089
|
+
const existingState = await this.findResumableStateFromCheckpoint(options.resumeFrom);
|
|
1090
|
+
if (!existingState) {
|
|
1091
|
+
throw new Error(`Checkpoint not found: ${options.resumeFrom}`);
|
|
1092
|
+
}
|
|
1093
|
+
state = existingState;
|
|
1094
|
+
cloneId = state.id;
|
|
1095
|
+
state.status = 'transferring';
|
|
1096
|
+
state.pauseRequested = false;
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
cloneId = crypto.randomUUID();
|
|
1100
|
+
state = {
|
|
1101
|
+
id: cloneId,
|
|
1102
|
+
targetNs: target,
|
|
1103
|
+
status: 'initializing',
|
|
1104
|
+
checkpoints: [],
|
|
1105
|
+
position: 0,
|
|
1106
|
+
batchSize,
|
|
1107
|
+
checkpointInterval,
|
|
1108
|
+
maxRetries,
|
|
1109
|
+
retryDelay,
|
|
1110
|
+
retryCount: 0,
|
|
1111
|
+
compress,
|
|
1112
|
+
maxBandwidth,
|
|
1113
|
+
checkpointRetentionMs,
|
|
1114
|
+
pauseRequested: false,
|
|
1115
|
+
cancelRequested: false,
|
|
1116
|
+
createdAt: new Date(),
|
|
1117
|
+
bytesTransferred: 0,
|
|
1118
|
+
totalBytes: 0,
|
|
1119
|
+
startedAt: null,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
// Acquire lock
|
|
1123
|
+
const lockId = crypto.randomUUID();
|
|
1124
|
+
const lock = {
|
|
1125
|
+
lockId,
|
|
1126
|
+
cloneId,
|
|
1127
|
+
target,
|
|
1128
|
+
acquiredAt: new Date(),
|
|
1129
|
+
expiresAt: new Date(now + lockTimeout),
|
|
1130
|
+
isStale: false,
|
|
1131
|
+
};
|
|
1132
|
+
this._cloneLocks.set(target, lock);
|
|
1133
|
+
await this.ctx.ctx.storage.put(`clone-lock:${target}`, lock);
|
|
1134
|
+
await this.ctx.emitEvent('clone.lock.acquired', { lockId, target, cloneId });
|
|
1135
|
+
this._resumableClones.set(cloneId, state);
|
|
1136
|
+
await this.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1137
|
+
this.cleanupOrphanedCheckpoints(checkpointRetentionMs).catch(() => { });
|
|
1138
|
+
const currentAlarm = await this.ctx.ctx.storage.getAlarm();
|
|
1139
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
1140
|
+
await this.ctx.ctx.storage.setAlarm(Date.now() + 100);
|
|
1141
|
+
}
|
|
1142
|
+
return this.createResumableCloneHandle(cloneId);
|
|
1143
|
+
}
|
|
1144
|
+
createResumableCloneHandle(cloneId) {
|
|
1145
|
+
const self = this;
|
|
1146
|
+
const handle = {
|
|
1147
|
+
id: cloneId,
|
|
1148
|
+
get status() {
|
|
1149
|
+
const state = self._resumableClones.get(cloneId);
|
|
1150
|
+
return state?.status || 'failed';
|
|
1151
|
+
},
|
|
1152
|
+
get checkpoints() {
|
|
1153
|
+
const state = self._resumableClones.get(cloneId);
|
|
1154
|
+
return state?.checkpoints || [];
|
|
1155
|
+
},
|
|
1156
|
+
async getProgress() {
|
|
1157
|
+
const state = await self.getResumableState(cloneId);
|
|
1158
|
+
return state?.progress || 0;
|
|
1159
|
+
},
|
|
1160
|
+
async pause() {
|
|
1161
|
+
const state = await self.getResumableState(cloneId);
|
|
1162
|
+
if (!state)
|
|
1163
|
+
throw new Error('Clone not found');
|
|
1164
|
+
state.pauseRequested = true;
|
|
1165
|
+
state.status = 'paused';
|
|
1166
|
+
await self.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1167
|
+
self._resumableClones.set(cloneId, state);
|
|
1168
|
+
const lastCheckpoint = state.checkpoints[state.checkpoints.length - 1];
|
|
1169
|
+
await self.ctx.emitEvent('clone.paused', { id: cloneId, checkpoint: lastCheckpoint, progress: state.progress });
|
|
1170
|
+
},
|
|
1171
|
+
async resume() {
|
|
1172
|
+
const state = await self.getResumableState(cloneId);
|
|
1173
|
+
if (!state)
|
|
1174
|
+
throw new Error('Clone not found');
|
|
1175
|
+
state.pauseRequested = false;
|
|
1176
|
+
state.status = 'transferring';
|
|
1177
|
+
await self.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1178
|
+
self._resumableClones.set(cloneId, state);
|
|
1179
|
+
const lastCheckpoint = state.checkpoints[state.checkpoints.length - 1];
|
|
1180
|
+
await self.ctx.emitEvent('clone.resumed', { id: cloneId, fromCheckpoint: lastCheckpoint });
|
|
1181
|
+
const currentAlarm = await self.ctx.ctx.storage.getAlarm();
|
|
1182
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
1183
|
+
await self.ctx.ctx.storage.setAlarm(Date.now() + 100);
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
async cancel() {
|
|
1187
|
+
const state = await self.getResumableState(cloneId);
|
|
1188
|
+
if (!state)
|
|
1189
|
+
throw new Error('Clone not found');
|
|
1190
|
+
const progress = state.progress || 0;
|
|
1191
|
+
const checkpointsCreated = state.checkpoints.length;
|
|
1192
|
+
state.cancelRequested = true;
|
|
1193
|
+
state.status = 'cancelled';
|
|
1194
|
+
await self.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1195
|
+
self._resumableClones.set(cloneId, state);
|
|
1196
|
+
await self.releaseCloneLock(state.targetNs, cloneId);
|
|
1197
|
+
await self.cleanupCloneCheckpoints(cloneId);
|
|
1198
|
+
await self.ctx.emitEvent('clone.cancelled', { id: cloneId, progress, checkpointsCreated });
|
|
1199
|
+
},
|
|
1200
|
+
async waitForCheckpoint() {
|
|
1201
|
+
const pollInterval = 50;
|
|
1202
|
+
const maxWait = 60000;
|
|
1203
|
+
const startTime = Date.now();
|
|
1204
|
+
return new Promise((resolve, reject) => {
|
|
1205
|
+
const poll = async () => {
|
|
1206
|
+
const state = await self.getResumableState(cloneId);
|
|
1207
|
+
if (!state) {
|
|
1208
|
+
reject(new Error('Clone not found'));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (state.checkpoints.length > 0) {
|
|
1212
|
+
resolve(state.checkpoints[state.checkpoints.length - 1]);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (Date.now() - startTime > maxWait) {
|
|
1216
|
+
reject(new Error('Timeout waiting for checkpoint'));
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
setTimeout(poll, pollInterval);
|
|
1220
|
+
};
|
|
1221
|
+
poll();
|
|
1222
|
+
});
|
|
1223
|
+
},
|
|
1224
|
+
async canResumeFrom(checkpointId) {
|
|
1225
|
+
const checkpoint = await self.ctx.ctx.storage.get(`checkpoint:${checkpointId}`);
|
|
1226
|
+
if (!checkpoint)
|
|
1227
|
+
return false;
|
|
1228
|
+
return self.validateCheckpointHash(checkpoint);
|
|
1229
|
+
},
|
|
1230
|
+
async getIntegrityHash() {
|
|
1231
|
+
const state = await self.getResumableState(cloneId);
|
|
1232
|
+
if (!state || state.checkpoints.length === 0)
|
|
1233
|
+
return '';
|
|
1234
|
+
return state.checkpoints[state.checkpoints.length - 1].hash;
|
|
1235
|
+
},
|
|
1236
|
+
async getLockInfo() {
|
|
1237
|
+
const state = await self.getResumableState(cloneId);
|
|
1238
|
+
if (!state)
|
|
1239
|
+
return null;
|
|
1240
|
+
const lock = self._cloneLocks.get(state.targetNs) ||
|
|
1241
|
+
(await self.ctx.ctx.storage.get(`clone-lock:${state.targetNs}`));
|
|
1242
|
+
if (!lock || lock.cloneId !== cloneId)
|
|
1243
|
+
return null;
|
|
1244
|
+
return {
|
|
1245
|
+
lockId: lock.lockId,
|
|
1246
|
+
cloneId: lock.cloneId,
|
|
1247
|
+
acquiredAt: new Date(lock.acquiredAt),
|
|
1248
|
+
expiresAt: new Date(lock.expiresAt),
|
|
1249
|
+
isStale: lock.isStale || new Date(lock.expiresAt).getTime() < Date.now(),
|
|
1250
|
+
};
|
|
1251
|
+
},
|
|
1252
|
+
async forceOverrideLock() {
|
|
1253
|
+
const state = await self.getResumableState(cloneId);
|
|
1254
|
+
if (!state)
|
|
1255
|
+
throw new Error('Clone not found');
|
|
1256
|
+
await self.releaseCloneLock(state.targetNs, cloneId);
|
|
1257
|
+
},
|
|
1258
|
+
};
|
|
1259
|
+
return handle;
|
|
1260
|
+
}
|
|
1261
|
+
async processResumableClones() {
|
|
1262
|
+
const storageKeys = await this.ctx.ctx.storage.list({ prefix: 'resumable:' });
|
|
1263
|
+
for (const [key, value] of storageKeys) {
|
|
1264
|
+
const cloneId = key.replace('resumable:', '');
|
|
1265
|
+
if (!this._resumableClones.has(cloneId)) {
|
|
1266
|
+
this._resumableClones.set(cloneId, value);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
for (const [cloneId, state] of this._resumableClones) {
|
|
1270
|
+
if (state.status === 'paused' || state.pauseRequested)
|
|
1271
|
+
continue;
|
|
1272
|
+
if (state.status === 'cancelled' || state.cancelRequested)
|
|
1273
|
+
continue;
|
|
1274
|
+
if (state.status === 'completed' || state.status === 'failed')
|
|
1275
|
+
continue;
|
|
1276
|
+
await this.processResumableCloneBatch(cloneId);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async processResumableCloneBatch(cloneId) {
|
|
1280
|
+
const state = await this.getResumableState(cloneId);
|
|
1281
|
+
if (!state)
|
|
1282
|
+
return;
|
|
1283
|
+
if (state.pauseRequested || state.status === 'paused')
|
|
1284
|
+
return;
|
|
1285
|
+
if (state.cancelRequested || state.status === 'cancelled')
|
|
1286
|
+
return;
|
|
1287
|
+
if (state.status === 'initializing') {
|
|
1288
|
+
state.status = 'transferring';
|
|
1289
|
+
state.startedAt = new Date();
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
const allThings = await this.ctx.db.select().from(schema.things);
|
|
1293
|
+
const nonDeletedThings = allThings.filter((t) => !t.deleted);
|
|
1294
|
+
const totalItems = nonDeletedThings.length;
|
|
1295
|
+
if (state.totalBytes === 0) {
|
|
1296
|
+
state.totalBytes = nonDeletedThings.reduce((acc, t) => acc + JSON.stringify(t).length, 0);
|
|
1297
|
+
}
|
|
1298
|
+
const batch = nonDeletedThings.slice(state.position, state.position + state.batchSize);
|
|
1299
|
+
if (batch.length === 0) {
|
|
1300
|
+
await this.completeResumableClone(cloneId, state);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
// Apply bandwidth throttling
|
|
1304
|
+
if (state.maxBandwidth) {
|
|
1305
|
+
const batchBytes = batch.reduce((acc, t) => acc + JSON.stringify(t).length, 0);
|
|
1306
|
+
const expectedTime = (batchBytes / state.maxBandwidth) * 1000;
|
|
1307
|
+
if (expectedTime > 0) {
|
|
1308
|
+
await this.sleep(Math.floor(expectedTime));
|
|
1309
|
+
if (expectedTime > 100) {
|
|
1310
|
+
await this.ctx.emitEvent('clone.throttled', { id: cloneId, delayMs: expectedTime, batchBytes });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
await this.transferBatchToTarget(state.targetNs, batch, state.compress);
|
|
1315
|
+
const batchBytes = batch.reduce((acc, t) => acc + JSON.stringify(t).length, 0);
|
|
1316
|
+
state.bytesTransferred += batchBytes;
|
|
1317
|
+
state.position += batch.length;
|
|
1318
|
+
state.progress = Math.round((state.position / totalItems) * 100);
|
|
1319
|
+
const batchNumber = Math.ceil(state.position / state.batchSize);
|
|
1320
|
+
await this.ctx.emitEvent('clone.batch.completed', {
|
|
1321
|
+
id: cloneId,
|
|
1322
|
+
batchNumber,
|
|
1323
|
+
itemsInBatch: batch.length,
|
|
1324
|
+
itemsProcessed: state.position,
|
|
1325
|
+
progress: state.progress,
|
|
1326
|
+
});
|
|
1327
|
+
const shouldCreateCheckpoint = batchNumber % state.checkpointInterval === 0;
|
|
1328
|
+
if (shouldCreateCheckpoint) {
|
|
1329
|
+
const checkpoint = await this.createResumableCheckpoint(cloneId, state, batch, batchNumber);
|
|
1330
|
+
state.checkpoints.push(checkpoint);
|
|
1331
|
+
await this.ctx.ctx.storage.put(`checkpoint:${cloneId}:${checkpoint.id}`, checkpoint);
|
|
1332
|
+
await this.ctx.ctx.storage.put(`checkpoint:${checkpoint.id}`, checkpoint);
|
|
1333
|
+
await this.ctx.emitEvent('clone.checkpoint', {
|
|
1334
|
+
id: cloneId,
|
|
1335
|
+
checkpoint,
|
|
1336
|
+
checkpointId: checkpoint.id,
|
|
1337
|
+
position: checkpoint.position,
|
|
1338
|
+
hash: checkpoint.hash,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
state.retryCount = 0;
|
|
1342
|
+
await this.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1343
|
+
this._resumableClones.set(cloneId, state);
|
|
1344
|
+
if (state.position < totalItems && !state.pauseRequested && !state.cancelRequested) {
|
|
1345
|
+
const currentAlarm = await this.ctx.ctx.storage.getAlarm();
|
|
1346
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
1347
|
+
await this.ctx.ctx.storage.setAlarm(Date.now() + 100);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
else if (state.position >= totalItems) {
|
|
1351
|
+
await this.completeResumableClone(cloneId, state);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
await this.handleResumableCloneError(cloneId, state, error);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
async createResumableCheckpoint(cloneId, state, batch, batchNumber) {
|
|
1359
|
+
const batchJson = JSON.stringify(batch);
|
|
1360
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(batchJson));
|
|
1361
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1362
|
+
const hash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
1363
|
+
return {
|
|
1364
|
+
id: crypto.randomUUID(),
|
|
1365
|
+
position: state.position,
|
|
1366
|
+
hash,
|
|
1367
|
+
timestamp: new Date(),
|
|
1368
|
+
itemsProcessed: state.position,
|
|
1369
|
+
batchNumber,
|
|
1370
|
+
cloneId,
|
|
1371
|
+
compressed: state.compress,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
async completeResumableClone(cloneId, state) {
|
|
1375
|
+
const duration = state.startedAt ? Date.now() - new Date(state.startedAt).getTime() : 0;
|
|
1376
|
+
state.status = 'completed';
|
|
1377
|
+
await this.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1378
|
+
this._resumableClones.set(cloneId, state);
|
|
1379
|
+
await this.releaseCloneLock(state.targetNs, cloneId);
|
|
1380
|
+
await this.cleanupCloneCheckpoints(cloneId);
|
|
1381
|
+
await this.ctx.emitEvent('clone.completed', {
|
|
1382
|
+
id: cloneId,
|
|
1383
|
+
totalCheckpoints: state.checkpoints.length,
|
|
1384
|
+
totalItems: state.position,
|
|
1385
|
+
duration,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async handleResumableCloneError(cloneId, state, error) {
|
|
1389
|
+
state.retryCount++;
|
|
1390
|
+
await this.ctx.emitEvent('clone.retry', { id: cloneId, attempt: state.retryCount, error: error.message });
|
|
1391
|
+
if (state.retryCount >= state.maxRetries) {
|
|
1392
|
+
state.status = 'failed';
|
|
1393
|
+
await this.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1394
|
+
this._resumableClones.set(cloneId, state);
|
|
1395
|
+
await this.ctx.emitEvent('clone.failed', { id: cloneId, error: error.message, retryCount: state.retryCount });
|
|
1396
|
+
await this.releaseCloneLock(state.targetNs, cloneId);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const baseDelay = state.retryDelay * Math.pow(2, state.retryCount - 1);
|
|
1400
|
+
const jitter = Math.random() * baseDelay * 0.25;
|
|
1401
|
+
const delay = Math.floor(baseDelay + jitter);
|
|
1402
|
+
await this.ctx.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1403
|
+
this._resumableClones.set(cloneId, state);
|
|
1404
|
+
const currentAlarm = await this.ctx.ctx.storage.getAlarm();
|
|
1405
|
+
if (!currentAlarm || currentAlarm > Date.now() + delay) {
|
|
1406
|
+
await this.ctx.ctx.storage.setAlarm(Date.now() + delay);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async transferBatchToTarget(targetNs, batch, compress) {
|
|
1410
|
+
if (!this.ctx.env.DO) {
|
|
1411
|
+
throw new Error('DO namespace not configured');
|
|
1412
|
+
}
|
|
1413
|
+
await Promise.resolve();
|
|
1414
|
+
}
|
|
1415
|
+
async getResumableState(cloneId) {
|
|
1416
|
+
let state = this._resumableClones.get(cloneId);
|
|
1417
|
+
if (state)
|
|
1418
|
+
return state;
|
|
1419
|
+
state = await this.ctx.ctx.storage.get(`resumable:${cloneId}`);
|
|
1420
|
+
if (state) {
|
|
1421
|
+
this._resumableClones.set(cloneId, state);
|
|
1422
|
+
}
|
|
1423
|
+
return state || null;
|
|
1424
|
+
}
|
|
1425
|
+
async findResumableStateFromCheckpoint(checkpointId) {
|
|
1426
|
+
const checkpoint = await this.ctx.ctx.storage.get(`checkpoint:${checkpointId}`);
|
|
1427
|
+
if (!checkpoint || !checkpoint.cloneId)
|
|
1428
|
+
return null;
|
|
1429
|
+
return this.getResumableState(checkpoint.cloneId);
|
|
1430
|
+
}
|
|
1431
|
+
async validateCheckpointHash(checkpoint) {
|
|
1432
|
+
if (!checkpoint.hash || !/^[a-f0-9]{64}$/.test(checkpoint.hash))
|
|
1433
|
+
return false;
|
|
1434
|
+
if (checkpoint.position < 0)
|
|
1435
|
+
return false;
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
async releaseCloneLock(target, cloneId) {
|
|
1439
|
+
const lock = this._cloneLocks.get(target) ||
|
|
1440
|
+
(await this.ctx.ctx.storage.get(`clone-lock:${target}`));
|
|
1441
|
+
if (lock && lock.cloneId === cloneId) {
|
|
1442
|
+
this._cloneLocks.delete(target);
|
|
1443
|
+
await this.ctx.ctx.storage.delete(`clone-lock:${target}`);
|
|
1444
|
+
await this.ctx.emitEvent('clone.lock.released', { lockId: lock.lockId, target });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async cleanupCloneCheckpoints(cloneId) {
|
|
1448
|
+
const state = await this.getResumableState(cloneId);
|
|
1449
|
+
if (!state)
|
|
1450
|
+
return;
|
|
1451
|
+
for (const checkpoint of state.checkpoints) {
|
|
1452
|
+
await this.ctx.ctx.storage.delete(`checkpoint:${cloneId}:${checkpoint.id}`);
|
|
1453
|
+
await this.ctx.ctx.storage.delete(`checkpoint:${checkpoint.id}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
async cleanupOrphanedCheckpoints(retentionMs) {
|
|
1457
|
+
const now = Date.now();
|
|
1458
|
+
const cutoff = now - retentionMs;
|
|
1459
|
+
const checkpointKeys = await this.ctx.ctx.storage.list({ prefix: 'checkpoint:' });
|
|
1460
|
+
for (const [key, value] of checkpointKeys) {
|
|
1461
|
+
const checkpoint = value;
|
|
1462
|
+
if (!checkpoint.timestamp)
|
|
1463
|
+
continue;
|
|
1464
|
+
const checkpointTime = new Date(checkpoint.timestamp).getTime();
|
|
1465
|
+
if (checkpointTime < cutoff) {
|
|
1466
|
+
if (checkpoint.cloneId) {
|
|
1467
|
+
const state = await this.getResumableState(checkpoint.cloneId);
|
|
1468
|
+
if (state && state.status === 'paused')
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
await this.ctx.ctx.storage.delete(key);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
sleep(ms) {
|
|
1476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1477
|
+
}
|
|
1478
|
+
// Test helper methods
|
|
1479
|
+
async _corruptCheckpoint(token, checkpointId) {
|
|
1480
|
+
const checkpointKey = `${CHECKPOINT_PREFIX}${token}:${checkpointId}`;
|
|
1481
|
+
const checkpoint = (await this.ctx.ctx.storage.get(checkpointKey));
|
|
1482
|
+
if (checkpoint) {
|
|
1483
|
+
checkpoint.checksum = 'corrupted-checksum';
|
|
1484
|
+
await this.ctx.ctx.storage.put(checkpointKey, checkpoint);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async _corruptStagingArea(token) {
|
|
1488
|
+
const staging = (await this.ctx.ctx.storage.get(`${STAGING_PREFIX}${token}`));
|
|
1489
|
+
if (staging) {
|
|
1490
|
+
staging.integrityHash = 'corrupted-hash';
|
|
1491
|
+
await this.ctx.ctx.storage.put(`${STAGING_PREFIX}${token}`, staging);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Export singleton factory
|
|
1496
|
+
export function createCloneModule() {
|
|
1497
|
+
return new CloneModule();
|
|
1498
|
+
}
|
|
1499
|
+
//# sourceMappingURL=Clone.js.map
|