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,2309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO - Core Durable Object with WorkflowContext
|
|
3
|
+
*
|
|
4
|
+
* Extends DOTiny (~80KB) with:
|
|
5
|
+
* - WorkflowContext ($)
|
|
6
|
+
* - Event handlers ($.on)
|
|
7
|
+
* - Stores (things, rels, actions, events, search, objects, dlq)
|
|
8
|
+
* - Scheduling ($.every, alarm)
|
|
9
|
+
* - Actor context
|
|
10
|
+
* - Collection accessors
|
|
11
|
+
* - Event emission and dispatch
|
|
12
|
+
*
|
|
13
|
+
* Does NOT include (see DOFull for these):
|
|
14
|
+
* - Lifecycle (fork, clone, compact, move)
|
|
15
|
+
* - Sharding (shard, unshard, routing)
|
|
16
|
+
* - Branching (branch, checkout, merge)
|
|
17
|
+
* - Promotion (promote, demote)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { DO } from 'dotdo'
|
|
22
|
+
*
|
|
23
|
+
* class MyDO extends DO {
|
|
24
|
+
* async onStart() {
|
|
25
|
+
* // Use workflow context
|
|
26
|
+
* this.$.on.Customer.created(async (event) => {
|
|
27
|
+
* console.log('Customer created:', event)
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* this.$.every.hour(async () => {
|
|
31
|
+
* // Hourly task
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { DO as DOTiny } from './DOTiny';
|
|
38
|
+
import { eq, sql } from 'drizzle-orm';
|
|
39
|
+
import * as schema from '../db';
|
|
40
|
+
import { isValidNounName } from '../db/nouns';
|
|
41
|
+
import { createMcpHandler, } from './transport/mcp-server';
|
|
42
|
+
import { RPCServer } from './transport/rpc-server';
|
|
43
|
+
import { SyncEngine } from './transport/sync-engine';
|
|
44
|
+
import { handleCapnWebRpc, isCapnWebRequest, } from './transport/capnweb-target';
|
|
45
|
+
import { handleRestRequest, handleGetIndex, } from './transport/rest-router';
|
|
46
|
+
import { createScheduleBuilderProxy } from '../workflows/schedule-builder';
|
|
47
|
+
import { ScheduleManager } from '../workflows/ScheduleManager';
|
|
48
|
+
import { ThingsStore, RelationshipsStore, ActionsStore, EventsStore, SearchStore, ObjectsStore, DLQStore, } from '../db/stores';
|
|
49
|
+
import { logBestEffortError } from '../lib/logging/error-logger';
|
|
50
|
+
import { parseNounId } from '../lib/noun-id';
|
|
51
|
+
import { ai as aiFunc, write as writeFunc, summarize as summarizeFunc, list as listFunc, extract as extractFunc, is as isFunc, decide as decideFunc, } from '../ai';
|
|
52
|
+
import { STORE_VISIBILITY, canAccessVisibility, getHighestRole, } from '../types/introspect';
|
|
53
|
+
import { codeToCity, coloRegion, regionToCF } from '../types/Location';
|
|
54
|
+
import { LOCATION_STORAGE_KEY } from '../lib/colo/caching';
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// CROSS-DO ERROR CLASS
|
|
57
|
+
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Custom error class for cross-DO call failures with rich context
|
|
60
|
+
*/
|
|
61
|
+
export class CrossDOError extends Error {
|
|
62
|
+
code;
|
|
63
|
+
context;
|
|
64
|
+
constructor(code, message, context = {}) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'CrossDOError';
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.context = context;
|
|
69
|
+
// Preserve stack trace in V8
|
|
70
|
+
if (Error.captureStackTrace) {
|
|
71
|
+
Error.captureStackTrace(this, CrossDOError);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
toJSON() {
|
|
75
|
+
return {
|
|
76
|
+
error: {
|
|
77
|
+
code: this.code,
|
|
78
|
+
message: this.message,
|
|
79
|
+
context: this.context,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// DO - Core Durable Object with WorkflowContext
|
|
86
|
+
// ============================================================================
|
|
87
|
+
export class DO extends DOTiny {
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// STATIC MCP CONFIGURATION
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
/**
|
|
92
|
+
* Static MCP configuration for exposing methods as MCP tools and data as resources.
|
|
93
|
+
* Override in subclasses to expose tools and resources.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* static $mcp = {
|
|
98
|
+
* tools: {
|
|
99
|
+
* search: {
|
|
100
|
+
* description: 'Search items',
|
|
101
|
+
* inputSchema: { query: { type: 'string' } },
|
|
102
|
+
* required: ['query'],
|
|
103
|
+
* },
|
|
104
|
+
* },
|
|
105
|
+
* resources: ['items', 'users'],
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
static $mcp;
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
111
|
+
// CAPABILITY MIXIN INFRASTRUCTURE
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
+
/**
|
|
114
|
+
* Static array of capability names supported by this class.
|
|
115
|
+
* Populated by capability mixins (e.g., withFS, withGit, withBash).
|
|
116
|
+
* Empty by default in base DO class.
|
|
117
|
+
*/
|
|
118
|
+
static capabilities = [];
|
|
119
|
+
/**
|
|
120
|
+
* Check if this DO instance has a specific capability.
|
|
121
|
+
* Capabilities are added via mixins and registered in the static capabilities array.
|
|
122
|
+
*
|
|
123
|
+
* @param name - Capability name to check (e.g., 'fs', 'git', 'bash')
|
|
124
|
+
* @returns true if the capability is registered on this class
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* if (this.hasCapability('fs')) {
|
|
129
|
+
* await this.$.fs.read('/config.json')
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
hasCapability(name) {
|
|
134
|
+
return this.constructor.capabilities?.includes(name) ?? false;
|
|
135
|
+
}
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
// HONO APP (for HTTP routing)
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
/**
|
|
140
|
+
* Optional Hono app for HTTP routing.
|
|
141
|
+
* Subclasses can create and configure this for custom routes.
|
|
142
|
+
*/
|
|
143
|
+
app;
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
145
|
+
// MCP SESSION STORAGE
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
+
/**
|
|
148
|
+
* MCP session storage for this DO instance.
|
|
149
|
+
*/
|
|
150
|
+
_mcpSessions = new Map();
|
|
151
|
+
/**
|
|
152
|
+
* Cached MCP handler for this class.
|
|
153
|
+
*/
|
|
154
|
+
_mcpHandler;
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// RPC SERVER
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
/**
|
|
159
|
+
* RPC Server instance for Cap'n Web RPC protocol support.
|
|
160
|
+
* Lazy-initialized on first access.
|
|
161
|
+
*/
|
|
162
|
+
_rpcServer;
|
|
163
|
+
/**
|
|
164
|
+
* Get the RPC server instance.
|
|
165
|
+
* Creates the server on first access.
|
|
166
|
+
*/
|
|
167
|
+
get rpcServer() {
|
|
168
|
+
if (!this._rpcServer) {
|
|
169
|
+
this._rpcServer = new RPCServer(this);
|
|
170
|
+
}
|
|
171
|
+
return this._rpcServer;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if a method is exposed via RPC.
|
|
175
|
+
* Note: This method is bound in the constructor to ensure `this` is always correct.
|
|
176
|
+
*/
|
|
177
|
+
isRpcExposed = (method) => {
|
|
178
|
+
return this.rpcServer.isRpcExposed(method);
|
|
179
|
+
};
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
// SYNC ENGINE
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
/**
|
|
184
|
+
* SyncEngine instance for WebSocket sync protocol support.
|
|
185
|
+
* Lazy-initialized on first access.
|
|
186
|
+
*/
|
|
187
|
+
_syncEngine;
|
|
188
|
+
/**
|
|
189
|
+
* Get the SyncEngine instance.
|
|
190
|
+
* Creates the engine on first access.
|
|
191
|
+
*/
|
|
192
|
+
get syncEngine() {
|
|
193
|
+
if (!this._syncEngine) {
|
|
194
|
+
this._syncEngine = new SyncEngine(this.things);
|
|
195
|
+
}
|
|
196
|
+
return this._syncEngine;
|
|
197
|
+
}
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
// ACTOR CONTEXT
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
201
|
+
/**
|
|
202
|
+
* Current actor for action logging.
|
|
203
|
+
* Format: 'Type/id' (e.g., 'Human/nathan', 'Agent/support')
|
|
204
|
+
*/
|
|
205
|
+
_currentActor = '';
|
|
206
|
+
/**
|
|
207
|
+
* Set the current actor for subsequent action logging.
|
|
208
|
+
*/
|
|
209
|
+
setActor(actor) {
|
|
210
|
+
this._currentActor = actor;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Clear the current actor.
|
|
214
|
+
*/
|
|
215
|
+
clearActor() {
|
|
216
|
+
this._currentActor = '';
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the current actor for action logging.
|
|
220
|
+
*/
|
|
221
|
+
getCurrentActor() {
|
|
222
|
+
return this._currentActor;
|
|
223
|
+
}
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
// STORE ACCESSORS (lazy-loaded)
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
_things;
|
|
228
|
+
_rels;
|
|
229
|
+
_actions;
|
|
230
|
+
_events;
|
|
231
|
+
_search;
|
|
232
|
+
_objects;
|
|
233
|
+
_dlq;
|
|
234
|
+
_typeCache = new Map();
|
|
235
|
+
// Event handler registry for $.on.Noun.verb() registration
|
|
236
|
+
_eventHandlers = new Map();
|
|
237
|
+
_handlerCounter = 0;
|
|
238
|
+
// Schedule handler registry for $.every scheduling
|
|
239
|
+
_scheduleHandlers = new Map();
|
|
240
|
+
_scheduleManager;
|
|
241
|
+
/**
|
|
242
|
+
* Get the schedule manager (lazy initialized)
|
|
243
|
+
*/
|
|
244
|
+
get scheduleManager() {
|
|
245
|
+
if (!this._scheduleManager) {
|
|
246
|
+
this._scheduleManager = new ScheduleManager(this.ctx);
|
|
247
|
+
this._scheduleManager.onScheduleTrigger(async (schedule) => {
|
|
248
|
+
const handler = this._scheduleHandlers.get(schedule.name);
|
|
249
|
+
if (handler) {
|
|
250
|
+
await handler();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return this._scheduleManager;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* ThingsStore - CRUD operations for Things
|
|
258
|
+
*/
|
|
259
|
+
get things() {
|
|
260
|
+
if (!this._things) {
|
|
261
|
+
this._things = new ThingsStore(this.getStoreContext());
|
|
262
|
+
}
|
|
263
|
+
return this._things;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* RelationshipsStore - Relationship management
|
|
267
|
+
*/
|
|
268
|
+
get rels() {
|
|
269
|
+
if (!this._rels) {
|
|
270
|
+
this._rels = new RelationshipsStore(this.getStoreContext());
|
|
271
|
+
}
|
|
272
|
+
return this._rels;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* ActionsStore - Action logging and lifecycle
|
|
276
|
+
*/
|
|
277
|
+
get actions() {
|
|
278
|
+
if (!this._actions) {
|
|
279
|
+
this._actions = new ActionsStore(this.getStoreContext());
|
|
280
|
+
}
|
|
281
|
+
return this._actions;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* EventsStore - Event emission and streaming
|
|
285
|
+
*/
|
|
286
|
+
get events() {
|
|
287
|
+
if (!this._events) {
|
|
288
|
+
this._events = new EventsStore(this.getStoreContext());
|
|
289
|
+
}
|
|
290
|
+
return this._events;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* SearchStore - Full-text and semantic search
|
|
294
|
+
*/
|
|
295
|
+
get search() {
|
|
296
|
+
if (!this._search) {
|
|
297
|
+
this._search = new SearchStore(this.getStoreContext());
|
|
298
|
+
}
|
|
299
|
+
return this._search;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* ObjectsStore - DO registry and resolution
|
|
303
|
+
*/
|
|
304
|
+
get objects() {
|
|
305
|
+
if (!this._objects) {
|
|
306
|
+
this._objects = new ObjectsStore(this.getStoreContext());
|
|
307
|
+
}
|
|
308
|
+
return this._objects;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* DLQStore - Dead Letter Queue for failed events
|
|
312
|
+
*/
|
|
313
|
+
get dlq() {
|
|
314
|
+
if (!this._dlq) {
|
|
315
|
+
const handlerMap = new Map();
|
|
316
|
+
for (const [eventKey, registrations] of this._eventHandlers) {
|
|
317
|
+
if (registrations.length > 0) {
|
|
318
|
+
handlerMap.set(eventKey, async (data) => {
|
|
319
|
+
const event = {
|
|
320
|
+
id: `dlq-replay-${crypto.randomUUID()}`,
|
|
321
|
+
verb: eventKey.split('.')[1] || '',
|
|
322
|
+
source: `https://${this.ns}/${eventKey.split('.')[0]}/replay`,
|
|
323
|
+
data,
|
|
324
|
+
timestamp: new Date(),
|
|
325
|
+
};
|
|
326
|
+
await this.dispatchEventToHandlers(event);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
this._dlq = new DLQStore(this.getStoreContext(), handlerMap);
|
|
331
|
+
}
|
|
332
|
+
return this._dlq;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get the store context for initializing stores
|
|
336
|
+
*/
|
|
337
|
+
getStoreContext() {
|
|
338
|
+
return {
|
|
339
|
+
db: this.db,
|
|
340
|
+
ns: this.ns,
|
|
341
|
+
currentBranch: this.currentBranch,
|
|
342
|
+
env: this.env,
|
|
343
|
+
typeCache: this._typeCache,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
347
|
+
// LOCATION DETECTION & CACHING
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
349
|
+
/** Cached location instance (in-memory) */
|
|
350
|
+
_cachedLocation;
|
|
351
|
+
/** Flag to track if location hook was already called */
|
|
352
|
+
_locationHookCalled = false;
|
|
353
|
+
/** Coordinates extracted from CF request headers */
|
|
354
|
+
_extractedCoordinates;
|
|
355
|
+
/**
|
|
356
|
+
* Get the DO's location (with caching).
|
|
357
|
+
*
|
|
358
|
+
* On first call, detects location via Cloudflare's trace endpoint,
|
|
359
|
+
* caches it in storage, and calls the onLocationDetected hook.
|
|
360
|
+
* Subsequent calls return the cached location immediately.
|
|
361
|
+
*
|
|
362
|
+
* @returns Promise resolving to the DO's location
|
|
363
|
+
*/
|
|
364
|
+
async getLocation() {
|
|
365
|
+
// Return cached location if available
|
|
366
|
+
if (this._cachedLocation) {
|
|
367
|
+
return this._cachedLocation;
|
|
368
|
+
}
|
|
369
|
+
// Check DO storage for persisted location
|
|
370
|
+
// Note: Storage may have legacy format with latitude/longitude or new format with lat/lng
|
|
371
|
+
const cached = await this.ctx.storage.get(LOCATION_STORAGE_KEY);
|
|
372
|
+
if (cached) {
|
|
373
|
+
// Restore from storage, converting coordinates to canonical format if needed
|
|
374
|
+
const coords = cached.coordinates
|
|
375
|
+
? {
|
|
376
|
+
lat: cached.coordinates.lat ?? cached.coordinates.latitude ?? 0,
|
|
377
|
+
lng: cached.coordinates.lng ?? cached.coordinates.longitude ?? 0,
|
|
378
|
+
}
|
|
379
|
+
: undefined;
|
|
380
|
+
this._cachedLocation = Object.freeze({
|
|
381
|
+
colo: cached.colo,
|
|
382
|
+
city: cached.city,
|
|
383
|
+
region: cached.region,
|
|
384
|
+
cfHint: cached.cfHint,
|
|
385
|
+
detectedAt: cached.detectedAt instanceof Date
|
|
386
|
+
? cached.detectedAt
|
|
387
|
+
: new Date(cached.detectedAt),
|
|
388
|
+
coordinates: coords,
|
|
389
|
+
});
|
|
390
|
+
return this._cachedLocation;
|
|
391
|
+
}
|
|
392
|
+
// Detect fresh location
|
|
393
|
+
const location = await this._detectLocation();
|
|
394
|
+
// Cache in memory (frozen for immutability)
|
|
395
|
+
this._cachedLocation = Object.freeze(location);
|
|
396
|
+
// Persist to storage
|
|
397
|
+
await this.ctx.storage.put(LOCATION_STORAGE_KEY, {
|
|
398
|
+
colo: location.colo,
|
|
399
|
+
city: location.city,
|
|
400
|
+
region: location.region,
|
|
401
|
+
cfHint: location.cfHint,
|
|
402
|
+
detectedAt: location.detectedAt.toISOString(),
|
|
403
|
+
coordinates: location.coordinates,
|
|
404
|
+
});
|
|
405
|
+
// Call lifecycle hook (only once)
|
|
406
|
+
if (!this._locationHookCalled) {
|
|
407
|
+
this._locationHookCalled = true;
|
|
408
|
+
try {
|
|
409
|
+
await this.onLocationDetected(this._cachedLocation);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
// Log but don't propagate hook errors
|
|
413
|
+
console.error('Error in onLocationDetected hook:', error);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return this._cachedLocation;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Internal method to detect location from Cloudflare's trace endpoint.
|
|
420
|
+
* Override in tests to provide mock location data.
|
|
421
|
+
*
|
|
422
|
+
* @returns Promise resolving to detected DOLocation
|
|
423
|
+
*/
|
|
424
|
+
async _detectLocation() {
|
|
425
|
+
try {
|
|
426
|
+
// Fetch from Cloudflare's trace endpoint
|
|
427
|
+
const response = await fetch('https://cloudflare.com/cdn-cgi/trace');
|
|
428
|
+
if (!response.ok) {
|
|
429
|
+
throw new Error(`Trace endpoint returned ${response.status}`);
|
|
430
|
+
}
|
|
431
|
+
const text = await response.text();
|
|
432
|
+
const lines = text.split('\n');
|
|
433
|
+
const data = {};
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
const [key, value] = line.split('=');
|
|
436
|
+
if (key && value) {
|
|
437
|
+
data[key.trim()] = value.trim();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const coloCode = (data.colo || 'lax').toLowerCase();
|
|
441
|
+
const city = (codeToCity[coloCode] || 'LosAngeles');
|
|
442
|
+
const region = (coloRegion[coloCode] || 'us-west');
|
|
443
|
+
const cfHint = (regionToCF[region] || 'wnam');
|
|
444
|
+
const location = {
|
|
445
|
+
colo: coloCode,
|
|
446
|
+
city,
|
|
447
|
+
region,
|
|
448
|
+
cfHint,
|
|
449
|
+
detectedAt: new Date(),
|
|
450
|
+
};
|
|
451
|
+
// Add coordinates if extracted from request
|
|
452
|
+
if (this._extractedCoordinates) {
|
|
453
|
+
location.coordinates = this._extractedCoordinates;
|
|
454
|
+
}
|
|
455
|
+
return location;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
// Fallback to default location on error
|
|
459
|
+
console.error('Failed to detect location:', error);
|
|
460
|
+
const location = {
|
|
461
|
+
colo: 'lax',
|
|
462
|
+
city: 'LosAngeles',
|
|
463
|
+
region: 'us-west',
|
|
464
|
+
cfHint: 'wnam',
|
|
465
|
+
detectedAt: new Date(),
|
|
466
|
+
};
|
|
467
|
+
if (this._extractedCoordinates) {
|
|
468
|
+
location.coordinates = this._extractedCoordinates;
|
|
469
|
+
}
|
|
470
|
+
return location;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Lifecycle hook called when location is first detected.
|
|
475
|
+
* Override in subclasses to perform custom actions.
|
|
476
|
+
*
|
|
477
|
+
* @param location - The detected DO location
|
|
478
|
+
*/
|
|
479
|
+
async onLocationDetected(location) {
|
|
480
|
+
// Default implementation does nothing
|
|
481
|
+
// Subclasses can override to react to location detection
|
|
482
|
+
}
|
|
483
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
484
|
+
// WORKFLOW CONTEXT ($)
|
|
485
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
486
|
+
$;
|
|
487
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
488
|
+
// CONSTRUCTOR
|
|
489
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
490
|
+
constructor(ctx, env) {
|
|
491
|
+
super(ctx, env);
|
|
492
|
+
// Initialize workflow context
|
|
493
|
+
this.$ = this.createWorkflowContext();
|
|
494
|
+
}
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
496
|
+
// WORKFLOW CONTEXT FACTORY
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
498
|
+
createWorkflowContext() {
|
|
499
|
+
const self = this;
|
|
500
|
+
// List of known properties for hasOwnProperty checks
|
|
501
|
+
const knownProperties = new Set([
|
|
502
|
+
'send', 'try', 'do', 'on', 'every', 'log', 'state', 'location',
|
|
503
|
+
'ai', 'write', 'summarize', 'list', 'extract', 'is', 'decide',
|
|
504
|
+
]);
|
|
505
|
+
return new Proxy({}, {
|
|
506
|
+
get(_, prop) {
|
|
507
|
+
switch (prop) {
|
|
508
|
+
// Execution modes
|
|
509
|
+
case 'send':
|
|
510
|
+
return self.send.bind(self);
|
|
511
|
+
case 'try':
|
|
512
|
+
return self.try.bind(self);
|
|
513
|
+
case 'do':
|
|
514
|
+
return self.do.bind(self);
|
|
515
|
+
// Event subscriptions and scheduling
|
|
516
|
+
case 'on':
|
|
517
|
+
return self.createOnProxy();
|
|
518
|
+
case 'every':
|
|
519
|
+
return self.createScheduleBuilder();
|
|
520
|
+
// Utilities
|
|
521
|
+
case 'log':
|
|
522
|
+
return self.log.bind(self);
|
|
523
|
+
case 'state':
|
|
524
|
+
return {};
|
|
525
|
+
// Location access (lazy, returns Promise)
|
|
526
|
+
case 'location':
|
|
527
|
+
return self.getLocation();
|
|
528
|
+
// AI Functions - Generation
|
|
529
|
+
case 'ai':
|
|
530
|
+
return aiFunc;
|
|
531
|
+
case 'write':
|
|
532
|
+
return writeFunc;
|
|
533
|
+
case 'summarize':
|
|
534
|
+
return summarizeFunc;
|
|
535
|
+
case 'list':
|
|
536
|
+
return listFunc;
|
|
537
|
+
case 'extract':
|
|
538
|
+
return extractFunc;
|
|
539
|
+
// AI Functions - Classification
|
|
540
|
+
case 'is':
|
|
541
|
+
return isFunc;
|
|
542
|
+
case 'decide':
|
|
543
|
+
return decideFunc;
|
|
544
|
+
default:
|
|
545
|
+
// Domain resolution: $.Noun(id)
|
|
546
|
+
return (id) => self.createDomainProxy(prop, id);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
has(_, prop) {
|
|
550
|
+
// Support `in` operator and hasOwnProperty checks for known properties
|
|
551
|
+
return knownProperties.has(String(prop));
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
556
|
+
// EXECUTION MODES
|
|
557
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
558
|
+
/**
|
|
559
|
+
* Default retry policy for durable execution
|
|
560
|
+
*/
|
|
561
|
+
static DEFAULT_RETRY_POLICY = {
|
|
562
|
+
maxAttempts: 3,
|
|
563
|
+
initialDelayMs: 100,
|
|
564
|
+
maxDelayMs: 30000,
|
|
565
|
+
backoffMultiplier: 2,
|
|
566
|
+
jitter: true,
|
|
567
|
+
};
|
|
568
|
+
static DEFAULT_TRY_TIMEOUT = 30000;
|
|
569
|
+
_stepCache = new Map();
|
|
570
|
+
/**
|
|
571
|
+
* Fire-and-forget event emission (non-blocking, non-durable)
|
|
572
|
+
* Errors are logged but don't propagate (by design for fire-and-forget)
|
|
573
|
+
*/
|
|
574
|
+
send(event, data) {
|
|
575
|
+
queueMicrotask(() => {
|
|
576
|
+
this.logAction('send', event, data).catch((error) => {
|
|
577
|
+
console.error(`[send] Failed to log action for ${event}:`, error);
|
|
578
|
+
this.emitSystemError('send.logAction.failed', event, error);
|
|
579
|
+
});
|
|
580
|
+
this.emitEvent(event, data).catch((error) => {
|
|
581
|
+
console.error(`[send] Failed to emit event ${event}:`, error);
|
|
582
|
+
this.emitSystemError('send.emitEvent.failed', event, error);
|
|
583
|
+
});
|
|
584
|
+
this.executeAction(event, data).catch((error) => {
|
|
585
|
+
console.error(`[send] Failed to execute action ${event}:`, error);
|
|
586
|
+
this.emitSystemError('send.executeAction.failed', event, error);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Emit a system error event for monitoring/observability
|
|
592
|
+
* This is a best-effort operation that should never throw
|
|
593
|
+
*/
|
|
594
|
+
emitSystemError(errorType, originalEvent, error) {
|
|
595
|
+
try {
|
|
596
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
597
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
598
|
+
// Log to console for immediate visibility
|
|
599
|
+
console.error(`[system.${errorType}]`, {
|
|
600
|
+
ns: this.ns,
|
|
601
|
+
originalEvent,
|
|
602
|
+
error: errorMessage,
|
|
603
|
+
stack: errorStack,
|
|
604
|
+
});
|
|
605
|
+
// Try to persist to DLQ for later replay
|
|
606
|
+
this.dlq.add({
|
|
607
|
+
eventId: `system-error-${crypto.randomUUID()}`,
|
|
608
|
+
verb: errorType,
|
|
609
|
+
source: this.ns,
|
|
610
|
+
data: { originalEvent, error: errorMessage },
|
|
611
|
+
error: errorMessage,
|
|
612
|
+
errorStack,
|
|
613
|
+
maxRetries: 3,
|
|
614
|
+
}).catch(() => {
|
|
615
|
+
// Absolute last resort - can't even log to DLQ
|
|
616
|
+
console.error(`[CRITICAL] Failed to add system error to DLQ: ${errorType}`);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
catch (catchError) {
|
|
620
|
+
// Never throw from error handler, but log the failure
|
|
621
|
+
logBestEffortError(catchError, {
|
|
622
|
+
operation: 'emitSystemError',
|
|
623
|
+
source: 'DOBase.emitSystemError',
|
|
624
|
+
context: { errorType, originalEvent, ns: this.ns },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Quick attempt without durability (blocking, non-durable)
|
|
630
|
+
*/
|
|
631
|
+
async try(action, data, options) {
|
|
632
|
+
const timeout = options?.timeout ?? DO.DEFAULT_TRY_TIMEOUT;
|
|
633
|
+
const startedAt = new Date();
|
|
634
|
+
const actionRecord = await this.logAction('try', action, data);
|
|
635
|
+
await this.updateActionStatus(actionRecord.id, 'running', { startedAt });
|
|
636
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
637
|
+
setTimeout(() => {
|
|
638
|
+
reject(new Error(`Action '${action}' timed out after ${timeout}ms`));
|
|
639
|
+
}, timeout);
|
|
640
|
+
});
|
|
641
|
+
try {
|
|
642
|
+
const result = await Promise.race([
|
|
643
|
+
this.executeAction(action, data),
|
|
644
|
+
timeoutPromise,
|
|
645
|
+
]);
|
|
646
|
+
const completedAt = new Date();
|
|
647
|
+
const duration = completedAt.getTime() - startedAt.getTime();
|
|
648
|
+
await this.completeAction(actionRecord.id, result, { completedAt, duration });
|
|
649
|
+
await this.emitEvent(`${action}.completed`, { result });
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
const completedAt = new Date();
|
|
654
|
+
const duration = completedAt.getTime() - startedAt.getTime();
|
|
655
|
+
const actionError = {
|
|
656
|
+
message: error.message,
|
|
657
|
+
name: error.name,
|
|
658
|
+
stack: error.stack,
|
|
659
|
+
};
|
|
660
|
+
await this.failAction(actionRecord.id, actionError, { completedAt, duration });
|
|
661
|
+
await this.emitEvent(`${action}.failed`, { error: actionError }).catch(() => { });
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Durable execution with retries (blocking, durable)
|
|
667
|
+
*/
|
|
668
|
+
async do(action, data, options) {
|
|
669
|
+
const retryPolicy = {
|
|
670
|
+
...DO.DEFAULT_RETRY_POLICY,
|
|
671
|
+
...options?.retry,
|
|
672
|
+
};
|
|
673
|
+
const stepId = options?.stepId ?? this.generateStepId(action, data);
|
|
674
|
+
const cachedResult = this._stepCache.get(stepId);
|
|
675
|
+
if (cachedResult) {
|
|
676
|
+
return cachedResult.result;
|
|
677
|
+
}
|
|
678
|
+
const startedAt = new Date();
|
|
679
|
+
const actionRecord = await this.logAction('do', action, data);
|
|
680
|
+
let lastError;
|
|
681
|
+
let attempts = 0;
|
|
682
|
+
for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
|
|
683
|
+
attempts = attempt;
|
|
684
|
+
const status = attempt === 1 ? 'running' : 'retrying';
|
|
685
|
+
await this.updateActionStatus(actionRecord.id, status, { startedAt, attempts });
|
|
686
|
+
try {
|
|
687
|
+
const result = await this.executeAction(action, data);
|
|
688
|
+
const completedAt = new Date();
|
|
689
|
+
const duration = completedAt.getTime() - startedAt.getTime();
|
|
690
|
+
await this.completeAction(actionRecord.id, result, { completedAt, duration, attempts });
|
|
691
|
+
this._stepCache.set(stepId, { result, completedAt: completedAt.getTime() });
|
|
692
|
+
await this.persistStepResult(stepId, result);
|
|
693
|
+
await this.emitEvent(`${action}.completed`, { result });
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
lastError = error;
|
|
698
|
+
await this.updateActionAttempts(actionRecord.id, attempts);
|
|
699
|
+
if (attempt < retryPolicy.maxAttempts) {
|
|
700
|
+
const delay = this.calculateBackoffDelay(attempt, retryPolicy);
|
|
701
|
+
await this.sleep(delay);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const completedAt = new Date();
|
|
706
|
+
const duration = completedAt.getTime() - startedAt.getTime();
|
|
707
|
+
const actionError = {
|
|
708
|
+
message: lastError.message,
|
|
709
|
+
name: lastError.name,
|
|
710
|
+
stack: lastError.stack,
|
|
711
|
+
};
|
|
712
|
+
await this.failAction(actionRecord.id, actionError, { completedAt, duration, attempts });
|
|
713
|
+
await this.emitEvent(`${action}.failed`, { error: actionError });
|
|
714
|
+
throw lastError;
|
|
715
|
+
}
|
|
716
|
+
calculateBackoffDelay(attempt, policy) {
|
|
717
|
+
let delay = policy.initialDelayMs * Math.pow(policy.backoffMultiplier, attempt - 1);
|
|
718
|
+
delay = Math.min(delay, policy.maxDelayMs);
|
|
719
|
+
if (policy.jitter) {
|
|
720
|
+
const jitterRange = delay * 0.25;
|
|
721
|
+
delay += Math.random() * jitterRange;
|
|
722
|
+
}
|
|
723
|
+
return Math.floor(delay);
|
|
724
|
+
}
|
|
725
|
+
generateStepId(action, data) {
|
|
726
|
+
const content = JSON.stringify({ action, data });
|
|
727
|
+
let hash = 0;
|
|
728
|
+
for (let i = 0; i < content.length; i++) {
|
|
729
|
+
const char = content.charCodeAt(i);
|
|
730
|
+
hash = ((hash << 5) - hash) + char;
|
|
731
|
+
hash = hash & hash;
|
|
732
|
+
}
|
|
733
|
+
return `${action}:${Math.abs(hash).toString(36)}`;
|
|
734
|
+
}
|
|
735
|
+
async persistStepResult(stepId, result) {
|
|
736
|
+
try {
|
|
737
|
+
await this.ctx.storage.put(`step:${stepId}`, { result, completedAt: Date.now() });
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
logBestEffortError(error, {
|
|
741
|
+
operation: 'persistStepResult',
|
|
742
|
+
source: 'DOBase.persistStepResult',
|
|
743
|
+
context: { stepId, ns: this.ns },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async loadPersistedSteps() {
|
|
748
|
+
try {
|
|
749
|
+
const steps = await this.ctx.storage.list({ prefix: 'step:' });
|
|
750
|
+
for (const [key, value] of steps) {
|
|
751
|
+
const stepId = key.replace('step:', '');
|
|
752
|
+
const data = value;
|
|
753
|
+
this._stepCache.set(stepId, data);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
logBestEffortError(error, {
|
|
758
|
+
operation: 'loadPersistedSteps',
|
|
759
|
+
source: 'DOBase.loadPersistedSteps',
|
|
760
|
+
context: { ns: this.ns },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
765
|
+
// ACTION LOGGING (append-only)
|
|
766
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
767
|
+
async logAction(durability, verb, input) {
|
|
768
|
+
const id = crypto.randomUUID();
|
|
769
|
+
await this.db
|
|
770
|
+
.insert(schema.actions)
|
|
771
|
+
// @ts-expect-error - Schema field names may differ
|
|
772
|
+
.values({
|
|
773
|
+
id,
|
|
774
|
+
verb,
|
|
775
|
+
target: this.ns,
|
|
776
|
+
actor: this._currentActor,
|
|
777
|
+
input: input,
|
|
778
|
+
durability,
|
|
779
|
+
status: 'pending',
|
|
780
|
+
createdAt: new Date(),
|
|
781
|
+
});
|
|
782
|
+
return { id, rowid: 0 };
|
|
783
|
+
}
|
|
784
|
+
async updateActionStatus(actionId, status, fields) {
|
|
785
|
+
try {
|
|
786
|
+
const updateData = { status };
|
|
787
|
+
if (fields?.startedAt) {
|
|
788
|
+
updateData.startedAt = fields.startedAt;
|
|
789
|
+
}
|
|
790
|
+
if (fields?.attempts !== undefined) {
|
|
791
|
+
updateData.options = JSON.stringify({ attempts: fields.attempts });
|
|
792
|
+
}
|
|
793
|
+
await this.db
|
|
794
|
+
.update(schema.actions)
|
|
795
|
+
.set(updateData)
|
|
796
|
+
.where(eq(schema.actions.id, actionId));
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
logBestEffortError(error, {
|
|
800
|
+
operation: 'updateActionStatus',
|
|
801
|
+
source: 'DOBase.updateActionStatus',
|
|
802
|
+
context: { actionId, status, ns: this.ns },
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async updateActionAttempts(actionId, attempts) {
|
|
807
|
+
try {
|
|
808
|
+
await this.db
|
|
809
|
+
.update(schema.actions)
|
|
810
|
+
.set({ options: JSON.stringify({ attempts }) })
|
|
811
|
+
.where(eq(schema.actions.id, actionId));
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
logBestEffortError(error, {
|
|
815
|
+
operation: 'updateActionAttempts',
|
|
816
|
+
source: 'DOBase.updateActionAttempts',
|
|
817
|
+
context: { actionId, attempts, ns: this.ns },
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async completeAction(actionId, output, fields) {
|
|
822
|
+
try {
|
|
823
|
+
const updateData = {
|
|
824
|
+
status: 'completed',
|
|
825
|
+
output: output,
|
|
826
|
+
};
|
|
827
|
+
if (fields?.completedAt) {
|
|
828
|
+
updateData.completedAt = fields.completedAt;
|
|
829
|
+
}
|
|
830
|
+
if (fields?.duration !== undefined) {
|
|
831
|
+
updateData.duration = fields.duration;
|
|
832
|
+
}
|
|
833
|
+
if (fields?.attempts !== undefined) {
|
|
834
|
+
updateData.options = JSON.stringify({ attempts: fields.attempts });
|
|
835
|
+
}
|
|
836
|
+
await this.db
|
|
837
|
+
.update(schema.actions)
|
|
838
|
+
.set(updateData)
|
|
839
|
+
.where(eq(schema.actions.id, actionId));
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
logBestEffortError(error, {
|
|
843
|
+
operation: 'completeAction',
|
|
844
|
+
source: 'DOBase.completeAction',
|
|
845
|
+
context: { actionId, ns: this.ns },
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async failAction(actionId, error, fields) {
|
|
850
|
+
try {
|
|
851
|
+
const updateData = {
|
|
852
|
+
status: 'failed',
|
|
853
|
+
error: error,
|
|
854
|
+
};
|
|
855
|
+
if (fields?.completedAt) {
|
|
856
|
+
updateData.completedAt = fields.completedAt;
|
|
857
|
+
}
|
|
858
|
+
if (fields?.duration !== undefined) {
|
|
859
|
+
updateData.duration = fields.duration;
|
|
860
|
+
}
|
|
861
|
+
if (fields?.attempts !== undefined) {
|
|
862
|
+
updateData.options = JSON.stringify({ attempts: fields.attempts });
|
|
863
|
+
}
|
|
864
|
+
await this.db
|
|
865
|
+
.update(schema.actions)
|
|
866
|
+
.set(updateData)
|
|
867
|
+
.where(eq(schema.actions.id, actionId));
|
|
868
|
+
}
|
|
869
|
+
catch (catchError) {
|
|
870
|
+
logBestEffortError(catchError, {
|
|
871
|
+
operation: 'failAction',
|
|
872
|
+
source: 'DOBase.failAction',
|
|
873
|
+
context: { actionId, errorMessage: error.message, ns: this.ns },
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Execute an action - override in subclasses to handle specific actions
|
|
879
|
+
*/
|
|
880
|
+
async executeAction(action, data) {
|
|
881
|
+
throw new Error(`Unknown action: ${action}`);
|
|
882
|
+
}
|
|
883
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
884
|
+
// EVENT EMISSION
|
|
885
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
886
|
+
async emitEvent(verb, data) {
|
|
887
|
+
const eventId = crypto.randomUUID();
|
|
888
|
+
let dbError = null;
|
|
889
|
+
let pipelineError = null;
|
|
890
|
+
// Attempt database insert with error capture
|
|
891
|
+
try {
|
|
892
|
+
await this.db.insert(schema.events).values({
|
|
893
|
+
id: eventId,
|
|
894
|
+
verb,
|
|
895
|
+
source: this.ns,
|
|
896
|
+
data: data,
|
|
897
|
+
sequence: 0,
|
|
898
|
+
streamed: false,
|
|
899
|
+
createdAt: new Date(),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
dbError = error instanceof Error ? error : new Error(String(error));
|
|
904
|
+
console.error(`[emitEvent] Database insert failed for ${verb}:`, dbError.message);
|
|
905
|
+
// Add to DLQ for retry
|
|
906
|
+
try {
|
|
907
|
+
await this.dlq.add({
|
|
908
|
+
eventId,
|
|
909
|
+
verb,
|
|
910
|
+
source: this.ns,
|
|
911
|
+
data: data,
|
|
912
|
+
error: dbError.message,
|
|
913
|
+
errorStack: dbError.stack,
|
|
914
|
+
maxRetries: 3,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
catch (dlqError) {
|
|
918
|
+
console.error(`[emitEvent] Failed to add to DLQ:`, dlqError);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// Attempt pipeline send with error capture and retry
|
|
922
|
+
if (this.env.PIPELINE) {
|
|
923
|
+
const maxPipelineRetries = 3;
|
|
924
|
+
const baseDelay = 100;
|
|
925
|
+
for (let attempt = 1; attempt <= maxPipelineRetries; attempt++) {
|
|
926
|
+
try {
|
|
927
|
+
await this.env.PIPELINE.send([{
|
|
928
|
+
verb,
|
|
929
|
+
source: this.ns,
|
|
930
|
+
$context: this.ns,
|
|
931
|
+
data,
|
|
932
|
+
timestamp: new Date().toISOString(),
|
|
933
|
+
}]);
|
|
934
|
+
pipelineError = null; // Success - clear any previous error
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
pipelineError = error instanceof Error ? error : new Error(String(error));
|
|
939
|
+
console.error(`[emitEvent] Pipeline send attempt ${attempt}/${maxPipelineRetries} failed for ${verb}:`, pipelineError.message);
|
|
940
|
+
if (attempt < maxPipelineRetries) {
|
|
941
|
+
// Exponential backoff
|
|
942
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
943
|
+
await this.sleep(delay);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// If all pipeline retries failed, log for metrics
|
|
948
|
+
if (pipelineError) {
|
|
949
|
+
console.error(`[emitEvent] Pipeline send failed after ${maxPipelineRetries} attempts for ${verb}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Emit system error event if either operation failed (for observability)
|
|
953
|
+
if (dbError || pipelineError) {
|
|
954
|
+
try {
|
|
955
|
+
// Use console for metrics visibility (can be scraped by log aggregators)
|
|
956
|
+
console.error('[metrics.event.emission.failure]', {
|
|
957
|
+
ns: this.ns,
|
|
958
|
+
verb,
|
|
959
|
+
dbError: dbError?.message ?? null,
|
|
960
|
+
pipelineError: pipelineError?.message ?? null,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
// Never throw from error reporting
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Emit an event (public wrapper for emitEvent)
|
|
970
|
+
*/
|
|
971
|
+
async emit(verb, data) {
|
|
972
|
+
return this.emitEvent(verb, data);
|
|
973
|
+
}
|
|
974
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
975
|
+
// NOUN FK RESOLUTION
|
|
976
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
977
|
+
async resolveNounToFK(noun) {
|
|
978
|
+
if (!noun || noun.trim() === '') {
|
|
979
|
+
throw new Error('Noun name cannot be empty');
|
|
980
|
+
}
|
|
981
|
+
if (!isValidNounName(noun)) {
|
|
982
|
+
throw new Error(`Invalid noun '${noun}': must be PascalCase`);
|
|
983
|
+
}
|
|
984
|
+
const cached = this._typeCache.get(noun);
|
|
985
|
+
if (cached !== undefined) {
|
|
986
|
+
return cached;
|
|
987
|
+
}
|
|
988
|
+
const results = await this.db
|
|
989
|
+
.select({
|
|
990
|
+
noun: schema.nouns.noun,
|
|
991
|
+
rowid: sql `rowid`,
|
|
992
|
+
})
|
|
993
|
+
.from(schema.nouns)
|
|
994
|
+
.where(eq(schema.nouns.noun, noun));
|
|
995
|
+
if (results.length === 0) {
|
|
996
|
+
throw new Error(`Noun '${noun}' not found in nouns table. Register it first with registerNoun().`);
|
|
997
|
+
}
|
|
998
|
+
const fk = results[0].rowid;
|
|
999
|
+
this._typeCache.set(noun, fk);
|
|
1000
|
+
return fk;
|
|
1001
|
+
}
|
|
1002
|
+
async registerNoun(noun, config) {
|
|
1003
|
+
if (!noun || noun.trim() === '') {
|
|
1004
|
+
throw new Error('Noun name cannot be empty');
|
|
1005
|
+
}
|
|
1006
|
+
if (!isValidNounName(noun)) {
|
|
1007
|
+
throw new Error(`Invalid noun '${noun}': must be PascalCase`);
|
|
1008
|
+
}
|
|
1009
|
+
const cached = this._typeCache.get(noun);
|
|
1010
|
+
if (cached !== undefined) {
|
|
1011
|
+
return cached;
|
|
1012
|
+
}
|
|
1013
|
+
const existing = await this.db
|
|
1014
|
+
.select({
|
|
1015
|
+
noun: schema.nouns.noun,
|
|
1016
|
+
rowid: sql `rowid`,
|
|
1017
|
+
})
|
|
1018
|
+
.from(schema.nouns)
|
|
1019
|
+
.where(eq(schema.nouns.noun, noun));
|
|
1020
|
+
if (existing.length > 0) {
|
|
1021
|
+
const fk = existing[0].rowid;
|
|
1022
|
+
this._typeCache.set(noun, fk);
|
|
1023
|
+
return fk;
|
|
1024
|
+
}
|
|
1025
|
+
await this.db.insert(schema.nouns).values({
|
|
1026
|
+
noun,
|
|
1027
|
+
plural: config?.plural ?? `${noun}s`,
|
|
1028
|
+
description: config?.description ?? null,
|
|
1029
|
+
schema: config?.schema ? JSON.stringify(config.schema) : null,
|
|
1030
|
+
doClass: config?.doClass ?? null,
|
|
1031
|
+
});
|
|
1032
|
+
const inserted = await this.db
|
|
1033
|
+
.select({
|
|
1034
|
+
noun: schema.nouns.noun,
|
|
1035
|
+
rowid: sql `rowid`,
|
|
1036
|
+
})
|
|
1037
|
+
.from(schema.nouns)
|
|
1038
|
+
.where(eq(schema.nouns.noun, noun));
|
|
1039
|
+
if (inserted.length === 0) {
|
|
1040
|
+
throw new Error(`Failed to register noun '${noun}'`);
|
|
1041
|
+
}
|
|
1042
|
+
const fk = inserted[0].rowid;
|
|
1043
|
+
this._typeCache.set(noun, fk);
|
|
1044
|
+
return fk;
|
|
1045
|
+
}
|
|
1046
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1047
|
+
// TYPED COLLECTION ACCESSORS
|
|
1048
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
collection(noun) {
|
|
1050
|
+
if (!noun || noun.trim() === '') {
|
|
1051
|
+
throw new Error('Noun name cannot be empty');
|
|
1052
|
+
}
|
|
1053
|
+
if (!isValidNounName(noun)) {
|
|
1054
|
+
throw new Error(`Invalid noun '${noun}': must be PascalCase`);
|
|
1055
|
+
}
|
|
1056
|
+
const self = this;
|
|
1057
|
+
return {
|
|
1058
|
+
get: async (id) => {
|
|
1059
|
+
const typeFK = await self.resolveNounToFK(noun);
|
|
1060
|
+
const results = await self.db.select().from(schema.things);
|
|
1061
|
+
const result = results.find((r) => r.id === id && r.type === typeFK && !r.deleted);
|
|
1062
|
+
if (!result)
|
|
1063
|
+
return null;
|
|
1064
|
+
const data = result.data;
|
|
1065
|
+
return { $id: result.id, $type: noun, ...data };
|
|
1066
|
+
},
|
|
1067
|
+
list: async () => {
|
|
1068
|
+
const typeFK = await self.resolveNounToFK(noun);
|
|
1069
|
+
const results = await self.db.select().from(schema.things);
|
|
1070
|
+
return results
|
|
1071
|
+
.filter((r) => r.type === typeFK && !r.deleted)
|
|
1072
|
+
.map((r) => {
|
|
1073
|
+
const data = r.data;
|
|
1074
|
+
return { $id: r.id, $type: noun, ...data };
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
1077
|
+
find: async (query) => {
|
|
1078
|
+
const typeFK = await self.resolveNounToFK(noun);
|
|
1079
|
+
const results = await self.db.select().from(schema.things);
|
|
1080
|
+
return results
|
|
1081
|
+
.filter((r) => {
|
|
1082
|
+
if (r.type !== typeFK || r.deleted)
|
|
1083
|
+
return false;
|
|
1084
|
+
const data = r.data;
|
|
1085
|
+
if (!data)
|
|
1086
|
+
return false;
|
|
1087
|
+
return Object.entries(query).every(([key, value]) => data[key] === value);
|
|
1088
|
+
})
|
|
1089
|
+
.map((r) => {
|
|
1090
|
+
const data = r.data;
|
|
1091
|
+
return { $id: r.id, $type: noun, ...data };
|
|
1092
|
+
});
|
|
1093
|
+
},
|
|
1094
|
+
create: async (data) => {
|
|
1095
|
+
const typeFK = await self.resolveNounToFK(noun);
|
|
1096
|
+
const id = data.$id || crypto.randomUUID();
|
|
1097
|
+
await self.db.insert(schema.things).values({
|
|
1098
|
+
id,
|
|
1099
|
+
type: typeFK,
|
|
1100
|
+
branch: self.currentBranch,
|
|
1101
|
+
data: data,
|
|
1102
|
+
deleted: false,
|
|
1103
|
+
});
|
|
1104
|
+
self._typeCache.set(noun, typeFK);
|
|
1105
|
+
return { ...data, $id: id, $type: noun };
|
|
1106
|
+
},
|
|
1107
|
+
update: async (id, data) => {
|
|
1108
|
+
const result = await self.things.update(id, {
|
|
1109
|
+
data: data,
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
$id: result.$id,
|
|
1113
|
+
$type: noun,
|
|
1114
|
+
...result.data,
|
|
1115
|
+
$rowid: result.version ?? 0,
|
|
1116
|
+
};
|
|
1117
|
+
},
|
|
1118
|
+
delete: async (id) => {
|
|
1119
|
+
const result = await self.things.delete(id);
|
|
1120
|
+
return {
|
|
1121
|
+
$id: result.$id,
|
|
1122
|
+
$type: noun,
|
|
1123
|
+
...result.data,
|
|
1124
|
+
$rowid: result.version ?? 0,
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Relationships table accessor
|
|
1131
|
+
*/
|
|
1132
|
+
get relationships() {
|
|
1133
|
+
return {
|
|
1134
|
+
create: async (data) => {
|
|
1135
|
+
const id = crypto.randomUUID();
|
|
1136
|
+
await this.db.insert(schema.relationships).values({
|
|
1137
|
+
id,
|
|
1138
|
+
verb: data.verb,
|
|
1139
|
+
from: data.from,
|
|
1140
|
+
to: data.to,
|
|
1141
|
+
data: data.data,
|
|
1142
|
+
createdAt: new Date(),
|
|
1143
|
+
});
|
|
1144
|
+
return { id };
|
|
1145
|
+
},
|
|
1146
|
+
list: async (query) => {
|
|
1147
|
+
const results = await this.db.select().from(schema.relationships);
|
|
1148
|
+
return results
|
|
1149
|
+
.filter((r) => {
|
|
1150
|
+
if (query?.from && r.from !== query.from)
|
|
1151
|
+
return false;
|
|
1152
|
+
if (query?.to && r.to !== query.to)
|
|
1153
|
+
return false;
|
|
1154
|
+
if (query?.verb && r.verb !== query.verb)
|
|
1155
|
+
return false;
|
|
1156
|
+
return true;
|
|
1157
|
+
})
|
|
1158
|
+
.map((r) => ({
|
|
1159
|
+
id: r.id,
|
|
1160
|
+
verb: r.verb,
|
|
1161
|
+
from: r.from,
|
|
1162
|
+
to: r.to,
|
|
1163
|
+
data: r.data,
|
|
1164
|
+
createdAt: r.createdAt,
|
|
1165
|
+
}));
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1170
|
+
// PROXY FACTORIES
|
|
1171
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1172
|
+
createOnProxy() {
|
|
1173
|
+
const self = this;
|
|
1174
|
+
return new Proxy({}, {
|
|
1175
|
+
get: (_, noun) => {
|
|
1176
|
+
return new Proxy({}, {
|
|
1177
|
+
get: (_, verb) => {
|
|
1178
|
+
return (handler, options) => {
|
|
1179
|
+
const eventKey = `${noun}.${verb}`;
|
|
1180
|
+
const registrations = self._eventHandlers.get(eventKey) ?? [];
|
|
1181
|
+
const handlerName = options?.name
|
|
1182
|
+
|| handler.name
|
|
1183
|
+
|| `handler_${++self._handlerCounter}`;
|
|
1184
|
+
const registration = {
|
|
1185
|
+
name: handlerName,
|
|
1186
|
+
priority: options?.priority ?? 0,
|
|
1187
|
+
registeredAt: Date.now(),
|
|
1188
|
+
sourceNs: self.ns,
|
|
1189
|
+
handler,
|
|
1190
|
+
filter: options?.filter,
|
|
1191
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
1192
|
+
executionCount: 0,
|
|
1193
|
+
successCount: 0,
|
|
1194
|
+
failureCount: 0,
|
|
1195
|
+
};
|
|
1196
|
+
registrations.push(registration);
|
|
1197
|
+
registrations.sort((a, b) => {
|
|
1198
|
+
if (b.priority !== a.priority) {
|
|
1199
|
+
return b.priority - a.priority;
|
|
1200
|
+
}
|
|
1201
|
+
return a.registeredAt - b.registeredAt;
|
|
1202
|
+
});
|
|
1203
|
+
self._eventHandlers.set(eventKey, registrations);
|
|
1204
|
+
};
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
createScheduleBuilder() {
|
|
1211
|
+
const self = this;
|
|
1212
|
+
const config = {
|
|
1213
|
+
state: this.ctx,
|
|
1214
|
+
onScheduleRegistered: (cron, name, handler) => {
|
|
1215
|
+
self._scheduleHandlers.set(name, handler);
|
|
1216
|
+
self.scheduleManager.schedule(cron, name).catch((error) => {
|
|
1217
|
+
console.error(`Failed to register schedule ${name}:`, error);
|
|
1218
|
+
});
|
|
1219
|
+
},
|
|
1220
|
+
};
|
|
1221
|
+
return createScheduleBuilderProxy(config);
|
|
1222
|
+
}
|
|
1223
|
+
createDomainProxy(noun, id) {
|
|
1224
|
+
const self = this;
|
|
1225
|
+
return new Proxy({}, {
|
|
1226
|
+
get(_, method) {
|
|
1227
|
+
if (method === 'then' || method === 'catch' || method === 'finally') {
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
return (...args) => {
|
|
1231
|
+
return self.invokeDomainMethod(noun, id, method, args);
|
|
1232
|
+
};
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
async invokeDomainMethod(noun, id, method, args) {
|
|
1237
|
+
const localMethod = this[method];
|
|
1238
|
+
if (typeof localMethod === 'function') {
|
|
1239
|
+
try {
|
|
1240
|
+
return await localMethod.apply(this, args);
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
throw error;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return this.invokeCrossDOMethod(noun, id, method, args);
|
|
1247
|
+
}
|
|
1248
|
+
// Circuit breaker state for cross-DO calls (per target DO)
|
|
1249
|
+
static _circuitBreakers = new Map();
|
|
1250
|
+
// Circuit breaker configuration
|
|
1251
|
+
static CIRCUIT_BREAKER_CONFIG = {
|
|
1252
|
+
failureThreshold: 5,
|
|
1253
|
+
resetTimeoutMs: 30000,
|
|
1254
|
+
halfOpenRequests: 1,
|
|
1255
|
+
};
|
|
1256
|
+
// Cross-DO retry configuration
|
|
1257
|
+
static CROSS_DO_RETRY_CONFIG = {
|
|
1258
|
+
maxAttempts: 3,
|
|
1259
|
+
initialDelayMs: 100,
|
|
1260
|
+
maxDelayMs: 5000,
|
|
1261
|
+
backoffMultiplier: 2,
|
|
1262
|
+
retryableStatuses: [500, 502, 503, 504],
|
|
1263
|
+
retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'],
|
|
1264
|
+
};
|
|
1265
|
+
// Default timeout for cross-DO calls
|
|
1266
|
+
static CROSS_DO_TIMEOUT_MS = 30000;
|
|
1267
|
+
async invokeCrossDOMethod(noun, id, method, args, options) {
|
|
1268
|
+
if (!this.env.DO) {
|
|
1269
|
+
throw new Error(`Method '${method}' not found and DO namespace not configured for cross-DO calls`);
|
|
1270
|
+
}
|
|
1271
|
+
const targetNs = `${noun}/${id}`;
|
|
1272
|
+
const timeout = options?.timeout ?? DO.CROSS_DO_TIMEOUT_MS;
|
|
1273
|
+
// Check circuit breaker
|
|
1274
|
+
const circuitState = this.checkCircuitBreaker(targetNs);
|
|
1275
|
+
if (circuitState === 'open') {
|
|
1276
|
+
throw new CrossDOError('CIRCUIT_BREAKER_OPEN', `Circuit breaker open for ${targetNs}`, { targetDO: targetNs, source: this.ns });
|
|
1277
|
+
}
|
|
1278
|
+
const doNamespace = this.env.DO;
|
|
1279
|
+
const doId = doNamespace.idFromName(targetNs);
|
|
1280
|
+
const stub = doNamespace.get(doId);
|
|
1281
|
+
const { maxAttempts, initialDelayMs, maxDelayMs, backoffMultiplier, retryableStatuses } = DO.CROSS_DO_RETRY_CONFIG;
|
|
1282
|
+
let lastError;
|
|
1283
|
+
let attempts = 0;
|
|
1284
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1285
|
+
attempts = attempt;
|
|
1286
|
+
try {
|
|
1287
|
+
const response = await this.fetchWithCrossDOTimeout(stub, `https://${targetNs}/rpc/${method}`, { args }, timeout);
|
|
1288
|
+
// Check for rate limiting
|
|
1289
|
+
if (response.status === 429) {
|
|
1290
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
1291
|
+
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
|
|
1292
|
+
if (attempt < maxAttempts) {
|
|
1293
|
+
await this.sleep(Math.min(delay, maxDelayMs));
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
// Non-retryable client errors
|
|
1298
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
1299
|
+
const errorText = await response.text();
|
|
1300
|
+
this.recordCircuitBreakerSuccess(targetNs);
|
|
1301
|
+
throw new CrossDOError('CROSS_DO_CLIENT_ERROR', `Cross-DO RPC failed: ${response.status} - ${errorText}`, { targetDO: targetNs, method, source: this.ns });
|
|
1302
|
+
}
|
|
1303
|
+
// Retryable server errors
|
|
1304
|
+
if (!response.ok && retryableStatuses.includes(response.status)) {
|
|
1305
|
+
const errorText = await response.text();
|
|
1306
|
+
lastError = new Error(`Cross-DO RPC failed: ${response.status} - ${errorText}`);
|
|
1307
|
+
if (attempt < maxAttempts) {
|
|
1308
|
+
const delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt - 1), maxDelayMs);
|
|
1309
|
+
await this.sleep(delay);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (!response.ok) {
|
|
1314
|
+
const errorText = await response.text();
|
|
1315
|
+
this.recordCircuitBreakerFailure(targetNs);
|
|
1316
|
+
throw new CrossDOError('CROSS_DO_ERROR', `Cross-DO RPC failed: ${response.status} - ${errorText}`, { targetDO: targetNs, method, attempts, source: this.ns });
|
|
1317
|
+
}
|
|
1318
|
+
const result = await response.json();
|
|
1319
|
+
if (result.error) {
|
|
1320
|
+
this.recordCircuitBreakerFailure(targetNs);
|
|
1321
|
+
throw new CrossDOError('CROSS_DO_ERROR', result.error, { targetDO: targetNs, method, source: this.ns });
|
|
1322
|
+
}
|
|
1323
|
+
// Success - record and return
|
|
1324
|
+
this.recordCircuitBreakerSuccess(targetNs);
|
|
1325
|
+
return result.result;
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1329
|
+
// Check for timeout
|
|
1330
|
+
if (lastError.name === 'AbortError' || lastError.message.includes('timeout')) {
|
|
1331
|
+
this.recordCircuitBreakerFailure(targetNs);
|
|
1332
|
+
throw new CrossDOError('CROSS_DO_TIMEOUT', `Cross-DO call to ${targetNs}.${method}() timed out after ${timeout}ms`, { targetDO: targetNs, method, source: this.ns });
|
|
1333
|
+
}
|
|
1334
|
+
// Check if error is retryable
|
|
1335
|
+
const isRetryable = DO.CROSS_DO_RETRY_CONFIG.retryableErrors.some(errType => lastError.message.includes(errType));
|
|
1336
|
+
if (isRetryable && attempt < maxAttempts) {
|
|
1337
|
+
const delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt - 1), maxDelayMs);
|
|
1338
|
+
await this.sleep(delay);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
// Not retryable or exhausted retries
|
|
1342
|
+
this.recordCircuitBreakerFailure(targetNs);
|
|
1343
|
+
throw lastError;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// Exhausted all retries
|
|
1347
|
+
this.recordCircuitBreakerFailure(targetNs);
|
|
1348
|
+
throw new CrossDOError('CROSS_DO_ERROR', `Cross-DO call failed after ${attempts} attempts`, {
|
|
1349
|
+
targetDO: targetNs,
|
|
1350
|
+
method,
|
|
1351
|
+
attempts,
|
|
1352
|
+
source: this.ns,
|
|
1353
|
+
originalError: lastError?.message,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Fetch with timeout for cross-DO calls
|
|
1358
|
+
*/
|
|
1359
|
+
async fetchWithCrossDOTimeout(stub, url, body, timeoutMs) {
|
|
1360
|
+
const controller = new AbortController();
|
|
1361
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1362
|
+
try {
|
|
1363
|
+
return await stub.fetch(new Request(url, {
|
|
1364
|
+
method: 'POST',
|
|
1365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1366
|
+
body: JSON.stringify(body),
|
|
1367
|
+
signal: controller.signal,
|
|
1368
|
+
}));
|
|
1369
|
+
}
|
|
1370
|
+
finally {
|
|
1371
|
+
clearTimeout(timeoutId);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Check circuit breaker state for a target DO
|
|
1376
|
+
*/
|
|
1377
|
+
checkCircuitBreaker(targetNs) {
|
|
1378
|
+
const breaker = DO._circuitBreakers.get(targetNs);
|
|
1379
|
+
if (!breaker)
|
|
1380
|
+
return 'closed';
|
|
1381
|
+
const { failureThreshold, resetTimeoutMs } = DO.CIRCUIT_BREAKER_CONFIG;
|
|
1382
|
+
if (breaker.state === 'open') {
|
|
1383
|
+
// Check if enough time has passed to try half-open
|
|
1384
|
+
if (Date.now() - breaker.lastFailure >= resetTimeoutMs) {
|
|
1385
|
+
breaker.state = 'half-open';
|
|
1386
|
+
return 'half-open';
|
|
1387
|
+
}
|
|
1388
|
+
return 'open';
|
|
1389
|
+
}
|
|
1390
|
+
if (breaker.failures >= failureThreshold) {
|
|
1391
|
+
breaker.state = 'open';
|
|
1392
|
+
return 'open';
|
|
1393
|
+
}
|
|
1394
|
+
return breaker.state;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Record a successful cross-DO call (reset circuit breaker)
|
|
1398
|
+
*/
|
|
1399
|
+
recordCircuitBreakerSuccess(targetNs) {
|
|
1400
|
+
DO._circuitBreakers.set(targetNs, {
|
|
1401
|
+
failures: 0,
|
|
1402
|
+
lastFailure: 0,
|
|
1403
|
+
state: 'closed',
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Record a failed cross-DO call
|
|
1408
|
+
*/
|
|
1409
|
+
recordCircuitBreakerFailure(targetNs) {
|
|
1410
|
+
const breaker = DO._circuitBreakers.get(targetNs) ?? {
|
|
1411
|
+
failures: 0,
|
|
1412
|
+
lastFailure: 0,
|
|
1413
|
+
state: 'closed',
|
|
1414
|
+
};
|
|
1415
|
+
breaker.failures++;
|
|
1416
|
+
breaker.lastFailure = Date.now();
|
|
1417
|
+
if (breaker.failures >= DO.CIRCUIT_BREAKER_CONFIG.failureThreshold) {
|
|
1418
|
+
breaker.state = 'open';
|
|
1419
|
+
}
|
|
1420
|
+
DO._circuitBreakers.set(targetNs, breaker);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Reset all static state - ONLY for testing.
|
|
1424
|
+
* This clears accumulated static Maps that persist across test runs.
|
|
1425
|
+
*/
|
|
1426
|
+
static _resetTestState() {
|
|
1427
|
+
DO._circuitBreakers.clear();
|
|
1428
|
+
}
|
|
1429
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1430
|
+
// EVENT HANDLER MANAGEMENT
|
|
1431
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1432
|
+
getEventHandlers(eventKey) {
|
|
1433
|
+
const registrations = this._eventHandlers.get(eventKey) ?? [];
|
|
1434
|
+
return registrations.map((r) => r.handler);
|
|
1435
|
+
}
|
|
1436
|
+
getHandlersByPriority(eventKey) {
|
|
1437
|
+
const registrations = this._eventHandlers.get(eventKey) ?? [];
|
|
1438
|
+
return registrations.map((r) => ({ handler: r.handler, priority: r.priority }));
|
|
1439
|
+
}
|
|
1440
|
+
getHandlerMetadata(eventKey, handlerName) {
|
|
1441
|
+
const registrations = this._eventHandlers.get(eventKey) ?? [];
|
|
1442
|
+
return registrations.find((r) => r.name === handlerName);
|
|
1443
|
+
}
|
|
1444
|
+
getHandlerRegistrations(eventKey) {
|
|
1445
|
+
return this._eventHandlers.get(eventKey) ?? [];
|
|
1446
|
+
}
|
|
1447
|
+
listAllHandlers() {
|
|
1448
|
+
return new Map(this._eventHandlers);
|
|
1449
|
+
}
|
|
1450
|
+
collectMatchingHandlers(noun, verb) {
|
|
1451
|
+
const matchingHandlers = [];
|
|
1452
|
+
const exactKey = `${noun}.${verb}`;
|
|
1453
|
+
for (const reg of this._eventHandlers.get(exactKey) ?? []) {
|
|
1454
|
+
matchingHandlers.push({ registration: reg, isWildcard: false });
|
|
1455
|
+
}
|
|
1456
|
+
for (const reg of this._eventHandlers.get(`*.${verb}`) ?? []) {
|
|
1457
|
+
matchingHandlers.push({ registration: reg, isWildcard: true });
|
|
1458
|
+
}
|
|
1459
|
+
for (const reg of this._eventHandlers.get(`${noun}.*`) ?? []) {
|
|
1460
|
+
matchingHandlers.push({ registration: reg, isWildcard: true });
|
|
1461
|
+
}
|
|
1462
|
+
for (const reg of this._eventHandlers.get('*.*') ?? []) {
|
|
1463
|
+
matchingHandlers.push({ registration: reg, isWildcard: true });
|
|
1464
|
+
}
|
|
1465
|
+
matchingHandlers.sort((a, b) => {
|
|
1466
|
+
if (b.registration.priority !== a.registration.priority) {
|
|
1467
|
+
return b.registration.priority - a.registration.priority;
|
|
1468
|
+
}
|
|
1469
|
+
if (a.isWildcard !== b.isWildcard) {
|
|
1470
|
+
return a.isWildcard ? 1 : -1;
|
|
1471
|
+
}
|
|
1472
|
+
return a.registration.registeredAt - b.registration.registeredAt;
|
|
1473
|
+
});
|
|
1474
|
+
return matchingHandlers.map((h) => h.registration);
|
|
1475
|
+
}
|
|
1476
|
+
async dispatchEventToHandlers(event) {
|
|
1477
|
+
const sourceParts = event.source.split('/');
|
|
1478
|
+
const noun = sourceParts[sourceParts.length - 2] || '';
|
|
1479
|
+
const registrations = this.collectMatchingHandlers(noun, event.verb);
|
|
1480
|
+
let handled = 0;
|
|
1481
|
+
let filtered = 0;
|
|
1482
|
+
let wildcardMatches = 0;
|
|
1483
|
+
const errors = [];
|
|
1484
|
+
const dlqEntries = [];
|
|
1485
|
+
const exactKey = `${noun}.${event.verb}`;
|
|
1486
|
+
const exactRegistrations = new Set((this._eventHandlers.get(exactKey) ?? []).map((r) => r.name));
|
|
1487
|
+
for (const registration of registrations) {
|
|
1488
|
+
if (!exactRegistrations.has(registration.name)) {
|
|
1489
|
+
wildcardMatches++;
|
|
1490
|
+
}
|
|
1491
|
+
if (registration.filter) {
|
|
1492
|
+
try {
|
|
1493
|
+
const shouldExecute = await registration.filter(event);
|
|
1494
|
+
if (!shouldExecute) {
|
|
1495
|
+
filtered++;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
filtered++;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
registration.executionCount++;
|
|
1505
|
+
registration.lastExecutedAt = Date.now();
|
|
1506
|
+
try {
|
|
1507
|
+
await registration.handler(event);
|
|
1508
|
+
registration.successCount++;
|
|
1509
|
+
handled++;
|
|
1510
|
+
}
|
|
1511
|
+
catch (e) {
|
|
1512
|
+
registration.failureCount++;
|
|
1513
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
1514
|
+
errors.push(error);
|
|
1515
|
+
try {
|
|
1516
|
+
const dlqEntry = await this.dlq.add({
|
|
1517
|
+
eventId: event.id,
|
|
1518
|
+
verb: `${noun}.${event.verb}`,
|
|
1519
|
+
source: event.source,
|
|
1520
|
+
data: event.data,
|
|
1521
|
+
error: error.message,
|
|
1522
|
+
errorStack: error.stack,
|
|
1523
|
+
maxRetries: registration.maxRetries,
|
|
1524
|
+
});
|
|
1525
|
+
dlqEntries.push(dlqEntry.id);
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
console.error('Failed to add event to DLQ');
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return { handled, errors, dlqEntries, filtered, wildcardMatches };
|
|
1533
|
+
}
|
|
1534
|
+
unregisterEventHandler(eventKey, handler) {
|
|
1535
|
+
const registrations = this._eventHandlers.get(eventKey);
|
|
1536
|
+
if (!registrations) {
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
const index = registrations.findIndex((r) => r.handler === handler);
|
|
1540
|
+
if (index > -1) {
|
|
1541
|
+
registrations.splice(index, 1);
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1547
|
+
// RESOLUTION
|
|
1548
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1549
|
+
/**
|
|
1550
|
+
* Resolve any URL to a Thing (local, cross-DO, or external)
|
|
1551
|
+
*/
|
|
1552
|
+
async resolve(url) {
|
|
1553
|
+
const parsed = new URL(url);
|
|
1554
|
+
const ns = `${parsed.protocol}//${parsed.host}`;
|
|
1555
|
+
const path = parsed.pathname.slice(1);
|
|
1556
|
+
const ref = parsed.hash.slice(1) || 'main';
|
|
1557
|
+
if (ns === this.ns) {
|
|
1558
|
+
return this.resolveLocal(path, ref);
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
return this.resolveCrossDO(ns, path, ref);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async resolveLocal(path, ref) {
|
|
1565
|
+
const parsed = parseNounId(path);
|
|
1566
|
+
const branch = parsed.branch ?? (ref || this.currentBranch);
|
|
1567
|
+
const thingId = parsed.id;
|
|
1568
|
+
if (parsed.version !== undefined || parsed.relativeVersion !== undefined) {
|
|
1569
|
+
const versions = await this.things.versions(thingId);
|
|
1570
|
+
if (versions.length === 0) {
|
|
1571
|
+
throw new Error(`Thing not found: ${path}`);
|
|
1572
|
+
}
|
|
1573
|
+
let targetVersion;
|
|
1574
|
+
if (parsed.version !== undefined) {
|
|
1575
|
+
const versionIndex = parsed.version - 1;
|
|
1576
|
+
if (versionIndex < 0 || versionIndex >= versions.length) {
|
|
1577
|
+
throw new Error(`Thing not found: ${path}`);
|
|
1578
|
+
}
|
|
1579
|
+
targetVersion = versions[versionIndex];
|
|
1580
|
+
}
|
|
1581
|
+
else if (parsed.relativeVersion !== undefined) {
|
|
1582
|
+
const targetIndex = versions.length - 1 - parsed.relativeVersion;
|
|
1583
|
+
if (targetIndex < 0) {
|
|
1584
|
+
throw new Error(`Relative version @~${parsed.relativeVersion} exceeds available versions (${versions.length} total)`);
|
|
1585
|
+
}
|
|
1586
|
+
targetVersion = versions[targetIndex];
|
|
1587
|
+
}
|
|
1588
|
+
if (!targetVersion) {
|
|
1589
|
+
throw new Error(`Version not found for path: ${path}`);
|
|
1590
|
+
}
|
|
1591
|
+
const fullId = this.ns ? `${this.ns}/${parsed.noun}/${parsed.id}` : `${parsed.noun}/${parsed.id}`;
|
|
1592
|
+
return {
|
|
1593
|
+
$id: fullId,
|
|
1594
|
+
$type: parsed.noun,
|
|
1595
|
+
name: targetVersion.name ?? undefined,
|
|
1596
|
+
data: targetVersion.data ?? undefined,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
const options = {};
|
|
1600
|
+
if (branch && branch !== 'main') {
|
|
1601
|
+
options.branch = branch;
|
|
1602
|
+
}
|
|
1603
|
+
const thing = await this.things.get(thingId, options);
|
|
1604
|
+
if (!thing) {
|
|
1605
|
+
throw new Error(`Thing not found: ${path}`);
|
|
1606
|
+
}
|
|
1607
|
+
const fullId = this.ns ? `${this.ns}/${parsed.noun}/${parsed.id}` : `${parsed.noun}/${parsed.id}`;
|
|
1608
|
+
return {
|
|
1609
|
+
$id: fullId,
|
|
1610
|
+
$type: parsed.noun,
|
|
1611
|
+
name: thing.name ?? undefined,
|
|
1612
|
+
data: thing.data ?? undefined,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
async resolveCrossDO(ns, path, ref) {
|
|
1616
|
+
const obj = await this.objects.get(ns);
|
|
1617
|
+
if (!obj) {
|
|
1618
|
+
throw new Error(`Unknown namespace: ${ns}`);
|
|
1619
|
+
}
|
|
1620
|
+
if (!this.env.DO) {
|
|
1621
|
+
throw new Error('DO namespace binding not configured');
|
|
1622
|
+
}
|
|
1623
|
+
const doNamespace = this.env.DO;
|
|
1624
|
+
const id = doNamespace.idFromString(obj.id);
|
|
1625
|
+
const stub = doNamespace.get(id);
|
|
1626
|
+
const resolveUrl = new URL(`${ns}/resolve`);
|
|
1627
|
+
resolveUrl.searchParams.set('path', path);
|
|
1628
|
+
resolveUrl.searchParams.set('ref', ref);
|
|
1629
|
+
const response = await stub.fetch(new Request(resolveUrl.toString(), {
|
|
1630
|
+
method: 'GET',
|
|
1631
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1632
|
+
}));
|
|
1633
|
+
if (!response.ok) {
|
|
1634
|
+
throw new Error(`Cross-DO resolution failed: ${response.status}`);
|
|
1635
|
+
}
|
|
1636
|
+
let thing;
|
|
1637
|
+
try {
|
|
1638
|
+
thing = await response.json();
|
|
1639
|
+
}
|
|
1640
|
+
catch {
|
|
1641
|
+
throw new Error('Invalid response from remote DO');
|
|
1642
|
+
}
|
|
1643
|
+
return thing;
|
|
1644
|
+
}
|
|
1645
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1646
|
+
// RELATIONSHIPS
|
|
1647
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1648
|
+
parent;
|
|
1649
|
+
async link(target, relationType = 'related') {
|
|
1650
|
+
const targetNs = typeof target === 'string' ? target : target.doId;
|
|
1651
|
+
const metadata = typeof target === 'string' ? undefined : target;
|
|
1652
|
+
await this.db.insert(schema.relationships).values({
|
|
1653
|
+
id: crypto.randomUUID(),
|
|
1654
|
+
verb: typeof target === 'string' ? relationType : target.role || relationType,
|
|
1655
|
+
from: this.ns,
|
|
1656
|
+
to: targetNs,
|
|
1657
|
+
data: metadata,
|
|
1658
|
+
createdAt: new Date(),
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
async getLinkedObjects(relationType) {
|
|
1662
|
+
const results = await this.db.select().from(schema.relationships);
|
|
1663
|
+
return results
|
|
1664
|
+
.filter((r) => r.from === this.ns && (!relationType || r.verb === relationType))
|
|
1665
|
+
.map((r) => ({
|
|
1666
|
+
ns: r.to,
|
|
1667
|
+
relationType: r.verb,
|
|
1668
|
+
doId: r.to,
|
|
1669
|
+
doClass: r.data?.doClass,
|
|
1670
|
+
data: r.data,
|
|
1671
|
+
}));
|
|
1672
|
+
}
|
|
1673
|
+
async createThing(data) {
|
|
1674
|
+
const id = crypto.randomUUID();
|
|
1675
|
+
// @ts-expect-error - Drizzle schema types may differ slightly
|
|
1676
|
+
await this.db.insert(schema.things).values({
|
|
1677
|
+
id,
|
|
1678
|
+
ns: this.ns,
|
|
1679
|
+
type: data.type,
|
|
1680
|
+
data: { name: data.name, ...data.data },
|
|
1681
|
+
version: 1,
|
|
1682
|
+
branch: this.currentBranch,
|
|
1683
|
+
createdAt: new Date(),
|
|
1684
|
+
updatedAt: new Date(),
|
|
1685
|
+
});
|
|
1686
|
+
return { id };
|
|
1687
|
+
}
|
|
1688
|
+
async createAction(data) {
|
|
1689
|
+
const id = crypto.randomUUID();
|
|
1690
|
+
// @ts-expect-error - Schema field names may differ
|
|
1691
|
+
await this.db.insert(schema.actions).values({
|
|
1692
|
+
id,
|
|
1693
|
+
verb: data.type,
|
|
1694
|
+
target: data.target,
|
|
1695
|
+
actor: data.actor,
|
|
1696
|
+
input: data.data,
|
|
1697
|
+
status: 'pending',
|
|
1698
|
+
createdAt: new Date(),
|
|
1699
|
+
});
|
|
1700
|
+
return { id };
|
|
1701
|
+
}
|
|
1702
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1703
|
+
// VISIBILITY HELPERS
|
|
1704
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1705
|
+
_currentActorContext = {};
|
|
1706
|
+
setActorContext(actor) {
|
|
1707
|
+
this._currentActorContext = actor;
|
|
1708
|
+
}
|
|
1709
|
+
getActorContext() {
|
|
1710
|
+
return this._currentActorContext;
|
|
1711
|
+
}
|
|
1712
|
+
clearActorContext() {
|
|
1713
|
+
this._currentActorContext = {};
|
|
1714
|
+
}
|
|
1715
|
+
canViewThing(thing) {
|
|
1716
|
+
if (!thing) {
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
const visibility = thing.data?.visibility ?? 'user';
|
|
1720
|
+
const actor = this._currentActorContext;
|
|
1721
|
+
if (visibility === 'public' || visibility === 'unlisted') {
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1724
|
+
if (visibility === 'org') {
|
|
1725
|
+
const dataObj = thing.data;
|
|
1726
|
+
const metaObj = dataObj?.meta;
|
|
1727
|
+
const thingOrgId = metaObj?.orgId ?? dataObj?.orgId;
|
|
1728
|
+
return !!actor.orgId && actor.orgId === thingOrgId;
|
|
1729
|
+
}
|
|
1730
|
+
const dataObj = thing.data;
|
|
1731
|
+
const metaObj = dataObj?.meta;
|
|
1732
|
+
const thingOwnerId = metaObj?.ownerId ?? dataObj?.ownerId;
|
|
1733
|
+
return !!actor.userId && actor.userId === thingOwnerId;
|
|
1734
|
+
}
|
|
1735
|
+
assertCanView(thing, message) {
|
|
1736
|
+
if (!thing) {
|
|
1737
|
+
throw new Error(message ?? 'Thing not found');
|
|
1738
|
+
}
|
|
1739
|
+
if (!this.canViewThing(thing)) {
|
|
1740
|
+
const visibility = thing.data?.visibility ?? 'user';
|
|
1741
|
+
let reason;
|
|
1742
|
+
switch (visibility) {
|
|
1743
|
+
case 'org':
|
|
1744
|
+
reason = 'Organization membership required';
|
|
1745
|
+
break;
|
|
1746
|
+
case 'user':
|
|
1747
|
+
reason = 'Owner access required';
|
|
1748
|
+
break;
|
|
1749
|
+
default:
|
|
1750
|
+
reason = 'Access denied';
|
|
1751
|
+
}
|
|
1752
|
+
throw new Error(message ?? reason);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
filterVisibleThings(things) {
|
|
1756
|
+
return things.filter((thing) => this.canViewThing(thing));
|
|
1757
|
+
}
|
|
1758
|
+
async getVisibleThing(id) {
|
|
1759
|
+
const thing = await this.things.get(id);
|
|
1760
|
+
if (!thing) {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
return this.canViewThing(thing) ? thing : null;
|
|
1764
|
+
}
|
|
1765
|
+
getVisibility(thing) {
|
|
1766
|
+
if (!thing) {
|
|
1767
|
+
return 'user';
|
|
1768
|
+
}
|
|
1769
|
+
return thing.data?.visibility ?? 'user';
|
|
1770
|
+
}
|
|
1771
|
+
isOwner(thing) {
|
|
1772
|
+
if (!thing) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
const dataObj = thing.data;
|
|
1776
|
+
const metaObj = dataObj?.meta;
|
|
1777
|
+
const thingOwnerId = metaObj?.ownerId ?? dataObj?.ownerId;
|
|
1778
|
+
const actor = this._currentActorContext;
|
|
1779
|
+
return !!actor.userId && actor.userId === thingOwnerId;
|
|
1780
|
+
}
|
|
1781
|
+
isInThingOrg(thing) {
|
|
1782
|
+
if (!thing) {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
const dataObj = thing.data;
|
|
1786
|
+
const metaObj = dataObj?.meta;
|
|
1787
|
+
const thingOrgId = metaObj?.orgId ?? dataObj?.orgId;
|
|
1788
|
+
const actor = this._currentActorContext;
|
|
1789
|
+
return !!actor.orgId && actor.orgId === thingOrgId;
|
|
1790
|
+
}
|
|
1791
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1792
|
+
// REST ROUTER CONTEXT
|
|
1793
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1794
|
+
/**
|
|
1795
|
+
* Get REST router context for handling REST API requests.
|
|
1796
|
+
* Provides the things store and namespace for CRUD operations.
|
|
1797
|
+
*/
|
|
1798
|
+
getRestRouterContext() {
|
|
1799
|
+
// Get the DO class type from static $type or constructor name
|
|
1800
|
+
const DOClass = this.constructor;
|
|
1801
|
+
const doType = DOClass.$type || this.constructor.name;
|
|
1802
|
+
return {
|
|
1803
|
+
things: this.things,
|
|
1804
|
+
ns: this.ns,
|
|
1805
|
+
contextUrl: 'https://dotdo.dev/context',
|
|
1806
|
+
nouns: this.getRegisteredNouns(),
|
|
1807
|
+
doType,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Get list of registered nouns for the index.
|
|
1812
|
+
* Override in subclasses to provide custom noun list.
|
|
1813
|
+
*/
|
|
1814
|
+
getRegisteredNouns() {
|
|
1815
|
+
// Return cached nouns from typeCache
|
|
1816
|
+
// This is populated as types are used
|
|
1817
|
+
const nouns = [];
|
|
1818
|
+
for (const [noun] of this._typeCache) {
|
|
1819
|
+
// Simple pluralization (subclasses can override for complex cases)
|
|
1820
|
+
const plural = noun.endsWith('y')
|
|
1821
|
+
? noun.slice(0, -1) + 'ies'
|
|
1822
|
+
: noun.endsWith('s') || noun.endsWith('x') || noun.endsWith('ch') || noun.endsWith('sh')
|
|
1823
|
+
? noun + 'es'
|
|
1824
|
+
: noun + 's';
|
|
1825
|
+
nouns.push({ noun, plural });
|
|
1826
|
+
}
|
|
1827
|
+
return nouns;
|
|
1828
|
+
}
|
|
1829
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1830
|
+
// MCP HANDLER
|
|
1831
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1832
|
+
/**
|
|
1833
|
+
* Handle MCP (Model Context Protocol) requests.
|
|
1834
|
+
* This method is exposed for direct MCP access and is also routed from /mcp path.
|
|
1835
|
+
*
|
|
1836
|
+
* @param request - The incoming HTTP request
|
|
1837
|
+
* @returns Response with JSON-RPC 2.0 formatted result
|
|
1838
|
+
*/
|
|
1839
|
+
async handleMcp(request) {
|
|
1840
|
+
const DOClass = this.constructor;
|
|
1841
|
+
// Initialize MCP handler if not already done
|
|
1842
|
+
if (!this._mcpHandler) {
|
|
1843
|
+
this._mcpHandler = createMcpHandler(DOClass);
|
|
1844
|
+
}
|
|
1845
|
+
return this._mcpHandler(this, request, this._mcpSessions);
|
|
1846
|
+
}
|
|
1847
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1848
|
+
// SYNC WEBSOCKET HANDLER
|
|
1849
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1850
|
+
/**
|
|
1851
|
+
* Handle WebSocket sync requests for TanStack DB integration.
|
|
1852
|
+
* Returns 426 Upgrade Required for non-WebSocket requests.
|
|
1853
|
+
*
|
|
1854
|
+
* @param request - The incoming HTTP request
|
|
1855
|
+
* @returns Response (101 for WebSocket upgrade, 426 for non-WebSocket)
|
|
1856
|
+
*/
|
|
1857
|
+
handleSyncWebSocket(request) {
|
|
1858
|
+
// Check for WebSocket upgrade
|
|
1859
|
+
const upgradeHeader = request.headers.get('upgrade');
|
|
1860
|
+
if (upgradeHeader?.toLowerCase() !== 'websocket') {
|
|
1861
|
+
return Response.json({ error: 'WebSocket upgrade required for /sync endpoint' }, {
|
|
1862
|
+
status: 426,
|
|
1863
|
+
headers: { 'Upgrade': 'websocket' },
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
// Create WebSocket pair
|
|
1867
|
+
const pair = new WebSocketPair();
|
|
1868
|
+
const [client, server] = Object.values(pair);
|
|
1869
|
+
// Accept the server side
|
|
1870
|
+
server.accept();
|
|
1871
|
+
// Register with sync engine
|
|
1872
|
+
this.syncEngine.accept(server);
|
|
1873
|
+
// Return the client side to the caller
|
|
1874
|
+
return new Response(null, {
|
|
1875
|
+
status: 101,
|
|
1876
|
+
webSocket: client,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1880
|
+
// HTTP HANDLER (Extended from DOTiny)
|
|
1881
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1882
|
+
async handleFetch(request) {
|
|
1883
|
+
const url = new URL(request.url);
|
|
1884
|
+
// Extract coordinates from CF request headers if available
|
|
1885
|
+
const cf = request.cf;
|
|
1886
|
+
if (cf?.latitude && cf?.longitude && !this._extractedCoordinates) {
|
|
1887
|
+
const lat = parseFloat(cf.latitude);
|
|
1888
|
+
const lng = parseFloat(cf.longitude);
|
|
1889
|
+
if (!isNaN(lat) && !isNaN(lng)) {
|
|
1890
|
+
this._extractedCoordinates = { lat, lng };
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// Built-in routes
|
|
1894
|
+
if (url.pathname === '/health') {
|
|
1895
|
+
return Response.json({ status: 'ok', ns: this.ns });
|
|
1896
|
+
}
|
|
1897
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1898
|
+
// ROOT ENDPOINT (/) - Cap'n Web RPC
|
|
1899
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1900
|
+
// POST / → capnweb RPC (HTTP batch)
|
|
1901
|
+
// WebSocket / → capnweb RPC (persistent connection)
|
|
1902
|
+
// GET / → info/discovery
|
|
1903
|
+
if (url.pathname === '/') {
|
|
1904
|
+
// Handle Cap'n Web RPC (POST or WebSocket upgrade)
|
|
1905
|
+
if (isCapnWebRequest(request)) {
|
|
1906
|
+
return this.#handleCapnWebRpc(request);
|
|
1907
|
+
}
|
|
1908
|
+
// GET request - return JSON-LD index with collections
|
|
1909
|
+
if (request.method === 'GET') {
|
|
1910
|
+
// Build REST router context for index generation
|
|
1911
|
+
const restCtx = this.getRestRouterContext();
|
|
1912
|
+
return handleGetIndex(restCtx, request);
|
|
1913
|
+
}
|
|
1914
|
+
// Other methods not supported at root
|
|
1915
|
+
return new Response('Method Not Allowed', {
|
|
1916
|
+
status: 405,
|
|
1917
|
+
headers: { 'Allow': 'GET, POST' },
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
// Handle /$introspect endpoint for schema discovery
|
|
1921
|
+
if (url.pathname === '/$introspect') {
|
|
1922
|
+
return this.handleIntrospectRoute(request);
|
|
1923
|
+
}
|
|
1924
|
+
// Handle /mcp endpoint for MCP transport
|
|
1925
|
+
if (url.pathname === '/mcp') {
|
|
1926
|
+
return this.handleMcp(request);
|
|
1927
|
+
}
|
|
1928
|
+
// Handle /rpc endpoint for RPC protocol (JSON-RPC 2.0 + Chain RPC)
|
|
1929
|
+
if (url.pathname === '/rpc') {
|
|
1930
|
+
// Check for WebSocket upgrade
|
|
1931
|
+
const upgradeHeader = request.headers.get('upgrade');
|
|
1932
|
+
const connectionHeader = request.headers.get('connection')?.toLowerCase() || '';
|
|
1933
|
+
const hasConnectionUpgrade = connectionHeader.includes('upgrade');
|
|
1934
|
+
if (upgradeHeader?.toLowerCase() === 'websocket' && hasConnectionUpgrade) {
|
|
1935
|
+
return this.rpcServer.handleWebSocketRpc();
|
|
1936
|
+
}
|
|
1937
|
+
// HTTP RPC request
|
|
1938
|
+
if (request.method === 'POST') {
|
|
1939
|
+
return this.rpcServer.handleRpcRequest(request);
|
|
1940
|
+
}
|
|
1941
|
+
// GET request - return RPC info
|
|
1942
|
+
return Response.json({
|
|
1943
|
+
message: 'RPC endpoint - use POST for HTTP batch mode or WebSocket for streaming',
|
|
1944
|
+
methods: this.rpcServer.methods,
|
|
1945
|
+
}, { headers: { 'Content-Type': 'application/json' } });
|
|
1946
|
+
}
|
|
1947
|
+
// Handle /sync endpoint for WebSocket sync protocol (TanStack DB)
|
|
1948
|
+
if (url.pathname === '/sync') {
|
|
1949
|
+
return this.handleSyncWebSocket(request);
|
|
1950
|
+
}
|
|
1951
|
+
// Handle /resolve endpoint for cross-DO resolution
|
|
1952
|
+
if (url.pathname === '/resolve') {
|
|
1953
|
+
const path = url.searchParams.get('path');
|
|
1954
|
+
const ref = url.searchParams.get('ref') || 'main';
|
|
1955
|
+
if (!path) {
|
|
1956
|
+
return Response.json({ error: 'Missing path parameter' }, { status: 400 });
|
|
1957
|
+
}
|
|
1958
|
+
try {
|
|
1959
|
+
const thing = await this.resolveLocal(path, ref);
|
|
1960
|
+
return Response.json(thing);
|
|
1961
|
+
}
|
|
1962
|
+
catch (error) {
|
|
1963
|
+
const message = error instanceof Error ? error.message : 'Resolution failed';
|
|
1964
|
+
return Response.json({ error: message }, { status: 404 });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
// Delegate to Hono app if configured
|
|
1968
|
+
if (this.app) {
|
|
1969
|
+
const response = await this.app.fetch(request, this.env);
|
|
1970
|
+
return response;
|
|
1971
|
+
}
|
|
1972
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1973
|
+
// REST API ROUTES (/:type and /:type/:id)
|
|
1974
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1975
|
+
// Handle REST routes for Things CRUD operations
|
|
1976
|
+
// GET /customers → list customers
|
|
1977
|
+
// GET /customers/cust-1 → get customer by id
|
|
1978
|
+
// POST /customers → create customer
|
|
1979
|
+
// PUT /customers/cust-1 → replace customer
|
|
1980
|
+
// PATCH /customers/cust-1 → update customer (merge)
|
|
1981
|
+
// DELETE /customers/cust-1 → delete customer
|
|
1982
|
+
const restCtx = this.getRestRouterContext();
|
|
1983
|
+
const restResponse = await handleRestRequest(request, restCtx);
|
|
1984
|
+
if (restResponse) {
|
|
1985
|
+
return restResponse;
|
|
1986
|
+
}
|
|
1987
|
+
// Default: 404 Not Found
|
|
1988
|
+
return new Response('Not Found', { status: 404 });
|
|
1989
|
+
}
|
|
1990
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1991
|
+
// CAP'N WEB RPC HANDLER
|
|
1992
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1993
|
+
/**
|
|
1994
|
+
* Cap'n Web RPC options. Override in subclasses to customize.
|
|
1995
|
+
*/
|
|
1996
|
+
get capnWebOptions() {
|
|
1997
|
+
return {
|
|
1998
|
+
includeStackTraces: this.env.ENVIRONMENT !== 'production',
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Handle Cap'n Web RPC requests (POST or WebSocket upgrade at root endpoint).
|
|
2003
|
+
*
|
|
2004
|
+
* This is marked with # prefix to indicate it's internal and should not
|
|
2005
|
+
* be exposed via RPC itself.
|
|
2006
|
+
*/
|
|
2007
|
+
async #handleCapnWebRpc(request) {
|
|
2008
|
+
return handleCapnWebRpc(request, this, this.capnWebOptions);
|
|
2009
|
+
}
|
|
2010
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2011
|
+
// ALARM HANDLER (for scheduled tasks)
|
|
2012
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2013
|
+
async alarm() {
|
|
2014
|
+
// Delegate to schedule manager to handle scheduled tasks
|
|
2015
|
+
if (this._scheduleManager) {
|
|
2016
|
+
await this._scheduleManager.handleAlarm();
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2020
|
+
// INTROSPECTION HTTP HANDLER
|
|
2021
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2022
|
+
/**
|
|
2023
|
+
* Handle the /$introspect HTTP route.
|
|
2024
|
+
* Requires authentication and returns the DOSchema.
|
|
2025
|
+
*/
|
|
2026
|
+
async handleIntrospectRoute(request) {
|
|
2027
|
+
// Only allow GET requests
|
|
2028
|
+
if (request.method !== 'GET') {
|
|
2029
|
+
return Response.json({ error: 'Method not allowed' }, { status: 405 });
|
|
2030
|
+
}
|
|
2031
|
+
// Check for Authorization header
|
|
2032
|
+
const authHeader = request.headers.get('Authorization');
|
|
2033
|
+
if (!authHeader) {
|
|
2034
|
+
return Response.json({ error: 'Authentication required' }, { status: 401 });
|
|
2035
|
+
}
|
|
2036
|
+
// Parse JWT from Bearer token
|
|
2037
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
2038
|
+
return Response.json({ error: 'Invalid authorization header' }, { status: 401 });
|
|
2039
|
+
}
|
|
2040
|
+
const token = authHeader.slice(7);
|
|
2041
|
+
// Parse and verify the JWT
|
|
2042
|
+
try {
|
|
2043
|
+
const parts = token.split('.');
|
|
2044
|
+
if (parts.length !== 3) {
|
|
2045
|
+
return Response.json({ error: 'Invalid token format' }, { status: 401 });
|
|
2046
|
+
}
|
|
2047
|
+
// Verify JWT signature if JWT_SECRET is configured
|
|
2048
|
+
const jwtSecret = this.env.JWT_SECRET;
|
|
2049
|
+
if (jwtSecret) {
|
|
2050
|
+
const isValid = await this.verifyJwtSignature(token, jwtSecret);
|
|
2051
|
+
if (!isValid) {
|
|
2052
|
+
return Response.json({ error: 'Invalid token signature' }, { status: 401 });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
// In development without JWT_SECRET, log warning but allow request
|
|
2057
|
+
console.warn('JWT_SECRET not configured - skipping signature verification');
|
|
2058
|
+
}
|
|
2059
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
2060
|
+
// Check expiration
|
|
2061
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2062
|
+
if (payload.exp && payload.exp < now) {
|
|
2063
|
+
return Response.json({ error: 'Token expired' }, { status: 401 });
|
|
2064
|
+
}
|
|
2065
|
+
// Build auth context from JWT claims
|
|
2066
|
+
const authContext = {
|
|
2067
|
+
authenticated: true,
|
|
2068
|
+
user: {
|
|
2069
|
+
id: payload.sub || 'anonymous',
|
|
2070
|
+
email: payload.email,
|
|
2071
|
+
name: payload.name,
|
|
2072
|
+
roles: payload.roles || [],
|
|
2073
|
+
permissions: payload.permissions || [],
|
|
2074
|
+
},
|
|
2075
|
+
token: {
|
|
2076
|
+
type: 'jwt',
|
|
2077
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(Date.now() + 3600000),
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
// Call $introspect with auth context
|
|
2081
|
+
const schema = await this.$introspect(authContext);
|
|
2082
|
+
return Response.json(schema);
|
|
2083
|
+
}
|
|
2084
|
+
catch (error) {
|
|
2085
|
+
return Response.json({ error: 'Invalid token' }, { status: 401 });
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2089
|
+
// INTROSPECTION ($introspect)
|
|
2090
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2091
|
+
/**
|
|
2092
|
+
* Introspect the DO schema, filtered by user role.
|
|
2093
|
+
*
|
|
2094
|
+
* Returns information about:
|
|
2095
|
+
* - Available classes and their methods
|
|
2096
|
+
* - MCP tools from static $mcp config
|
|
2097
|
+
* - REST endpoints from static $rest config
|
|
2098
|
+
* - Available stores (filtered by role)
|
|
2099
|
+
* - Storage capabilities (filtered by role)
|
|
2100
|
+
* - Registered nouns and verbs
|
|
2101
|
+
*
|
|
2102
|
+
* @param authContext - Optional auth context for role-based filtering
|
|
2103
|
+
* @returns DOSchema object with introspection data
|
|
2104
|
+
*/
|
|
2105
|
+
async $introspect(authContext) {
|
|
2106
|
+
// Determine role from auth context
|
|
2107
|
+
const role = this.determineRole(authContext);
|
|
2108
|
+
const scopes = authContext?.user?.permissions || [];
|
|
2109
|
+
// Build the schema response
|
|
2110
|
+
const schema = {
|
|
2111
|
+
ns: this.ns,
|
|
2112
|
+
permissions: {
|
|
2113
|
+
role,
|
|
2114
|
+
scopes,
|
|
2115
|
+
},
|
|
2116
|
+
classes: this.introspectClasses(role),
|
|
2117
|
+
nouns: await this.introspectNouns(),
|
|
2118
|
+
verbs: await this.introspectVerbs(),
|
|
2119
|
+
stores: this.introspectStores(role),
|
|
2120
|
+
storage: this.introspectStorage(role),
|
|
2121
|
+
};
|
|
2122
|
+
return schema;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Determine the effective role from auth context
|
|
2126
|
+
*/
|
|
2127
|
+
determineRole(authContext) {
|
|
2128
|
+
if (!authContext || !authContext.authenticated) {
|
|
2129
|
+
return 'public';
|
|
2130
|
+
}
|
|
2131
|
+
const roles = authContext.user?.roles || [];
|
|
2132
|
+
if (roles.length === 0) {
|
|
2133
|
+
// Authenticated but no specific roles - default to 'user'
|
|
2134
|
+
return 'user';
|
|
2135
|
+
}
|
|
2136
|
+
return getHighestRole(roles);
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Introspect available classes
|
|
2140
|
+
*/
|
|
2141
|
+
introspectClasses(role) {
|
|
2142
|
+
const classes = [];
|
|
2143
|
+
// Get class info from constructor
|
|
2144
|
+
const DOClass = this.constructor;
|
|
2145
|
+
const className = DOClass.$type || this.constructor.name;
|
|
2146
|
+
// Determine class visibility based on config
|
|
2147
|
+
// Default to 'user' visibility for the class itself
|
|
2148
|
+
const classVisibility = 'user';
|
|
2149
|
+
// Only include class if caller can access it
|
|
2150
|
+
if (!canAccessVisibility(role, classVisibility)) {
|
|
2151
|
+
return classes;
|
|
2152
|
+
}
|
|
2153
|
+
// Build tools list from $mcp config, filtered by role
|
|
2154
|
+
const tools = [];
|
|
2155
|
+
if (DOClass.$mcp?.tools) {
|
|
2156
|
+
for (const [name, config] of Object.entries(DOClass.$mcp.tools)) {
|
|
2157
|
+
const toolVisibility = config.visibility || 'user';
|
|
2158
|
+
if (canAccessVisibility(role, toolVisibility)) {
|
|
2159
|
+
tools.push({
|
|
2160
|
+
name,
|
|
2161
|
+
description: config.description,
|
|
2162
|
+
inputSchema: config.inputSchema,
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// Build endpoints list from $rest config, filtered by role
|
|
2168
|
+
const endpoints = [];
|
|
2169
|
+
if (DOClass.$rest?.endpoints) {
|
|
2170
|
+
for (const endpoint of DOClass.$rest.endpoints) {
|
|
2171
|
+
const endpointVisibility = endpoint.visibility || 'user';
|
|
2172
|
+
if (canAccessVisibility(role, endpointVisibility)) {
|
|
2173
|
+
endpoints.push({
|
|
2174
|
+
method: endpoint.method,
|
|
2175
|
+
path: endpoint.path,
|
|
2176
|
+
description: endpoint.description,
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Build class schema
|
|
2182
|
+
classes.push({
|
|
2183
|
+
name: className,
|
|
2184
|
+
type: 'thing', // Default to 'thing', could be 'collection' for collection DOs
|
|
2185
|
+
pattern: `/:type/:id`,
|
|
2186
|
+
visibility: classVisibility,
|
|
2187
|
+
tools,
|
|
2188
|
+
endpoints,
|
|
2189
|
+
properties: [], // Could be populated from static schema
|
|
2190
|
+
actions: [], // Could be populated from method decorators
|
|
2191
|
+
});
|
|
2192
|
+
return classes;
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Introspect available stores, filtered by role
|
|
2196
|
+
*/
|
|
2197
|
+
introspectStores(role) {
|
|
2198
|
+
const stores = [];
|
|
2199
|
+
const storeDefinitions = [
|
|
2200
|
+
{ name: 'things', type: 'things' },
|
|
2201
|
+
{ name: 'relationships', type: 'relationships' },
|
|
2202
|
+
{ name: 'actions', type: 'actions' },
|
|
2203
|
+
{ name: 'events', type: 'events' },
|
|
2204
|
+
{ name: 'search', type: 'search' },
|
|
2205
|
+
{ name: 'objects', type: 'objects' },
|
|
2206
|
+
{ name: 'dlq', type: 'dlq' },
|
|
2207
|
+
];
|
|
2208
|
+
for (const store of storeDefinitions) {
|
|
2209
|
+
const storeVisibility = STORE_VISIBILITY[store.type];
|
|
2210
|
+
if (canAccessVisibility(role, storeVisibility)) {
|
|
2211
|
+
stores.push({
|
|
2212
|
+
name: store.name,
|
|
2213
|
+
type: store.type,
|
|
2214
|
+
visibility: storeVisibility,
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return stores;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Introspect storage capabilities, filtered by role
|
|
2222
|
+
*/
|
|
2223
|
+
introspectStorage(role) {
|
|
2224
|
+
// Capability visibility levels:
|
|
2225
|
+
// - fsx, gitx: user and above
|
|
2226
|
+
// - bashx, r2, sql: admin and above
|
|
2227
|
+
// - iceberg, edgevec: system only
|
|
2228
|
+
const isUser = canAccessVisibility(role, 'user');
|
|
2229
|
+
const isAdmin = canAccessVisibility(role, 'admin');
|
|
2230
|
+
const isSystem = canAccessVisibility(role, 'system');
|
|
2231
|
+
return {
|
|
2232
|
+
fsx: isUser,
|
|
2233
|
+
gitx: isUser,
|
|
2234
|
+
bashx: isAdmin,
|
|
2235
|
+
r2: {
|
|
2236
|
+
enabled: isAdmin,
|
|
2237
|
+
buckets: isAdmin ? [] : undefined, // Would list actual buckets from env
|
|
2238
|
+
},
|
|
2239
|
+
sql: {
|
|
2240
|
+
enabled: isAdmin,
|
|
2241
|
+
tables: isAdmin ? [] : undefined, // Would list actual tables from schema
|
|
2242
|
+
},
|
|
2243
|
+
iceberg: isSystem,
|
|
2244
|
+
edgevec: isSystem,
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Introspect registered nouns
|
|
2249
|
+
*/
|
|
2250
|
+
async introspectNouns() {
|
|
2251
|
+
// Query nouns from the database (simplified for now)
|
|
2252
|
+
// In full implementation, would query from db.nouns table
|
|
2253
|
+
return [];
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Introspect registered verbs
|
|
2257
|
+
*/
|
|
2258
|
+
async introspectVerbs() {
|
|
2259
|
+
// TODO: Query verbs from the database
|
|
2260
|
+
// In full implementation, would query from db.verbs table
|
|
2261
|
+
return [];
|
|
2262
|
+
}
|
|
2263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2264
|
+
// JWT VERIFICATION HELPERS
|
|
2265
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2266
|
+
/**
|
|
2267
|
+
* Verify JWT signature using HMAC-SHA256.
|
|
2268
|
+
*
|
|
2269
|
+
* @param token - The full JWT token string
|
|
2270
|
+
* @param secret - The secret key for verification
|
|
2271
|
+
* @returns true if signature is valid, false otherwise
|
|
2272
|
+
*/
|
|
2273
|
+
async verifyJwtSignature(token, secret) {
|
|
2274
|
+
try {
|
|
2275
|
+
const parts = token.split('.');
|
|
2276
|
+
if (parts.length !== 3)
|
|
2277
|
+
return false;
|
|
2278
|
+
const signatureInput = `${parts[0]}.${parts[1]}`;
|
|
2279
|
+
const signature = parts[2];
|
|
2280
|
+
const encoder = new TextEncoder();
|
|
2281
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
2282
|
+
// Base64url decode the signature
|
|
2283
|
+
const signatureBytes = this.base64UrlDecode(signature);
|
|
2284
|
+
return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(signatureInput));
|
|
2285
|
+
}
|
|
2286
|
+
catch {
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Decode a base64url-encoded string to Uint8Array.
|
|
2292
|
+
*/
|
|
2293
|
+
base64UrlDecode(str) {
|
|
2294
|
+
// Replace base64url chars with base64 chars
|
|
2295
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
2296
|
+
// Pad with '=' to make it valid base64
|
|
2297
|
+
while (base64.length % 4) {
|
|
2298
|
+
base64 += '=';
|
|
2299
|
+
}
|
|
2300
|
+
const binary = atob(base64);
|
|
2301
|
+
const bytes = new Uint8Array(binary.length);
|
|
2302
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2303
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2304
|
+
}
|
|
2305
|
+
return bytes;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
export default DO;
|
|
2309
|
+
//# sourceMappingURL=DOBase.js.map
|