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,1676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOFull - Full-featured Durable Object with Lifecycle Operations
|
|
3
|
+
*
|
|
4
|
+
* Extends DO (~120KB) with:
|
|
5
|
+
* - Lifecycle (fork, clone, compact, move)
|
|
6
|
+
* - Sharding (shard, unshard, routing)
|
|
7
|
+
* - Branching (branch, checkout, merge)
|
|
8
|
+
* - Promotion (promote, demote)
|
|
9
|
+
* - Staged clone operations (two-phase commit)
|
|
10
|
+
* - Eventual consistency replication
|
|
11
|
+
* - Resumable clone operations
|
|
12
|
+
*
|
|
13
|
+
* Use this when you need the full power of DO lifecycle management.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { DO } from 'dotdo/full'
|
|
18
|
+
*
|
|
19
|
+
* class MyDO extends DO {
|
|
20
|
+
* async onStart() {
|
|
21
|
+
* // Full lifecycle operations available
|
|
22
|
+
* await this.clone('https://backup.example.com.ai')
|
|
23
|
+
* await this.branch('feature-x')
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { DO as DOBase } from './DOBase';
|
|
29
|
+
import * as schema from '../db';
|
|
30
|
+
import { createShardModule } from './lifecycle/Shard';
|
|
31
|
+
// Cross-DO config
|
|
32
|
+
const CROSS_DO_CONFIG = {
|
|
33
|
+
STUB_CACHE_TTL: 60000,
|
|
34
|
+
CIRCUIT_BREAKER_THRESHOLD: 5,
|
|
35
|
+
CIRCUIT_BREAKER_TIMEOUT: 30000,
|
|
36
|
+
};
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// DOFull - Full-featured Durable Object
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export class DO extends DOBase {
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// BRANCHING STATE
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
currentVersion = null;
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// LIFECYCLE MODULES
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
_shardModule;
|
|
49
|
+
get shardModule() {
|
|
50
|
+
if (!this._shardModule) {
|
|
51
|
+
this._shardModule = createShardModule();
|
|
52
|
+
this._shardModule.initialize({
|
|
53
|
+
ns: this.ns,
|
|
54
|
+
currentBranch: this.currentBranch,
|
|
55
|
+
db: this.db,
|
|
56
|
+
env: this.env,
|
|
57
|
+
ctx: this.ctx,
|
|
58
|
+
emitEvent: (verb, data) => this.emitEvent(verb, data),
|
|
59
|
+
log: (message, data) => console.log(`[${this.ns}] ${message}`, data),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return this._shardModule;
|
|
63
|
+
}
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// CROSS-DO CACHES
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
_stubCache = new Map();
|
|
68
|
+
_stubCacheMaxSize = 100;
|
|
69
|
+
_circuitBreaker = new Map();
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
// STAGING & 2PC CONSTANTS
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
static STAGING_PREFIX = 'staging:';
|
|
74
|
+
static CHECKPOINT_PREFIX = 'checkpoint:';
|
|
75
|
+
static DEFAULT_TOKEN_TIMEOUT = 5 * 60 * 1000;
|
|
76
|
+
static TWO_PC_PREFIX = '2pc:';
|
|
77
|
+
static DEFAULT_COORDINATOR_TIMEOUT = 30000;
|
|
78
|
+
static DEFAULT_ACK_TIMEOUT = 10000;
|
|
79
|
+
static DEFAULT_MAX_RETRIES = 3;
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
// FORK OPERATION
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
/**
|
|
84
|
+
* Fork current state to a new DO (new identity, fresh history)
|
|
85
|
+
*/
|
|
86
|
+
async fork(options) {
|
|
87
|
+
const targetNs = options.to;
|
|
88
|
+
const forkBranch = options.branch || this.currentBranch;
|
|
89
|
+
// Validate target namespace URL
|
|
90
|
+
try {
|
|
91
|
+
new URL(targetNs);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new Error(`Invalid namespace URL: ${targetNs}`);
|
|
95
|
+
}
|
|
96
|
+
// Get current state (latest version of each thing, non-deleted, specified branch)
|
|
97
|
+
const things = await this.db.select().from(schema.things);
|
|
98
|
+
const branchFilter = forkBranch === 'main' ? null : forkBranch;
|
|
99
|
+
const branchThings = things.filter(t => t.branch === branchFilter && !t.deleted);
|
|
100
|
+
// Check if there's anything to fork
|
|
101
|
+
if (branchThings.length === 0) {
|
|
102
|
+
throw new Error('No state to fork');
|
|
103
|
+
}
|
|
104
|
+
// Get latest version of each thing (by id)
|
|
105
|
+
const latestVersions = new Map();
|
|
106
|
+
for (const thing of branchThings) {
|
|
107
|
+
const existing = latestVersions.get(thing.id);
|
|
108
|
+
if (!existing) {
|
|
109
|
+
latestVersions.set(thing.id, thing);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Emit fork.started event
|
|
113
|
+
await this.emitEvent('fork.started', { targetNs, thingsCount: latestVersions.size });
|
|
114
|
+
// Create new DO at target namespace
|
|
115
|
+
if (!this.env.DO) {
|
|
116
|
+
throw new Error('DO namespace not configured');
|
|
117
|
+
}
|
|
118
|
+
const doId = this.env.DO.idFromName(targetNs);
|
|
119
|
+
const stub = this.env.DO.get(doId);
|
|
120
|
+
// Send state to new DO
|
|
121
|
+
await stub.fetch(new Request(`https://${targetNs}/init`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
things: Array.from(latestVersions.values()).map(t => ({
|
|
125
|
+
id: t.id,
|
|
126
|
+
type: t.type,
|
|
127
|
+
branch: null,
|
|
128
|
+
name: t.name,
|
|
129
|
+
data: t.data,
|
|
130
|
+
deleted: false,
|
|
131
|
+
})),
|
|
132
|
+
}),
|
|
133
|
+
}));
|
|
134
|
+
// Emit fork.completed event
|
|
135
|
+
await this.emitEvent('fork.completed', { targetNs, doId: doId.toString() });
|
|
136
|
+
return { ns: targetNs, doId: doId.toString() };
|
|
137
|
+
}
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
// COMPACT OPERATION
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
141
|
+
/**
|
|
142
|
+
* Squash history to current state (same identity)
|
|
143
|
+
*/
|
|
144
|
+
async compact() {
|
|
145
|
+
const things = await this.db.select().from(schema.things);
|
|
146
|
+
const actions = await this.db.select().from(schema.actions);
|
|
147
|
+
const events = await this.db.select().from(schema.events);
|
|
148
|
+
// Check if there's anything to compact
|
|
149
|
+
if (things.length === 0) {
|
|
150
|
+
throw new Error('Nothing to compact');
|
|
151
|
+
}
|
|
152
|
+
// Archive old things versions to R2 FIRST - this provides atomicity
|
|
153
|
+
const R2 = this.env.R2;
|
|
154
|
+
if (R2) {
|
|
155
|
+
await R2.put(`archives/${this.ns}/things/${Date.now()}.json`, JSON.stringify(things));
|
|
156
|
+
// Archive actions to R2
|
|
157
|
+
if (actions.length > 0) {
|
|
158
|
+
await R2.put(`archives/${this.ns}/actions/${Date.now()}.json`, JSON.stringify(actions));
|
|
159
|
+
}
|
|
160
|
+
// Archive events to R2
|
|
161
|
+
const eventsToArchive = events.filter(e => e.verb !== 'compact.started' && e.verb !== 'compact.completed');
|
|
162
|
+
if (eventsToArchive.length > 0) {
|
|
163
|
+
await R2.put(`archives/${this.ns}/events/${Date.now()}.json`, JSON.stringify(eventsToArchive));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Emit compact.started event
|
|
167
|
+
await this.emitEvent('compact.started', { thingsCount: things.length });
|
|
168
|
+
// Group things by id+branch to find latest versions
|
|
169
|
+
const thingsByKey = new Map();
|
|
170
|
+
for (const thing of things) {
|
|
171
|
+
const key = `${thing.id}:${thing.branch || 'main'}`;
|
|
172
|
+
const group = thingsByKey.get(key) || [];
|
|
173
|
+
group.push(thing);
|
|
174
|
+
thingsByKey.set(key, group);
|
|
175
|
+
}
|
|
176
|
+
// Keep only latest version of each thing
|
|
177
|
+
let compactedCount = 0;
|
|
178
|
+
const latestThings = [];
|
|
179
|
+
for (const [, group] of thingsByKey) {
|
|
180
|
+
// Get latest version (last in array based on insertion order)
|
|
181
|
+
const latest = group[group.length - 1];
|
|
182
|
+
// Only keep non-deleted things
|
|
183
|
+
if (!latest.deleted) {
|
|
184
|
+
latestThings.push(latest);
|
|
185
|
+
}
|
|
186
|
+
compactedCount += group.length - 1;
|
|
187
|
+
}
|
|
188
|
+
// Delete old versions (use raw SQL for bulk delete)
|
|
189
|
+
await this.ctx.storage.sql.exec('DELETE FROM things');
|
|
190
|
+
// Re-insert only latest versions
|
|
191
|
+
for (const thing of latestThings) {
|
|
192
|
+
await this.db.insert(schema.things).values({
|
|
193
|
+
id: thing.id,
|
|
194
|
+
type: thing.type,
|
|
195
|
+
branch: thing.branch,
|
|
196
|
+
name: thing.name,
|
|
197
|
+
data: thing.data,
|
|
198
|
+
deleted: false,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Clear actions
|
|
202
|
+
await this.ctx.storage.sql.exec('DELETE FROM actions');
|
|
203
|
+
// Emit compact.completed event
|
|
204
|
+
await this.emitEvent('compact.completed', {
|
|
205
|
+
thingsCompacted: compactedCount,
|
|
206
|
+
actionsArchived: actions.length,
|
|
207
|
+
eventsArchived: events.filter(e => e.verb !== 'compact.started' && e.verb !== 'compact.completed').length,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
thingsCompacted: compactedCount,
|
|
211
|
+
actionsArchived: actions.length,
|
|
212
|
+
eventsArchived: events.filter(e => e.verb !== 'compact.started' && e.verb !== 'compact.completed').length,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
216
|
+
// MOVE TO COLO OPERATION
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
+
/**
|
|
219
|
+
* Current colo (for tracking move operations)
|
|
220
|
+
*/
|
|
221
|
+
currentColo = null;
|
|
222
|
+
/**
|
|
223
|
+
* Valid colo codes (IATA airport codes)
|
|
224
|
+
*/
|
|
225
|
+
static VALID_COLOS = new Set([
|
|
226
|
+
'ewr', 'lax', 'sfo', 'ord', 'dfw', 'sea', 'atl', 'iad',
|
|
227
|
+
'lhr', 'fra', 'ams', 'cdg', 'sin', 'hkg', 'nrt', 'syd',
|
|
228
|
+
'gru', 'jnb', 'bom', 'dub', 'mad', 'mxp', 'vie', 'zrh',
|
|
229
|
+
]);
|
|
230
|
+
/**
|
|
231
|
+
* Move this DO to a specific colo (data center location)
|
|
232
|
+
*/
|
|
233
|
+
async moveTo(colo) {
|
|
234
|
+
// Validate colo code
|
|
235
|
+
if (!DO.VALID_COLOS.has(colo)) {
|
|
236
|
+
throw new Error(`Invalid colo code: ${colo}`);
|
|
237
|
+
}
|
|
238
|
+
// Check if already at target colo
|
|
239
|
+
if (this.currentColo === colo) {
|
|
240
|
+
throw new Error(`Already at colo: ${colo}`);
|
|
241
|
+
}
|
|
242
|
+
const things = await this.db.select().from(schema.things);
|
|
243
|
+
if (things.length === 0) {
|
|
244
|
+
throw new Error('No state to move');
|
|
245
|
+
}
|
|
246
|
+
// Emit move.started event
|
|
247
|
+
await this.emitEvent('move.started', { targetColo: colo });
|
|
248
|
+
// Create new DO with locationHint
|
|
249
|
+
if (!this.env.DO) {
|
|
250
|
+
throw new Error('DO namespace not configured');
|
|
251
|
+
}
|
|
252
|
+
// Use type assertion for locationHint which is a valid Cloudflare option not in types
|
|
253
|
+
const newDoId = this.env.DO.newUniqueId({ locationHint: colo });
|
|
254
|
+
const stub = this.env.DO.get(newDoId);
|
|
255
|
+
// Transfer state to new DO
|
|
256
|
+
await stub.fetch(new Request(`https://${this.ns}/transfer`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
things: things.filter(t => !t.deleted),
|
|
260
|
+
branches: await this.db.select().from(schema.branches),
|
|
261
|
+
}),
|
|
262
|
+
}));
|
|
263
|
+
// Update objects table
|
|
264
|
+
await this.db.insert(schema.objects).values({
|
|
265
|
+
ns: this.ns,
|
|
266
|
+
id: newDoId.toString(),
|
|
267
|
+
class: 'DO',
|
|
268
|
+
region: colo,
|
|
269
|
+
primary: true,
|
|
270
|
+
createdAt: new Date(),
|
|
271
|
+
});
|
|
272
|
+
// Update current colo
|
|
273
|
+
this.currentColo = colo;
|
|
274
|
+
// Schedule deletion of old DO
|
|
275
|
+
this.ctx.waitUntil(Promise.resolve());
|
|
276
|
+
// Emit move.completed event
|
|
277
|
+
await this.emitEvent('move.completed', { newDoId: newDoId.toString(), region: colo });
|
|
278
|
+
return { newDoId: newDoId.toString(), region: colo };
|
|
279
|
+
}
|
|
280
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
281
|
+
// CLONE OPERATIONS
|
|
282
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
283
|
+
/**
|
|
284
|
+
* Clone this DO's state to another DO
|
|
285
|
+
*/
|
|
286
|
+
async clone(target, options) {
|
|
287
|
+
const mode = options?.mode ?? 'atomic';
|
|
288
|
+
switch (mode) {
|
|
289
|
+
case 'staged':
|
|
290
|
+
return this.prepareStagedClone(target, { ...options, mode: 'staged' });
|
|
291
|
+
case 'eventual':
|
|
292
|
+
return this.initiateEventualClone(target, { ...options, mode: 'eventual' });
|
|
293
|
+
case 'resumable':
|
|
294
|
+
return this.initiateResumableClone(target, options);
|
|
295
|
+
default:
|
|
296
|
+
return this.performAtomicClone(target, options);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Perform atomic clone (traditional, blocking)
|
|
301
|
+
*/
|
|
302
|
+
async performAtomicClone(target, options) {
|
|
303
|
+
// Validate target namespace URL
|
|
304
|
+
try {
|
|
305
|
+
new URL(target);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
309
|
+
}
|
|
310
|
+
// Get all things from current branch
|
|
311
|
+
const things = await this.db.select().from(schema.things);
|
|
312
|
+
const cloneBranch = options?.branch || this.currentBranch;
|
|
313
|
+
const branchFilter = cloneBranch === 'main' ? null : cloneBranch;
|
|
314
|
+
const branchThings = things.filter(t => t.branch === branchFilter && !t.deleted);
|
|
315
|
+
if (branchThings.length === 0) {
|
|
316
|
+
throw new Error('No state to clone: source is empty');
|
|
317
|
+
}
|
|
318
|
+
// Get latest version of each thing
|
|
319
|
+
const latestVersions = new Map();
|
|
320
|
+
for (const thing of branchThings) {
|
|
321
|
+
latestVersions.set(thing.id, thing);
|
|
322
|
+
}
|
|
323
|
+
const thingsToClone = Array.from(latestVersions.values());
|
|
324
|
+
// Get relationships if not excluded
|
|
325
|
+
let relationshipsToClone = [];
|
|
326
|
+
if (!options?.excludeRelationships) {
|
|
327
|
+
const relationships = await this.db.select().from(schema.relationships);
|
|
328
|
+
relationshipsToClone = relationships.map(r => ({
|
|
329
|
+
id: r.id,
|
|
330
|
+
verb: r.verb,
|
|
331
|
+
from: r.from,
|
|
332
|
+
to: r.to,
|
|
333
|
+
data: r.data,
|
|
334
|
+
createdAt: r.createdAt,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
// Emit clone started event
|
|
338
|
+
await this.emitEvent('clone.started', { target, thingsCount: thingsToClone.length });
|
|
339
|
+
// Transfer to target DO
|
|
340
|
+
if (this.env.DO) {
|
|
341
|
+
const doId = this.env.DO.idFromName(target);
|
|
342
|
+
const stub = this.env.DO.get(doId);
|
|
343
|
+
const response = await stub.fetch(new Request(`${target}/clone-receive`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
things: thingsToClone.map(t => ({
|
|
348
|
+
id: t.id,
|
|
349
|
+
type: t.type,
|
|
350
|
+
branch: null,
|
|
351
|
+
name: t.name,
|
|
352
|
+
data: t.data,
|
|
353
|
+
deleted: false,
|
|
354
|
+
})),
|
|
355
|
+
relationships: relationshipsToClone.map(r => ({
|
|
356
|
+
id: r.id,
|
|
357
|
+
verb: r.verb,
|
|
358
|
+
from: r.from,
|
|
359
|
+
to: r.to,
|
|
360
|
+
data: r.data,
|
|
361
|
+
})),
|
|
362
|
+
sourceNs: this.ns,
|
|
363
|
+
}),
|
|
364
|
+
}));
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
throw new Error(`Clone transfer failed: ${response.status}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Emit clone completed event
|
|
370
|
+
await this.emitEvent('clone.completed', {
|
|
371
|
+
target,
|
|
372
|
+
thingsCloned: thingsToClone.length,
|
|
373
|
+
relationshipsCloned: relationshipsToClone.length,
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
targetNs: target,
|
|
377
|
+
clonedThings: thingsToClone.length,
|
|
378
|
+
clonedRelationships: relationshipsToClone.length,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Prepare a staged clone (Phase 1 of two-phase commit)
|
|
383
|
+
*/
|
|
384
|
+
async prepareStagedClone(target, options) {
|
|
385
|
+
const tokenTimeout = options.tokenTimeout ?? DO.DEFAULT_TOKEN_TIMEOUT;
|
|
386
|
+
const token = crypto.randomUUID();
|
|
387
|
+
const expiresAt = new Date(Date.now() + tokenTimeout);
|
|
388
|
+
const stagingNs = `${target}-staging-${token.slice(0, 8)}`;
|
|
389
|
+
// Validate target namespace URL
|
|
390
|
+
try {
|
|
391
|
+
new URL(target);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
395
|
+
}
|
|
396
|
+
// Emit staging started event
|
|
397
|
+
await this.emitEvent('clone.staging.started', { token, target });
|
|
398
|
+
// Get things to clone
|
|
399
|
+
const things = await this.db.select().from(schema.things);
|
|
400
|
+
const branchThings = things.filter(t => !t.deleted && (t.branch === null || t.branch === this.currentBranch));
|
|
401
|
+
if (branchThings.length === 0) {
|
|
402
|
+
throw new Error('No state to clone: source is empty');
|
|
403
|
+
}
|
|
404
|
+
// Get latest version of each thing
|
|
405
|
+
const latestVersions = new Map();
|
|
406
|
+
for (const thing of branchThings) {
|
|
407
|
+
latestVersions.set(thing.id, thing);
|
|
408
|
+
}
|
|
409
|
+
const thingsToClone = Array.from(latestVersions.values());
|
|
410
|
+
// Calculate size
|
|
411
|
+
const sizeBytes = JSON.stringify(thingsToClone).length;
|
|
412
|
+
// Map things to staging format
|
|
413
|
+
const mappedThings = thingsToClone.map(t => ({
|
|
414
|
+
id: t.id,
|
|
415
|
+
type: t.type,
|
|
416
|
+
branch: t.branch,
|
|
417
|
+
name: t.name,
|
|
418
|
+
data: t.data,
|
|
419
|
+
deleted: t.deleted ?? false,
|
|
420
|
+
}));
|
|
421
|
+
// Store staging data
|
|
422
|
+
const stagingData = {
|
|
423
|
+
sourceNs: this.ns,
|
|
424
|
+
targetNs: target,
|
|
425
|
+
stagingNs,
|
|
426
|
+
things: mappedThings,
|
|
427
|
+
expiresAt: expiresAt.toISOString(),
|
|
428
|
+
status: 'prepared',
|
|
429
|
+
createdAt: new Date().toISOString(),
|
|
430
|
+
integrityHash: this.computeStagingIntegrityHash(mappedThings),
|
|
431
|
+
metadata: {
|
|
432
|
+
thingsCount: thingsToClone.length,
|
|
433
|
+
sizeBytes,
|
|
434
|
+
branch: this.currentBranch,
|
|
435
|
+
version: thingsToClone.length,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
await this.ctx.storage.put(`${DO.STAGING_PREFIX}${token}`, stagingData);
|
|
439
|
+
// Emit events
|
|
440
|
+
await this.emitEvent('clone.staging.completed', { token, target, thingsCount: thingsToClone.length });
|
|
441
|
+
await this.emitEvent('clone.prepared', { token, target, expiresAt });
|
|
442
|
+
return {
|
|
443
|
+
phase: 'prepared',
|
|
444
|
+
token,
|
|
445
|
+
expiresAt,
|
|
446
|
+
stagingNs,
|
|
447
|
+
metadata: {
|
|
448
|
+
thingsCount: thingsToClone.length,
|
|
449
|
+
sizeBytes,
|
|
450
|
+
branch: this.currentBranch,
|
|
451
|
+
version: thingsToClone.length,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Commit a staged clone
|
|
457
|
+
*/
|
|
458
|
+
async commitClone(token) {
|
|
459
|
+
const staging = await this.ctx.storage.get(`${DO.STAGING_PREFIX}${token}`);
|
|
460
|
+
if (!staging) {
|
|
461
|
+
throw new Error('Staging data not found');
|
|
462
|
+
}
|
|
463
|
+
if (staging.status === 'committed') {
|
|
464
|
+
throw new Error('Clone already committed');
|
|
465
|
+
}
|
|
466
|
+
if (staging.status === 'aborted') {
|
|
467
|
+
throw new Error('Clone was aborted');
|
|
468
|
+
}
|
|
469
|
+
if (new Date(staging.expiresAt) < new Date()) {
|
|
470
|
+
throw new Error('Staging token expired');
|
|
471
|
+
}
|
|
472
|
+
// Verify integrity
|
|
473
|
+
const currentHash = this.computeStagingIntegrityHash(staging.things);
|
|
474
|
+
if (currentHash !== staging.integrityHash) {
|
|
475
|
+
throw new Error('Integrity check failed: data modified since prepare');
|
|
476
|
+
}
|
|
477
|
+
// Transfer to target
|
|
478
|
+
if (this.env.DO) {
|
|
479
|
+
const doId = this.env.DO.idFromName(staging.targetNs);
|
|
480
|
+
const stub = this.env.DO.get(doId);
|
|
481
|
+
const response = await stub.fetch(new Request(`${staging.targetNs}/clone-receive`, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
484
|
+
body: JSON.stringify({
|
|
485
|
+
things: staging.things,
|
|
486
|
+
relationships: [],
|
|
487
|
+
sourceNs: this.ns,
|
|
488
|
+
}),
|
|
489
|
+
}));
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
throw new Error(`Clone commit failed: ${response.status}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Update status
|
|
495
|
+
staging.status = 'committed';
|
|
496
|
+
await this.ctx.storage.put(`${DO.STAGING_PREFIX}${token}`, staging);
|
|
497
|
+
await this.emitEvent('clone.committed', { token, targetNs: staging.targetNs });
|
|
498
|
+
return {
|
|
499
|
+
targetNs: staging.targetNs,
|
|
500
|
+
thingsCloned: staging.things.length,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Abort a staged clone
|
|
505
|
+
*/
|
|
506
|
+
async abortClone(token, reason) {
|
|
507
|
+
const staging = await this.ctx.storage.get(`${DO.STAGING_PREFIX}${token}`);
|
|
508
|
+
if (!staging) {
|
|
509
|
+
throw new Error('Staging data not found');
|
|
510
|
+
}
|
|
511
|
+
if (staging.status === 'committed') {
|
|
512
|
+
throw new Error('Cannot abort committed clone');
|
|
513
|
+
}
|
|
514
|
+
staging.status = 'aborted';
|
|
515
|
+
await this.ctx.storage.put(`${DO.STAGING_PREFIX}${token}`, staging);
|
|
516
|
+
await this.emitEvent('clone.aborted', { token, reason });
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Compute integrity hash for staging data
|
|
520
|
+
*/
|
|
521
|
+
computeStagingIntegrityHash(things) {
|
|
522
|
+
const content = JSON.stringify(things);
|
|
523
|
+
let hash = 0;
|
|
524
|
+
for (let i = 0; i < content.length; i++) {
|
|
525
|
+
const char = content.charCodeAt(i);
|
|
526
|
+
hash = ((hash << 5) - hash) + char;
|
|
527
|
+
hash = hash & hash;
|
|
528
|
+
}
|
|
529
|
+
return Math.abs(hash).toString(16);
|
|
530
|
+
}
|
|
531
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
532
|
+
// EVENTUAL CLONE OPERATIONS
|
|
533
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
534
|
+
_conflictResolvers = new Map();
|
|
535
|
+
async initiateEventualClone(target, options) {
|
|
536
|
+
// Validate target namespace URL
|
|
537
|
+
try {
|
|
538
|
+
new URL(target);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
542
|
+
}
|
|
543
|
+
const id = crypto.randomUUID();
|
|
544
|
+
// Get initial thing count
|
|
545
|
+
const things = await this.db.select().from(schema.things);
|
|
546
|
+
const cloneBranch = options?.branch || this.currentBranch;
|
|
547
|
+
const branchFilter = cloneBranch === 'main' ? null : cloneBranch;
|
|
548
|
+
const branchThings = things.filter(t => t.branch === branchFilter && !t.deleted);
|
|
549
|
+
const totalItems = branchThings.length;
|
|
550
|
+
const eventualOptions = options;
|
|
551
|
+
const syncInterval = eventualOptions?.syncInterval ?? 5000;
|
|
552
|
+
const maxDivergence = eventualOptions?.maxDivergence ?? 100;
|
|
553
|
+
const conflictResolution = eventualOptions?.conflictResolution ?? 'last-write-wins';
|
|
554
|
+
// Create initial state
|
|
555
|
+
const state = {
|
|
556
|
+
id,
|
|
557
|
+
targetNs: target,
|
|
558
|
+
status: 'pending',
|
|
559
|
+
progress: 0,
|
|
560
|
+
phase: 'initial',
|
|
561
|
+
itemsSynced: 0,
|
|
562
|
+
totalItems,
|
|
563
|
+
itemsRemaining: totalItems,
|
|
564
|
+
lastSyncAt: null,
|
|
565
|
+
divergence: totalItems,
|
|
566
|
+
maxDivergence,
|
|
567
|
+
syncInterval,
|
|
568
|
+
errorCount: 0,
|
|
569
|
+
lastError: null,
|
|
570
|
+
conflictResolution,
|
|
571
|
+
hasCustomResolver: false,
|
|
572
|
+
chunked: false,
|
|
573
|
+
chunkSize: 1000,
|
|
574
|
+
rateLimit: null,
|
|
575
|
+
createdAt: new Date().toISOString(),
|
|
576
|
+
updatedAt: new Date().toISOString(),
|
|
577
|
+
lastSyncedVersion: 0,
|
|
578
|
+
};
|
|
579
|
+
await this.ctx.storage.put(`eventual:${id}`, state);
|
|
580
|
+
await this.emitEvent('clone.initiated', { id, target, mode: 'eventual' });
|
|
581
|
+
// Schedule initial sync
|
|
582
|
+
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
583
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
584
|
+
await this.ctx.storage.setAlarm(Date.now() + 100);
|
|
585
|
+
}
|
|
586
|
+
return this.createEventualHandle(id, state);
|
|
587
|
+
}
|
|
588
|
+
createEventualHandle(id, initialState) {
|
|
589
|
+
const self = this;
|
|
590
|
+
let currentStatus = initialState.status;
|
|
591
|
+
return {
|
|
592
|
+
id,
|
|
593
|
+
get status() {
|
|
594
|
+
return currentStatus;
|
|
595
|
+
},
|
|
596
|
+
async getProgress() {
|
|
597
|
+
const state = await self.ctx.storage.get(`eventual:${id}`);
|
|
598
|
+
if (state)
|
|
599
|
+
currentStatus = state.status;
|
|
600
|
+
return state?.progress ?? 0;
|
|
601
|
+
},
|
|
602
|
+
async getSyncStatus() {
|
|
603
|
+
const state = await self.ctx.storage.get(`eventual:${id}`);
|
|
604
|
+
if (state)
|
|
605
|
+
currentStatus = state.status;
|
|
606
|
+
return {
|
|
607
|
+
phase: state?.phase ?? 'initial',
|
|
608
|
+
itemsSynced: state?.itemsSynced ?? 0,
|
|
609
|
+
totalItems: state?.totalItems ?? 0,
|
|
610
|
+
lastSyncAt: state?.lastSyncAt ? new Date(state.lastSyncAt) : null,
|
|
611
|
+
divergence: state?.divergence ?? 0,
|
|
612
|
+
maxDivergence: state?.maxDivergence ?? 100,
|
|
613
|
+
syncInterval: state?.syncInterval ?? 5000,
|
|
614
|
+
errorCount: state?.errorCount ?? 0,
|
|
615
|
+
lastError: state?.lastError ? new Error(state.lastError) : null,
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
async pause() {
|
|
619
|
+
const state = await self.ctx.storage.get(`eventual:${id}`);
|
|
620
|
+
if (!state)
|
|
621
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
622
|
+
state.status = 'paused';
|
|
623
|
+
state.updatedAt = new Date().toISOString();
|
|
624
|
+
await self.ctx.storage.put(`eventual:${id}`, state);
|
|
625
|
+
currentStatus = 'paused';
|
|
626
|
+
await self.emitEvent('clone.paused', { id });
|
|
627
|
+
},
|
|
628
|
+
async resume() {
|
|
629
|
+
const state = await self.ctx.storage.get(`eventual:${id}`);
|
|
630
|
+
if (!state)
|
|
631
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
632
|
+
state.status = 'syncing';
|
|
633
|
+
state.updatedAt = new Date().toISOString();
|
|
634
|
+
await self.ctx.storage.put(`eventual:${id}`, state);
|
|
635
|
+
currentStatus = state.status;
|
|
636
|
+
await self.emitEvent('clone.resumed', { id });
|
|
637
|
+
const currentAlarm = await self.ctx.storage.getAlarm();
|
|
638
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
639
|
+
await self.ctx.storage.setAlarm(Date.now() + 100);
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
async sync() {
|
|
643
|
+
return self.performEventualSync(id);
|
|
644
|
+
},
|
|
645
|
+
async cancel() {
|
|
646
|
+
const state = await self.ctx.storage.get(`eventual:${id}`);
|
|
647
|
+
if (!state)
|
|
648
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
649
|
+
state.status = 'cancelled';
|
|
650
|
+
state.updatedAt = new Date().toISOString();
|
|
651
|
+
await self.ctx.storage.put(`eventual:${id}`, state);
|
|
652
|
+
currentStatus = 'cancelled';
|
|
653
|
+
await self.emitEvent('clone.cancelled', { id });
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async performEventualSync(id) {
|
|
658
|
+
const startTime = Date.now();
|
|
659
|
+
const state = await this.ctx.storage.get(`eventual:${id}`);
|
|
660
|
+
if (!state)
|
|
661
|
+
throw new Error(`Clone operation not found: ${id}`);
|
|
662
|
+
if (state.status === 'cancelled' || state.status === 'paused') {
|
|
663
|
+
return { itemsSynced: 0, duration: 0, conflicts: [] };
|
|
664
|
+
}
|
|
665
|
+
let itemsSynced = 0;
|
|
666
|
+
const conflicts = [];
|
|
667
|
+
try {
|
|
668
|
+
if (state.status === 'pending') {
|
|
669
|
+
state.status = 'syncing';
|
|
670
|
+
state.phase = 'bulk';
|
|
671
|
+
await this.ctx.storage.put(`eventual:${id}`, state);
|
|
672
|
+
}
|
|
673
|
+
const things = await this.db.select().from(schema.things);
|
|
674
|
+
const branchThings = things.filter(t => t.branch === null && !t.deleted);
|
|
675
|
+
const latestVersions = new Map();
|
|
676
|
+
for (const thing of branchThings) {
|
|
677
|
+
latestVersions.set(thing.id, thing);
|
|
678
|
+
}
|
|
679
|
+
let itemsToSync = Array.from(latestVersions.values());
|
|
680
|
+
if (state.phase !== 'bulk' && state.phase !== 'initial') {
|
|
681
|
+
itemsToSync = itemsToSync.filter((_, idx) => idx >= state.lastSyncedVersion);
|
|
682
|
+
}
|
|
683
|
+
if (state.chunked && itemsToSync.length > state.chunkSize) {
|
|
684
|
+
itemsToSync = itemsToSync.slice(0, state.chunkSize);
|
|
685
|
+
}
|
|
686
|
+
if (this.env.DO && itemsToSync.length > 0) {
|
|
687
|
+
const doId = this.env.DO.idFromName(state.targetNs);
|
|
688
|
+
const stub = this.env.DO.get(doId);
|
|
689
|
+
const response = await stub.fetch(new Request(`https://${state.targetNs}/sync`, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
body: JSON.stringify({
|
|
692
|
+
cloneId: id,
|
|
693
|
+
things: itemsToSync.map(t => ({
|
|
694
|
+
id: t.id,
|
|
695
|
+
type: t.type,
|
|
696
|
+
branch: null,
|
|
697
|
+
name: t.name,
|
|
698
|
+
data: t.data,
|
|
699
|
+
deleted: false,
|
|
700
|
+
})),
|
|
701
|
+
}),
|
|
702
|
+
}));
|
|
703
|
+
if (response.ok) {
|
|
704
|
+
itemsSynced = itemsToSync.length;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
state.itemsSynced += itemsSynced;
|
|
708
|
+
state.lastSyncedVersion += itemsSynced;
|
|
709
|
+
state.lastSyncAt = new Date().toISOString();
|
|
710
|
+
state.itemsRemaining = Math.max(0, state.totalItems - state.itemsSynced);
|
|
711
|
+
state.progress = state.totalItems > 0 ? Math.floor((state.itemsSynced / state.totalItems) * 100) : 100;
|
|
712
|
+
state.divergence = state.itemsRemaining;
|
|
713
|
+
state.errorCount = 0;
|
|
714
|
+
state.lastError = null;
|
|
715
|
+
state.updatedAt = new Date().toISOString();
|
|
716
|
+
if (state.progress >= 100) {
|
|
717
|
+
state.status = 'active';
|
|
718
|
+
state.phase = 'delta';
|
|
719
|
+
await this.emitEvent('clone.active', { id, target: state.targetNs });
|
|
720
|
+
}
|
|
721
|
+
await this.ctx.storage.put(`eventual:${id}`, state);
|
|
722
|
+
const duration = Date.now() - startTime;
|
|
723
|
+
return { itemsSynced, duration, conflicts };
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
state.errorCount++;
|
|
727
|
+
state.lastError = error.message;
|
|
728
|
+
state.updatedAt = new Date().toISOString();
|
|
729
|
+
if (state.errorCount >= 10) {
|
|
730
|
+
state.status = 'error';
|
|
731
|
+
await this.emitEvent('clone.error', { id, error: state.lastError });
|
|
732
|
+
}
|
|
733
|
+
await this.ctx.storage.put(`eventual:${id}`, state);
|
|
734
|
+
return { itemsSynced: 0, duration: Date.now() - startTime, conflicts: [] };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
738
|
+
// RESUMABLE CLONE OPERATIONS
|
|
739
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
740
|
+
_resumableClones = new Map();
|
|
741
|
+
_cloneLocks = new Map();
|
|
742
|
+
async initiateResumableClone(target, options) {
|
|
743
|
+
const batchSize = options?.batchSize || 100;
|
|
744
|
+
const checkpointInterval = options?.checkpointInterval || 1;
|
|
745
|
+
const maxRetries = options?.maxRetries || 3;
|
|
746
|
+
const retryDelay = options?.retryDelay || 1000;
|
|
747
|
+
const lockTimeout = options?.lockTimeout || 300000;
|
|
748
|
+
const checkpointRetentionMs = options?.checkpointRetentionMs || 3600000;
|
|
749
|
+
const compress = options?.compress || false;
|
|
750
|
+
const maxBandwidth = options?.maxBandwidth;
|
|
751
|
+
// Validate target namespace URL
|
|
752
|
+
try {
|
|
753
|
+
new URL(target);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
throw new Error(`Invalid namespace URL: ${target}`);
|
|
757
|
+
}
|
|
758
|
+
// Check for existing lock
|
|
759
|
+
const existingLock = this._cloneLocks.get(target) || await this.ctx.storage.get(`clone-lock:${target}`);
|
|
760
|
+
const now = Date.now();
|
|
761
|
+
if (existingLock && !existingLock.isStale && new Date(existingLock.expiresAt).getTime() > now) {
|
|
762
|
+
if (!options?.forceLock) {
|
|
763
|
+
throw new Error(`Clone operation already in progress for target: ${target}`);
|
|
764
|
+
}
|
|
765
|
+
await this.releaseCloneLock(target, existingLock.cloneId);
|
|
766
|
+
}
|
|
767
|
+
const cloneId = crypto.randomUUID();
|
|
768
|
+
const state = {
|
|
769
|
+
id: cloneId,
|
|
770
|
+
targetNs: target,
|
|
771
|
+
status: 'initializing',
|
|
772
|
+
checkpoints: [],
|
|
773
|
+
position: 0,
|
|
774
|
+
batchSize,
|
|
775
|
+
checkpointInterval,
|
|
776
|
+
maxRetries,
|
|
777
|
+
retryDelay,
|
|
778
|
+
retryCount: 0,
|
|
779
|
+
compress,
|
|
780
|
+
maxBandwidth,
|
|
781
|
+
checkpointRetentionMs,
|
|
782
|
+
pauseRequested: false,
|
|
783
|
+
cancelRequested: false,
|
|
784
|
+
createdAt: new Date(),
|
|
785
|
+
bytesTransferred: 0,
|
|
786
|
+
totalBytes: 0,
|
|
787
|
+
startedAt: null,
|
|
788
|
+
};
|
|
789
|
+
// Acquire lock
|
|
790
|
+
const lockId = crypto.randomUUID();
|
|
791
|
+
const lock = {
|
|
792
|
+
lockId,
|
|
793
|
+
cloneId,
|
|
794
|
+
target,
|
|
795
|
+
acquiredAt: new Date(),
|
|
796
|
+
expiresAt: new Date(now + lockTimeout),
|
|
797
|
+
isStale: false,
|
|
798
|
+
};
|
|
799
|
+
this._cloneLocks.set(target, lock);
|
|
800
|
+
await this.ctx.storage.put(`clone-lock:${target}`, lock);
|
|
801
|
+
await this.emitEvent('clone.lock.acquired', { lockId, target, cloneId });
|
|
802
|
+
this._resumableClones.set(cloneId, state);
|
|
803
|
+
await this.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
804
|
+
// Schedule the clone
|
|
805
|
+
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
806
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
807
|
+
await this.ctx.storage.setAlarm(Date.now() + 100);
|
|
808
|
+
}
|
|
809
|
+
return this.createResumableCloneHandle(cloneId);
|
|
810
|
+
}
|
|
811
|
+
createResumableCloneHandle(cloneId) {
|
|
812
|
+
const self = this;
|
|
813
|
+
return {
|
|
814
|
+
id: cloneId,
|
|
815
|
+
get status() {
|
|
816
|
+
const state = self._resumableClones.get(cloneId);
|
|
817
|
+
return state?.status || 'failed';
|
|
818
|
+
},
|
|
819
|
+
get checkpoints() {
|
|
820
|
+
const state = self._resumableClones.get(cloneId);
|
|
821
|
+
return state?.checkpoints || [];
|
|
822
|
+
},
|
|
823
|
+
async getProgress() {
|
|
824
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
825
|
+
return state?.progress || 0;
|
|
826
|
+
},
|
|
827
|
+
async pause() {
|
|
828
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
829
|
+
if (!state)
|
|
830
|
+
throw new Error('Clone not found');
|
|
831
|
+
state.pauseRequested = true;
|
|
832
|
+
state.status = 'paused';
|
|
833
|
+
await self.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
834
|
+
self._resumableClones.set(cloneId, state);
|
|
835
|
+
await self.emitEvent('clone.paused', { id: cloneId, progress: state.progress });
|
|
836
|
+
},
|
|
837
|
+
async resume() {
|
|
838
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
839
|
+
if (!state)
|
|
840
|
+
throw new Error('Clone not found');
|
|
841
|
+
state.pauseRequested = false;
|
|
842
|
+
state.status = 'transferring';
|
|
843
|
+
await self.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
844
|
+
self._resumableClones.set(cloneId, state);
|
|
845
|
+
await self.emitEvent('clone.resumed', { id: cloneId });
|
|
846
|
+
const currentAlarm = await self.ctx.storage.getAlarm();
|
|
847
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
848
|
+
await self.ctx.storage.setAlarm(Date.now() + 100);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
async cancel() {
|
|
852
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
853
|
+
if (!state)
|
|
854
|
+
throw new Error('Clone not found');
|
|
855
|
+
state.cancelRequested = true;
|
|
856
|
+
state.status = 'cancelled';
|
|
857
|
+
await self.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
858
|
+
self._resumableClones.set(cloneId, state);
|
|
859
|
+
await self.releaseCloneLock(state.targetNs, cloneId);
|
|
860
|
+
await self.emitEvent('clone.cancelled', { id: cloneId, progress: state.progress });
|
|
861
|
+
},
|
|
862
|
+
async waitForCheckpoint() {
|
|
863
|
+
const pollInterval = 50;
|
|
864
|
+
const maxWait = 60000;
|
|
865
|
+
const startTime = Date.now();
|
|
866
|
+
return new Promise((resolve, reject) => {
|
|
867
|
+
const poll = async () => {
|
|
868
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
869
|
+
if (!state) {
|
|
870
|
+
reject(new Error('Clone not found'));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (state.checkpoints.length > 0) {
|
|
874
|
+
resolve(state.checkpoints[state.checkpoints.length - 1]);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (Date.now() - startTime > maxWait) {
|
|
878
|
+
reject(new Error('Timeout waiting for checkpoint'));
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
setTimeout(poll, pollInterval);
|
|
882
|
+
};
|
|
883
|
+
poll();
|
|
884
|
+
});
|
|
885
|
+
},
|
|
886
|
+
async canResumeFrom(checkpointId) {
|
|
887
|
+
const checkpoint = await self.ctx.storage.get(`checkpoint:${checkpointId}`);
|
|
888
|
+
if (!checkpoint)
|
|
889
|
+
return false;
|
|
890
|
+
if (!checkpoint.hash || !/^[a-f0-9]{64}$/.test(checkpoint.hash))
|
|
891
|
+
return false;
|
|
892
|
+
if (checkpoint.position < 0)
|
|
893
|
+
return false;
|
|
894
|
+
return true;
|
|
895
|
+
},
|
|
896
|
+
async getIntegrityHash() {
|
|
897
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
898
|
+
if (!state || state.checkpoints.length === 0)
|
|
899
|
+
return '';
|
|
900
|
+
return state.checkpoints[state.checkpoints.length - 1].hash;
|
|
901
|
+
},
|
|
902
|
+
async getLockInfo() {
|
|
903
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
904
|
+
if (!state)
|
|
905
|
+
return null;
|
|
906
|
+
const lock = self._cloneLocks.get(state.targetNs) || await self.ctx.storage.get(`clone-lock:${state.targetNs}`);
|
|
907
|
+
if (!lock || lock.cloneId !== cloneId)
|
|
908
|
+
return null;
|
|
909
|
+
return {
|
|
910
|
+
lockId: lock.lockId,
|
|
911
|
+
cloneId: lock.cloneId,
|
|
912
|
+
acquiredAt: new Date(lock.acquiredAt),
|
|
913
|
+
expiresAt: new Date(lock.expiresAt),
|
|
914
|
+
isStale: lock.isStale || new Date(lock.expiresAt).getTime() < Date.now(),
|
|
915
|
+
};
|
|
916
|
+
},
|
|
917
|
+
async forceOverrideLock() {
|
|
918
|
+
const state = await self.ctx.storage.get(`resumable:${cloneId}`);
|
|
919
|
+
if (!state)
|
|
920
|
+
throw new Error('Clone not found');
|
|
921
|
+
await self.releaseCloneLock(state.targetNs, cloneId);
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
async releaseCloneLock(target, cloneId) {
|
|
926
|
+
const lock = this._cloneLocks.get(target) || await this.ctx.storage.get(`clone-lock:${target}`);
|
|
927
|
+
if (lock && lock.cloneId === cloneId) {
|
|
928
|
+
this._cloneLocks.delete(target);
|
|
929
|
+
await this.ctx.storage.delete(`clone-lock:${target}`);
|
|
930
|
+
await this.emitEvent('clone.lock.released', { lockId: lock.lockId, target });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
934
|
+
// PROMOTE / DEMOTE
|
|
935
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
936
|
+
/**
|
|
937
|
+
* Promote a Thing to its own DO
|
|
938
|
+
*/
|
|
939
|
+
async promote(thingId, options) {
|
|
940
|
+
const { targetNs: customNs, preserveHistory = true, mode = 'atomic' } = options ?? {};
|
|
941
|
+
// Get the Thing
|
|
942
|
+
const thing = await this.things.get(thingId);
|
|
943
|
+
if (!thing) {
|
|
944
|
+
throw new Error(`Thing not found: ${thingId}`);
|
|
945
|
+
}
|
|
946
|
+
// Generate target namespace
|
|
947
|
+
const targetNs = customNs || `https://${thingId}.do`;
|
|
948
|
+
// Validate target URL
|
|
949
|
+
try {
|
|
950
|
+
new URL(targetNs);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
throw new Error(`Invalid target URL: ${targetNs}`);
|
|
954
|
+
}
|
|
955
|
+
// Cannot promote to self
|
|
956
|
+
if (targetNs === this.ns) {
|
|
957
|
+
throw new Error('Cannot promote to self');
|
|
958
|
+
}
|
|
959
|
+
// Check if DO binding is available
|
|
960
|
+
if (!this.env.DO) {
|
|
961
|
+
throw new Error('DO binding is unavailable');
|
|
962
|
+
}
|
|
963
|
+
await this.emitEvent('promote.started', { thingId, targetNs, mode });
|
|
964
|
+
try {
|
|
965
|
+
// Create new DO and transfer state
|
|
966
|
+
const doId = this.env.DO.idFromName(targetNs);
|
|
967
|
+
const stub = this.env.DO.get(doId);
|
|
968
|
+
// Initialize the new DO
|
|
969
|
+
const initResponse = await stub.fetch(new Request(`${targetNs}/initialize`, {
|
|
970
|
+
method: 'POST',
|
|
971
|
+
headers: { 'Content-Type': 'application/json' },
|
|
972
|
+
body: JSON.stringify({
|
|
973
|
+
ns: targetNs,
|
|
974
|
+
parent: this.ns,
|
|
975
|
+
}),
|
|
976
|
+
}));
|
|
977
|
+
if (!initResponse.ok) {
|
|
978
|
+
throw new Error(`Failed to initialize target DO: ${initResponse.status}`);
|
|
979
|
+
}
|
|
980
|
+
// Transfer the Thing state
|
|
981
|
+
const transferResponse = await stub.fetch(new Request(`${targetNs}/clone-receive`, {
|
|
982
|
+
method: 'POST',
|
|
983
|
+
headers: { 'Content-Type': 'application/json' },
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
things: [{
|
|
986
|
+
id: 'root',
|
|
987
|
+
$type: thing.$type,
|
|
988
|
+
branch: null,
|
|
989
|
+
name: thing.name,
|
|
990
|
+
data: thing.data,
|
|
991
|
+
deleted: false,
|
|
992
|
+
}],
|
|
993
|
+
relationships: [],
|
|
994
|
+
sourceNs: this.ns,
|
|
995
|
+
promotedFrom: thingId,
|
|
996
|
+
}),
|
|
997
|
+
}));
|
|
998
|
+
if (!transferResponse.ok) {
|
|
999
|
+
throw new Error(`Failed to transfer state: ${transferResponse.status}`);
|
|
1000
|
+
}
|
|
1001
|
+
// Mark original Thing as promoted
|
|
1002
|
+
await this.things.update(thingId, {
|
|
1003
|
+
data: {
|
|
1004
|
+
...thing.data,
|
|
1005
|
+
$promotedTo: targetNs,
|
|
1006
|
+
$promotedAt: new Date().toISOString(),
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
await this.emitEvent('promote.completed', { thingId, targetNs });
|
|
1010
|
+
return {
|
|
1011
|
+
newNs: targetNs,
|
|
1012
|
+
thingId,
|
|
1013
|
+
originalNs: this.ns,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
await this.emitEvent('promote.failed', { thingId, targetNs, error: error.message });
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Demote this DO back into a parent DO as a Thing
|
|
1023
|
+
*/
|
|
1024
|
+
async demote(targetNs, options) {
|
|
1025
|
+
if (!targetNs || typeof targetNs !== 'string' || targetNs.trim() === '') {
|
|
1026
|
+
throw new Error('targetNs is required for demote');
|
|
1027
|
+
}
|
|
1028
|
+
const { thingId: customThingId, preserveHistory = true, type, force = false, compress = false, mode = 'atomic', preserveId = false, } = options ?? {};
|
|
1029
|
+
// Validate URL format
|
|
1030
|
+
try {
|
|
1031
|
+
new URL(targetNs);
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
throw new Error(`Invalid target URL: ${targetNs}`);
|
|
1035
|
+
}
|
|
1036
|
+
// Cannot demote to self
|
|
1037
|
+
if (targetNs === this.ns) {
|
|
1038
|
+
throw new Error('Cannot demote to self');
|
|
1039
|
+
}
|
|
1040
|
+
// Check if DO binding is available
|
|
1041
|
+
if (!this.env.DO) {
|
|
1042
|
+
throw new Error('DO binding is unavailable');
|
|
1043
|
+
}
|
|
1044
|
+
// Validate type if provided
|
|
1045
|
+
if (type) {
|
|
1046
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(type) || type.length > 25) {
|
|
1047
|
+
throw new Error(`Invalid type: "${type}"`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
await this.emitEvent('demote.started', { targetNs, sourceNs: this.ns, mode });
|
|
1051
|
+
try {
|
|
1052
|
+
// Get all state from this DO
|
|
1053
|
+
const things = await this.db.select().from(schema.things);
|
|
1054
|
+
const actions = await this.db.select().from(schema.actions);
|
|
1055
|
+
const events = await this.db.select().from(schema.events);
|
|
1056
|
+
const relationships = await this.db.select().from(schema.relationships);
|
|
1057
|
+
const activeThings = things.filter(t => !t.deleted);
|
|
1058
|
+
// Generate new thing ID
|
|
1059
|
+
const newThingId = customThingId
|
|
1060
|
+
? customThingId
|
|
1061
|
+
: preserveId
|
|
1062
|
+
? this.ns.replace(/^https?:\/\//, '').replace(/\.do$/, '')
|
|
1063
|
+
: `demoted-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1064
|
+
// Prepare transfer payload
|
|
1065
|
+
const transferPayload = {
|
|
1066
|
+
things: activeThings.length > 0 ? activeThings.map(t => ({
|
|
1067
|
+
...t,
|
|
1068
|
+
id: t.id === 'root' ? newThingId : `${newThingId}/${t.id}`,
|
|
1069
|
+
})) : [{
|
|
1070
|
+
id: newThingId,
|
|
1071
|
+
type: 0,
|
|
1072
|
+
branch: null,
|
|
1073
|
+
name: `Demoted from ${this.ns}`,
|
|
1074
|
+
data: { $demotedFrom: this.ns },
|
|
1075
|
+
deleted: false,
|
|
1076
|
+
}],
|
|
1077
|
+
actions: compress ? [] : actions,
|
|
1078
|
+
events: compress ? [] : events,
|
|
1079
|
+
relationships: relationships.map(r => ({
|
|
1080
|
+
...r,
|
|
1081
|
+
from: r.from?.replace(this.ns, `${targetNs}/Thing/${newThingId}`),
|
|
1082
|
+
to: r.to?.replace(this.ns, `${targetNs}/Thing/${newThingId}`),
|
|
1083
|
+
})),
|
|
1084
|
+
demotedFrom: {
|
|
1085
|
+
ns: this.ns,
|
|
1086
|
+
thingsCount: activeThings.length,
|
|
1087
|
+
compress,
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
// Transfer to parent DO
|
|
1091
|
+
const parentId = this.env.DO.idFromName(targetNs);
|
|
1092
|
+
const parentStub = this.env.DO.get(parentId);
|
|
1093
|
+
const response = await parentStub.fetch(new Request(`${targetNs}/transfer`, {
|
|
1094
|
+
method: 'POST',
|
|
1095
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1096
|
+
body: JSON.stringify(transferPayload),
|
|
1097
|
+
}));
|
|
1098
|
+
if (!response.ok) {
|
|
1099
|
+
throw new Error(`Transfer to ${targetNs} failed: ${response.status}`);
|
|
1100
|
+
}
|
|
1101
|
+
// Clear local state
|
|
1102
|
+
await this.ctx.storage.sql.exec('DELETE FROM things');
|
|
1103
|
+
await this.ctx.storage.sql.exec('DELETE FROM actions');
|
|
1104
|
+
await this.ctx.storage.sql.exec('DELETE FROM events');
|
|
1105
|
+
await this.ctx.storage.sql.exec('DELETE FROM relationships');
|
|
1106
|
+
await this.emitEvent('demote.completed', {
|
|
1107
|
+
thingId: newThingId,
|
|
1108
|
+
parentNs: targetNs,
|
|
1109
|
+
deletedNs: this.ns,
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
thingId: newThingId,
|
|
1113
|
+
parentNs: targetNs,
|
|
1114
|
+
deletedNs: this.ns,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
await this.emitEvent('demote.failed', { targetNs, error: error.message });
|
|
1119
|
+
throw error;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1123
|
+
// BRANCHING & VERSION CONTROL
|
|
1124
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1125
|
+
/**
|
|
1126
|
+
* Create a new branch at current HEAD
|
|
1127
|
+
*/
|
|
1128
|
+
async branch(name) {
|
|
1129
|
+
if (!name || name.trim() === '') {
|
|
1130
|
+
throw new Error('Branch name cannot be empty');
|
|
1131
|
+
}
|
|
1132
|
+
if (name.includes(' ')) {
|
|
1133
|
+
throw new Error('Branch name cannot contain spaces');
|
|
1134
|
+
}
|
|
1135
|
+
if (name === 'main') {
|
|
1136
|
+
throw new Error('Cannot create branch named "main" - it is reserved');
|
|
1137
|
+
}
|
|
1138
|
+
// Check if branch already exists
|
|
1139
|
+
const branches = await this.db.select().from(schema.branches);
|
|
1140
|
+
if (branches.some(b => b.name === name)) {
|
|
1141
|
+
throw new Error(`Branch "${name}" already exists`);
|
|
1142
|
+
}
|
|
1143
|
+
// Find current HEAD
|
|
1144
|
+
const things = await this.db.select().from(schema.things);
|
|
1145
|
+
const currentBranchThings = things.filter(t => this.currentBranch === 'main' ? t.branch === null : t.branch === this.currentBranch);
|
|
1146
|
+
if (currentBranchThings.length === 0) {
|
|
1147
|
+
throw new Error('No commits on current branch');
|
|
1148
|
+
}
|
|
1149
|
+
const head = currentBranchThings.length;
|
|
1150
|
+
// Create branch record
|
|
1151
|
+
await this.db.insert(schema.branches).values({
|
|
1152
|
+
name,
|
|
1153
|
+
thingId: currentBranchThings[0].id,
|
|
1154
|
+
head,
|
|
1155
|
+
base: head,
|
|
1156
|
+
forkedFrom: this.currentBranch,
|
|
1157
|
+
description: null,
|
|
1158
|
+
createdAt: new Date(),
|
|
1159
|
+
updatedAt: new Date(),
|
|
1160
|
+
});
|
|
1161
|
+
await this.emitEvent('branch.created', { name, head, forkedFrom: this.currentBranch });
|
|
1162
|
+
return { name, head };
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Switch to a branch or version
|
|
1166
|
+
*/
|
|
1167
|
+
async checkout(ref) {
|
|
1168
|
+
const things = await this.db.select().from(schema.things);
|
|
1169
|
+
let targetRef = ref.startsWith('@') ? ref.slice(1) : ref;
|
|
1170
|
+
// Check for version reference (@v1234)
|
|
1171
|
+
if (targetRef.startsWith('v')) {
|
|
1172
|
+
const version = parseInt(targetRef.slice(1), 10);
|
|
1173
|
+
if (version < 1 || version > things.length) {
|
|
1174
|
+
throw new Error(`Version not found: ${version}`);
|
|
1175
|
+
}
|
|
1176
|
+
this.currentVersion = version;
|
|
1177
|
+
await this.emitEvent('checkout', { version });
|
|
1178
|
+
return { version };
|
|
1179
|
+
}
|
|
1180
|
+
// Check for relative reference (@~N)
|
|
1181
|
+
if (targetRef.startsWith('~')) {
|
|
1182
|
+
const offset = parseInt(targetRef.slice(1), 10);
|
|
1183
|
+
const currentBranchThings = things.filter(t => this.currentBranch === 'main' ? t.branch === null : t.branch === this.currentBranch);
|
|
1184
|
+
if (offset >= currentBranchThings.length) {
|
|
1185
|
+
throw new Error(`Cannot go back ${offset} versions - only ${currentBranchThings.length} versions exist`);
|
|
1186
|
+
}
|
|
1187
|
+
const version = currentBranchThings.length - offset;
|
|
1188
|
+
this.currentVersion = version;
|
|
1189
|
+
await this.emitEvent('checkout', { version, relative: `~${offset}` });
|
|
1190
|
+
return { version };
|
|
1191
|
+
}
|
|
1192
|
+
// Branch reference
|
|
1193
|
+
const branchName = targetRef;
|
|
1194
|
+
if (branchName === 'main') {
|
|
1195
|
+
this.currentBranch = 'main';
|
|
1196
|
+
this.currentVersion = null;
|
|
1197
|
+
await this.emitEvent('checkout', { branch: 'main' });
|
|
1198
|
+
return { branch: 'main' };
|
|
1199
|
+
}
|
|
1200
|
+
const branches = await this.db.select().from(schema.branches);
|
|
1201
|
+
const branchExists = branches.some(b => b.name === branchName);
|
|
1202
|
+
const thingsOnBranch = things.filter(t => t.branch === branchName);
|
|
1203
|
+
if (!branchExists && thingsOnBranch.length === 0) {
|
|
1204
|
+
throw new Error(`Branch not found: ${branchName}`);
|
|
1205
|
+
}
|
|
1206
|
+
this.currentBranch = branchName;
|
|
1207
|
+
this.currentVersion = null;
|
|
1208
|
+
await this.emitEvent('checkout', { branch: branchName });
|
|
1209
|
+
return { branch: branchName };
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Merge a branch into current
|
|
1213
|
+
*/
|
|
1214
|
+
async merge(branch) {
|
|
1215
|
+
if (this.currentVersion !== null) {
|
|
1216
|
+
throw new Error('Cannot merge into detached HEAD state');
|
|
1217
|
+
}
|
|
1218
|
+
if (branch === this.currentBranch || (branch === 'main' && this.currentBranch === 'main')) {
|
|
1219
|
+
throw new Error('Cannot merge branch into itself');
|
|
1220
|
+
}
|
|
1221
|
+
const things = await this.db.select().from(schema.things);
|
|
1222
|
+
const branches = await this.db.select().from(schema.branches);
|
|
1223
|
+
const sourceBranch = branches.find(b => b.name === branch);
|
|
1224
|
+
const sourceThings = things.filter(t => t.branch === branch);
|
|
1225
|
+
if (!sourceBranch && sourceThings.length === 0) {
|
|
1226
|
+
throw new Error(`Branch not found: ${branch}`);
|
|
1227
|
+
}
|
|
1228
|
+
await this.emitEvent('merge.started', { source: branch, target: this.currentBranch });
|
|
1229
|
+
const targetBranchFilter = this.currentBranch === 'main' ? null : this.currentBranch;
|
|
1230
|
+
const targetThings = things.filter(t => t.branch === targetBranchFilter);
|
|
1231
|
+
// Group by id
|
|
1232
|
+
const sourceById = new Map();
|
|
1233
|
+
for (const t of sourceThings) {
|
|
1234
|
+
const group = sourceById.get(t.id) || [];
|
|
1235
|
+
group.push(t);
|
|
1236
|
+
sourceById.set(t.id, group);
|
|
1237
|
+
}
|
|
1238
|
+
const targetById = new Map();
|
|
1239
|
+
for (const t of targetThings) {
|
|
1240
|
+
const group = targetById.get(t.id) || [];
|
|
1241
|
+
group.push(t);
|
|
1242
|
+
targetById.set(t.id, group);
|
|
1243
|
+
}
|
|
1244
|
+
// Detect conflicts
|
|
1245
|
+
const conflicts = [];
|
|
1246
|
+
const toMerge = [];
|
|
1247
|
+
for (const [id, sourceVersions] of sourceById) {
|
|
1248
|
+
const latestSource = sourceVersions[sourceVersions.length - 1];
|
|
1249
|
+
const targetVersions = targetById.get(id) || [];
|
|
1250
|
+
if (targetVersions.length === 0) {
|
|
1251
|
+
toMerge.push({
|
|
1252
|
+
...latestSource,
|
|
1253
|
+
branch: targetBranchFilter,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
const latestTarget = targetVersions[targetVersions.length - 1];
|
|
1258
|
+
const sourceData = (latestSource.data || {});
|
|
1259
|
+
const targetData = (latestTarget.data || {});
|
|
1260
|
+
const baseVersion = targetVersions[0] || sourceVersions[0];
|
|
1261
|
+
const baseData = (baseVersion?.data || {});
|
|
1262
|
+
const sourceChanges = new Set();
|
|
1263
|
+
const targetChanges = new Set();
|
|
1264
|
+
for (const key of Object.keys(sourceData)) {
|
|
1265
|
+
if (JSON.stringify(sourceData[key]) !== JSON.stringify(baseData[key])) {
|
|
1266
|
+
sourceChanges.add(key);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
for (const key of Object.keys(targetData)) {
|
|
1270
|
+
if (JSON.stringify(targetData[key]) !== JSON.stringify(baseData[key])) {
|
|
1271
|
+
targetChanges.add(key);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const conflictingFields = [];
|
|
1275
|
+
for (const field of sourceChanges) {
|
|
1276
|
+
if (targetChanges.has(field) &&
|
|
1277
|
+
JSON.stringify(sourceData[field]) !== JSON.stringify(targetData[field])) {
|
|
1278
|
+
conflictingFields.push(field);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (conflictingFields.length > 0) {
|
|
1282
|
+
conflicts.push(`${id}:${conflictingFields.join(',')}`);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
const mergedData = { ...baseData };
|
|
1286
|
+
for (const field of sourceChanges) {
|
|
1287
|
+
mergedData[field] = sourceData[field];
|
|
1288
|
+
}
|
|
1289
|
+
for (const field of targetChanges) {
|
|
1290
|
+
mergedData[field] = targetData[field];
|
|
1291
|
+
}
|
|
1292
|
+
if (latestSource.deleted || latestTarget.deleted || Object.keys(mergedData).length > 0) {
|
|
1293
|
+
toMerge.push({
|
|
1294
|
+
...latestTarget,
|
|
1295
|
+
data: mergedData,
|
|
1296
|
+
deleted: latestSource.deleted || latestTarget.deleted,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (conflicts.length > 0) {
|
|
1303
|
+
await this.emitEvent('merge.conflict', { source: branch, conflicts });
|
|
1304
|
+
return { merged: false, conflicts };
|
|
1305
|
+
}
|
|
1306
|
+
// Apply merge
|
|
1307
|
+
for (const thing of toMerge) {
|
|
1308
|
+
await this.db.insert(schema.things).values({
|
|
1309
|
+
id: thing.id,
|
|
1310
|
+
type: thing.type,
|
|
1311
|
+
branch: thing.branch,
|
|
1312
|
+
name: thing.name,
|
|
1313
|
+
data: thing.data,
|
|
1314
|
+
deleted: thing.deleted,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
await this.emitEvent('merge.completed', { source: branch, target: this.currentBranch, merged: toMerge.length });
|
|
1318
|
+
return { merged: true };
|
|
1319
|
+
}
|
|
1320
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1321
|
+
// ALARM HANDLER (Extended)
|
|
1322
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1323
|
+
async alarm() {
|
|
1324
|
+
// Handle eventual clone syncing
|
|
1325
|
+
await this.handleEventualCloneAlarms();
|
|
1326
|
+
// Handle resumable clones
|
|
1327
|
+
await this.processResumableClones();
|
|
1328
|
+
// Call parent alarm handler for scheduling
|
|
1329
|
+
await super.alarm();
|
|
1330
|
+
}
|
|
1331
|
+
async handleEventualCloneAlarms() {
|
|
1332
|
+
const keys = await this.ctx.storage.list({ prefix: 'eventual:' });
|
|
1333
|
+
let nextAlarmTime = null;
|
|
1334
|
+
for (const [key, value] of keys) {
|
|
1335
|
+
const state = value;
|
|
1336
|
+
if (state.status === 'cancelled' || state.status === 'paused' || state.status === 'error') {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const lastSync = state.lastSyncAt ? new Date(state.lastSyncAt).getTime() : 0;
|
|
1340
|
+
const now = Date.now();
|
|
1341
|
+
const nextSync = lastSync + state.syncInterval;
|
|
1342
|
+
const needsSync = now >= nextSync || state.divergence > state.maxDivergence;
|
|
1343
|
+
if (needsSync || state.status === 'pending') {
|
|
1344
|
+
await this.performEventualSync(state.id);
|
|
1345
|
+
const updatedState = await this.ctx.storage.get(`eventual:${state.id}`);
|
|
1346
|
+
if (updatedState && updatedState.status !== 'active' && updatedState.status !== 'cancelled' && updatedState.status !== 'error') {
|
|
1347
|
+
const nextSyncTime = Date.now() + updatedState.syncInterval;
|
|
1348
|
+
if (!nextAlarmTime || nextSyncTime < nextAlarmTime) {
|
|
1349
|
+
nextAlarmTime = nextSyncTime;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
if (!nextAlarmTime || nextSync < nextAlarmTime) {
|
|
1355
|
+
nextAlarmTime = nextSync;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (nextAlarmTime) {
|
|
1360
|
+
await this.ctx.storage.setAlarm(nextAlarmTime);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async processResumableClones() {
|
|
1364
|
+
const storageKeys = await this.ctx.storage.list({ prefix: 'resumable:' });
|
|
1365
|
+
for (const [key, value] of storageKeys) {
|
|
1366
|
+
const cloneId = key.replace('resumable:', '');
|
|
1367
|
+
if (!this._resumableClones.has(cloneId)) {
|
|
1368
|
+
this._resumableClones.set(cloneId, value);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
for (const [cloneId, state] of this._resumableClones) {
|
|
1372
|
+
if (state.status === 'paused' || state.pauseRequested)
|
|
1373
|
+
continue;
|
|
1374
|
+
if (state.status === 'cancelled' || state.cancelRequested)
|
|
1375
|
+
continue;
|
|
1376
|
+
if (state.status === 'completed' || state.status === 'failed')
|
|
1377
|
+
continue;
|
|
1378
|
+
await this.processResumableCloneBatch(cloneId);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async processResumableCloneBatch(cloneId) {
|
|
1382
|
+
const state = this._resumableClones.get(cloneId) || await this.ctx.storage.get(`resumable:${cloneId}`);
|
|
1383
|
+
if (!state)
|
|
1384
|
+
return;
|
|
1385
|
+
if (state.pauseRequested || state.status === 'paused')
|
|
1386
|
+
return;
|
|
1387
|
+
if (state.cancelRequested || state.status === 'cancelled')
|
|
1388
|
+
return;
|
|
1389
|
+
if (state.status === 'initializing') {
|
|
1390
|
+
state.status = 'transferring';
|
|
1391
|
+
state.startedAt = new Date();
|
|
1392
|
+
}
|
|
1393
|
+
try {
|
|
1394
|
+
const allThings = await this.db.select().from(schema.things);
|
|
1395
|
+
const nonDeletedThings = allThings.filter(t => !t.deleted);
|
|
1396
|
+
const totalItems = nonDeletedThings.length;
|
|
1397
|
+
if (state.totalBytes === 0) {
|
|
1398
|
+
state.totalBytes = nonDeletedThings.reduce((acc, t) => acc + JSON.stringify(t).length, 0);
|
|
1399
|
+
}
|
|
1400
|
+
const batch = nonDeletedThings.slice(state.position, state.position + state.batchSize);
|
|
1401
|
+
if (batch.length === 0) {
|
|
1402
|
+
state.status = 'completed';
|
|
1403
|
+
await this.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1404
|
+
this._resumableClones.set(cloneId, state);
|
|
1405
|
+
await this.releaseCloneLock(state.targetNs, cloneId);
|
|
1406
|
+
await this.emitEvent('clone.completed', { id: cloneId, totalItems: state.position });
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
// Transfer batch (simulated)
|
|
1410
|
+
state.position += batch.length;
|
|
1411
|
+
state.progress = Math.round((state.position / totalItems) * 100);
|
|
1412
|
+
state.bytesTransferred += batch.reduce((acc, t) => acc + JSON.stringify(t).length, 0);
|
|
1413
|
+
// Create checkpoint
|
|
1414
|
+
const batchNumber = Math.ceil(state.position / state.batchSize);
|
|
1415
|
+
if (batchNumber % state.checkpointInterval === 0) {
|
|
1416
|
+
const batchJson = JSON.stringify(batch);
|
|
1417
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(batchJson));
|
|
1418
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1419
|
+
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1420
|
+
const checkpoint = {
|
|
1421
|
+
id: crypto.randomUUID(),
|
|
1422
|
+
position: state.position,
|
|
1423
|
+
hash,
|
|
1424
|
+
timestamp: new Date(),
|
|
1425
|
+
itemsProcessed: state.position,
|
|
1426
|
+
batchNumber,
|
|
1427
|
+
cloneId,
|
|
1428
|
+
compressed: state.compress,
|
|
1429
|
+
};
|
|
1430
|
+
state.checkpoints.push(checkpoint);
|
|
1431
|
+
await this.ctx.storage.put(`checkpoint:${checkpoint.id}`, checkpoint);
|
|
1432
|
+
await this.emitEvent('clone.checkpoint', { id: cloneId, checkpoint });
|
|
1433
|
+
}
|
|
1434
|
+
state.retryCount = 0;
|
|
1435
|
+
await this.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1436
|
+
this._resumableClones.set(cloneId, state);
|
|
1437
|
+
if (state.position < totalItems && !state.pauseRequested && !state.cancelRequested) {
|
|
1438
|
+
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
1439
|
+
if (!currentAlarm || currentAlarm > Date.now() + 100) {
|
|
1440
|
+
await this.ctx.storage.setAlarm(Date.now() + 100);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
state.retryCount++;
|
|
1446
|
+
await this.emitEvent('clone.retry', { id: cloneId, attempt: state.retryCount, error: error.message });
|
|
1447
|
+
if (state.retryCount >= state.maxRetries) {
|
|
1448
|
+
state.status = 'failed';
|
|
1449
|
+
await this.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1450
|
+
this._resumableClones.set(cloneId, state);
|
|
1451
|
+
await this.emitEvent('clone.failed', { id: cloneId, error: error.message });
|
|
1452
|
+
await this.releaseCloneLock(state.targetNs, cloneId);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const delay = state.retryDelay * Math.pow(2, state.retryCount - 1);
|
|
1456
|
+
await this.ctx.storage.put(`resumable:${cloneId}`, state);
|
|
1457
|
+
this._resumableClones.set(cloneId, state);
|
|
1458
|
+
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
1459
|
+
if (!currentAlarm || currentAlarm > Date.now() + delay) {
|
|
1460
|
+
await this.ctx.storage.setAlarm(Date.now() + delay);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1465
|
+
// CROSS-DO RESOLUTION (with circuit breaker)
|
|
1466
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1467
|
+
async resolveCrossDO(ns, path, ref) {
|
|
1468
|
+
// Check circuit breaker state
|
|
1469
|
+
const circuitState = this._circuitBreaker.get(ns);
|
|
1470
|
+
if (circuitState) {
|
|
1471
|
+
const now = Date.now();
|
|
1472
|
+
if (circuitState.state === 'open') {
|
|
1473
|
+
if (now >= circuitState.openUntil) {
|
|
1474
|
+
circuitState.state = 'half-open';
|
|
1475
|
+
circuitState.halfOpenTestInProgress = false;
|
|
1476
|
+
this._circuitBreaker.set(ns, circuitState);
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
throw new Error(`Circuit breaker open for namespace: ${ns}`);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (circuitState.state === 'half-open') {
|
|
1483
|
+
if (circuitState.halfOpenTestInProgress) {
|
|
1484
|
+
throw new Error('Circuit breaker in half-open test');
|
|
1485
|
+
}
|
|
1486
|
+
circuitState.halfOpenTestInProgress = true;
|
|
1487
|
+
this._circuitBreaker.set(ns, circuitState);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const obj = await this.objects.get(ns);
|
|
1491
|
+
if (!obj) {
|
|
1492
|
+
throw new Error(`Unknown namespace: ${ns}`);
|
|
1493
|
+
}
|
|
1494
|
+
if (!this.env.DO) {
|
|
1495
|
+
throw new Error('DO namespace binding not configured');
|
|
1496
|
+
}
|
|
1497
|
+
const stub = this.getOrCreateStub(ns, obj.id);
|
|
1498
|
+
const resolveUrl = new URL(`${ns}/resolve`);
|
|
1499
|
+
resolveUrl.searchParams.set('path', path);
|
|
1500
|
+
resolveUrl.searchParams.set('ref', ref);
|
|
1501
|
+
try {
|
|
1502
|
+
const response = await stub.fetch(new Request(resolveUrl.toString(), {
|
|
1503
|
+
method: 'GET',
|
|
1504
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1505
|
+
}));
|
|
1506
|
+
if (!response.ok) {
|
|
1507
|
+
this.recordFailure(ns);
|
|
1508
|
+
throw new Error(`Cross-DO resolution failed: ${response.status}`);
|
|
1509
|
+
}
|
|
1510
|
+
this._circuitBreaker.delete(ns);
|
|
1511
|
+
let thing;
|
|
1512
|
+
try {
|
|
1513
|
+
thing = await response.json();
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
throw new Error('Invalid response from remote DO');
|
|
1517
|
+
}
|
|
1518
|
+
return thing;
|
|
1519
|
+
}
|
|
1520
|
+
catch (error) {
|
|
1521
|
+
if (error instanceof Error &&
|
|
1522
|
+
!error.message.startsWith('Invalid response') &&
|
|
1523
|
+
!error.message.startsWith('Cross-DO resolution failed')) {
|
|
1524
|
+
this.recordFailure(ns);
|
|
1525
|
+
}
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
getOrCreateStub(ns, doId) {
|
|
1530
|
+
const now = Date.now();
|
|
1531
|
+
const cached = this._stubCache.get(ns);
|
|
1532
|
+
if (cached && now - cached.cachedAt < CROSS_DO_CONFIG.STUB_CACHE_TTL) {
|
|
1533
|
+
cached.lastUsed = now;
|
|
1534
|
+
return cached.stub;
|
|
1535
|
+
}
|
|
1536
|
+
const doNamespace = this.env.DO;
|
|
1537
|
+
const id = doNamespace.idFromString(doId);
|
|
1538
|
+
const stub = doNamespace.get(id);
|
|
1539
|
+
this.evictLRUStubs();
|
|
1540
|
+
this._stubCache.set(ns, { stub, cachedAt: now, lastUsed: now });
|
|
1541
|
+
return stub;
|
|
1542
|
+
}
|
|
1543
|
+
evictLRUStubs() {
|
|
1544
|
+
while (this._stubCache.size >= this._stubCacheMaxSize) {
|
|
1545
|
+
let lruNs = null;
|
|
1546
|
+
let lruLastUsed = Infinity;
|
|
1547
|
+
for (const [ns, entry] of this._stubCache) {
|
|
1548
|
+
if (entry.lastUsed < lruLastUsed) {
|
|
1549
|
+
lruLastUsed = entry.lastUsed;
|
|
1550
|
+
lruNs = ns;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (lruNs) {
|
|
1554
|
+
this._stubCache.delete(lruNs);
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
recordFailure(ns) {
|
|
1562
|
+
const state = this._circuitBreaker.get(ns) || { failures: 0, openUntil: 0, state: 'closed' };
|
|
1563
|
+
state.failures++;
|
|
1564
|
+
if (state.failures >= CROSS_DO_CONFIG.CIRCUIT_BREAKER_THRESHOLD) {
|
|
1565
|
+
state.state = 'open';
|
|
1566
|
+
state.openUntil = Date.now() + CROSS_DO_CONFIG.CIRCUIT_BREAKER_TIMEOUT;
|
|
1567
|
+
state.failures = 0;
|
|
1568
|
+
this._stubCache.delete(ns);
|
|
1569
|
+
}
|
|
1570
|
+
if (state.state === 'half-open') {
|
|
1571
|
+
state.state = 'open';
|
|
1572
|
+
state.openUntil = Date.now() + CROSS_DO_CONFIG.CIRCUIT_BREAKER_TIMEOUT;
|
|
1573
|
+
state.halfOpenTestInProgress = false;
|
|
1574
|
+
}
|
|
1575
|
+
this._circuitBreaker.set(ns, state);
|
|
1576
|
+
}
|
|
1577
|
+
clearCrossDoCache(ns) {
|
|
1578
|
+
if (ns) {
|
|
1579
|
+
this._stubCache.delete(ns);
|
|
1580
|
+
this._circuitBreaker.delete(ns);
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
this._stubCache.clear();
|
|
1584
|
+
this._circuitBreaker.clear();
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1588
|
+
// EVENT EMISSION (protected for this module)
|
|
1589
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1590
|
+
async emitEvent(verb, data) {
|
|
1591
|
+
try {
|
|
1592
|
+
await this.db.insert(schema.events).values({
|
|
1593
|
+
id: crypto.randomUUID(),
|
|
1594
|
+
verb,
|
|
1595
|
+
source: this.ns,
|
|
1596
|
+
data: data,
|
|
1597
|
+
sequence: 0,
|
|
1598
|
+
streamed: false,
|
|
1599
|
+
createdAt: new Date(),
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
// Best-effort database insert
|
|
1604
|
+
}
|
|
1605
|
+
if (this.env.PIPELINE) {
|
|
1606
|
+
try {
|
|
1607
|
+
await this.env.PIPELINE.send([{
|
|
1608
|
+
verb,
|
|
1609
|
+
source: this.ns,
|
|
1610
|
+
$context: this.ns,
|
|
1611
|
+
data,
|
|
1612
|
+
timestamp: new Date().toISOString(),
|
|
1613
|
+
}]);
|
|
1614
|
+
}
|
|
1615
|
+
catch {
|
|
1616
|
+
// Best-effort streaming
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1621
|
+
// SHARD OPERATIONS
|
|
1622
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1623
|
+
/**
|
|
1624
|
+
* Shard this DO into multiple DOs for horizontal scaling
|
|
1625
|
+
*
|
|
1626
|
+
* @param options Sharding configuration
|
|
1627
|
+
* @returns Shard result with shard endpoints and distribution stats
|
|
1628
|
+
*/
|
|
1629
|
+
async shard(options) {
|
|
1630
|
+
return this.shardModule.shard(options);
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Unshard (merge) sharded DOs back into one
|
|
1634
|
+
*
|
|
1635
|
+
* @param options Unshard configuration
|
|
1636
|
+
*/
|
|
1637
|
+
async unshard(options) {
|
|
1638
|
+
return this.shardModule.unshard(options);
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Check if this DO is sharded
|
|
1642
|
+
*
|
|
1643
|
+
* @returns True if the DO is sharded
|
|
1644
|
+
*/
|
|
1645
|
+
async isSharded() {
|
|
1646
|
+
return this.shardModule.isSharded();
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Discover shards in this shard set
|
|
1650
|
+
*
|
|
1651
|
+
* @returns Registry and health status of all shards
|
|
1652
|
+
*/
|
|
1653
|
+
async discoverShards() {
|
|
1654
|
+
return this.shardModule.discoverShards();
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Query across all shards
|
|
1658
|
+
*
|
|
1659
|
+
* @param options Query configuration
|
|
1660
|
+
* @returns Aggregated results from all shards
|
|
1661
|
+
*/
|
|
1662
|
+
async queryShards(options) {
|
|
1663
|
+
return this.shardModule.queryShards(options);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Rebalance shards (add/remove shards or redistribute data)
|
|
1667
|
+
*
|
|
1668
|
+
* @param options Rebalance configuration
|
|
1669
|
+
* @returns Rebalance result with stats
|
|
1670
|
+
*/
|
|
1671
|
+
async rebalanceShards(options) {
|
|
1672
|
+
return this.shardModule.rebalanceShards(options);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
export default DO;
|
|
1676
|
+
//# sourceMappingURL=DOFull.js.map
|