@justscale/core 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 +21 -0
- package/README.md +261 -0
- package/bin/just.js +34 -0
- package/dist/app.d.ts +56 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +182 -0
- package/dist/app.js.map +1 -0
- package/dist/builder/build-context.d.ts +8 -0
- package/dist/builder/build-context.d.ts.map +1 -0
- package/dist/builder/build-context.js +4 -0
- package/dist/builder/build-context.js.map +1 -0
- package/dist/builder/builder.d.ts +60 -0
- package/dist/builder/builder.d.ts.map +1 -0
- package/dist/builder/builder.js +72 -0
- package/dist/builder/builder.js.map +1 -0
- package/dist/builder/create-builder.d.ts +36 -0
- package/dist/builder/create-builder.d.ts.map +1 -0
- package/dist/builder/create-builder.js +66 -0
- package/dist/builder/create-builder.js.map +1 -0
- package/dist/builder/execute.d.ts +37 -0
- package/dist/builder/execute.d.ts.map +1 -0
- package/dist/builder/execute.js +62 -0
- package/dist/builder/execute.js.map +1 -0
- package/dist/builder/feature-builder.d.ts +147 -0
- package/dist/builder/feature-builder.d.ts.map +1 -0
- package/dist/builder/feature-builder.js +138 -0
- package/dist/builder/feature-builder.js.map +1 -0
- package/dist/builder/index.d.ts +35 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +37 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/plugin.d.ts +90 -0
- package/dist/builder/plugin.d.ts.map +1 -0
- package/dist/builder/plugin.js +101 -0
- package/dist/builder/plugin.js.map +1 -0
- package/dist/builder/plugins/query.d.ts +31 -0
- package/dist/builder/plugins/query.d.ts.map +1 -0
- package/dist/builder/plugins/query.js +42 -0
- package/dist/builder/plugins/query.js.map +1 -0
- package/dist/builder/plugins/validation.d.ts +12 -0
- package/dist/builder/plugins/validation.d.ts.map +1 -0
- package/dist/builder/plugins/validation.js +12 -0
- package/dist/builder/plugins/validation.js.map +1 -0
- package/dist/builder/sort.d.ts +27 -0
- package/dist/builder/sort.d.ts.map +1 -0
- package/dist/builder/sort.js +210 -0
- package/dist/builder/sort.js.map +1 -0
- package/dist/builder/stop.d.ts +24 -0
- package/dist/builder/stop.d.ts.map +1 -0
- package/dist/builder/stop.js +27 -0
- package/dist/builder/stop.js.map +1 -0
- package/dist/builder/test/permits-type-spike.d.ts +8 -0
- package/dist/builder/test/permits-type-spike.d.ts.map +1 -0
- package/dist/builder/test/permits-type-spike.js +117 -0
- package/dist/builder/test/permits-type-spike.js.map +1 -0
- package/dist/builder/types.d.ts +678 -0
- package/dist/builder/types.d.ts.map +1 -0
- package/dist/builder/types.js +98 -0
- package/dist/builder/types.js.map +1 -0
- package/dist/builder/validation.d.ts +101 -0
- package/dist/builder/validation.d.ts.map +1 -0
- package/dist/builder/validation.js +335 -0
- package/dist/builder/validation.js.map +1 -0
- package/dist/cli/adapter.d.ts +23 -0
- package/dist/cli/adapter.d.ts.map +1 -0
- package/dist/cli/adapter.js +26 -0
- package/dist/cli/adapter.js.map +1 -0
- package/dist/cli/args.d.ts +150 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +172 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/assemble.d.ts +20 -0
- package/dist/cli/assemble.d.ts.map +1 -0
- package/dist/cli/assemble.js +55 -0
- package/dist/cli/assemble.js.map +1 -0
- package/dist/cli/bin/main.d.ts +26 -0
- package/dist/cli/bin/main.d.ts.map +1 -0
- package/dist/cli/bin/main.js +475 -0
- package/dist/cli/bin/main.js.map +1 -0
- package/dist/cli/build/migrations-plugin.d.ts +21 -0
- package/dist/cli/build/migrations-plugin.d.ts.map +1 -0
- package/dist/cli/build/migrations-plugin.js +41 -0
- package/dist/cli/build/migrations-plugin.js.map +1 -0
- package/dist/cli/build/process-plugin.d.ts +29 -0
- package/dist/cli/build/process-plugin.d.ts.map +1 -0
- package/dist/cli/build/process-plugin.js +66 -0
- package/dist/cli/build/process-plugin.js.map +1 -0
- package/dist/cli/builder/create-cli-builder.d.ts +42 -0
- package/dist/cli/builder/create-cli-builder.d.ts.map +1 -0
- package/dist/cli/builder/create-cli-builder.js +104 -0
- package/dist/cli/builder/create-cli-builder.js.map +1 -0
- package/dist/cli/builder/index.d.ts +8 -0
- package/dist/cli/builder/index.d.ts.map +1 -0
- package/dist/cli/builder/index.js +7 -0
- package/dist/cli/builder/index.js.map +1 -0
- package/dist/cli/builder/types.d.ts +113 -0
- package/dist/cli/builder/types.d.ts.map +1 -0
- package/dist/cli/builder/types.js +7 -0
- package/dist/cli/builder/types.js.map +1 -0
- package/dist/cli/cluster.d.ts +8 -0
- package/dist/cli/cluster.d.ts.map +1 -0
- package/dist/cli/cluster.js +145 -0
- package/dist/cli/cluster.js.map +1 -0
- package/dist/cli/current-app.d.ts +36 -0
- package/dist/cli/current-app.d.ts.map +1 -0
- package/dist/cli/current-app.js +21 -0
- package/dist/cli/current-app.js.map +1 -0
- package/dist/cli/define-app.d.ts +35 -0
- package/dist/cli/define-app.d.ts.map +1 -0
- package/dist/cli/define-app.js +79 -0
- package/dist/cli/define-app.js.map +1 -0
- package/dist/cli/define-main.d.ts +33 -0
- package/dist/cli/define-main.d.ts.map +1 -0
- package/dist/cli/define-main.js +67 -0
- package/dist/cli/define-main.js.map +1 -0
- package/dist/cli/define-project.d.ts +93 -0
- package/dist/cli/define-project.d.ts.map +1 -0
- package/dist/cli/define-project.js +85 -0
- package/dist/cli/define-project.js.map +1 -0
- package/dist/cli/dev-server.d.ts +20 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +131 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/discovery.d.ts +29 -0
- package/dist/cli/discovery.d.ts.map +1 -0
- package/dist/cli/discovery.js +142 -0
- package/dist/cli/discovery.js.map +1 -0
- package/dist/cli/factory.d.ts +43 -0
- package/dist/cli/factory.d.ts.map +1 -0
- package/dist/cli/factory.js +52 -0
- package/dist/cli/factory.js.map +1 -0
- package/dist/cli/generators/ai.d.ts +3 -0
- package/dist/cli/generators/ai.d.ts.map +1 -0
- package/dist/cli/generators/ai.js +65 -0
- package/dist/cli/generators/ai.js.map +1 -0
- package/dist/cli/generators/ci.d.ts +5 -0
- package/dist/cli/generators/ci.d.ts.map +1 -0
- package/dist/cli/generators/ci.js +102 -0
- package/dist/cli/generators/ci.js.map +1 -0
- package/dist/cli/generators/detect.d.ts +15 -0
- package/dist/cli/generators/detect.d.ts.map +1 -0
- package/dist/cli/generators/detect.js +75 -0
- package/dist/cli/generators/detect.js.map +1 -0
- package/dist/cli/generators/ide.d.ts +3 -0
- package/dist/cli/generators/ide.d.ts.map +1 -0
- package/dist/cli/generators/ide.js +179 -0
- package/dist/cli/generators/ide.js.map +1 -0
- package/dist/cli/generators/index.d.ts +5 -0
- package/dist/cli/generators/index.d.ts.map +1 -0
- package/dist/cli/generators/index.js +5 -0
- package/dist/cli/generators/index.js.map +1 -0
- package/dist/cli/index.d.ts +81 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +88 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/io.d.ts +131 -0
- package/dist/cli/io.d.ts.map +1 -0
- package/dist/cli/io.js +373 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +23 -0
- package/dist/cli/mcp/server.d.ts.map +1 -0
- package/dist/cli/mcp/server.js +148 -0
- package/dist/cli/mcp/server.js.map +1 -0
- package/dist/cli/parser.d.ts +106 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +520 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/runner.d.ts +75 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +422 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/service.d.ts +61 -0
- package/dist/cli/service.d.ts.map +1 -0
- package/dist/cli/service.js +95 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/types.d.ts +47 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +20 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/wizard.d.ts +11 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +2 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/cli/workspace-controller.d.ts +36 -0
- package/dist/cli/workspace-controller.d.ts.map +1 -0
- package/dist/cli/workspace-controller.js +896 -0
- package/dist/cli/workspace-controller.js.map +1 -0
- package/dist/cluster/client.d.ts +101 -0
- package/dist/cluster/client.d.ts.map +1 -0
- package/dist/cluster/client.js +256 -0
- package/dist/cluster/client.js.map +1 -0
- package/dist/cluster/cluster.d.ts +82 -0
- package/dist/cluster/cluster.d.ts.map +1 -0
- package/dist/cluster/cluster.js +27 -0
- package/dist/cluster/cluster.js.map +1 -0
- package/dist/cluster/coordinator/cluster-node.model.d.ts +14 -0
- package/dist/cluster/coordinator/cluster-node.model.d.ts.map +1 -0
- package/dist/cluster/coordinator/cluster-node.model.js +15 -0
- package/dist/cluster/coordinator/cluster-node.model.js.map +1 -0
- package/dist/cluster/coordinator/cluster-signals.d.ts +45 -0
- package/dist/cluster/coordinator/cluster-signals.d.ts.map +1 -0
- package/dist/cluster/coordinator/cluster-signals.js +24 -0
- package/dist/cluster/coordinator/cluster-signals.js.map +1 -0
- package/dist/cluster/coordinator/coordinator.process.d.ts +21 -0
- package/dist/cluster/coordinator/coordinator.process.d.ts.map +1 -0
- package/dist/cluster/coordinator/coordinator.process.js +221 -0
- package/dist/cluster/coordinator/coordinator.process.js.map +1 -0
- package/dist/cluster/coordinator/index.d.ts +13 -0
- package/dist/cluster/coordinator/index.d.ts.map +1 -0
- package/dist/cluster/coordinator/index.js +13 -0
- package/dist/cluster/coordinator/index.js.map +1 -0
- package/dist/cluster/coordinator/node-lifecycle.d.ts +31 -0
- package/dist/cluster/coordinator/node-lifecycle.d.ts.map +1 -0
- package/dist/cluster/coordinator/node-lifecycle.js +178 -0
- package/dist/cluster/coordinator/node-lifecycle.js.map +1 -0
- package/dist/cluster/index.d.ts +45 -0
- package/dist/cluster/index.d.ts.map +1 -0
- package/dist/cluster/index.js +57 -0
- package/dist/cluster/index.js.map +1 -0
- package/dist/cluster/protocol.d.ts +204 -0
- package/dist/cluster/protocol.d.ts.map +1 -0
- package/dist/cluster/protocol.js +274 -0
- package/dist/cluster/protocol.js.map +1 -0
- package/dist/cluster/scheduled-task/builder.d.ts +24 -0
- package/dist/cluster/scheduled-task/builder.d.ts.map +1 -0
- package/dist/cluster/scheduled-task/builder.js +63 -0
- package/dist/cluster/scheduled-task/builder.js.map +1 -0
- package/dist/cluster/scheduled-task/factory.d.ts +76 -0
- package/dist/cluster/scheduled-task/factory.d.ts.map +1 -0
- package/dist/cluster/scheduled-task/factory.js +64 -0
- package/dist/cluster/scheduled-task/factory.js.map +1 -0
- package/dist/cluster/scheduled-task/index.d.ts +43 -0
- package/dist/cluster/scheduled-task/index.d.ts.map +1 -0
- package/dist/cluster/scheduled-task/index.js +45 -0
- package/dist/cluster/scheduled-task/index.js.map +1 -0
- package/dist/cluster/scheduled-task/transport.d.ts +12 -0
- package/dist/cluster/scheduled-task/transport.d.ts.map +1 -0
- package/dist/cluster/scheduled-task/transport.js +146 -0
- package/dist/cluster/scheduled-task/transport.js.map +1 -0
- package/dist/cluster/scheduled-task/types.d.ts +89 -0
- package/dist/cluster/scheduled-task/types.d.ts.map +1 -0
- package/dist/cluster/scheduled-task/types.js +7 -0
- package/dist/cluster/scheduled-task/types.js.map +1 -0
- package/dist/cluster/server.d.ts +87 -0
- package/dist/cluster/server.d.ts.map +1 -0
- package/dist/cluster/server.js +290 -0
- package/dist/cluster/server.js.map +1 -0
- package/dist/cluster/transport.d.ts +86 -0
- package/dist/cluster/transport.d.ts.map +1 -0
- package/dist/cluster/transport.js +138 -0
- package/dist/cluster/transport.js.map +1 -0
- package/dist/core/container-hooks.d.ts +22 -0
- package/dist/core/container-hooks.d.ts.map +1 -0
- package/dist/core/container-hooks.js +29 -0
- package/dist/core/container-hooks.js.map +1 -0
- package/dist/core/container-reflection.d.ts +71 -0
- package/dist/core/container-reflection.d.ts.map +1 -0
- package/dist/core/container-reflection.js +60 -0
- package/dist/core/container-reflection.js.map +1 -0
- package/dist/core/context.d.ts +146 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +155 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/contribution.d.ts +152 -0
- package/dist/core/contribution.d.ts.map +1 -0
- package/dist/core/contribution.js +213 -0
- package/dist/core/contribution.js.map +1 -0
- package/dist/core/controller.contextual.d.ts +193 -0
- package/dist/core/controller.contextual.d.ts.map +1 -0
- package/dist/core/controller.contextual.js +459 -0
- package/dist/core/controller.contextual.js.map +1 -0
- package/dist/core/controller.d.ts +510 -0
- package/dist/core/controller.d.ts.map +1 -0
- package/dist/core/controller.js +411 -0
- package/dist/core/controller.js.map +1 -0
- package/dist/core/controller.procedure.d.ts +147 -0
- package/dist/core/controller.procedure.d.ts.map +1 -0
- package/dist/core/controller.procedure.js +115 -0
- package/dist/core/controller.procedure.js.map +1 -0
- package/dist/core/disposable.d.ts +126 -0
- package/dist/core/disposable.d.ts.map +1 -0
- package/dist/core/disposable.js +179 -0
- package/dist/core/disposable.js.map +1 -0
- package/dist/core/hmr.d.ts +83 -0
- package/dist/core/hmr.d.ts.map +1 -0
- package/dist/core/hmr.js +211 -0
- package/dist/core/hmr.js.map +1 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +25 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/internal/routes.d.ts +26 -0
- package/dist/core/internal/routes.d.ts.map +1 -0
- package/dist/core/internal/routes.js +48 -0
- package/dist/core/internal/routes.js.map +1 -0
- package/dist/core/lifecycle-impl.d.ts +45 -0
- package/dist/core/lifecycle-impl.d.ts.map +1 -0
- package/dist/core/lifecycle-impl.js +102 -0
- package/dist/core/lifecycle-impl.js.map +1 -0
- package/dist/core/lifecycle.d.ts +86 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +38 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/logger.d.ts +282 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +368 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/middleware.d.ts +108 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +60 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/openapi-methods.d.ts +61 -0
- package/dist/core/openapi-methods.d.ts.map +1 -0
- package/dist/core/openapi-methods.js +53 -0
- package/dist/core/openapi-methods.js.map +1 -0
- package/dist/core/plugin.d.ts +209 -0
- package/dist/core/plugin.d.ts.map +1 -0
- package/dist/core/plugin.js +36 -0
- package/dist/core/plugin.js.map +1 -0
- package/dist/core/scope-bridge.d.ts +19 -0
- package/dist/core/scope-bridge.d.ts.map +1 -0
- package/dist/core/scope-bridge.js +34 -0
- package/dist/core/scope-bridge.js.map +1 -0
- package/dist/core/service.d.ts +429 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/core/service.js +875 -0
- package/dist/core/service.js.map +1 -0
- package/dist/features/channel/backend.d.ts +98 -0
- package/dist/features/channel/backend.d.ts.map +1 -0
- package/dist/features/channel/backend.js +75 -0
- package/dist/features/channel/backend.js.map +1 -0
- package/dist/features/channel/channel.d.ts +18 -0
- package/dist/features/channel/channel.d.ts.map +1 -0
- package/dist/features/channel/channel.js +219 -0
- package/dist/features/channel/channel.js.map +1 -0
- package/dist/features/channel/channels.d.ts +87 -0
- package/dist/features/channel/channels.d.ts.map +1 -0
- package/dist/features/channel/channels.js +252 -0
- package/dist/features/channel/channels.js.map +1 -0
- package/dist/features/channel/feature.d.ts +40 -0
- package/dist/features/channel/feature.d.ts.map +1 -0
- package/dist/features/channel/feature.js +44 -0
- package/dist/features/channel/feature.js.map +1 -0
- package/dist/features/channel/index.d.ts +41 -0
- package/dist/features/channel/index.d.ts.map +1 -0
- package/dist/features/channel/index.js +41 -0
- package/dist/features/channel/index.js.map +1 -0
- package/dist/features/channel/types.d.ts +165 -0
- package/dist/features/channel/types.d.ts.map +1 -0
- package/dist/features/channel/types.js +10 -0
- package/dist/features/channel/types.js.map +1 -0
- package/dist/features/config/cli/config-controller.d.ts +77 -0
- package/dist/features/config/cli/config-controller.d.ts.map +1 -0
- package/dist/features/config/cli/config-controller.js +209 -0
- package/dist/features/config/cli/config-controller.js.map +1 -0
- package/dist/features/config/cli/index.d.ts +9 -0
- package/dist/features/config/cli/index.d.ts.map +1 -0
- package/dist/features/config/cli/index.js +9 -0
- package/dist/features/config/cli/index.js.map +1 -0
- package/dist/features/config/cli/profile-controller.d.ts +87 -0
- package/dist/features/config/cli/profile-controller.d.ts.map +1 -0
- package/dist/features/config/cli/profile-controller.js +223 -0
- package/dist/features/config/cli/profile-controller.js.map +1 -0
- package/dist/features/config/cli/utils.d.ts +14 -0
- package/dist/features/config/cli/utils.d.ts.map +1 -0
- package/dist/features/config/cli/utils.js +29 -0
- package/dist/features/config/cli/utils.js.map +1 -0
- package/dist/features/config/config-of.d.ts +36 -0
- package/dist/features/config/config-of.d.ts.map +1 -0
- package/dist/features/config/config-of.js +36 -0
- package/dist/features/config/config-of.js.map +1 -0
- package/dist/features/config/config-service.d.ts +54 -0
- package/dist/features/config/config-service.d.ts.map +1 -0
- package/dist/features/config/config-service.js +184 -0
- package/dist/features/config/config-service.js.map +1 -0
- package/dist/features/config/create-config.d.ts +21 -0
- package/dist/features/config/create-config.d.ts.map +1 -0
- package/dist/features/config/create-config.js +16 -0
- package/dist/features/config/create-config.js.map +1 -0
- package/dist/features/config/define-config-partial.d.ts +13 -0
- package/dist/features/config/define-config-partial.d.ts.map +1 -0
- package/dist/features/config/define-config-partial.js +19 -0
- package/dist/features/config/define-config-partial.js.map +1 -0
- package/dist/features/config/env-service.d.ts +54 -0
- package/dist/features/config/env-service.d.ts.map +1 -0
- package/dist/features/config/env-service.js +115 -0
- package/dist/features/config/env-service.js.map +1 -0
- package/dist/features/config/file-watcher.d.ts +13 -0
- package/dist/features/config/file-watcher.d.ts.map +1 -0
- package/dist/features/config/file-watcher.js +98 -0
- package/dist/features/config/file-watcher.js.map +1 -0
- package/dist/features/config/index.d.ts +21 -0
- package/dist/features/config/index.d.ts.map +1 -0
- package/dist/features/config/index.js +24 -0
- package/dist/features/config/index.js.map +1 -0
- package/dist/features/config/profile-service.d.ts +59 -0
- package/dist/features/config/profile-service.d.ts.map +1 -0
- package/dist/features/config/profile-service.js +114 -0
- package/dist/features/config/profile-service.js.map +1 -0
- package/dist/features/config/types.d.ts +38 -0
- package/dist/features/config/types.d.ts.map +1 -0
- package/dist/features/config/types.js +17 -0
- package/dist/features/config/types.js.map +1 -0
- package/dist/features/contract/contract.d.ts +264 -0
- package/dist/features/contract/contract.d.ts.map +1 -0
- package/dist/features/contract/contract.js +183 -0
- package/dist/features/contract/contract.js.map +1 -0
- package/dist/features/contract/index.d.ts +2 -0
- package/dist/features/contract/index.d.ts.map +1 -0
- package/dist/features/contract/index.js +2 -0
- package/dist/features/contract/index.js.map +1 -0
- package/dist/features/env/contribute.d.ts +70 -0
- package/dist/features/env/contribute.d.ts.map +1 -0
- package/dist/features/env/contribute.js +195 -0
- package/dist/features/env/contribute.js.map +1 -0
- package/dist/features/environment/create-environment.d.ts +58 -0
- package/dist/features/environment/create-environment.d.ts.map +1 -0
- package/dist/features/environment/create-environment.js +22 -0
- package/dist/features/environment/create-environment.js.map +1 -0
- package/dist/features/environment/index.d.ts +12 -0
- package/dist/features/environment/index.d.ts.map +1 -0
- package/dist/features/environment/index.js +10 -0
- package/dist/features/environment/index.js.map +1 -0
- package/dist/features/environment/load.d.ts +59 -0
- package/dist/features/environment/load.d.ts.map +1 -0
- package/dist/features/environment/load.js +117 -0
- package/dist/features/environment/load.js.map +1 -0
- package/dist/features/environment/types.d.ts +165 -0
- package/dist/features/environment/types.d.ts.map +1 -0
- package/dist/features/environment/types.js +18 -0
- package/dist/features/environment/types.js.map +1 -0
- package/dist/features/feature-flags/create-feature-flag-provider.d.ts +21 -0
- package/dist/features/feature-flags/create-feature-flag-provider.d.ts.map +1 -0
- package/dist/features/feature-flags/create-feature-flag-provider.js +16 -0
- package/dist/features/feature-flags/create-feature-flag-provider.js.map +1 -0
- package/dist/features/feature-flags/define-feature-flag-partial.d.ts +20 -0
- package/dist/features/feature-flags/define-feature-flag-partial.d.ts.map +1 -0
- package/dist/features/feature-flags/define-feature-flag-partial.js +26 -0
- package/dist/features/feature-flags/define-feature-flag-partial.js.map +1 -0
- package/dist/features/feature-flags/feature-flag-of.d.ts +16 -0
- package/dist/features/feature-flags/feature-flag-of.d.ts.map +1 -0
- package/dist/features/feature-flags/feature-flag-of.js +16 -0
- package/dist/features/feature-flags/feature-flag-of.js.map +1 -0
- package/dist/features/feature-flags/feature-flag-service.d.ts +22 -0
- package/dist/features/feature-flags/feature-flag-service.d.ts.map +1 -0
- package/dist/features/feature-flags/feature-flag-service.js +112 -0
- package/dist/features/feature-flags/feature-flag-service.js.map +1 -0
- package/dist/features/feature-flags/index.d.ts +15 -0
- package/dist/features/feature-flags/index.d.ts.map +1 -0
- package/dist/features/feature-flags/index.js +12 -0
- package/dist/features/feature-flags/index.js.map +1 -0
- package/dist/features/feature-flags/types.d.ts +30 -0
- package/dist/features/feature-flags/types.d.ts.map +1 -0
- package/dist/features/feature-flags/types.js +8 -0
- package/dist/features/feature-flags/types.js.map +1 -0
- package/dist/features/index.d.ts +6 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +7 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/lock/index.d.ts +4 -0
- package/dist/features/lock/index.d.ts.map +1 -0
- package/dist/features/lock/index.js +4 -0
- package/dist/features/lock/index.js.map +1 -0
- package/dist/features/lock/lock-service.d.ts +74 -0
- package/dist/features/lock/lock-service.d.ts.map +1 -0
- package/dist/features/lock/lock-service.js +210 -0
- package/dist/features/lock/lock-service.js.map +1 -0
- package/dist/features/lock/memory.d.ts +60 -0
- package/dist/features/lock/memory.d.ts.map +1 -0
- package/dist/features/lock/memory.js +194 -0
- package/dist/features/lock/memory.js.map +1 -0
- package/dist/features/lock/types.d.ts +151 -0
- package/dist/features/lock/types.d.ts.map +1 -0
- package/dist/features/lock/types.js +17 -0
- package/dist/features/lock/types.js.map +1 -0
- package/dist/features/memory/index.d.ts +18 -0
- package/dist/features/memory/index.d.ts.map +1 -0
- package/dist/features/memory/index.js +18 -0
- package/dist/features/memory/index.js.map +1 -0
- package/dist/features/memory/lock-feature.d.ts +21 -0
- package/dist/features/memory/lock-feature.d.ts.map +1 -0
- package/dist/features/memory/lock-feature.js +24 -0
- package/dist/features/memory/lock-feature.js.map +1 -0
- package/dist/features/secrets/create-secret-provider.d.ts +31 -0
- package/dist/features/secrets/create-secret-provider.d.ts.map +1 -0
- package/dist/features/secrets/create-secret-provider.js +26 -0
- package/dist/features/secrets/create-secret-provider.js.map +1 -0
- package/dist/features/secrets/define-secret-partial.d.ts +16 -0
- package/dist/features/secrets/define-secret-partial.d.ts.map +1 -0
- package/dist/features/secrets/define-secret-partial.js +26 -0
- package/dist/features/secrets/define-secret-partial.js.map +1 -0
- package/dist/features/secrets/index.d.ts +17 -0
- package/dist/features/secrets/index.d.ts.map +1 -0
- package/dist/features/secrets/index.js +14 -0
- package/dist/features/secrets/index.js.map +1 -0
- package/dist/features/secrets/secret-of.d.ts +19 -0
- package/dist/features/secrets/secret-of.d.ts.map +1 -0
- package/dist/features/secrets/secret-of.js +21 -0
- package/dist/features/secrets/secret-of.js.map +1 -0
- package/dist/features/secrets/secret-service.d.ts +21 -0
- package/dist/features/secrets/secret-service.d.ts.map +1 -0
- package/dist/features/secrets/secret-service.js +28 -0
- package/dist/features/secrets/secret-service.js.map +1 -0
- package/dist/features/secrets/types.d.ts +30 -0
- package/dist/features/secrets/types.d.ts.map +1 -0
- package/dist/features/secrets/types.js +8 -0
- package/dist/features/secrets/types.js.map +1 -0
- package/dist/features/vault/env-var-vault.d.ts +24 -0
- package/dist/features/vault/env-var-vault.d.ts.map +1 -0
- package/dist/features/vault/env-var-vault.js +43 -0
- package/dist/features/vault/env-var-vault.js.map +1 -0
- package/dist/features/vault/hardcoded-vault.d.ts +34 -0
- package/dist/features/vault/hardcoded-vault.d.ts.map +1 -0
- package/dist/features/vault/hardcoded-vault.js +46 -0
- package/dist/features/vault/hardcoded-vault.js.map +1 -0
- package/dist/features/vault/hashicorp-vault.d.ts +32 -0
- package/dist/features/vault/hashicorp-vault.d.ts.map +1 -0
- package/dist/features/vault/hashicorp-vault.js +69 -0
- package/dist/features/vault/hashicorp-vault.js.map +1 -0
- package/dist/features/vault/index.d.ts +13 -0
- package/dist/features/vault/index.d.ts.map +1 -0
- package/dist/features/vault/index.js +12 -0
- package/dist/features/vault/index.js.map +1 -0
- package/dist/features/vault/kubernetes-vault.d.ts +27 -0
- package/dist/features/vault/kubernetes-vault.d.ts.map +1 -0
- package/dist/features/vault/kubernetes-vault.js +51 -0
- package/dist/features/vault/kubernetes-vault.js.map +1 -0
- package/dist/features/vault/types.d.ts +41 -0
- package/dist/features/vault/types.d.ts.map +1 -0
- package/dist/features/vault/types.js +21 -0
- package/dist/features/vault/types.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/justscale.d.ts +63 -0
- package/dist/justscale.d.ts.map +1 -0
- package/dist/justscale.js +501 -0
- package/dist/justscale.js.map +1 -0
- package/dist/kernel/adapter.d.ts +9 -0
- package/dist/kernel/adapter.d.ts.map +1 -0
- package/dist/kernel/adapter.js +2 -0
- package/dist/kernel/adapter.js.map +1 -0
- package/dist/kernel/kernel.d.ts +15 -0
- package/dist/kernel/kernel.d.ts.map +1 -0
- package/dist/kernel/kernel.js +134 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/models/access.d.ts +26 -0
- package/dist/models/access.d.ts.map +1 -0
- package/dist/models/access.js +126 -0
- package/dist/models/access.js.map +1 -0
- package/dist/models/apply-types-config.d.ts +52 -0
- package/dist/models/apply-types-config.d.ts.map +1 -0
- package/dist/models/apply-types-config.js +47 -0
- package/dist/models/apply-types-config.js.map +1 -0
- package/dist/models/define-model.d.ts +249 -0
- package/dist/models/define-model.d.ts.map +1 -0
- package/dist/models/define-model.js +388 -0
- package/dist/models/define-model.js.map +1 -0
- package/dist/models/field.d.ts +309 -0
- package/dist/models/field.d.ts.map +1 -0
- package/dist/models/field.js +312 -0
- package/dist/models/field.js.map +1 -0
- package/dist/models/in-memory/condition-evaluator.d.ts +53 -0
- package/dist/models/in-memory/condition-evaluator.d.ts.map +1 -0
- package/dist/models/in-memory/condition-evaluator.js +593 -0
- package/dist/models/in-memory/condition-evaluator.js.map +1 -0
- package/dist/models/in-memory/in-memory-model.d.ts +89 -0
- package/dist/models/in-memory/in-memory-model.d.ts.map +1 -0
- package/dist/models/in-memory/in-memory-model.js +101 -0
- package/dist/models/in-memory/in-memory-model.js.map +1 -0
- package/dist/models/in-memory/in-memory-repository.d.ts +208 -0
- package/dist/models/in-memory/in-memory-repository.d.ts.map +1 -0
- package/dist/models/in-memory/in-memory-repository.js +618 -0
- package/dist/models/in-memory/in-memory-repository.js.map +1 -0
- package/dist/models/in-memory/in-memory-scheduled-task.repository.d.ts +92 -0
- package/dist/models/in-memory/in-memory-scheduled-task.repository.d.ts.map +1 -0
- package/dist/models/in-memory/in-memory-scheduled-task.repository.js +395 -0
- package/dist/models/in-memory/in-memory-scheduled-task.repository.js.map +1 -0
- package/dist/models/in-memory/index.d.ts +35 -0
- package/dist/models/in-memory/index.d.ts.map +1 -0
- package/dist/models/in-memory/index.js +36 -0
- package/dist/models/in-memory/index.js.map +1 -0
- package/dist/models/index.d.ts +52 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +86 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/model-name-registry.d.ts +16 -0
- package/dist/models/model-name-registry.d.ts.map +1 -0
- package/dist/models/model-name-registry.js +19 -0
- package/dist/models/model-name-registry.js.map +1 -0
- package/dist/models/model.d.ts +15 -0
- package/dist/models/model.d.ts.map +1 -0
- package/dist/models/model.js +114 -0
- package/dist/models/model.js.map +1 -0
- package/dist/models/model.repository.d.ts +318 -0
- package/dist/models/model.repository.d.ts.map +1 -0
- package/dist/models/model.repository.js +146 -0
- package/dist/models/model.repository.js.map +1 -0
- package/dist/models/observable.d.ts +15 -0
- package/dist/models/observable.d.ts.map +1 -0
- package/dist/models/observable.js +64 -0
- package/dist/models/observable.js.map +1 -0
- package/dist/models/proxy.d.ts +5 -0
- package/dist/models/proxy.d.ts.map +1 -0
- package/dist/models/proxy.js +407 -0
- package/dist/models/proxy.js.map +1 -0
- package/dist/models/query.d.ts +574 -0
- package/dist/models/query.d.ts.map +1 -0
- package/dist/models/query.js +701 -0
- package/dist/models/query.js.map +1 -0
- package/dist/models/reference/reference.d.ts +229 -0
- package/dist/models/reference/reference.d.ts.map +1 -0
- package/dist/models/reference/reference.js +331 -0
- package/dist/models/reference/reference.js.map +1 -0
- package/dist/models/reference/transient-ref.d.ts +123 -0
- package/dist/models/reference/transient-ref.d.ts.map +1 -0
- package/dist/models/reference/transient-ref.js +152 -0
- package/dist/models/reference/transient-ref.js.map +1 -0
- package/dist/models/repository.d.ts +53 -0
- package/dist/models/repository.d.ts.map +1 -0
- package/dist/models/repository.js +37 -0
- package/dist/models/repository.js.map +1 -0
- package/dist/models/scheduled-task/index.d.ts +13 -0
- package/dist/models/scheduled-task/index.d.ts.map +1 -0
- package/dist/models/scheduled-task/index.js +12 -0
- package/dist/models/scheduled-task/index.js.map +1 -0
- package/dist/models/scheduled-task/scheduled-task.d.ts +73 -0
- package/dist/models/scheduled-task/scheduled-task.d.ts.map +1 -0
- package/dist/models/scheduled-task/scheduled-task.js +95 -0
- package/dist/models/scheduled-task/scheduled-task.js.map +1 -0
- package/dist/models/scheduled-task/scheduled-task.repository.d.ts +150 -0
- package/dist/models/scheduled-task/scheduled-task.repository.d.ts.map +1 -0
- package/dist/models/scheduled-task/scheduled-task.repository.js +40 -0
- package/dist/models/scheduled-task/scheduled-task.repository.js.map +1 -0
- package/dist/models/stream.d.ts +139 -0
- package/dist/models/stream.d.ts.map +1 -0
- package/dist/models/stream.js +153 -0
- package/dist/models/stream.js.map +1 -0
- package/dist/models/symbols.d.ts +73 -0
- package/dist/models/symbols.d.ts.map +1 -0
- package/dist/models/symbols.js +97 -0
- package/dist/models/symbols.js.map +1 -0
- package/dist/models/types.d.ts +291 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +50 -0
- package/dist/models/types.js.map +1 -0
- package/dist/models/watch.d.ts +27 -0
- package/dist/models/watch.d.ts.map +1 -0
- package/dist/models/watch.js +124 -0
- package/dist/models/watch.js.map +1 -0
- package/dist/models/zod-ref.d.ts +46 -0
- package/dist/models/zod-ref.d.ts.map +1 -0
- package/dist/models/zod-ref.js +31 -0
- package/dist/models/zod-ref.js.map +1 -0
- package/dist/process/builtin-serializers.d.ts +19 -0
- package/dist/process/builtin-serializers.d.ts.map +1 -0
- package/dist/process/builtin-serializers.js +213 -0
- package/dist/process/builtin-serializers.js.map +1 -0
- package/dist/process/cluster-plugin.d.ts +129 -0
- package/dist/process/cluster-plugin.d.ts.map +1 -0
- package/dist/process/cluster-plugin.js +175 -0
- package/dist/process/cluster-plugin.js.map +1 -0
- package/dist/process/createProcess.d.ts +67 -0
- package/dist/process/createProcess.d.ts.map +1 -0
- package/dist/process/createProcess.js +111 -0
- package/dist/process/createProcess.js.map +1 -0
- package/dist/process/define-signals.d.ts +113 -0
- package/dist/process/define-signals.d.ts.map +1 -0
- package/dist/process/define-signals.js +222 -0
- package/dist/process/define-signals.js.map +1 -0
- package/dist/process/delay-controller.d.ts +35 -0
- package/dist/process/delay-controller.d.ts.map +1 -0
- package/dist/process/delay-controller.js +55 -0
- package/dist/process/delay-controller.js.map +1 -0
- package/dist/process/index.d.ts +38 -0
- package/dist/process/index.d.ts.map +1 -0
- package/dist/process/index.js +47 -0
- package/dist/process/index.js.map +1 -0
- package/dist/process/primitives.d.ts +393 -0
- package/dist/process/primitives.d.ts.map +1 -0
- package/dist/process/primitives.js +325 -0
- package/dist/process/primitives.js.map +1 -0
- package/dist/process/serialization.d.ts +58 -0
- package/dist/process/serialization.d.ts.map +1 -0
- package/dist/process/serialization.js +220 -0
- package/dist/process/serialization.js.map +1 -0
- package/dist/process/stream-utils.d.ts +123 -0
- package/dist/process/stream-utils.d.ts.map +1 -0
- package/dist/process/stream-utils.js +247 -0
- package/dist/process/stream-utils.js.map +1 -0
- package/dist/process/testing/clock.d.ts +115 -0
- package/dist/process/testing/clock.d.ts.map +1 -0
- package/dist/process/testing/clock.js +166 -0
- package/dist/process/testing/clock.js.map +1 -0
- package/dist/process/testing/index.d.ts +9 -0
- package/dist/process/testing/index.d.ts.map +1 -0
- package/dist/process/testing/index.js +9 -0
- package/dist/process/testing/index.js.map +1 -0
- package/dist/process/testing.d.ts +50 -0
- package/dist/process/testing.d.ts.map +1 -0
- package/dist/process/testing.js +59 -0
- package/dist/process/testing.js.map +1 -0
- package/dist/process/types.d.ts +540 -0
- package/dist/process/types.d.ts.map +1 -0
- package/dist/process/types.js +21 -0
- package/dist/process/types.js.map +1 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +2 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +34 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +108 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/runtime/process/compiled.d.ts +56 -0
- package/dist/runtime/process/compiled.d.ts.map +1 -0
- package/dist/runtime/process/compiled.js +221 -0
- package/dist/runtime/process/compiled.js.map +1 -0
- package/dist/runtime/process/executor.d.ts +279 -0
- package/dist/runtime/process/executor.d.ts.map +1 -0
- package/dist/runtime/process/executor.js +1941 -0
- package/dist/runtime/process/executor.js.map +1 -0
- package/dist/runtime/process/factory.d.ts +72 -0
- package/dist/runtime/process/factory.d.ts.map +1 -0
- package/dist/runtime/process/factory.js +78 -0
- package/dist/runtime/process/factory.js.map +1 -0
- package/dist/runtime/process/freeze.d.ts +5 -0
- package/dist/runtime/process/freeze.d.ts.map +1 -0
- package/dist/runtime/process/freeze.js +94 -0
- package/dist/runtime/process/freeze.js.map +1 -0
- package/dist/runtime/process/scheduled-task-timer.d.ts +52 -0
- package/dist/runtime/process/scheduled-task-timer.d.ts.map +1 -0
- package/dist/runtime/process/scheduled-task-timer.js +104 -0
- package/dist/runtime/process/scheduled-task-timer.js.map +1 -0
- package/dist/runtime/process/signal-bus.d.ts +186 -0
- package/dist/runtime/process/signal-bus.d.ts.map +1 -0
- package/dist/runtime/process/signal-bus.js +256 -0
- package/dist/runtime/process/signal-bus.js.map +1 -0
- package/dist/runtime/process/state-serializer.d.ts +30 -0
- package/dist/runtime/process/state-serializer.d.ts.map +1 -0
- package/dist/runtime/process/state-serializer.js +244 -0
- package/dist/runtime/process/state-serializer.js.map +1 -0
- package/dist/runtime/process/storage.d.ts +96 -0
- package/dist/runtime/process/storage.d.ts.map +1 -0
- package/dist/runtime/process/storage.js +165 -0
- package/dist/runtime/process/storage.js.map +1 -0
- package/dist/runtime/process/timer-scheduler.d.ts +115 -0
- package/dist/runtime/process/timer-scheduler.d.ts.map +1 -0
- package/dist/runtime/process/timer-scheduler.js +192 -0
- package/dist/runtime/process/timer-scheduler.js.map +1 -0
- package/dist/runtime/process/trace.d.ts +17 -0
- package/dist/runtime/process/trace.d.ts.map +1 -0
- package/dist/runtime/process/trace.js +26 -0
- package/dist/runtime/process/trace.js.map +1 -0
- package/dist/runtime/protobuf/encoding/index.d.ts +26 -0
- package/dist/runtime/protobuf/encoding/index.d.ts.map +1 -0
- package/dist/runtime/protobuf/encoding/index.js +30 -0
- package/dist/runtime/protobuf/encoding/index.js.map +1 -0
- package/dist/runtime/protobuf/encoding/reader.d.ts +182 -0
- package/dist/runtime/protobuf/encoding/reader.d.ts.map +1 -0
- package/dist/runtime/protobuf/encoding/reader.js +353 -0
- package/dist/runtime/protobuf/encoding/reader.js.map +1 -0
- package/dist/runtime/protobuf/encoding/varint.d.ts +67 -0
- package/dist/runtime/protobuf/encoding/varint.d.ts.map +1 -0
- package/dist/runtime/protobuf/encoding/varint.js +117 -0
- package/dist/runtime/protobuf/encoding/varint.js.map +1 -0
- package/dist/runtime/protobuf/encoding/wire-types.d.ts +62 -0
- package/dist/runtime/protobuf/encoding/wire-types.d.ts.map +1 -0
- package/dist/runtime/protobuf/encoding/wire-types.js +103 -0
- package/dist/runtime/protobuf/encoding/wire-types.js.map +1 -0
- package/dist/runtime/protobuf/encoding/writer.d.ts +147 -0
- package/dist/runtime/protobuf/encoding/writer.d.ts.map +1 -0
- package/dist/runtime/protobuf/encoding/writer.js +214 -0
- package/dist/runtime/protobuf/encoding/writer.js.map +1 -0
- package/dist/runtime/protobuf/index.d.ts +3 -0
- package/dist/runtime/protobuf/index.d.ts.map +1 -0
- package/dist/runtime/protobuf/index.js +3 -0
- package/dist/runtime/protobuf/index.js.map +1 -0
- package/dist/runtime/protobuf/serialized.d.ts +48 -0
- package/dist/runtime/protobuf/serialized.d.ts.map +1 -0
- package/dist/runtime/protobuf/serialized.js +517 -0
- package/dist/runtime/protobuf/serialized.js.map +1 -0
- package/package.json +209 -0
|
@@ -0,0 +1,1941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @justscale/process - Process Executor
|
|
3
|
+
*
|
|
4
|
+
* Switch-based executor for durable processes.
|
|
5
|
+
* Uses pluggable storage, signal bus, and timer scheduler.
|
|
6
|
+
*/
|
|
7
|
+
import { serializeState, deserializeState } from './state-serializer.js';
|
|
8
|
+
import { encodeProcessable, decodeProcessable } from '../../process/serialization.js';
|
|
9
|
+
import { defineAbstract } from '../../core/index.js';
|
|
10
|
+
import { runInFullRequestScope, getRequestContext } from '../../core/context.js';
|
|
11
|
+
import { runWithLockTracking, DoubleLockError } from '../../features/lock/lock-service.js';
|
|
12
|
+
import { SIGNAL_BRAND, } from '../../process/types.js';
|
|
13
|
+
import { resolveStreamWildcard as resolveStreamWildcardUtil, } from '../../process/stream-utils.js';
|
|
14
|
+
import { createTracer } from './trace.js';
|
|
15
|
+
import { freezeExports } from './freeze.js';
|
|
16
|
+
const { trace } = createTracer('ProcessExecutor');
|
|
17
|
+
function createDeferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
let reject;
|
|
20
|
+
const promise = new Promise((res, rej) => {
|
|
21
|
+
resolve = res;
|
|
22
|
+
reject = rej;
|
|
23
|
+
});
|
|
24
|
+
return { promise, resolve, reject };
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Instance ID Generation
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a param value: if it has an `identifier` property (e.g. a Reference
|
|
31
|
+
* or Model.ref(entity)), use that. Otherwise stringify.
|
|
32
|
+
*/
|
|
33
|
+
function resolveParam(value) {
|
|
34
|
+
if (value != null && typeof value === 'object' && 'identifier' in value) {
|
|
35
|
+
return String(value.identifier);
|
|
36
|
+
}
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
import { applyTypesConfig } from '../../models/apply-types-config.js';
|
|
40
|
+
export function generateInstanceId(path, params) {
|
|
41
|
+
let paramIndex = 0;
|
|
42
|
+
const segments = path.split('/').filter(Boolean);
|
|
43
|
+
const resolved = segments.map(segment => {
|
|
44
|
+
if (segment.startsWith(':')) {
|
|
45
|
+
const value = params[paramIndex++];
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
throw new Error(`Missing parameter for ${segment} in path ${path}`);
|
|
48
|
+
}
|
|
49
|
+
return resolveParam(value);
|
|
50
|
+
}
|
|
51
|
+
return segment;
|
|
52
|
+
});
|
|
53
|
+
return resolved.join('/');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Detect whether an instanceId belongs to a subprocess and split it into
|
|
57
|
+
* `${parentInstanceId}/${subKey}` where `subKey` starts with `__sub:`.
|
|
58
|
+
* Returns { parentInstanceId: null, subKey: null } for non-subprocess IDs.
|
|
59
|
+
* A subprocess may itself have subprocesses — we split at the LAST `/__sub:`
|
|
60
|
+
* boundary so the direct parent is always returned.
|
|
61
|
+
*/
|
|
62
|
+
export function parseChildInstanceId(instanceId) {
|
|
63
|
+
const marker = '/__sub:';
|
|
64
|
+
const idx = instanceId.lastIndexOf(marker);
|
|
65
|
+
if (idx === -1)
|
|
66
|
+
return { parentInstanceId: null, subKey: null };
|
|
67
|
+
return {
|
|
68
|
+
parentInstanceId: instanceId.slice(0, idx),
|
|
69
|
+
subKey: instanceId.slice(idx + 1),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a path pattern with params to a concrete path.
|
|
74
|
+
*/
|
|
75
|
+
export function resolvePath(path, params) {
|
|
76
|
+
let paramIndex = 0;
|
|
77
|
+
return path.replace(/:([^/]+)/g, () => {
|
|
78
|
+
const value = params[paramIndex++];
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
throw new Error(`Missing parameter in path ${path}`);
|
|
81
|
+
}
|
|
82
|
+
return String(value);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract identity map from path params.
|
|
87
|
+
*
|
|
88
|
+
* Throws on missing params to stay consistent with generateInstanceId.
|
|
89
|
+
* A process that lacks a required path param must fail loudly on both
|
|
90
|
+
* id generation and identity extraction; silent coercion to "undefined"
|
|
91
|
+
* hides routing bugs.
|
|
92
|
+
*/
|
|
93
|
+
export function extractIdentity(path, params) {
|
|
94
|
+
const identity = {};
|
|
95
|
+
let paramIndex = 0;
|
|
96
|
+
const segments = path.split('/').filter(Boolean);
|
|
97
|
+
for (const segment of segments) {
|
|
98
|
+
if (segment.startsWith(':')) {
|
|
99
|
+
const name = segment.slice(1);
|
|
100
|
+
const value = params[paramIndex++];
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
throw new Error(`Missing parameter for ${segment} in path ${path}`);
|
|
103
|
+
}
|
|
104
|
+
identity[name] = resolveParam(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return identity;
|
|
108
|
+
}
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Process Handle Implementation
|
|
111
|
+
// ============================================================================
|
|
112
|
+
class ProcessHandleImpl {
|
|
113
|
+
id;
|
|
114
|
+
path;
|
|
115
|
+
getState;
|
|
116
|
+
completionDeferred;
|
|
117
|
+
cancelFn;
|
|
118
|
+
exportsMetadata;
|
|
119
|
+
_data = undefined;
|
|
120
|
+
_dataProxy = undefined;
|
|
121
|
+
// Broadcast set: each entry is a pending resolve for one independent iterator.
|
|
122
|
+
// setExportsData fans out to ALL of them simultaneously.
|
|
123
|
+
_dataSubscribers = new Set();
|
|
124
|
+
_done = false;
|
|
125
|
+
_status = 'pending';
|
|
126
|
+
_statusSubscribers = new Set();
|
|
127
|
+
constructor(id, path, getState, completionDeferred, cancelFn, exportsMetadata) {
|
|
128
|
+
this.id = id;
|
|
129
|
+
this.path = path;
|
|
130
|
+
this.getState = getState;
|
|
131
|
+
this.completionDeferred = completionDeferred;
|
|
132
|
+
this.cancelFn = cancelFn;
|
|
133
|
+
this.exportsMetadata = exportsMetadata;
|
|
134
|
+
// When the process completes, close all data iterators and status iterators
|
|
135
|
+
this.completionDeferred.promise.then(() => {
|
|
136
|
+
this._done = true;
|
|
137
|
+
for (const resolve of this._dataSubscribers)
|
|
138
|
+
resolve(undefined);
|
|
139
|
+
this._dataSubscribers.clear();
|
|
140
|
+
// Status was already pushed by updateStatus before resolve — no extra push needed
|
|
141
|
+
for (const resolve of this._statusSubscribers)
|
|
142
|
+
resolve(undefined);
|
|
143
|
+
this._statusSubscribers.clear();
|
|
144
|
+
}).catch(() => {
|
|
145
|
+
this._done = true;
|
|
146
|
+
for (const resolve of this._dataSubscribers)
|
|
147
|
+
resolve(undefined);
|
|
148
|
+
this._dataSubscribers.clear();
|
|
149
|
+
for (const resolve of this._statusSubscribers)
|
|
150
|
+
resolve(undefined);
|
|
151
|
+
this._statusSubscribers.clear();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
get status() {
|
|
155
|
+
return this._status;
|
|
156
|
+
}
|
|
157
|
+
updateStatus(status) {
|
|
158
|
+
this._status = status;
|
|
159
|
+
for (const resolve of this._statusSubscribers)
|
|
160
|
+
resolve(status);
|
|
161
|
+
this._statusSubscribers.clear();
|
|
162
|
+
}
|
|
163
|
+
get statusChanges() {
|
|
164
|
+
const self = this;
|
|
165
|
+
return {
|
|
166
|
+
[Symbol.asyncIterator]() {
|
|
167
|
+
let localDone = false;
|
|
168
|
+
let pendingCb = null;
|
|
169
|
+
return {
|
|
170
|
+
next() {
|
|
171
|
+
if (localDone || self._done) {
|
|
172
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
173
|
+
}
|
|
174
|
+
return new Promise(resolve => {
|
|
175
|
+
const cb = (status) => {
|
|
176
|
+
pendingCb = null;
|
|
177
|
+
self._statusSubscribers.delete(cb);
|
|
178
|
+
if (status === undefined) {
|
|
179
|
+
localDone = true;
|
|
180
|
+
resolve({ value: undefined, done: true });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
resolve({ value: status, done: false });
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
pendingCb = cb;
|
|
187
|
+
self._statusSubscribers.add(cb);
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
return() {
|
|
191
|
+
localDone = true;
|
|
192
|
+
if (pendingCb !== null) {
|
|
193
|
+
self._statusSubscribers.delete(pendingCb);
|
|
194
|
+
pendingCb = null;
|
|
195
|
+
}
|
|
196
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
get result() {
|
|
203
|
+
return undefined; // Must await for result via wait()
|
|
204
|
+
}
|
|
205
|
+
get error() {
|
|
206
|
+
return undefined; // Must check via wait()
|
|
207
|
+
}
|
|
208
|
+
get data() {
|
|
209
|
+
if (this._data === undefined)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (this._dataProxy)
|
|
212
|
+
return this._dataProxy;
|
|
213
|
+
const self = this;
|
|
214
|
+
const base = this._data;
|
|
215
|
+
if (typeof base !== 'object' || base === null)
|
|
216
|
+
return base;
|
|
217
|
+
if (base[Symbol.asyncIterator])
|
|
218
|
+
return base;
|
|
219
|
+
// Create and cache a Proxy that adds Symbol.asyncIterator.
|
|
220
|
+
// Each call to [Symbol.asyncIterator]() creates an INDEPENDENT subscriber
|
|
221
|
+
// that receives every broadcast snapshot (fan-out, not fan-in).
|
|
222
|
+
this._dataProxy = new Proxy(base, {
|
|
223
|
+
get(_target, prop, _receiver) {
|
|
224
|
+
if (prop === Symbol.asyncIterator) {
|
|
225
|
+
return function () {
|
|
226
|
+
// Late subscribers immediately receive the current snapshot, then wait for subsequent broadcasts.
|
|
227
|
+
let seededInitial = false;
|
|
228
|
+
let localDone = false;
|
|
229
|
+
let cb = null;
|
|
230
|
+
return {
|
|
231
|
+
next() {
|
|
232
|
+
if (localDone)
|
|
233
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
234
|
+
if (!seededInitial) {
|
|
235
|
+
seededInitial = true;
|
|
236
|
+
if (self._data !== undefined) {
|
|
237
|
+
return Promise.resolve({ value: self._data, done: false });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (self._done)
|
|
241
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
242
|
+
return new Promise(resolve => {
|
|
243
|
+
cb = (newData) => {
|
|
244
|
+
cb = null;
|
|
245
|
+
if (newData === undefined) {
|
|
246
|
+
localDone = true;
|
|
247
|
+
resolve({ value: undefined, done: true });
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
resolve({ value: newData, done: false });
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
self._dataSubscribers.add(cb);
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
return() {
|
|
257
|
+
localDone = true;
|
|
258
|
+
if (cb !== null) {
|
|
259
|
+
const pendingCb = cb;
|
|
260
|
+
cb = null;
|
|
261
|
+
self._dataSubscribers.delete(pendingCb);
|
|
262
|
+
// Resolve the pending next() with done so the caller unblocks
|
|
263
|
+
pendingCb(undefined);
|
|
264
|
+
}
|
|
265
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// For non-asyncIterator access, read from the LATEST _data (not the original base)
|
|
271
|
+
return Reflect.get(self._data, prop, _receiver);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
return this._dataProxy;
|
|
275
|
+
}
|
|
276
|
+
/** Set the exports data (called by executor after loading/updating state) */
|
|
277
|
+
setExportsData(data) {
|
|
278
|
+
this._data = data;
|
|
279
|
+
const subs = [...this._dataSubscribers];
|
|
280
|
+
this._dataSubscribers.clear();
|
|
281
|
+
for (const resolve of subs) {
|
|
282
|
+
resolve(data);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
wait() {
|
|
286
|
+
return this.completionDeferred.promise;
|
|
287
|
+
}
|
|
288
|
+
cancel() {
|
|
289
|
+
return this.cancelFn(this.id);
|
|
290
|
+
}
|
|
291
|
+
/** Update cached status from storage (kept for backwards compat) */
|
|
292
|
+
async refresh() {
|
|
293
|
+
const state = await this.getState();
|
|
294
|
+
if (state) {
|
|
295
|
+
this.updateStatus(state.status);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function createYieldQueue() {
|
|
300
|
+
const consumers = [];
|
|
301
|
+
let completed = false;
|
|
302
|
+
return {
|
|
303
|
+
push(value) {
|
|
304
|
+
for (const consumer of consumers) {
|
|
305
|
+
consumer.callback(value);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
complete() {
|
|
309
|
+
completed = true;
|
|
310
|
+
for (const consumer of consumers) {
|
|
311
|
+
consumer.onComplete();
|
|
312
|
+
}
|
|
313
|
+
consumers.length = 0;
|
|
314
|
+
},
|
|
315
|
+
subscribe(callback, onComplete) {
|
|
316
|
+
if (completed) {
|
|
317
|
+
onComplete();
|
|
318
|
+
return () => { };
|
|
319
|
+
}
|
|
320
|
+
const entry = { callback, onComplete };
|
|
321
|
+
consumers.push(entry);
|
|
322
|
+
return () => {
|
|
323
|
+
const idx = consumers.indexOf(entry);
|
|
324
|
+
if (idx !== -1)
|
|
325
|
+
consumers.splice(idx, 1);
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Process Continuation Implementation
|
|
332
|
+
// ============================================================================
|
|
333
|
+
class ProcessContinuationImpl {
|
|
334
|
+
id;
|
|
335
|
+
consumerId;
|
|
336
|
+
yieldQueue;
|
|
337
|
+
getYieldsFromStorage;
|
|
338
|
+
_status;
|
|
339
|
+
cursor;
|
|
340
|
+
completionDeferred;
|
|
341
|
+
persistCursor;
|
|
342
|
+
constructor(id, consumerId, initialCursor, initialStatus, yieldQueue, getYieldsFromStorage, completionDeferred, persistCursor) {
|
|
343
|
+
this.id = id;
|
|
344
|
+
this.consumerId = consumerId;
|
|
345
|
+
this.yieldQueue = yieldQueue;
|
|
346
|
+
this.getYieldsFromStorage = getYieldsFromStorage;
|
|
347
|
+
this._status = initialStatus;
|
|
348
|
+
this.cursor = initialCursor;
|
|
349
|
+
this.completionDeferred = completionDeferred;
|
|
350
|
+
this.persistCursor = persistCursor;
|
|
351
|
+
}
|
|
352
|
+
get status() {
|
|
353
|
+
return this._status;
|
|
354
|
+
}
|
|
355
|
+
get result() {
|
|
356
|
+
return this.completionDeferred.promise;
|
|
357
|
+
}
|
|
358
|
+
async cancel() {
|
|
359
|
+
await this.persistCursor(this.consumerId, this.cursor);
|
|
360
|
+
}
|
|
361
|
+
[Symbol.asyncIterator]() {
|
|
362
|
+
let unsubscribe = null;
|
|
363
|
+
let pendingResolve = null;
|
|
364
|
+
const buffer = [];
|
|
365
|
+
let done = false;
|
|
366
|
+
let storageHighWater = 0;
|
|
367
|
+
const self = this;
|
|
368
|
+
// Subscribe to live yield events
|
|
369
|
+
unsubscribe = this.yieldQueue.subscribe((value) => {
|
|
370
|
+
if (pendingResolve) {
|
|
371
|
+
const resolve = pendingResolve;
|
|
372
|
+
pendingResolve = null;
|
|
373
|
+
self.cursor++;
|
|
374
|
+
resolve({ value: value, done: false });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
buffer.push(value);
|
|
378
|
+
}
|
|
379
|
+
}, () => {
|
|
380
|
+
done = true;
|
|
381
|
+
if (pendingResolve) {
|
|
382
|
+
const resolve = pendingResolve;
|
|
383
|
+
pendingResolve = null;
|
|
384
|
+
resolve({ value: undefined, done: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
async next() {
|
|
389
|
+
// First: drain any yields from storage that we haven't consumed yet
|
|
390
|
+
const storedYields = await self.getYieldsFromStorage();
|
|
391
|
+
// Discard buffer items that are now also in storage (both paths receive the same values)
|
|
392
|
+
const newInStorage = storedYields.length - storageHighWater;
|
|
393
|
+
if (newInStorage > 0) {
|
|
394
|
+
buffer.splice(0, newInStorage);
|
|
395
|
+
storageHighWater = storedYields.length;
|
|
396
|
+
}
|
|
397
|
+
if (self.cursor < storedYields.length) {
|
|
398
|
+
const value = storedYields[self.cursor++];
|
|
399
|
+
return { value: value, done: false };
|
|
400
|
+
}
|
|
401
|
+
// Then: check live buffer (only items beyond storage)
|
|
402
|
+
if (buffer.length > 0) {
|
|
403
|
+
self.cursor++;
|
|
404
|
+
return { value: buffer.shift(), done: false };
|
|
405
|
+
}
|
|
406
|
+
// Then: check if already done
|
|
407
|
+
if (done) {
|
|
408
|
+
return { value: undefined, done: true };
|
|
409
|
+
}
|
|
410
|
+
// Wait for next live value or completion
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
pendingResolve = resolve;
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
async return() {
|
|
416
|
+
unsubscribe?.();
|
|
417
|
+
unsubscribe = null;
|
|
418
|
+
await self.persistCursor(self.consumerId, self.cursor);
|
|
419
|
+
return { value: undefined, done: true };
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Abstract ProcessExecutor for dependency injection.
|
|
426
|
+
*
|
|
427
|
+
* Inject this into services to create signals bound to the executor:
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```typescript
|
|
431
|
+
* const OrderSignals = defineService({
|
|
432
|
+
* inject: { executor: AbstractProcessExecutor },
|
|
433
|
+
* factory: ({ executor }) => ({
|
|
434
|
+
* shipped: executor.createSignal<[orderId: string]>('orders.shipped', ['orderId']),
|
|
435
|
+
* })
|
|
436
|
+
* })
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
export class AbstractProcessExecutor extends defineAbstract('AbstractProcessExecutor') {
|
|
440
|
+
}
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// Process Executor
|
|
443
|
+
// ============================================================================
|
|
444
|
+
/**
|
|
445
|
+
* Executes durable processes using switch-based execution model.
|
|
446
|
+
*
|
|
447
|
+
* Features:
|
|
448
|
+
* - VM-style switch execution with suspension/resumption
|
|
449
|
+
* - Signal routing via SignalBus
|
|
450
|
+
* - Timer scheduling via TimerScheduler
|
|
451
|
+
* - State persistence via ProcessStorage
|
|
452
|
+
*/
|
|
453
|
+
export class ProcessExecutor extends AbstractProcessExecutor {
|
|
454
|
+
container;
|
|
455
|
+
resolve;
|
|
456
|
+
storage;
|
|
457
|
+
signalBus;
|
|
458
|
+
timerScheduler;
|
|
459
|
+
lockProvider;
|
|
460
|
+
// Unique identifier for this executor instance (for lock ownership)
|
|
461
|
+
executorId = `exec_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
462
|
+
// In-memory completion promises (not persisted)
|
|
463
|
+
completions = new Map();
|
|
464
|
+
// Cache resolved services per process definition
|
|
465
|
+
resolvedServicesCache = new Map();
|
|
466
|
+
// Active subscriptions by instance ID (for cleanup)
|
|
467
|
+
subscriptions = new Map();
|
|
468
|
+
// Registry of process definitions by ID (for resume)
|
|
469
|
+
processRegistry = new Map();
|
|
470
|
+
// Origin request context by instance ID (for tracing)
|
|
471
|
+
originContexts = new Map();
|
|
472
|
+
// Yield queues for generator processes (live consumers)
|
|
473
|
+
yieldQueues = new Map();
|
|
474
|
+
// Live handle registry: executor pushes exports + status into active handles
|
|
475
|
+
handles = new Map();
|
|
476
|
+
// Lock options for process execution
|
|
477
|
+
lockOptions = {
|
|
478
|
+
ttl: 60_000, // 60s TTL
|
|
479
|
+
timeout: 30_000, // 30s wait for acquisition
|
|
480
|
+
key: '', // Set per-lock
|
|
481
|
+
heartbeat: false,
|
|
482
|
+
heartbeatInterval: 20_000,
|
|
483
|
+
};
|
|
484
|
+
publishExports;
|
|
485
|
+
constructor(options) {
|
|
486
|
+
super();
|
|
487
|
+
this.container = options.container ?? null;
|
|
488
|
+
this.resolve = options.resolve;
|
|
489
|
+
this.storage = options.storage;
|
|
490
|
+
this.signalBus = options.signalBus;
|
|
491
|
+
this.timerScheduler = options.timerScheduler;
|
|
492
|
+
this.lockProvider = options.lockProvider ?? null;
|
|
493
|
+
this.publishExports = options.publishExports ?? null;
|
|
494
|
+
// Listen for signal matches
|
|
495
|
+
// Signal bus handles re-entrancy via processingInstances tracking and queuing.
|
|
496
|
+
// We must await handleSignalMatch so that emit() callers can await process completion.
|
|
497
|
+
this.signalBus.onMatch(async (match) => {
|
|
498
|
+
await this.handleSignalMatch(match);
|
|
499
|
+
});
|
|
500
|
+
// Listen for timer fires
|
|
501
|
+
// Timers can use setImmediate since they're fire-and-forget (no caller awaits)
|
|
502
|
+
this.timerScheduler.onFire(fired => {
|
|
503
|
+
setImmediate(() => this.handleTimerFired(fired));
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Register a process definition for resumption after signals.
|
|
508
|
+
*/
|
|
509
|
+
register(process) {
|
|
510
|
+
this.processRegistry.set(process.id, process);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Start or resume a process.
|
|
514
|
+
*/
|
|
515
|
+
async start(process, params) {
|
|
516
|
+
trace('start', { processId: process.id, params });
|
|
517
|
+
// Register process for resumption
|
|
518
|
+
this.register(process);
|
|
519
|
+
const instanceId = generateInstanceId(process.path, params);
|
|
520
|
+
trace('instanceId', { instanceId });
|
|
521
|
+
const resolvedPath = resolvePath(process.path, params);
|
|
522
|
+
const identity = extractIdentity(process.path, params);
|
|
523
|
+
// Acquire process lock before checking/creating state
|
|
524
|
+
const lockKey = `process:${instanceId}`;
|
|
525
|
+
trace('start.acquiringLock', { instanceId });
|
|
526
|
+
await this.acquireLock(lockKey);
|
|
527
|
+
trace('start.lockAcquired', { instanceId });
|
|
528
|
+
try {
|
|
529
|
+
// Check for existing process
|
|
530
|
+
const existing = await this.storage.load(instanceId);
|
|
531
|
+
trace('start.existingCheck', { instanceId, exists: !!existing, status: existing?.status });
|
|
532
|
+
// rt-3: if the existing instance is cancelled, discard it and start fresh
|
|
533
|
+
if (existing && existing.status === 'cancelled') {
|
|
534
|
+
trace('start.cancelledRestart', { instanceId });
|
|
535
|
+
await this.storage.delete(instanceId);
|
|
536
|
+
// Remove stale completion/handle so fresh ones are created below
|
|
537
|
+
this.completions.delete(instanceId);
|
|
538
|
+
this.handles.delete(instanceId);
|
|
539
|
+
}
|
|
540
|
+
else if (existing) {
|
|
541
|
+
let completion = this.completions.get(instanceId);
|
|
542
|
+
if (!completion) {
|
|
543
|
+
const newCompletion = createDeferred();
|
|
544
|
+
this.completions.set(instanceId, newCompletion);
|
|
545
|
+
completion = newCompletion;
|
|
546
|
+
if (existing.status === 'completed') {
|
|
547
|
+
newCompletion.resolve(existing.result);
|
|
548
|
+
}
|
|
549
|
+
else if (existing.status === 'failed') {
|
|
550
|
+
newCompletion.reject(new Error(existing.error ?? 'Process failed'));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Release lock if process already completed/failed (no execution needed)
|
|
554
|
+
if (existing.status === 'completed' || existing.status === 'failed') {
|
|
555
|
+
await this.releaseLock(lockKey);
|
|
556
|
+
}
|
|
557
|
+
else if (existing.status === 'suspended') {
|
|
558
|
+
// Re-subscribe to signals for suspended process (in-memory signal bus doesn't persist)
|
|
559
|
+
trace('start.resubscribing', { instanceId });
|
|
560
|
+
await this.resubscribeSuspended(existing, identity, process.types);
|
|
561
|
+
trace('start.releasingLock', { instanceId });
|
|
562
|
+
await this.releaseLock(lockKey);
|
|
563
|
+
trace('start.lockReleased', { instanceId });
|
|
564
|
+
}
|
|
565
|
+
const handle = new ProcessHandleImpl(instanceId, resolvedPath, () => this.loadState(instanceId), completion, (id) => this.cancel(id), process.exports);
|
|
566
|
+
handle.updateStatus(existing.status);
|
|
567
|
+
this.handles.set(instanceId, handle);
|
|
568
|
+
// Populate handle with exports from stored state (deserialize first)
|
|
569
|
+
if (process.exports && existing.variables?.exports) {
|
|
570
|
+
const deserialized = deserializeState({ exports: existing.variables.exports });
|
|
571
|
+
if (deserialized.exports) {
|
|
572
|
+
handle.setExportsData(this.buildFrozenExports(deserialized.exports, process.exports));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return handle;
|
|
576
|
+
}
|
|
577
|
+
// Create new process state
|
|
578
|
+
const state = {
|
|
579
|
+
processId: process.id,
|
|
580
|
+
instanceId,
|
|
581
|
+
version: process.version,
|
|
582
|
+
step: 0,
|
|
583
|
+
persistedStep: Object.entries(process.stepMap).find(([_, v]) => v === 0)?.[0] ?? 'entry',
|
|
584
|
+
vars: {
|
|
585
|
+
__identity: identity,
|
|
586
|
+
__params: params,
|
|
587
|
+
...identity,
|
|
588
|
+
},
|
|
589
|
+
timers: [],
|
|
590
|
+
createdAt: new Date(),
|
|
591
|
+
updatedAt: new Date(),
|
|
592
|
+
status: 'pending',
|
|
593
|
+
};
|
|
594
|
+
await this.saveState(state);
|
|
595
|
+
const completion = createDeferred();
|
|
596
|
+
this.completions.set(instanceId, completion);
|
|
597
|
+
// Capture origin request context for tracing
|
|
598
|
+
const originCtx = getRequestContext();
|
|
599
|
+
if (originCtx) {
|
|
600
|
+
this.originContexts.set(instanceId, {
|
|
601
|
+
type: originCtx.type,
|
|
602
|
+
name: originCtx.name,
|
|
603
|
+
id: originCtx.id,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const handle = new ProcessHandleImpl(instanceId, resolvedPath, () => this.loadState(instanceId), completion, (id) => this.cancel(id), process.exports);
|
|
607
|
+
this.handles.set(instanceId, handle);
|
|
608
|
+
// Start execution (lock released in suspend/complete/fail).
|
|
609
|
+
// saveState inside execute() pushes exports and status into the handle.
|
|
610
|
+
await this.execute(state, process, identity);
|
|
611
|
+
return handle;
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
// Release lock on error
|
|
615
|
+
await this.releaseLock(lockKey);
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Emit a signal to waiting processes.
|
|
621
|
+
*
|
|
622
|
+
* Lock acquisition happens in handleSignalMatch() when the signal is delivered.
|
|
623
|
+
* This avoids deadlock since handleSignalMatch() is called synchronously from emit().
|
|
624
|
+
*/
|
|
625
|
+
async emit(signal, identity, payload) {
|
|
626
|
+
// Encode Processable payloads so they survive JSONB round-trips
|
|
627
|
+
const encoded = encodeProcessable(payload);
|
|
628
|
+
return this.signalBus.emit(signal, identity, encoded);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Create a signal bound to this executor.
|
|
632
|
+
*
|
|
633
|
+
* The returned signal is callable directly and will emit through this executor,
|
|
634
|
+
* which handles locking to prevent dead letters.
|
|
635
|
+
*
|
|
636
|
+
* @param name - Signal name for routing (e.g., 'orders.shipped')
|
|
637
|
+
* @param identityParams - Names of identity parameters (e.g., ['orderId'])
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```typescript
|
|
641
|
+
* const OrderSignals = defineService({
|
|
642
|
+
* inject: { executor: ProcessExecutor },
|
|
643
|
+
* factory: ({ executor }) => ({
|
|
644
|
+
* shipped: executor.createSignal<[orderId: string], { trackingNumber: string }>(
|
|
645
|
+
* 'orders.shipped',
|
|
646
|
+
* ['orderId']
|
|
647
|
+
* ),
|
|
648
|
+
* })
|
|
649
|
+
* })
|
|
650
|
+
*
|
|
651
|
+
* // Usage: await orderSignals.shipped('order-123', { trackingNumber: 'ABC' })
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
createSignal(name, identityParams = []) {
|
|
655
|
+
const executor = this;
|
|
656
|
+
const signal = async (...args) => {
|
|
657
|
+
trace('signal.invoke', { name, args: args.slice(0, identityParams.length) });
|
|
658
|
+
// Build identity record from args and param names
|
|
659
|
+
const identity = {};
|
|
660
|
+
for (let i = 0; i < identityParams.length && i < args.length; i++) {
|
|
661
|
+
identity[identityParams[i]] = String(args[i]);
|
|
662
|
+
}
|
|
663
|
+
// Payload is the last arg if there are more args than identity params
|
|
664
|
+
const payload = args.length > identityParams.length
|
|
665
|
+
? args[identityParams.length]
|
|
666
|
+
: undefined;
|
|
667
|
+
trace('signal.emit', { name, identity, hasPayload: payload !== undefined });
|
|
668
|
+
await executor.emit(name, identity, payload);
|
|
669
|
+
trace('signal.emit.complete', { name });
|
|
670
|
+
};
|
|
671
|
+
// Add brand and metadata
|
|
672
|
+
Object.defineProperty(signal, SIGNAL_BRAND, { value: SIGNAL_BRAND });
|
|
673
|
+
Object.defineProperty(signal, 'signalName', { value: name });
|
|
674
|
+
Object.defineProperty(signal, '__identityParams', { value: identityParams });
|
|
675
|
+
// PromiseLike support - allows `await signal` in process handlers
|
|
676
|
+
// At compile time this is transformed; at runtime it throws if used incorrectly
|
|
677
|
+
Object.defineProperty(signal, 'then', {
|
|
678
|
+
value: () => {
|
|
679
|
+
throw new Error(`Cannot await signal "${name}" directly. Use signal() inside a process handler.`);
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
return signal;
|
|
683
|
+
}
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// Execution Engine
|
|
686
|
+
// ============================================================================
|
|
687
|
+
/**
|
|
688
|
+
* Execute a switch-based process.
|
|
689
|
+
* Runs within a full request scope for context propagation (if container available).
|
|
690
|
+
*/
|
|
691
|
+
async execute(state, process, identity) {
|
|
692
|
+
// Inner execution logic
|
|
693
|
+
const executeInner = async () => {
|
|
694
|
+
const services = await this.resolveServices(process);
|
|
695
|
+
try {
|
|
696
|
+
// Get or create yield queue for this instance
|
|
697
|
+
let yieldQueue = this.yieldQueues.get(state.instanceId);
|
|
698
|
+
if (!yieldQueue) {
|
|
699
|
+
yieldQueue = createYieldQueue();
|
|
700
|
+
this.yieldQueues.set(state.instanceId, yieldQueue);
|
|
701
|
+
}
|
|
702
|
+
// Initialize __yields array if not present
|
|
703
|
+
if (!state.vars.__yields) {
|
|
704
|
+
state.vars.__yields = [];
|
|
705
|
+
}
|
|
706
|
+
const yields = state.vars.__yields;
|
|
707
|
+
const currentYieldQueue = yieldQueue;
|
|
708
|
+
const ctx = {
|
|
709
|
+
state,
|
|
710
|
+
services,
|
|
711
|
+
signalPayload: state.vars.__signalPayload,
|
|
712
|
+
emit: (value) => {
|
|
713
|
+
yields.push(value);
|
|
714
|
+
currentYieldQueue.push(value);
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
// Reattach methods on resume — methods serialize as null in JSONB,
|
|
718
|
+
// so we restore them from exports.methods before executing
|
|
719
|
+
if (state.step > 0 && process.exports && state.vars.exports) {
|
|
720
|
+
const exportsObj = state.vars.exports;
|
|
721
|
+
for (const [name, fn] of Object.entries(process.exports.methods)) {
|
|
722
|
+
exportsObj[name] = fn;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Re-apply types config on every execution (including resume).
|
|
726
|
+
// References don't survive serialization, so we re-wrap from the
|
|
727
|
+
// persisted string identity on each run.
|
|
728
|
+
if (process.types) {
|
|
729
|
+
const typed = applyTypesConfig(identity, process.types);
|
|
730
|
+
for (const [key, value] of Object.entries(typed)) {
|
|
731
|
+
state.vars[key] = value;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
state.status = 'running';
|
|
735
|
+
// Clear recoverable-error marker optimistically — if the handler
|
|
736
|
+
// throws DoubleLockError again it'll be re-stamped in the catch.
|
|
737
|
+
state.lastError = undefined;
|
|
738
|
+
state.lastErrorAt = undefined;
|
|
739
|
+
trace('execute.beforeExecute', { instanceId: state.instanceId, step: state.step, varsKeys: Object.keys(state.vars) });
|
|
740
|
+
const result = await process.execute(ctx);
|
|
741
|
+
trace('execute.afterExecute', { instanceId: state.instanceId, resultType: result[0], resultPayload: (() => { try {
|
|
742
|
+
return JSON.stringify(result[1], (_k, v) => typeof v === 'bigint' ? `${v}n` : v)?.slice(0, 200);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return String(result[1]);
|
|
746
|
+
} })() });
|
|
747
|
+
// result is [type, payload]
|
|
748
|
+
if (result[0] === 1) {
|
|
749
|
+
// SUSPEND
|
|
750
|
+
await this.suspend(state, process, result[1], identity);
|
|
751
|
+
}
|
|
752
|
+
else if (result[0] === 2) {
|
|
753
|
+
// SUBPROCESS — spawn a subprocess and run it within parent's context
|
|
754
|
+
const spawnConfig = result[1];
|
|
755
|
+
await this.spawnSubprocess(state, process, spawnConfig, identity);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
// DONE
|
|
759
|
+
await this.complete(state, result[1]);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
trace('execute.error', { instanceId: state.instanceId, error: error instanceof Error ? error.message : String(error) });
|
|
764
|
+
if (error instanceof DoubleLockError) {
|
|
765
|
+
// Step acquired a lock it already held in this async context.
|
|
766
|
+
// `using lock = await repo.lock(...)` always runs BEFORE mutations,
|
|
767
|
+
// so no side effects occurred. Keep the process SUSPENDED at its
|
|
768
|
+
// prior step so a future execution (next firing, or post-deploy
|
|
769
|
+
// restart) can retry against updated code. Terminating the process
|
|
770
|
+
// with `fail()` would destroy weeks-old durable state because of a
|
|
771
|
+
// bug that might land in the very next PR.
|
|
772
|
+
error.message += ` (process ${process.id}/${state.instanceId} step ${state.step})`;
|
|
773
|
+
console.error(`[ProcessExecutor] ${error.message}`);
|
|
774
|
+
state.lastError = error.message;
|
|
775
|
+
state.lastErrorAt = new Date();
|
|
776
|
+
state.status = 'suspended';
|
|
777
|
+
// Re-register prior subscriptions so the next firing re-triggers
|
|
778
|
+
// execute. Cleanup existing tracked subs first to avoid leaks.
|
|
779
|
+
this.cleanupSubscriptions(state.instanceId);
|
|
780
|
+
const priorRace = state.vars.__raceBranches;
|
|
781
|
+
if (priorRace) {
|
|
782
|
+
await this.suspend(state, process, { race: priorRace }, identity);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
// First-run failure or non-race suspend point — just persist
|
|
786
|
+
// suspended state. Post-deploy re-hydration will retry.
|
|
787
|
+
await this.saveState(state, process);
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
await this.fail(state, error instanceof Error ? error : new Error(String(error)));
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
// Wrap in lock tracking so acquire() can detect re-entrant locks within
|
|
795
|
+
// the same async context. Each execution (fresh + resume) gets its own
|
|
796
|
+
// tracking set.
|
|
797
|
+
const tracked = () => runWithLockTracking(executeInner);
|
|
798
|
+
// If container is available, run in request scope for context propagation
|
|
799
|
+
if (this.container) {
|
|
800
|
+
// Get origin context for tracing (if this process was started from a request)
|
|
801
|
+
const originCtx = this.originContexts.get(state.instanceId);
|
|
802
|
+
await runInFullRequestScope({
|
|
803
|
+
container: this.container,
|
|
804
|
+
type: 'process',
|
|
805
|
+
name: `process:${process.id}/${state.instanceId}`,
|
|
806
|
+
metadata: {
|
|
807
|
+
'process.id': process.id,
|
|
808
|
+
'process.instanceId': state.instanceId,
|
|
809
|
+
'process.step': state.step,
|
|
810
|
+
// Include origin context for tracing
|
|
811
|
+
...(originCtx && {
|
|
812
|
+
'origin.type': originCtx.type,
|
|
813
|
+
'origin.name': originCtx.name,
|
|
814
|
+
'origin.id': originCtx.id,
|
|
815
|
+
}),
|
|
816
|
+
},
|
|
817
|
+
}, tracked);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
// No container - just run directly
|
|
821
|
+
await tracked();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Handle process suspension.
|
|
826
|
+
*/
|
|
827
|
+
async suspend(state, process, config, identity) {
|
|
828
|
+
trace('suspend', { instanceId: state.instanceId, config });
|
|
829
|
+
state.status = 'suspended';
|
|
830
|
+
state.suspendedAt = new Date();
|
|
831
|
+
state.persistedStep = Object.entries(process.stepMap)
|
|
832
|
+
.find(([_, v]) => v === state.step)?.[0] ?? String(state.step);
|
|
833
|
+
if ('race' in config) {
|
|
834
|
+
// Race - subscribe to all branches
|
|
835
|
+
const branches = config.race.map(branch => {
|
|
836
|
+
let signalName = branch.signal;
|
|
837
|
+
let isStream = false;
|
|
838
|
+
// Resolve stream wildcards: stream:ModelName:*:fieldName -> stream:ModelName:entityId:fieldName
|
|
839
|
+
// The * placeholder is set by the compiler and resolved here using process identity
|
|
840
|
+
if (signalName?.startsWith('stream:')) {
|
|
841
|
+
isStream = true;
|
|
842
|
+
if (signalName.includes(':*:')) {
|
|
843
|
+
signalName = this.resolveStreamWildcard(signalName, identity, process.types);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
branchId: branch.id,
|
|
848
|
+
signal: signalName,
|
|
849
|
+
// Stream signals encode the entity ID in the signal name itself,
|
|
850
|
+
// so the identity map is redundant. Using it as a subscription filter
|
|
851
|
+
// breaks when publisher/subscriber use different key conventions
|
|
852
|
+
// (e.g. publisher emits { roomRef: id }, subscriber has { room: id }).
|
|
853
|
+
identity: signalName ? (isStream ? {} : identity) : undefined,
|
|
854
|
+
expiresAt: branch.timer ? this.calculateExpiry(branch.timer) : undefined,
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
const subscriptionId = await this.signalBus.subscribeRace(state.instanceId, branches);
|
|
858
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
859
|
+
// Store race branches in state for resume
|
|
860
|
+
state.vars.__raceBranches = config.race;
|
|
861
|
+
// Schedule timers
|
|
862
|
+
for (const branch of config.race) {
|
|
863
|
+
if (branch.timer) {
|
|
864
|
+
const expiresAt = this.calculateExpiry(branch.timer);
|
|
865
|
+
const timerId = await this.timerScheduler.schedule(state.instanceId, expiresAt, branch.id);
|
|
866
|
+
this.trackSubscription(state.instanceId, timerId);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else if ('signal' in config) {
|
|
871
|
+
const subscriptionId = await this.signalBus.subscribe(state.instanceId, config.signal, identity);
|
|
872
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
873
|
+
// Persist signal name so resubscribeSuspended can restore it after restart
|
|
874
|
+
state.vars.__suspendSignal = config.signal;
|
|
875
|
+
}
|
|
876
|
+
else if ('timer' in config) {
|
|
877
|
+
const expiresAt = this.calculateExpiry(config.timer);
|
|
878
|
+
const timerId = await this.timerScheduler.schedule(state.instanceId, expiresAt, '__timer__');
|
|
879
|
+
this.trackSubscription(state.instanceId, timerId);
|
|
880
|
+
}
|
|
881
|
+
else if ('parallel' in config) {
|
|
882
|
+
// Parallel (signal.all / signal.settled): subscribe each signal branch individually
|
|
883
|
+
const parallel = config.parallel;
|
|
884
|
+
const branchSubs = [];
|
|
885
|
+
for (let i = 0; i < parallel.branches.length; i++) {
|
|
886
|
+
const branch = parallel.branches[i];
|
|
887
|
+
if (branch.type === 'signal' && branch.expr) {
|
|
888
|
+
const signalName = branch.expr.signalName;
|
|
889
|
+
if (signalName) {
|
|
890
|
+
const subscriptionId = await this.signalBus.subscribe(state.instanceId, signalName, identity);
|
|
891
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
892
|
+
branchSubs.push({ branchIndex: i, subscriptionId });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
else if (branch.type === 'delay') {
|
|
896
|
+
const timerDuration = branch.expr ?? {};
|
|
897
|
+
const expiresAt = this.calculateExpiry(timerDuration);
|
|
898
|
+
const timerId = await this.timerScheduler.schedule(state.instanceId, expiresAt, `__parallel_${parallel.parallelId}_${i}`);
|
|
899
|
+
this.trackSubscription(state.instanceId, timerId);
|
|
900
|
+
branchSubs.push({ branchIndex: i, subscriptionId: timerId });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Store parallel tracking info in state.vars for handleSignalMatch
|
|
904
|
+
state.vars.__parallelBranches = branchSubs.map(bs => ({
|
|
905
|
+
branchIndex: bs.branchIndex,
|
|
906
|
+
subscriptionId: bs.subscriptionId,
|
|
907
|
+
parallelId: parallel.parallelId,
|
|
908
|
+
}));
|
|
909
|
+
}
|
|
910
|
+
else if ('scope' in config) {
|
|
911
|
+
// Scope suspension: parallel fan-out over entities
|
|
912
|
+
await this.handleScopeSuspend(state, process, config.scope, identity);
|
|
913
|
+
// Empty scope: handleScopeSuspend sets status='running' and advances the step.
|
|
914
|
+
// Re-execute instead of suspending.
|
|
915
|
+
if (state.status === 'running') {
|
|
916
|
+
await this.saveState(state, process);
|
|
917
|
+
await this.execute(state, process, identity);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
await this.saveState(state, process);
|
|
922
|
+
// Release lock AFTER subscriptions are registered
|
|
923
|
+
// This ensures signals wait for subscription before being delivered
|
|
924
|
+
await this.releaseLock(`process:${state.instanceId}`);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Re-subscribe a suspended process to its signals.
|
|
928
|
+
* Called when loading a suspended process from storage, since in-memory
|
|
929
|
+
* signal bus doesn't persist subscriptions across restarts.
|
|
930
|
+
*/
|
|
931
|
+
async resubscribeSuspended(state, identity, types) {
|
|
932
|
+
// Check if this process has already been re-subscribed
|
|
933
|
+
if (this.subscriptions.has(state.instanceId)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const raceBranches = state.variables.__raceBranches;
|
|
937
|
+
const suspendSignal = state.variables.__suspendSignal;
|
|
938
|
+
if (raceBranches) {
|
|
939
|
+
// Re-subscribe to race branches
|
|
940
|
+
const branches = raceBranches.map(branch => {
|
|
941
|
+
let signalName = branch.signal;
|
|
942
|
+
let isStream = false;
|
|
943
|
+
// Resolve stream wildcards (same as in suspend)
|
|
944
|
+
if (signalName?.startsWith('stream:')) {
|
|
945
|
+
isStream = true;
|
|
946
|
+
if (signalName.includes(':*:')) {
|
|
947
|
+
signalName = this.resolveStreamWildcard(signalName, identity, types);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
branchId: branch.id,
|
|
952
|
+
signal: signalName,
|
|
953
|
+
identity: signalName ? (isStream ? {} : identity) : undefined,
|
|
954
|
+
expiresAt: branch.timer ? this.calculateExpiry(branch.timer) : undefined,
|
|
955
|
+
};
|
|
956
|
+
});
|
|
957
|
+
const subscriptionId = await this.signalBus.subscribeRace(state.instanceId, branches);
|
|
958
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
959
|
+
// Schedule timers for any timer branches
|
|
960
|
+
for (const branch of raceBranches) {
|
|
961
|
+
if (branch.timer) {
|
|
962
|
+
const expiresAt = this.calculateExpiry(branch.timer);
|
|
963
|
+
const timerId = await this.timerScheduler.schedule(state.instanceId, expiresAt, branch.id);
|
|
964
|
+
this.trackSubscription(state.instanceId, timerId);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
else if (suspendSignal) {
|
|
969
|
+
// Re-subscribe to plain signal suspension (await signal(x))
|
|
970
|
+
const subscriptionId = await this.signalBus.subscribe(state.instanceId, suspendSignal, identity);
|
|
971
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// ============================================================================
|
|
975
|
+
// Signal & Timer Handling
|
|
976
|
+
// ============================================================================
|
|
977
|
+
async handleSignalMatch(match) {
|
|
978
|
+
trace('handleSignalMatch', { instanceId: match.instanceId, branchId: match.branchId });
|
|
979
|
+
// Subprocess instanceIds carry an `/__sub:...` suffix — route those to
|
|
980
|
+
// the child-resume path, which loads the parent row and resumes the
|
|
981
|
+
// nested child state in place.
|
|
982
|
+
if (match.instanceId.includes('/__sub:')) {
|
|
983
|
+
await this.resumeChild(match);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
// Check if this subscription belongs to this executor instance.
|
|
987
|
+
// In multi-instance scenarios, all executors receive the same NOTIFY via Postgres,
|
|
988
|
+
// but only the executor that registered the subscription should process it.
|
|
989
|
+
const trackedSubs = this.subscriptions.get(match.instanceId);
|
|
990
|
+
if (!trackedSubs || !trackedSubs.includes(match.subscriptionId)) {
|
|
991
|
+
trace('handleSignalMatch.skipped', { instanceId: match.instanceId, reason: 'subscription not owned by this executor' });
|
|
992
|
+
return; // This subscription was registered by a different executor instance
|
|
993
|
+
}
|
|
994
|
+
// Acquire lock before resuming process.
|
|
995
|
+
// By design, the lock is always available: the process is suspended (nobody holds it).
|
|
996
|
+
// If this fails, something is fundamentally wrong — propagate the error.
|
|
997
|
+
const lockKey = `process:${match.instanceId}`;
|
|
998
|
+
await this.acquireLock(lockKey);
|
|
999
|
+
trace('handleSignalMatch.lockAcquired', { instanceId: match.instanceId });
|
|
1000
|
+
try {
|
|
1001
|
+
// Note: Don't cleanup subscriptions here - do it after execute
|
|
1002
|
+
// This prevents race conditions when multiple signals arrive quickly
|
|
1003
|
+
const state = await this.loadState(match.instanceId);
|
|
1004
|
+
trace('handleSignalMatch.stateLoaded', { instanceId: match.instanceId, status: state?.status });
|
|
1005
|
+
if (!state) {
|
|
1006
|
+
trace('handleSignalMatch.noState', { instanceId: match.instanceId });
|
|
1007
|
+
await this.releaseLock(lockKey);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (state.status !== 'suspended') {
|
|
1011
|
+
trace('handleSignalMatch.notSuspended', { instanceId: match.instanceId, status: state.status });
|
|
1012
|
+
await this.releaseLock(lockKey);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
// Find the process definition
|
|
1016
|
+
const process = this.processRegistry.get(state.processId);
|
|
1017
|
+
if (!process) {
|
|
1018
|
+
trace('handleSignalMatch.noProcess', { processId: state.processId });
|
|
1019
|
+
await this.releaseLock(lockKey);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
trace('handleSignalMatch.processFound', { processId: state.processId });
|
|
1023
|
+
// Check for parallel branch subscription
|
|
1024
|
+
const parallelBranches = state.vars.__parallelBranches;
|
|
1025
|
+
if (parallelBranches) {
|
|
1026
|
+
// Check if this subscription matches a parallel branch
|
|
1027
|
+
const parallelBranch = parallelBranches.find(pb => pb.subscriptionId === match.subscriptionId);
|
|
1028
|
+
if (parallelBranch) {
|
|
1029
|
+
// This is a parallel branch signal - update parallel state in state.vars
|
|
1030
|
+
const parallelVarName = `__parallel_${parallelBranch.parallelId}`;
|
|
1031
|
+
const parallelState = state.vars[parallelVarName];
|
|
1032
|
+
if (parallelState) {
|
|
1033
|
+
parallelState.results[parallelBranch.branchIndex] = decodeProcessable(match.payload);
|
|
1034
|
+
parallelState.pending--;
|
|
1035
|
+
trace('handleSignalMatch.parallelBranch', {
|
|
1036
|
+
parallelId: parallelBranch.parallelId,
|
|
1037
|
+
branchIndex: parallelBranch.branchIndex,
|
|
1038
|
+
pending: parallelState.pending,
|
|
1039
|
+
});
|
|
1040
|
+
// Remove this subscription from tracking
|
|
1041
|
+
this.untrackSubscription(match.instanceId, match.subscriptionId);
|
|
1042
|
+
if (parallelState.pending > 0) {
|
|
1043
|
+
// Not all branches done - save state and release lock, don't resume
|
|
1044
|
+
await this.saveState(state, process);
|
|
1045
|
+
await this.releaseLock(lockKey);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
// All branches completed - clean up parallel tracking and resume
|
|
1049
|
+
delete state.vars.__parallelBranches;
|
|
1050
|
+
// Get identity from state
|
|
1051
|
+
const identity = state.vars.__identity;
|
|
1052
|
+
// Snapshot remaining subscriptions for cleanup
|
|
1053
|
+
const oldSubs = [...(this.subscriptions.get(match.instanceId) ?? [])];
|
|
1054
|
+
state.status = 'running';
|
|
1055
|
+
trace('handleSignalMatch.parallelComplete', { instanceId: match.instanceId, step: state.step });
|
|
1056
|
+
await this.execute(state, process, identity);
|
|
1057
|
+
trace('handleSignalMatch.executeComplete', { instanceId: match.instanceId });
|
|
1058
|
+
// Cleanup remaining subscriptions
|
|
1059
|
+
for (const subId of oldSubs) {
|
|
1060
|
+
this.signalBus.unsubscribe(subId);
|
|
1061
|
+
this.timerScheduler.cancel(subId);
|
|
1062
|
+
this.untrackSubscription(match.instanceId, subId);
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// Handle race vs simple signal
|
|
1069
|
+
if (match.branchId) {
|
|
1070
|
+
// Race result - find the winning branch and set step to its resumeStep
|
|
1071
|
+
const branches = state.vars.__raceBranches;
|
|
1072
|
+
trace('handleSignalMatch.raceBranches', { branchId: match.branchId, hasBranches: !!branches, branchCount: branches?.length });
|
|
1073
|
+
if (branches) {
|
|
1074
|
+
const branch = branches.find(b => b.id === match.branchId);
|
|
1075
|
+
if (branch) {
|
|
1076
|
+
state.step = branch.resumeStep;
|
|
1077
|
+
// Stream branches expect { value: T }, signals expect T directly
|
|
1078
|
+
// The branch ID for streams starts with "stream:"
|
|
1079
|
+
if (match.branchId.startsWith('stream:')) {
|
|
1080
|
+
state.vars.__raceResult = { value: decodeProcessable(match.payload) };
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
state.vars.__raceResult = decodeProcessable(match.payload);
|
|
1084
|
+
}
|
|
1085
|
+
delete state.vars.__raceBranches;
|
|
1086
|
+
trace('handleSignalMatch.branchFound', { branchId: match.branchId, resumeStep: branch.resumeStep });
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
trace('handleSignalMatch.branchNotFound', { branchId: match.branchId, availableBranches: branches.map(b => b.id) });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
// Simple signal - payload goes into signalPayload for the execute context
|
|
1095
|
+
state.vars.__signalPayload = decodeProcessable(match.payload);
|
|
1096
|
+
delete state.vars.__suspendSignal;
|
|
1097
|
+
}
|
|
1098
|
+
// Get identity from state
|
|
1099
|
+
const identity = state.vars.__identity;
|
|
1100
|
+
// Snapshot all OLD subscriptions before re-executing.
|
|
1101
|
+
// A race registers both signal subscriptions and timers under different IDs.
|
|
1102
|
+
// When one branch wins, ALL losing branches must be cancelled — not just the matched one.
|
|
1103
|
+
const oldSubs = [...(this.subscriptions.get(match.instanceId) ?? [])];
|
|
1104
|
+
// Resume execution (lock released in suspend/complete/fail)
|
|
1105
|
+
trace('handleSignalMatch.execute', { instanceId: match.instanceId, step: state.step });
|
|
1106
|
+
await this.execute(state, process, identity);
|
|
1107
|
+
trace('handleSignalMatch.executeComplete', { instanceId: match.instanceId });
|
|
1108
|
+
// Cleanup ALL old subscriptions (signals + timers from the previous suspend).
|
|
1109
|
+
// The process has now re-suspended with NEW subscriptions (different IDs)
|
|
1110
|
+
// or completed (which already called cleanupSubscriptions).
|
|
1111
|
+
for (const subId of oldSubs) {
|
|
1112
|
+
this.signalBus.unsubscribe(subId);
|
|
1113
|
+
this.timerScheduler.cancel(subId);
|
|
1114
|
+
this.untrackSubscription(match.instanceId, subId);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch (err) {
|
|
1118
|
+
// Release lock on error
|
|
1119
|
+
await this.releaseLock(lockKey);
|
|
1120
|
+
throw err;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async handleTimerFired(fired) {
|
|
1124
|
+
// Timer fired - treat as signal match with void payload
|
|
1125
|
+
const match = {
|
|
1126
|
+
subscriptionId: fired.timerId,
|
|
1127
|
+
instanceId: fired.instanceId,
|
|
1128
|
+
payload: undefined,
|
|
1129
|
+
branchId: fired.branchId,
|
|
1130
|
+
};
|
|
1131
|
+
await this.handleSignalMatch(match);
|
|
1132
|
+
}
|
|
1133
|
+
// External entry point for adapters that source timer fires outside the
|
|
1134
|
+
// bound timerScheduler (e.g. ScheduledTask delivery via HTTP/WS). Mirrors
|
|
1135
|
+
// the internal onFire path: setImmediate so we don't run the dispatch
|
|
1136
|
+
// synchronously inside the caller's stack, then route into the same
|
|
1137
|
+
// private handler. handleSignalMatch absorbs unknown subscriptions, so a
|
|
1138
|
+
// stale fire after subscribe-then-cancel is harmless.
|
|
1139
|
+
receiveTimerFire(fired) {
|
|
1140
|
+
setImmediate(() => {
|
|
1141
|
+
this.handleTimerFired(fired).catch(err => {
|
|
1142
|
+
trace('receiveTimerFire dispatch error', { err: String(err) });
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
// ============================================================================
|
|
1147
|
+
// Scope Handling (parallel fan-out)
|
|
1148
|
+
// ============================================================================
|
|
1149
|
+
/**
|
|
1150
|
+
* Handle scope suspension: fan-out over entities.
|
|
1151
|
+
*
|
|
1152
|
+
* For signal-first form: subscribe to the signal for each entity's identity.
|
|
1153
|
+
* For handler form: spawn sub-processes for each entity.
|
|
1154
|
+
*
|
|
1155
|
+
* When all entities complete, resume the parent process at resumeStep.
|
|
1156
|
+
*/
|
|
1157
|
+
async handleScopeSuspend(state, process, scopeConfig, identity) {
|
|
1158
|
+
const { scopeId, resumeStep } = scopeConfig;
|
|
1159
|
+
const entities = state.vars[`__scope_${scopeId}_entities`];
|
|
1160
|
+
const idFn = state.vars[`__scope_${scopeId}_idFn`];
|
|
1161
|
+
if (!entities || entities.length === 0) {
|
|
1162
|
+
// No entities — skip suspension, continue to resume step
|
|
1163
|
+
state.step = resumeStep;
|
|
1164
|
+
state.status = 'running';
|
|
1165
|
+
state.vars[`__scope_${scopeId}_results`] = {};
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
// Extract entity IDs
|
|
1169
|
+
const entityIds = entities.map((entity, index) => {
|
|
1170
|
+
if (idFn)
|
|
1171
|
+
return idFn(entity);
|
|
1172
|
+
// Auto-derive ID: if entity has an 'id' property, use it
|
|
1173
|
+
if (typeof entity === 'object' && entity !== null && 'id' in entity) {
|
|
1174
|
+
return String(entity.id);
|
|
1175
|
+
}
|
|
1176
|
+
return String(index);
|
|
1177
|
+
});
|
|
1178
|
+
// TSP3008: Check for duplicate entity IDs (runtime check)
|
|
1179
|
+
const seen = new Set();
|
|
1180
|
+
for (const id of entityIds) {
|
|
1181
|
+
if (seen.has(id)) {
|
|
1182
|
+
throw new Error(`Duplicate entity ID '${id}' in scope(). Each entity must have a unique identity.`);
|
|
1183
|
+
}
|
|
1184
|
+
seen.add(id);
|
|
1185
|
+
}
|
|
1186
|
+
// TSP3006: Check item limit (runtime check)
|
|
1187
|
+
if (entities.length > 1000) {
|
|
1188
|
+
throw new Error(`scope() exceeded maximum item limit (1000). Got ${entities.length} entities.`);
|
|
1189
|
+
}
|
|
1190
|
+
// Initialize scope tracking
|
|
1191
|
+
state.vars[`__scope_${scopeId}_remaining`] = entities.length;
|
|
1192
|
+
state.vars[`__scope_${scopeId}_results`] = {};
|
|
1193
|
+
state.vars[`__scope_${scopeId}_entityIds`] = entityIds;
|
|
1194
|
+
state.vars[`__scope_${scopeId}_resumeStep`] = resumeStep;
|
|
1195
|
+
if (scopeConfig.type === 'signal') {
|
|
1196
|
+
// Signal-first form: subscribe to signal for each entity
|
|
1197
|
+
// Each entity gets a race-style subscription that resolves independently
|
|
1198
|
+
for (let i = 0; i < entityIds.length; i++) {
|
|
1199
|
+
const entityId = entityIds[i];
|
|
1200
|
+
const entityIdentity = { ...identity, __scopeEntityId: entityId };
|
|
1201
|
+
const subscriptionId = await this.signalBus.subscribe(state.instanceId, String(scopeConfig.signal), entityIdentity);
|
|
1202
|
+
this.trackSubscription(state.instanceId, subscriptionId);
|
|
1203
|
+
}
|
|
1204
|
+
// Store scope info for signal matching
|
|
1205
|
+
state.vars.__scopeActive = scopeId;
|
|
1206
|
+
}
|
|
1207
|
+
// Handler form would spawn sub-processes here (future: compile handler body)
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Notify that a scope entity has completed.
|
|
1211
|
+
* Decrements the remaining counter and resumes parent if all done.
|
|
1212
|
+
*/
|
|
1213
|
+
async notifyScopeEntityComplete(instanceId, scopeId, entityId, result) {
|
|
1214
|
+
const state = await this.loadState(instanceId);
|
|
1215
|
+
if (!state)
|
|
1216
|
+
return;
|
|
1217
|
+
const results = state.vars[`__scope_${scopeId}_results`];
|
|
1218
|
+
results[entityId] = result;
|
|
1219
|
+
const remaining = state.vars[`__scope_${scopeId}_remaining`] - 1;
|
|
1220
|
+
state.vars[`__scope_${scopeId}_remaining`] = remaining;
|
|
1221
|
+
const process = this.processRegistry.get(state.processId);
|
|
1222
|
+
if (remaining === 0) {
|
|
1223
|
+
// All entities complete — results already a plain object, resume
|
|
1224
|
+
const resumeStep = state.vars[`__scope_${scopeId}_resumeStep`];
|
|
1225
|
+
state.step = resumeStep;
|
|
1226
|
+
state.status = 'running';
|
|
1227
|
+
await this.saveState(state, process);
|
|
1228
|
+
// Resume execution
|
|
1229
|
+
if (process) {
|
|
1230
|
+
const identity = state.vars.__identity;
|
|
1231
|
+
await this.execute(state, process, identity);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
await this.saveState(state, process);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
// State Management
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
async loadState(instanceId) {
|
|
1242
|
+
const stored = await this.storage.load(instanceId);
|
|
1243
|
+
if (!stored)
|
|
1244
|
+
return null;
|
|
1245
|
+
// Convert from storage format to SwitchProcessState
|
|
1246
|
+
return {
|
|
1247
|
+
processId: stored.processId,
|
|
1248
|
+
instanceId: stored.instanceId,
|
|
1249
|
+
version: stored.version,
|
|
1250
|
+
step: stored.pc,
|
|
1251
|
+
persistedStep: stored.variables.__persistedStep ?? 'entry',
|
|
1252
|
+
vars: deserializeState(stored.variables),
|
|
1253
|
+
timers: stored.timers,
|
|
1254
|
+
createdAt: stored.createdAt,
|
|
1255
|
+
updatedAt: stored.updatedAt,
|
|
1256
|
+
suspendedAt: stored.suspendedAt,
|
|
1257
|
+
completedAt: stored.completedAt,
|
|
1258
|
+
status: stored.status,
|
|
1259
|
+
result: stored.result,
|
|
1260
|
+
error: stored.error,
|
|
1261
|
+
lastError: stored.lastError,
|
|
1262
|
+
lastErrorAt: stored.lastErrorAt,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
async saveState(state, process) {
|
|
1266
|
+
// Store persistedStep in vars so it survives round-trip
|
|
1267
|
+
state.vars.__persistedStep = state.persistedStep;
|
|
1268
|
+
// Convert to storage format
|
|
1269
|
+
const serializedVars = serializeState(state.vars);
|
|
1270
|
+
await this.storage.save({
|
|
1271
|
+
processId: state.processId,
|
|
1272
|
+
instanceId: state.instanceId,
|
|
1273
|
+
version: state.version,
|
|
1274
|
+
pc: state.step,
|
|
1275
|
+
variables: serializedVars,
|
|
1276
|
+
timers: state.timers,
|
|
1277
|
+
createdAt: state.createdAt,
|
|
1278
|
+
updatedAt: new Date(),
|
|
1279
|
+
suspendedAt: state.suspendedAt,
|
|
1280
|
+
completedAt: state.completedAt,
|
|
1281
|
+
status: state.status,
|
|
1282
|
+
result: state.result,
|
|
1283
|
+
error: state.error,
|
|
1284
|
+
lastError: state.lastError,
|
|
1285
|
+
lastErrorAt: state.lastErrorAt,
|
|
1286
|
+
});
|
|
1287
|
+
const handle = this.handles.get(state.instanceId);
|
|
1288
|
+
if (handle) {
|
|
1289
|
+
handle.updateStatus(state.status);
|
|
1290
|
+
}
|
|
1291
|
+
// Broadcast exports if the process has them.
|
|
1292
|
+
// Use state.vars.exports (pre-serialization) — serializedVars has Processable-encoded
|
|
1293
|
+
// forms (Maps → __$type tags) which aren't useful for subscribers.
|
|
1294
|
+
if (process?.exports && state.vars.exports) {
|
|
1295
|
+
const exportsData = {};
|
|
1296
|
+
const exportsObj = state.vars.exports;
|
|
1297
|
+
for (const fieldName of process.exports.fields) {
|
|
1298
|
+
exportsData[fieldName] = exportsObj[fieldName];
|
|
1299
|
+
}
|
|
1300
|
+
const frozenSnap = this.buildFrozenExports(exportsObj, process.exports);
|
|
1301
|
+
if (handle) {
|
|
1302
|
+
handle.setExportsData(frozenSnap);
|
|
1303
|
+
}
|
|
1304
|
+
// Also publish to external channel backend if wired
|
|
1305
|
+
if (this.publishExports) {
|
|
1306
|
+
try {
|
|
1307
|
+
await this.publishExports({
|
|
1308
|
+
instanceId: state.instanceId,
|
|
1309
|
+
processId: state.processId,
|
|
1310
|
+
exports: exportsData,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
catch {
|
|
1314
|
+
// Don't fail the process if export broadcast fails
|
|
1315
|
+
trace('saveState.exportsBroadcastFailed', { instanceId: state.instanceId });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// ============================================================================
|
|
1321
|
+
// Helpers
|
|
1322
|
+
// ============================================================================
|
|
1323
|
+
async spawnSubprocess(state, process, spawnConfig, identity) {
|
|
1324
|
+
const subDef = process.subprocesses?.find(s => s.name === spawnConfig.name);
|
|
1325
|
+
if (!subDef) {
|
|
1326
|
+
throw new Error(`Subprocess '${spawnConfig.name}' not found on process '${process.id}'`);
|
|
1327
|
+
}
|
|
1328
|
+
const awaited = spawnConfig.awaited !== false;
|
|
1329
|
+
// Create a unique key for this subprocess instance based on args
|
|
1330
|
+
const subKey = `__sub:${spawnConfig.name}:${spawnConfig.args.map(String).join(':')}`;
|
|
1331
|
+
const childInstanceId = `${state.instanceId}/${subKey}`;
|
|
1332
|
+
// Blob layout under parent.vars[subKey]:
|
|
1333
|
+
// { vars: {...user+signal state...}, step, done, result, raceBranches?, suspendSignal? }
|
|
1334
|
+
// User-space child variables live under `.vars` so they never collide with
|
|
1335
|
+
// the blob's own metadata. Idempotent: if already-done blob exists and the
|
|
1336
|
+
// spawn is awaited, flow the cached result straight into storeVar.
|
|
1337
|
+
let blob = state.vars[subKey];
|
|
1338
|
+
if (blob?.done === true && awaited) {
|
|
1339
|
+
if (spawnConfig.storeVar) {
|
|
1340
|
+
state.vars[spawnConfig.storeVar] = blob.result;
|
|
1341
|
+
}
|
|
1342
|
+
await this.saveState(state, process);
|
|
1343
|
+
await this.execute(state, process, identity);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (!blob) {
|
|
1347
|
+
blob = { vars: {}, step: 0, done: false };
|
|
1348
|
+
for (let i = 0; i < subDef.params.length; i++) {
|
|
1349
|
+
blob.vars[subDef.params[i]] = spawnConfig.args[i];
|
|
1350
|
+
}
|
|
1351
|
+
state.vars[subKey] = blob;
|
|
1352
|
+
}
|
|
1353
|
+
// Build a minimal SwitchProcessState for the child execute function.
|
|
1354
|
+
// The child shares the parent's services (same inject map on parent process).
|
|
1355
|
+
const childState = {
|
|
1356
|
+
processId: `${process.id}/${subKey}`,
|
|
1357
|
+
instanceId: childInstanceId,
|
|
1358
|
+
version: '0',
|
|
1359
|
+
step: blob.step ?? 0,
|
|
1360
|
+
persistedStep: String(blob.step ?? 0),
|
|
1361
|
+
vars: blob.vars,
|
|
1362
|
+
timers: [],
|
|
1363
|
+
createdAt: new Date(),
|
|
1364
|
+
updatedAt: new Date(),
|
|
1365
|
+
status: 'running',
|
|
1366
|
+
};
|
|
1367
|
+
const services = await this.resolveServices(process);
|
|
1368
|
+
const childCtx = {
|
|
1369
|
+
state: childState,
|
|
1370
|
+
services,
|
|
1371
|
+
emit: () => { },
|
|
1372
|
+
};
|
|
1373
|
+
// Child signal identity = parent's identity + child's path-param args.
|
|
1374
|
+
// Without the child-specific keys, two siblings (alice/bob) would both
|
|
1375
|
+
// match emits scoped to one (e.g. signals.childTick(p1, alice) would
|
|
1376
|
+
// also wake bob). Merging params lets the bus route per-child.
|
|
1377
|
+
const childIdentity = { ...identity };
|
|
1378
|
+
for (let i = 0; i < subDef.params.length; i++) {
|
|
1379
|
+
childIdentity[subDef.params[i]] = String(spawnConfig.args[i]);
|
|
1380
|
+
}
|
|
1381
|
+
const childResult = await subDef.execute(childCtx);
|
|
1382
|
+
await this.applyChildResult(state, process, identity, childIdentity, subKey, childState, childResult, spawnConfig, /* fromSpawn */ true);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Apply a child execute() result — from either the initial spawn or a
|
|
1386
|
+
* signal-driven resume. Mutates parent.vars[subKey] for DONE/SUSPEND,
|
|
1387
|
+
* resumes the parent on awaited DONE, and manages the parent lock.
|
|
1388
|
+
*
|
|
1389
|
+
* `fromSpawn=true` means the parent is currently inside its own execute()
|
|
1390
|
+
* loop and should resume there for detached outcomes. `fromSpawn=false`
|
|
1391
|
+
* means resumeChild called us and the parent was otherwise idle/suspended;
|
|
1392
|
+
* detached outcomes just save + release.
|
|
1393
|
+
*/
|
|
1394
|
+
async applyChildResult(state, process, parentIdentity, childIdentity, subKey, childState, childResult, spawnConfig, fromSpawn) {
|
|
1395
|
+
const awaited = spawnConfig.awaited !== false;
|
|
1396
|
+
const childInstanceId = childState.instanceId;
|
|
1397
|
+
const blob = state.vars[subKey];
|
|
1398
|
+
const parentLockKey = `process:${state.instanceId}`;
|
|
1399
|
+
if (childResult[0] === 0) {
|
|
1400
|
+
// DONE
|
|
1401
|
+
blob.done = true;
|
|
1402
|
+
blob.result = childResult[1];
|
|
1403
|
+
blob.step = childState.step;
|
|
1404
|
+
delete blob.raceBranches;
|
|
1405
|
+
delete blob.suspendSignal;
|
|
1406
|
+
if (awaited) {
|
|
1407
|
+
if (spawnConfig.storeVar) {
|
|
1408
|
+
state.vars[spawnConfig.storeVar] = childResult[1];
|
|
1409
|
+
}
|
|
1410
|
+
delete state.vars.__pendingChildAwait;
|
|
1411
|
+
state.status = 'running';
|
|
1412
|
+
await this.saveState(state, process);
|
|
1413
|
+
await this.execute(state, process, parentIdentity);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
// Detached DONE — refresh the handle ref
|
|
1417
|
+
if (spawnConfig.storeVar) {
|
|
1418
|
+
state.vars[spawnConfig.storeVar] = { __subRef: true, key: subKey, name: spawnConfig.name, result: childResult[1], done: true };
|
|
1419
|
+
}
|
|
1420
|
+
await this.saveState(state, process);
|
|
1421
|
+
if (fromSpawn) {
|
|
1422
|
+
await this.execute(state, process, parentIdentity);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
await this.releaseLock(parentLockKey);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (childResult[0] === 2) {
|
|
1430
|
+
throw new Error(`Subprocess '${spawnConfig.name}' attempted to spawn a nested subprocess; ` +
|
|
1431
|
+
'nested subprocesses are not yet implemented.');
|
|
1432
|
+
}
|
|
1433
|
+
// SUSPEND — register the child's subscriptions under childInstanceId
|
|
1434
|
+
const childConfig = childResult[1];
|
|
1435
|
+
blob.step = childState.step;
|
|
1436
|
+
await this.subscribeChildSuspension(childInstanceId, childConfig, childIdentity, blob);
|
|
1437
|
+
if (awaited) {
|
|
1438
|
+
state.vars.__pendingChildAwait = { subKey, storeVar: spawnConfig.storeVar };
|
|
1439
|
+
state.status = 'suspended';
|
|
1440
|
+
state.suspendedAt = new Date();
|
|
1441
|
+
await this.saveState(state, process);
|
|
1442
|
+
await this.releaseLock(parentLockKey);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
// Detached SUSPEND — parent continues / stays as-is
|
|
1446
|
+
if (spawnConfig.storeVar) {
|
|
1447
|
+
state.vars[spawnConfig.storeVar] = { __subRef: true, key: subKey, name: spawnConfig.name, done: false };
|
|
1448
|
+
}
|
|
1449
|
+
await this.saveState(state, process);
|
|
1450
|
+
if (fromSpawn) {
|
|
1451
|
+
await this.execute(state, process, parentIdentity);
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
await this.releaseLock(parentLockKey);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
async subscribeChildSuspension(childInstanceId, config, identity, blob) {
|
|
1458
|
+
if ('race' in config) {
|
|
1459
|
+
const branches = config.race.map(branch => {
|
|
1460
|
+
let signalName = branch.signal;
|
|
1461
|
+
let isStream = false;
|
|
1462
|
+
if (signalName?.startsWith('stream:')) {
|
|
1463
|
+
isStream = true;
|
|
1464
|
+
if (signalName.includes(':*:')) {
|
|
1465
|
+
signalName = this.resolveStreamWildcard(signalName, identity);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
branchId: branch.id,
|
|
1470
|
+
signal: signalName,
|
|
1471
|
+
identity: signalName ? (isStream ? {} : identity) : undefined,
|
|
1472
|
+
expiresAt: branch.timer ? this.calculateExpiry(branch.timer) : undefined,
|
|
1473
|
+
};
|
|
1474
|
+
});
|
|
1475
|
+
const subscriptionId = await this.signalBus.subscribeRace(childInstanceId, branches);
|
|
1476
|
+
this.trackSubscription(childInstanceId, subscriptionId);
|
|
1477
|
+
blob.raceBranches = config.race;
|
|
1478
|
+
for (const branch of config.race) {
|
|
1479
|
+
if (branch.timer) {
|
|
1480
|
+
const expiresAt = this.calculateExpiry(branch.timer);
|
|
1481
|
+
const timerId = await this.timerScheduler.schedule(childInstanceId, expiresAt, branch.id);
|
|
1482
|
+
this.trackSubscription(childInstanceId, timerId);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
else if ('signal' in config) {
|
|
1487
|
+
const subscriptionId = await this.signalBus.subscribe(childInstanceId, config.signal, identity);
|
|
1488
|
+
this.trackSubscription(childInstanceId, subscriptionId);
|
|
1489
|
+
blob.suspendSignal = config.signal;
|
|
1490
|
+
}
|
|
1491
|
+
else if ('timer' in config) {
|
|
1492
|
+
const expiresAt = this.calculateExpiry(config.timer);
|
|
1493
|
+
const timerId = await this.timerScheduler.schedule(childInstanceId, expiresAt, '__timer__');
|
|
1494
|
+
this.trackSubscription(childInstanceId, timerId);
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
throw new Error('Subprocess suspended with unsupported config shape ' +
|
|
1498
|
+
'(scope/parallel inside subprocesses is not yet implemented).');
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Resume a child process after one of its signal subscriptions fired.
|
|
1503
|
+
* The child's instanceId encodes its scope as `${parentInstanceId}/__sub:name:args`.
|
|
1504
|
+
* We load the parent row, extract the child's nested state, apply the signal
|
|
1505
|
+
* match, and re-enter child.execute(). On child DONE, applyChildResult
|
|
1506
|
+
* resumes the parent if the parent was awaiting.
|
|
1507
|
+
*/
|
|
1508
|
+
async resumeChild(match) {
|
|
1509
|
+
const { parentInstanceId, subKey } = parseChildInstanceId(match.instanceId);
|
|
1510
|
+
if (!parentInstanceId || !subKey) {
|
|
1511
|
+
trace('resumeChild.invalidInstanceId', { instanceId: match.instanceId });
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const trackedSubs = this.subscriptions.get(match.instanceId);
|
|
1515
|
+
if (!trackedSubs || !trackedSubs.includes(match.subscriptionId)) {
|
|
1516
|
+
trace('resumeChild.notOwned', { instanceId: match.instanceId });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const parentLockKey = `process:${parentInstanceId}`;
|
|
1520
|
+
await this.acquireLock(parentLockKey);
|
|
1521
|
+
try {
|
|
1522
|
+
const parentState = await this.loadState(parentInstanceId);
|
|
1523
|
+
if (!parentState) {
|
|
1524
|
+
trace('resumeChild.noParent', { parentInstanceId });
|
|
1525
|
+
await this.releaseLock(parentLockKey);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const blob = parentState.vars[subKey];
|
|
1529
|
+
if (!blob) {
|
|
1530
|
+
trace('resumeChild.noChildBlob', { parentInstanceId, subKey });
|
|
1531
|
+
await this.releaseLock(parentLockKey);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const process = this.processRegistry.get(parentState.processId);
|
|
1535
|
+
if (!process) {
|
|
1536
|
+
trace('resumeChild.noProcess', { processId: parentState.processId });
|
|
1537
|
+
await this.releaseLock(parentLockKey);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const subName = subKey.split(':')[1];
|
|
1541
|
+
const subDef = process.subprocesses?.find(s => s.name === subName);
|
|
1542
|
+
if (!subDef) {
|
|
1543
|
+
trace('resumeChild.noSubDef', { subName });
|
|
1544
|
+
await this.releaseLock(parentLockKey);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
// Apply the signal match to the child's blob — mirrors handleSignalMatch
|
|
1548
|
+
// race/simple-signal dispatch but on the nested blob.
|
|
1549
|
+
if (match.branchId) {
|
|
1550
|
+
const branches = blob.raceBranches;
|
|
1551
|
+
if (branches) {
|
|
1552
|
+
const branch = branches.find(b => b.id === match.branchId);
|
|
1553
|
+
if (branch) {
|
|
1554
|
+
blob.step = branch.resumeStep;
|
|
1555
|
+
if (match.branchId.startsWith('stream:')) {
|
|
1556
|
+
blob.vars.__raceResult = { value: decodeProcessable(match.payload) };
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
blob.vars.__raceResult = decodeProcessable(match.payload);
|
|
1560
|
+
}
|
|
1561
|
+
delete blob.raceBranches;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
else {
|
|
1566
|
+
blob.vars.__signalPayload = decodeProcessable(match.payload);
|
|
1567
|
+
delete blob.suspendSignal;
|
|
1568
|
+
}
|
|
1569
|
+
const childState = {
|
|
1570
|
+
processId: `${process.id}/${subKey}`,
|
|
1571
|
+
instanceId: match.instanceId,
|
|
1572
|
+
version: '0',
|
|
1573
|
+
step: blob.step ?? 0,
|
|
1574
|
+
persistedStep: String(blob.step ?? 0),
|
|
1575
|
+
vars: blob.vars,
|
|
1576
|
+
timers: [],
|
|
1577
|
+
createdAt: new Date(),
|
|
1578
|
+
updatedAt: new Date(),
|
|
1579
|
+
status: 'running',
|
|
1580
|
+
};
|
|
1581
|
+
const services = await this.resolveServices(process);
|
|
1582
|
+
const childCtx = { state: childState, services, emit: () => { } };
|
|
1583
|
+
const oldSubs = [...(this.subscriptions.get(match.instanceId) ?? [])];
|
|
1584
|
+
const childResult = await subDef.execute(childCtx);
|
|
1585
|
+
// Reconstruct spawnConfig from __pendingChildAwait so applyChildResult
|
|
1586
|
+
// knows the awaited/storeVar context. For detached, neither is set.
|
|
1587
|
+
const pendingAwait = parentState.vars.__pendingChildAwait;
|
|
1588
|
+
const spawnConfig = {
|
|
1589
|
+
name: subName,
|
|
1590
|
+
args: subDef.params.map(p => blob.vars[p]),
|
|
1591
|
+
storeVar: pendingAwait?.subKey === subKey ? pendingAwait.storeVar : undefined,
|
|
1592
|
+
awaited: pendingAwait?.subKey === subKey,
|
|
1593
|
+
};
|
|
1594
|
+
const parentIdentity = parentState.vars.__identity ?? {};
|
|
1595
|
+
const childIdentity = { ...parentIdentity };
|
|
1596
|
+
for (const p of subDef.params) {
|
|
1597
|
+
childIdentity[p] = String(blob.vars[p]);
|
|
1598
|
+
}
|
|
1599
|
+
await this.applyChildResult(parentState, process, parentIdentity, childIdentity, subKey, childState, childResult, spawnConfig, /* fromSpawn */ false);
|
|
1600
|
+
for (const subId of oldSubs) {
|
|
1601
|
+
this.signalBus.unsubscribe(subId);
|
|
1602
|
+
this.timerScheduler.cancel(subId);
|
|
1603
|
+
this.untrackSubscription(match.instanceId, subId);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
catch (err) {
|
|
1607
|
+
await this.releaseLock(parentLockKey);
|
|
1608
|
+
throw err;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
buildFrozenExports(exportsObj, metadata) {
|
|
1612
|
+
const data = {};
|
|
1613
|
+
for (const fieldName of metadata.fields) {
|
|
1614
|
+
data[fieldName] = exportsObj[fieldName];
|
|
1615
|
+
}
|
|
1616
|
+
return freezeExports(data, metadata.methods);
|
|
1617
|
+
}
|
|
1618
|
+
async resolveServices(process) {
|
|
1619
|
+
const cached = this.resolvedServicesCache.get(process.id);
|
|
1620
|
+
if (cached)
|
|
1621
|
+
return cached;
|
|
1622
|
+
const resolved = {};
|
|
1623
|
+
for (const [key, token] of Object.entries(process.inject)) {
|
|
1624
|
+
resolved[key] = await this.resolve(token);
|
|
1625
|
+
}
|
|
1626
|
+
this.resolvedServicesCache.set(process.id, resolved);
|
|
1627
|
+
return resolved;
|
|
1628
|
+
}
|
|
1629
|
+
calculateExpiry(timer) {
|
|
1630
|
+
const ms = (timer.days ?? 0) * 24 * 60 * 60 * 1000 +
|
|
1631
|
+
(timer.hours ?? 0) * 60 * 60 * 1000 +
|
|
1632
|
+
(timer.minutes ?? 0) * 60 * 1000 +
|
|
1633
|
+
(timer.seconds ?? 0) * 1000;
|
|
1634
|
+
return new Date(Date.now() + ms);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Resolve stream signal wildcard to actual entity ID.
|
|
1638
|
+
*
|
|
1639
|
+
* Stream signals from the compiler have format: stream:ModelName:*:fieldName
|
|
1640
|
+
* The * needs to be resolved using the process identity at runtime.
|
|
1641
|
+
*
|
|
1642
|
+
* Uses the shared stream-utils for proper camelCase conversion that handles:
|
|
1643
|
+
* - Standard models: Order → orderId
|
|
1644
|
+
* - Acronyms: ABC → abcId, HTTPServer → httpServerId
|
|
1645
|
+
* - Numbers: V2Order → v2OrderId
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* Input: stream:Order:*:statusUpdates, identity: { orderId: 'abc123' }
|
|
1649
|
+
* Output: stream:Order:abc123:statusUpdates
|
|
1650
|
+
*/
|
|
1651
|
+
resolveStreamWildcard(signalName, identity, types) {
|
|
1652
|
+
const { resolved, result } = resolveStreamWildcardUtil(signalName, identity, types);
|
|
1653
|
+
if (result.success) {
|
|
1654
|
+
if (result.usedFallback) {
|
|
1655
|
+
trace('resolveStreamWildcard.fallback', {
|
|
1656
|
+
from: signalName,
|
|
1657
|
+
to: resolved,
|
|
1658
|
+
usedKey: result.usedKey,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
trace('resolveStreamWildcard', { from: signalName, to: resolved });
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
trace('resolveStreamWildcard.failed', { signalName, error: result.error });
|
|
1667
|
+
// Log warning to help users debug
|
|
1668
|
+
console.warn(`[Stream] Warning: ${result.error}`);
|
|
1669
|
+
}
|
|
1670
|
+
return resolved;
|
|
1671
|
+
}
|
|
1672
|
+
trackSubscription(instanceId, subscriptionId) {
|
|
1673
|
+
const subs = this.subscriptions.get(instanceId) ?? [];
|
|
1674
|
+
subs.push(subscriptionId);
|
|
1675
|
+
this.subscriptions.set(instanceId, subs);
|
|
1676
|
+
}
|
|
1677
|
+
untrackSubscription(instanceId, subscriptionId) {
|
|
1678
|
+
const subs = this.subscriptions.get(instanceId);
|
|
1679
|
+
if (subs) {
|
|
1680
|
+
const idx = subs.indexOf(subscriptionId);
|
|
1681
|
+
if (idx !== -1) {
|
|
1682
|
+
subs.splice(idx, 1);
|
|
1683
|
+
}
|
|
1684
|
+
if (subs.length === 0) {
|
|
1685
|
+
this.subscriptions.delete(instanceId);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
cleanupSubscriptions(instanceId) {
|
|
1690
|
+
const subs = this.subscriptions.get(instanceId);
|
|
1691
|
+
if (subs) {
|
|
1692
|
+
for (const subId of subs) {
|
|
1693
|
+
this.signalBus.unsubscribe(subId);
|
|
1694
|
+
this.timerScheduler.cancel(subId);
|
|
1695
|
+
}
|
|
1696
|
+
this.subscriptions.delete(instanceId);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Cascade-cancel all live subprocesses of a parent reaching a terminal
|
|
1701
|
+
* state. Lexical scope dictates lifetime: children's subscriptions are
|
|
1702
|
+
* torn down so they can no longer receive signals and resume. Nested
|
|
1703
|
+
* blob stays in parent.vars for post-mortem inspection.
|
|
1704
|
+
*/
|
|
1705
|
+
cleanupChildSubscriptions(state) {
|
|
1706
|
+
for (const key of Object.keys(state.vars)) {
|
|
1707
|
+
if (!key.startsWith('__sub:'))
|
|
1708
|
+
continue;
|
|
1709
|
+
const blob = state.vars[key];
|
|
1710
|
+
if (!blob || blob.done)
|
|
1711
|
+
continue;
|
|
1712
|
+
const childInstanceId = `${state.instanceId}/${key}`;
|
|
1713
|
+
this.cleanupSubscriptions(childInstanceId);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Acquire a process lock. No-op if no lock provider configured.
|
|
1718
|
+
* Blocks until lock is acquired - JustScale locks never fail.
|
|
1719
|
+
*/
|
|
1720
|
+
async acquireLock(lockKey) {
|
|
1721
|
+
if (!this.lockProvider)
|
|
1722
|
+
return;
|
|
1723
|
+
await this.lockProvider.acquire(lockKey, { ...this.lockOptions, key: lockKey }, this.executorId);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Release a process lock. No-op if no lock provider configured.
|
|
1727
|
+
*/
|
|
1728
|
+
async releaseLock(lockKey) {
|
|
1729
|
+
trace('releaseLock', { lockKey });
|
|
1730
|
+
if (!this.lockProvider)
|
|
1731
|
+
return;
|
|
1732
|
+
await this.lockProvider.release(lockKey, this.executorId);
|
|
1733
|
+
trace('releaseLock.done', { lockKey });
|
|
1734
|
+
}
|
|
1735
|
+
async complete(state, result) {
|
|
1736
|
+
state.status = 'completed';
|
|
1737
|
+
state.result = result;
|
|
1738
|
+
state.completedAt = new Date();
|
|
1739
|
+
// Cascade: parent reached terminal state, tear down any live children.
|
|
1740
|
+
// Lexical scope dictates lifetime; detached mode is a v1.1 concern.
|
|
1741
|
+
this.cleanupChildSubscriptions(state);
|
|
1742
|
+
const process = this.processRegistry.get(state.processId);
|
|
1743
|
+
await this.saveState(state, process);
|
|
1744
|
+
await this.storage.complete(state.instanceId, result);
|
|
1745
|
+
this.cleanupSubscriptions(state.instanceId);
|
|
1746
|
+
this.originContexts.delete(state.instanceId);
|
|
1747
|
+
this.handles.delete(state.instanceId);
|
|
1748
|
+
// Signal yield queue completion
|
|
1749
|
+
const yieldQueue = this.yieldQueues.get(state.instanceId);
|
|
1750
|
+
if (yieldQueue) {
|
|
1751
|
+
yieldQueue.complete();
|
|
1752
|
+
this.yieldQueues.delete(state.instanceId);
|
|
1753
|
+
}
|
|
1754
|
+
// Release process lock after completion
|
|
1755
|
+
await this.releaseLock(`process:${state.instanceId}`);
|
|
1756
|
+
const completion = this.completions.get(state.instanceId);
|
|
1757
|
+
if (completion) {
|
|
1758
|
+
completion.resolve(result);
|
|
1759
|
+
this.completions.delete(state.instanceId);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async fail(state, error) {
|
|
1763
|
+
state.status = 'failed';
|
|
1764
|
+
state.error = error.message;
|
|
1765
|
+
state.completedAt = new Date();
|
|
1766
|
+
this.cleanupChildSubscriptions(state);
|
|
1767
|
+
const process = this.processRegistry.get(state.processId);
|
|
1768
|
+
await this.saveState(state, process);
|
|
1769
|
+
await this.storage.fail(state.instanceId, error.message);
|
|
1770
|
+
this.cleanupSubscriptions(state.instanceId);
|
|
1771
|
+
this.originContexts.delete(state.instanceId);
|
|
1772
|
+
this.handles.delete(state.instanceId);
|
|
1773
|
+
// Signal yield queue completion (consumers see done)
|
|
1774
|
+
const yieldQueue = this.yieldQueues.get(state.instanceId);
|
|
1775
|
+
if (yieldQueue) {
|
|
1776
|
+
yieldQueue.complete();
|
|
1777
|
+
this.yieldQueues.delete(state.instanceId);
|
|
1778
|
+
}
|
|
1779
|
+
// Release process lock after failure
|
|
1780
|
+
await this.releaseLock(`process:${state.instanceId}`);
|
|
1781
|
+
const completion = this.completions.get(state.instanceId);
|
|
1782
|
+
if (completion) {
|
|
1783
|
+
completion.reject(error);
|
|
1784
|
+
this.completions.delete(state.instanceId);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
// ============================================================================
|
|
1788
|
+
// Query Methods
|
|
1789
|
+
// ============================================================================
|
|
1790
|
+
/**
|
|
1791
|
+
* Get a process state by instance ID.
|
|
1792
|
+
*/
|
|
1793
|
+
async get(instanceId) {
|
|
1794
|
+
return this.loadState(instanceId);
|
|
1795
|
+
}
|
|
1796
|
+
async *queryByStatus(status) {
|
|
1797
|
+
for await (const stored of this.storage.findByStatus(status)) {
|
|
1798
|
+
const state = await this.loadState(stored.instanceId);
|
|
1799
|
+
if (state)
|
|
1800
|
+
yield state;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
async *queryByProcessId(processId) {
|
|
1804
|
+
for await (const stored of this.storage.findByProcessId(processId)) {
|
|
1805
|
+
const state = await this.loadState(stored.instanceId);
|
|
1806
|
+
if (state)
|
|
1807
|
+
yield state;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
// ============================================================================
|
|
1811
|
+
// Cancellation
|
|
1812
|
+
// ============================================================================
|
|
1813
|
+
/**
|
|
1814
|
+
* Cancel a process instance.
|
|
1815
|
+
* Only processes in 'pending' or 'suspended' status can be cancelled.
|
|
1816
|
+
* Cleans up subscriptions, timers, and rejects the completion promise.
|
|
1817
|
+
*
|
|
1818
|
+
* @returns true if the process was cancelled, false if it was already completed/failed
|
|
1819
|
+
*/
|
|
1820
|
+
async cancel(instanceId) {
|
|
1821
|
+
const lockKey = `process:${instanceId}`;
|
|
1822
|
+
await this.acquireLock(lockKey);
|
|
1823
|
+
try {
|
|
1824
|
+
const state = await this.loadState(instanceId);
|
|
1825
|
+
if (!state) {
|
|
1826
|
+
await this.releaseLock(lockKey);
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
if (state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled') {
|
|
1830
|
+
await this.releaseLock(lockKey);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
state.status = 'cancelled';
|
|
1834
|
+
state.completedAt = new Date();
|
|
1835
|
+
this.cleanupChildSubscriptions(state);
|
|
1836
|
+
const process = this.processRegistry.get(state.processId);
|
|
1837
|
+
await this.saveState(state, process);
|
|
1838
|
+
this.cleanupSubscriptions(instanceId);
|
|
1839
|
+
this.originContexts.delete(instanceId);
|
|
1840
|
+
this.handles.delete(instanceId);
|
|
1841
|
+
// Signal yield queue completion
|
|
1842
|
+
const yieldQueue = this.yieldQueues.get(instanceId);
|
|
1843
|
+
if (yieldQueue) {
|
|
1844
|
+
yieldQueue.complete();
|
|
1845
|
+
this.yieldQueues.delete(instanceId);
|
|
1846
|
+
}
|
|
1847
|
+
await this.releaseLock(lockKey);
|
|
1848
|
+
// Reject completion promise
|
|
1849
|
+
const completion = this.completions.get(instanceId);
|
|
1850
|
+
if (completion) {
|
|
1851
|
+
completion.reject(new Error('Process cancelled'));
|
|
1852
|
+
this.completions.delete(instanceId);
|
|
1853
|
+
}
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
catch (err) {
|
|
1857
|
+
await this.releaseLock(lockKey);
|
|
1858
|
+
throw err;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
// ============================================================================
|
|
1862
|
+
// Yield / Continuation
|
|
1863
|
+
// ============================================================================
|
|
1864
|
+
/**
|
|
1865
|
+
* Create a ProcessContinuation for a generator process.
|
|
1866
|
+
* Allows iterating over yielded values with a durable consumer cursor.
|
|
1867
|
+
*
|
|
1868
|
+
* @param instanceId - The process instance ID
|
|
1869
|
+
* @param consumerId - Optional consumer ID for reconnecting to a previous cursor position.
|
|
1870
|
+
* If not provided, a new consumer is created starting from the beginning.
|
|
1871
|
+
*/
|
|
1872
|
+
async createContinuation(instanceId, consumerId) {
|
|
1873
|
+
const state = await this.loadState(instanceId);
|
|
1874
|
+
if (!state) {
|
|
1875
|
+
throw new Error(`Process instance not found: ${instanceId}`);
|
|
1876
|
+
}
|
|
1877
|
+
const resolvedConsumerId = consumerId ?? `consumer_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
1878
|
+
// Load persisted cursor for this consumer
|
|
1879
|
+
const consumers = (state.vars.__yieldConsumers ?? {});
|
|
1880
|
+
const initialCursor = consumers[resolvedConsumerId]?.cursor ?? 0;
|
|
1881
|
+
// Get or create yield queue
|
|
1882
|
+
let yieldQueue = this.yieldQueues.get(instanceId);
|
|
1883
|
+
if (!yieldQueue) {
|
|
1884
|
+
yieldQueue = createYieldQueue();
|
|
1885
|
+
this.yieldQueues.set(instanceId, yieldQueue);
|
|
1886
|
+
// If process already finished, signal completion on the new queue
|
|
1887
|
+
// so consumers don't hang after draining stored yields
|
|
1888
|
+
if (state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled') {
|
|
1889
|
+
yieldQueue.complete();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
// Get or create completion deferred
|
|
1893
|
+
let completion = this.completions.get(instanceId);
|
|
1894
|
+
if (!completion) {
|
|
1895
|
+
const newCompletion = createDeferred();
|
|
1896
|
+
this.completions.set(instanceId, newCompletion);
|
|
1897
|
+
completion = newCompletion;
|
|
1898
|
+
if (state.status === 'completed') {
|
|
1899
|
+
newCompletion.resolve(state.result);
|
|
1900
|
+
}
|
|
1901
|
+
else if (state.status === 'failed') {
|
|
1902
|
+
newCompletion.reject(new Error(state.error ?? 'Process failed'));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const storage = this.storage;
|
|
1906
|
+
const impl = new ProcessContinuationImpl(instanceId, resolvedConsumerId, initialCursor, state.status, yieldQueue, async () => {
|
|
1907
|
+
const s = await storage.load(instanceId);
|
|
1908
|
+
return s?.variables?.__yields ?? [];
|
|
1909
|
+
}, completion, async (cid, cursor) => {
|
|
1910
|
+
const current = await storage.load(instanceId);
|
|
1911
|
+
if (current) {
|
|
1912
|
+
const vars = current.variables;
|
|
1913
|
+
const yieldConsumers = (vars.__yieldConsumers ?? {});
|
|
1914
|
+
yieldConsumers[cid] = { cursor };
|
|
1915
|
+
vars.__yieldConsumers = yieldConsumers;
|
|
1916
|
+
await storage.save(current);
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
return impl;
|
|
1920
|
+
}
|
|
1921
|
+
// ============================================================================
|
|
1922
|
+
// Lifecycle
|
|
1923
|
+
// ============================================================================
|
|
1924
|
+
/** Start the timer scheduler */
|
|
1925
|
+
startTimers() {
|
|
1926
|
+
this.timerScheduler.start();
|
|
1927
|
+
}
|
|
1928
|
+
/** Stop the timer scheduler */
|
|
1929
|
+
stopTimers() {
|
|
1930
|
+
this.timerScheduler.stop();
|
|
1931
|
+
}
|
|
1932
|
+
/** Clear caches (for testing) */
|
|
1933
|
+
clearCaches() {
|
|
1934
|
+
this.resolvedServicesCache.clear();
|
|
1935
|
+
this.completions.clear();
|
|
1936
|
+
this.subscriptions.clear();
|
|
1937
|
+
this.originContexts.clear();
|
|
1938
|
+
this.yieldQueues.clear();
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
//# sourceMappingURL=executor.js.map
|