@smthrs/control 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/cjs/ApprovalAuthority.d.ts +73 -0
- package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
- package/dist/cjs/ApprovalAuthority.js +62 -0
- package/dist/cjs/ApprovalAuthority.js.map +7 -0
- package/dist/cjs/Cancellation.d.ts +107 -0
- package/dist/cjs/Cancellation.d.ts.map +1 -0
- package/dist/cjs/Cancellation.js +72 -0
- package/dist/cjs/Cancellation.js.map +7 -0
- package/dist/cjs/Channels.d.ts +170 -0
- package/dist/cjs/Channels.d.ts.map +1 -0
- package/dist/cjs/Channels.js +278 -0
- package/dist/cjs/Channels.js.map +7 -0
- package/dist/cjs/Control.d.ts +202 -0
- package/dist/cjs/Control.d.ts.map +1 -0
- package/dist/cjs/Control.js +47 -0
- package/dist/cjs/Control.js.map +7 -0
- package/dist/cjs/ControlClient.d.ts +52 -0
- package/dist/cjs/ControlClient.d.ts.map +1 -0
- package/dist/cjs/ControlClient.js +191 -0
- package/dist/cjs/ControlClient.js.map +7 -0
- package/dist/cjs/ControlError.d.ts +318 -0
- package/dist/cjs/ControlError.d.ts.map +1 -0
- package/dist/cjs/ControlError.js +249 -0
- package/dist/cjs/ControlError.js.map +7 -0
- package/dist/cjs/ControlExecutor.d.ts +372 -0
- package/dist/cjs/ControlExecutor.d.ts.map +1 -0
- package/dist/cjs/ControlExecutor.js +123 -0
- package/dist/cjs/ControlExecutor.js.map +7 -0
- package/dist/cjs/ControlFacts.d.ts +454 -0
- package/dist/cjs/ControlFacts.d.ts.map +1 -0
- package/dist/cjs/ControlFacts.js +261 -0
- package/dist/cjs/ControlFacts.js.map +7 -0
- package/dist/cjs/ControlLive.d.ts +23 -0
- package/dist/cjs/ControlLive.d.ts.map +1 -0
- package/dist/cjs/ControlLive.js +1280 -0
- package/dist/cjs/ControlLive.js.map +7 -0
- package/dist/cjs/ControlRpcs.d.ts +1204 -0
- package/dist/cjs/ControlRpcs.d.ts.map +1 -0
- package/dist/cjs/ControlRpcs.js +247 -0
- package/dist/cjs/ControlRpcs.js.map +7 -0
- package/dist/cjs/ControlRuntime.d.ts +635 -0
- package/dist/cjs/ControlRuntime.d.ts.map +1 -0
- package/dist/cjs/ControlRuntime.js +740 -0
- package/dist/cjs/ControlRuntime.js.map +7 -0
- package/dist/cjs/ControlSchema.d.ts +2642 -0
- package/dist/cjs/ControlSchema.d.ts.map +1 -0
- package/dist/cjs/ControlSchema.js +634 -0
- package/dist/cjs/ControlSchema.js.map +7 -0
- package/dist/cjs/ControlServer.d.ts +51 -0
- package/dist/cjs/ControlServer.d.ts.map +1 -0
- package/dist/cjs/ControlServer.js +121 -0
- package/dist/cjs/ControlServer.js.map +7 -0
- package/dist/cjs/Credential.d.ts +136 -0
- package/dist/cjs/Credential.d.ts.map +1 -0
- package/dist/cjs/Credential.js +168 -0
- package/dist/cjs/Credential.js.map +7 -0
- package/dist/cjs/CredentialCipher.d.ts +90 -0
- package/dist/cjs/CredentialCipher.d.ts.map +1 -0
- package/dist/cjs/CredentialCipher.js +45 -0
- package/dist/cjs/CredentialCipher.js.map +7 -0
- package/dist/cjs/CredentialStore.d.ts +97 -0
- package/dist/cjs/CredentialStore.d.ts.map +1 -0
- package/dist/cjs/CredentialStore.js +81 -0
- package/dist/cjs/CredentialStore.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +112 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +45 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Health.d.ts +333 -0
- package/dist/cjs/Health.d.ts.map +1 -0
- package/dist/cjs/Health.js +311 -0
- package/dist/cjs/Health.js.map +7 -0
- package/dist/cjs/JevSessionChecker.d.ts +57 -0
- package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
- package/dist/cjs/JevSessionChecker.js +113 -0
- package/dist/cjs/JevSessionChecker.js.map +7 -0
- package/dist/cjs/Lineage.d.ts +131 -0
- package/dist/cjs/Lineage.d.ts.map +1 -0
- package/dist/cjs/Lineage.js +81 -0
- package/dist/cjs/Lineage.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +34 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +60 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/Monitor.d.ts +282 -0
- package/dist/cjs/Monitor.d.ts.map +1 -0
- package/dist/cjs/Monitor.js +283 -0
- package/dist/cjs/Monitor.js.map +7 -0
- package/dist/cjs/ScopedToken.d.ts +193 -0
- package/dist/cjs/ScopedToken.d.ts.map +1 -0
- package/dist/cjs/ScopedToken.js +135 -0
- package/dist/cjs/ScopedToken.js.map +7 -0
- package/dist/cjs/SqlControlRuntime.d.ts +161 -0
- package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
- package/dist/cjs/SqlControlRuntime.js +1521 -0
- package/dist/cjs/SqlControlRuntime.js.map +7 -0
- package/dist/cjs/SqlCredentialStore.d.ts +43 -0
- package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
- package/dist/cjs/SqlCredentialStore.js +113 -0
- package/dist/cjs/SqlCredentialStore.js.map +7 -0
- package/dist/cjs/Steering.d.ts +69 -0
- package/dist/cjs/Steering.d.ts.map +1 -0
- package/dist/cjs/Steering.js +49 -0
- package/dist/cjs/Steering.js.map +7 -0
- package/dist/cjs/SystemFlows.d.ts +223 -0
- package/dist/cjs/SystemFlows.d.ts.map +1 -0
- package/dist/cjs/SystemFlows.js +195 -0
- package/dist/cjs/SystemFlows.js.map +7 -0
- package/dist/cjs/WebCryptoCipher.d.ts +49 -0
- package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
- package/dist/cjs/WebCryptoCipher.js +129 -0
- package/dist/cjs/WebCryptoCipher.js.map +7 -0
- package/dist/cjs/WebhookChannel.d.ts +113 -0
- package/dist/cjs/WebhookChannel.d.ts.map +1 -0
- package/dist/cjs/WebhookChannel.js +98 -0
- package/dist/cjs/WebhookChannel.js.map +7 -0
- package/dist/cjs/index.d.ts +160 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +91 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
- package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/MutationBoundary.js +50 -0
- package/dist/cjs/internal/MutationBoundary.js.map +7 -0
- package/dist/cjs/internal/activeFibers.d.ts +12 -0
- package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
- package/dist/cjs/internal/activeFibers.js +30 -0
- package/dist/cjs/internal/activeFibers.js.map +7 -0
- package/dist/cjs/internal/issues.d.ts +28 -0
- package/dist/cjs/internal/issues.d.ts.map +1 -0
- package/dist/cjs/internal/issues.js +34 -0
- package/dist/cjs/internal/issues.js.map +7 -0
- package/dist/cjs/internal/planning.d.ts +347 -0
- package/dist/cjs/internal/planning.d.ts.map +1 -0
- package/dist/cjs/internal/planning.js +137 -0
- package/dist/cjs/internal/planning.js.map +7 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
- package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_control_tables.js +115 -0
- package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_run_keys.js +44 -0
- package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_signal_commands.js +50 -0
- package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
- package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_signal_principals.js +45 -0
- package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_run_principals.js +49 -0
- package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_resume_consent.js +46 -0
- package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestControl.d.ts +19 -0
- package/dist/cjs/test/TestControl.d.ts.map +1 -0
- package/dist/cjs/test/TestControl.js +62 -0
- package/dist/cjs/test/TestControl.js.map +7 -0
- package/dist/esm/ApprovalAuthority.d.ts +73 -0
- package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
- package/dist/esm/ApprovalAuthority.js +72 -0
- package/dist/esm/ApprovalAuthority.js.map +1 -0
- package/dist/esm/Cancellation.d.ts +107 -0
- package/dist/esm/Cancellation.d.ts.map +1 -0
- package/dist/esm/Cancellation.js +116 -0
- package/dist/esm/Cancellation.js.map +1 -0
- package/dist/esm/Channels.d.ts +170 -0
- package/dist/esm/Channels.d.ts.map +1 -0
- package/dist/esm/Channels.js +312 -0
- package/dist/esm/Channels.js.map +1 -0
- package/dist/esm/Control.d.ts +202 -0
- package/dist/esm/Control.d.ts.map +1 -0
- package/dist/esm/Control.js +42 -0
- package/dist/esm/Control.js.map +1 -0
- package/dist/esm/ControlClient.d.ts +52 -0
- package/dist/esm/ControlClient.d.ts.map +1 -0
- package/dist/esm/ControlClient.js +217 -0
- package/dist/esm/ControlClient.js.map +1 -0
- package/dist/esm/ControlError.d.ts +318 -0
- package/dist/esm/ControlError.d.ts.map +1 -0
- package/dist/esm/ControlError.js +359 -0
- package/dist/esm/ControlError.js.map +1 -0
- package/dist/esm/ControlExecutor.d.ts +372 -0
- package/dist/esm/ControlExecutor.d.ts.map +1 -0
- package/dist/esm/ControlExecutor.js +212 -0
- package/dist/esm/ControlExecutor.js.map +1 -0
- package/dist/esm/ControlFacts.d.ts +454 -0
- package/dist/esm/ControlFacts.d.ts.map +1 -0
- package/dist/esm/ControlFacts.js +324 -0
- package/dist/esm/ControlFacts.js.map +1 -0
- package/dist/esm/ControlLive.d.ts +23 -0
- package/dist/esm/ControlLive.d.ts.map +1 -0
- package/dist/esm/ControlLive.js +1585 -0
- package/dist/esm/ControlLive.js.map +1 -0
- package/dist/esm/ControlRpcs.d.ts +1204 -0
- package/dist/esm/ControlRpcs.d.ts.map +1 -0
- package/dist/esm/ControlRpcs.js +299 -0
- package/dist/esm/ControlRpcs.js.map +1 -0
- package/dist/esm/ControlRuntime.d.ts +635 -0
- package/dist/esm/ControlRuntime.d.ts.map +1 -0
- package/dist/esm/ControlRuntime.js +807 -0
- package/dist/esm/ControlRuntime.js.map +1 -0
- package/dist/esm/ControlSchema.d.ts +2642 -0
- package/dist/esm/ControlSchema.d.ts.map +1 -0
- package/dist/esm/ControlSchema.js +1030 -0
- package/dist/esm/ControlSchema.js.map +1 -0
- package/dist/esm/ControlServer.d.ts +51 -0
- package/dist/esm/ControlServer.d.ts.map +1 -0
- package/dist/esm/ControlServer.js +145 -0
- package/dist/esm/ControlServer.js.map +1 -0
- package/dist/esm/Credential.d.ts +136 -0
- package/dist/esm/Credential.d.ts.map +1 -0
- package/dist/esm/Credential.js +190 -0
- package/dist/esm/Credential.js.map +1 -0
- package/dist/esm/CredentialCipher.d.ts +90 -0
- package/dist/esm/CredentialCipher.d.ts.map +1 -0
- package/dist/esm/CredentialCipher.js +56 -0
- package/dist/esm/CredentialCipher.js.map +1 -0
- package/dist/esm/CredentialStore.d.ts +97 -0
- package/dist/esm/CredentialStore.d.ts.map +1 -0
- package/dist/esm/CredentialStore.js +101 -0
- package/dist/esm/CredentialStore.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +112 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +76 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Health.d.ts +333 -0
- package/dist/esm/Health.d.ts.map +1 -0
- package/dist/esm/Health.js +400 -0
- package/dist/esm/Health.js.map +1 -0
- package/dist/esm/JevSessionChecker.d.ts +57 -0
- package/dist/esm/JevSessionChecker.d.ts.map +1 -0
- package/dist/esm/JevSessionChecker.js +108 -0
- package/dist/esm/JevSessionChecker.js.map +1 -0
- package/dist/esm/Lineage.d.ts +131 -0
- package/dist/esm/Lineage.d.ts.map +1 -0
- package/dist/esm/Lineage.js +174 -0
- package/dist/esm/Lineage.js.map +1 -0
- package/dist/esm/Migrations.d.ts +34 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +53 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/Monitor.d.ts +282 -0
- package/dist/esm/Monitor.d.ts.map +1 -0
- package/dist/esm/Monitor.js +415 -0
- package/dist/esm/Monitor.js.map +1 -0
- package/dist/esm/ScopedToken.d.ts +193 -0
- package/dist/esm/ScopedToken.d.ts.map +1 -0
- package/dist/esm/ScopedToken.js +224 -0
- package/dist/esm/ScopedToken.js.map +1 -0
- package/dist/esm/SqlControlRuntime.d.ts +161 -0
- package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
- package/dist/esm/SqlControlRuntime.js +1756 -0
- package/dist/esm/SqlControlRuntime.js.map +1 -0
- package/dist/esm/SqlCredentialStore.d.ts +43 -0
- package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
- package/dist/esm/SqlCredentialStore.js +97 -0
- package/dist/esm/SqlCredentialStore.js.map +1 -0
- package/dist/esm/Steering.d.ts +69 -0
- package/dist/esm/Steering.d.ts.map +1 -0
- package/dist/esm/Steering.js +89 -0
- package/dist/esm/Steering.js.map +1 -0
- package/dist/esm/SystemFlows.d.ts +223 -0
- package/dist/esm/SystemFlows.d.ts.map +1 -0
- package/dist/esm/SystemFlows.js +198 -0
- package/dist/esm/SystemFlows.js.map +1 -0
- package/dist/esm/WebCryptoCipher.d.ts +49 -0
- package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
- package/dist/esm/WebCryptoCipher.js +123 -0
- package/dist/esm/WebCryptoCipher.js.map +1 -0
- package/dist/esm/WebhookChannel.d.ts +113 -0
- package/dist/esm/WebhookChannel.d.ts.map +1 -0
- package/dist/esm/WebhookChannel.js +109 -0
- package/dist/esm/WebhookChannel.js.map +1 -0
- package/dist/esm/index.d.ts +160 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +160 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/MutationBoundary.d.ts +27 -0
- package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/esm/internal/MutationBoundary.js +40 -0
- package/dist/esm/internal/MutationBoundary.js.map +1 -0
- package/dist/esm/internal/activeFibers.d.ts +12 -0
- package/dist/esm/internal/activeFibers.d.ts.map +1 -0
- package/dist/esm/internal/activeFibers.js +17 -0
- package/dist/esm/internal/activeFibers.js.map +1 -0
- package/dist/esm/internal/issues.d.ts +28 -0
- package/dist/esm/internal/issues.d.ts.map +1 -0
- package/dist/esm/internal/issues.js +35 -0
- package/dist/esm/internal/issues.js.map +1 -0
- package/dist/esm/internal/planning.d.ts +347 -0
- package/dist/esm/internal/planning.d.ts.map +1 -0
- package/dist/esm/internal/planning.js +199 -0
- package/dist/esm/internal/planning.js.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.js +38 -0
- package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
- package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
- package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/esm/migrations/0001_control_tables.js +96 -0
- package/dist/esm/migrations/0001_control_tables.js.map +1 -0
- package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
- package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/esm/migrations/0002_run_keys.js +22 -0
- package/dist/esm/migrations/0002_run_keys.js.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.js +26 -0
- package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.js +25 -0
- package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.js +27 -0
- package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
- package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
- package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0006_run_principals.js +30 -0
- package/dist/esm/migrations/0006_run_principals.js.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.js +27 -0
- package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
- package/dist/esm/test/TestControl.d.ts +19 -0
- package/dist/esm/test/TestControl.d.ts.map +1 -0
- package/dist/esm/test/TestControl.js +30 -0
- package/dist/esm/test/TestControl.js.map +1 -0
- package/docs/README.md +189 -0
- package/docs/api.md +982 -0
- package/docs/concepts/authority.md +109 -0
- package/docs/concepts/cancellation.md +129 -0
- package/docs/concepts/lineage.md +132 -0
- package/docs/concepts/ownership.md +139 -0
- package/docs/concepts/projections.md +203 -0
- package/docs/concepts/receipts.md +128 -0
- package/docs/guides/approvals.md +284 -0
- package/docs/guides/cancel-and-resume.md +162 -0
- package/docs/guides/durable-storage.md +147 -0
- package/docs/guides/implement-an-executor.md +173 -0
- package/docs/guides/ingest-a-webhook.md +177 -0
- package/docs/guides/list-runs.md +160 -0
- package/docs/guides/monitor-runs.md +176 -0
- package/docs/guides/observe-health.md +147 -0
- package/docs/guides/postgres-tests.md +5 -0
- package/docs/guides/serve-over-rpc.md +220 -0
- package/docs/guides/signal-a-run.md +53 -0
- package/docs/guides/steer-a-run.md +138 -0
- package/docs/guides/store-credentials.md +164 -0
- package/docs/guides/testing.md +139 -0
- package/docs/guides/watch-a-run.md +154 -0
- package/docs/installation.md +106 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +208 -0
- package/package.json +405 -3
- package/src/ApprovalAuthority.ts +114 -0
- package/src/Cancellation.ts +172 -0
- package/src/Channels.ts +493 -0
- package/src/Control.ts +337 -0
- package/src/ControlClient.ts +319 -0
- package/src/ControlError.ts +378 -0
- package/src/ControlExecutor.ts +490 -0
- package/src/ControlFacts.ts +383 -0
- package/src/ControlLive.ts +2113 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1597 -0
- package/src/ControlSchema.ts +1380 -0
- package/src/ControlServer.ts +182 -0
- package/src/Credential.ts +310 -0
- package/src/CredentialCipher.ts +110 -0
- package/src/CredentialStore.ts +152 -0
- package/src/DispatchReader.ts +122 -0
- package/src/Health.ts +591 -0
- package/src/JevSessionChecker.ts +127 -0
- package/src/Lineage.ts +203 -0
- package/src/Migrations.ts +56 -0
- package/src/Monitor.ts +600 -0
- package/src/ScopedToken.ts +306 -0
- package/src/SqlControlRuntime.ts +2476 -0
- package/src/SqlCredentialStore.ts +148 -0
- package/src/Steering.ts +96 -0
- package/src/SystemFlows.ts +225 -0
- package/src/WebCryptoCipher.ts +169 -0
- package/src/WebhookChannel.ts +166 -0
- package/src/index.ts +188 -0
- package/src/internal/MutationBoundary.ts +46 -0
- package/src/internal/activeFibers.ts +22 -0
- package/src/internal/issues.ts +40 -0
- package/src/internal/planning.ts +262 -0
- package/src/internal/sqlSchemaErrors.ts +37 -0
- package/src/migrations/0001_control_tables.ts +99 -0
- package/src/migrations/0002_run_keys.ts +23 -0
- package/src/migrations/0003_signal_commands.ts +27 -0
- package/src/migrations/0004_approval_decisions.ts +25 -0
- package/src/migrations/0005_signal_principals.ts +27 -0
- package/src/migrations/0006_run_principals.ts +31 -0
- package/src/migrations/0007_resume_consent.ts +28 -0
- package/src/test/TestControl.ts +47 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe port between the control plane and the execution engine, with a
|
|
3
|
+
* deterministic in-memory implementation.
|
|
4
|
+
*
|
|
5
|
+
* `layerMemory` is the deterministic one: it models the production fence and
|
|
6
|
+
* approval ordering seams but keeps everything in a `Map`, so nothing it
|
|
7
|
+
* decides survives the process. The durable adapter is
|
|
8
|
+
* {@link SqlControlRuntime}, and both are held to one shared contract suite —
|
|
9
|
+
* see `test/ControlContract.ts`.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
import * as Fault from "@smthrs/flow/Fault";
|
|
14
|
+
import { Context, Crypto, Effect, Fiber, Layer, Option, Schema } from "effect";
|
|
15
|
+
import * as ApprovalAuthority from "./ApprovalAuthority.js";
|
|
16
|
+
import { AlreadyResolved, ClaimLost, EnvelopeMismatch, FlowNotFound, InvalidInput, PersistenceError, PlanDenied, PlanDigestMismatch, PlanNotFound, RunNotFound } from "./ControlError.js";
|
|
17
|
+
import { GrantScope as GrantScopeSchema, Principal as PrincipalSchema } from "./ControlSchema.js";
|
|
18
|
+
import { canonicalIssue } from "./internal/issues.js";
|
|
19
|
+
import { accepted, adoptedCode, alreadyApplied as replayReceipt, budgeted, canonical, codeDriftOf, emptyEnvelope, planCard, planFingerprint, sameEnvelope } from "./internal/planning.js";
|
|
20
|
+
import { plannable } from "./SystemFlows.js";
|
|
21
|
+
/**
|
|
22
|
+
* Whether `run` was launched by the principal `launcher` names: the same `id`,
|
|
23
|
+
* and the same `kind` when `launcher` gives one. A run with no recorded
|
|
24
|
+
* launcher never matches.
|
|
25
|
+
*
|
|
26
|
+
* @category predicates
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
*/
|
|
29
|
+
export const launchedByMatches = (run, launcher) => run.launchedBy !== undefined && run.launchedBy.id === launcher.id &&
|
|
30
|
+
(launcher.kind === undefined || run.launchedBy.kind === launcher.kind);
|
|
31
|
+
/**
|
|
32
|
+
* The durable answer to an approval request. A decision is not inferred from
|
|
33
|
+
* the presence of a grant or from a boolean that also means denial.
|
|
34
|
+
*
|
|
35
|
+
* @category models
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
*/
|
|
38
|
+
export const ApprovalDecision = Schema.Union([
|
|
39
|
+
Schema.Struct({ _tag: Schema.Literal("Pending") }),
|
|
40
|
+
Schema.Struct({
|
|
41
|
+
_tag: Schema.Literal("Approved"),
|
|
42
|
+
decisionPrincipal: PrincipalSchema,
|
|
43
|
+
decidedAt: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)),
|
|
44
|
+
scope: GrantScopeSchema
|
|
45
|
+
}),
|
|
46
|
+
Schema.Struct({
|
|
47
|
+
_tag: Schema.Literal("Denied"),
|
|
48
|
+
decisionPrincipal: PrincipalSchema,
|
|
49
|
+
decidedAt: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))
|
|
50
|
+
})
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* A gate that still needs a decision.
|
|
54
|
+
* @category errors
|
|
55
|
+
* @since 1.0.0
|
|
56
|
+
*/
|
|
57
|
+
export class ApprovalPending extends Schema.TaggedError()("/control/ApprovalPending", {
|
|
58
|
+
tokenId: Schema.String
|
|
59
|
+
}) {
|
|
60
|
+
get message() {
|
|
61
|
+
return "Approval is still pending. Wait for a decision before proceeding.";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A gate that was denied, not merely resolved.
|
|
66
|
+
* @category errors
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
*/
|
|
69
|
+
export class ApprovalDenied extends Schema.TaggedError()("/control/ApprovalDenied", {
|
|
70
|
+
tokenId: Schema.String,
|
|
71
|
+
decisionPrincipal: PrincipalSchema
|
|
72
|
+
}) {
|
|
73
|
+
get message() {
|
|
74
|
+
return "Approval was denied. This request cannot authorize the gated work.";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Opens only an explicitly approved gate. Callers may park on ApprovalPending;
|
|
79
|
+
* ApprovalDenied is terminal. This reads a runtime-issued token, not an
|
|
80
|
+
* authentication credential, and does not itself install permissions.
|
|
81
|
+
* @category combinators
|
|
82
|
+
* @since 1.0.0
|
|
83
|
+
*/
|
|
84
|
+
export const requireApproved = (token) => token._tag === "Approved"
|
|
85
|
+
? Effect.succeed(token)
|
|
86
|
+
: token._tag === "Pending"
|
|
87
|
+
? Effect.fail(new ApprovalPending({ tokenId: token.tokenId }))
|
|
88
|
+
: Effect.fail(new ApprovalDenied({ tokenId: token.tokenId, decisionPrincipal: token.decisionPrincipal }));
|
|
89
|
+
/**
|
|
90
|
+
* Service key for the execution-engine port.
|
|
91
|
+
*
|
|
92
|
+
* @category services
|
|
93
|
+
* @since 0.1.0
|
|
94
|
+
*/
|
|
95
|
+
export class ControlRuntime extends Context.Service()("/control/ControlRuntime") {
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Constructs a runtime service from an implementation record.
|
|
99
|
+
*
|
|
100
|
+
* @category constructors
|
|
101
|
+
* @since 0.1.0
|
|
102
|
+
*/
|
|
103
|
+
export const make = (implementation) => ControlRuntime.of(implementation);
|
|
104
|
+
/**
|
|
105
|
+
* Refuses an id page size outside 1 through 500, the `queryRuns` bound.
|
|
106
|
+
*
|
|
107
|
+
* @category validation
|
|
108
|
+
* @since 1.0.0
|
|
109
|
+
*/
|
|
110
|
+
export const idPageLimit = (limit) => Number.isSafeInteger(limit) && limit >= 1 && limit <= 500
|
|
111
|
+
? Effect.void
|
|
112
|
+
: Effect.fail(new InvalidInput({ issue: "limit: must be an integer between 1 and 500" }));
|
|
113
|
+
/**
|
|
114
|
+
* One inventory page over dense insertion positions, where `idAt` resolves a
|
|
115
|
+
* position by direct lookup. Visits at most `limit` positions, so a page
|
|
116
|
+
* costs and holds one page however many entries exist.
|
|
117
|
+
*/
|
|
118
|
+
const pageByPosition = (request, newest, idAt) => {
|
|
119
|
+
const through = request.through ?? newest;
|
|
120
|
+
const ids = [];
|
|
121
|
+
let position = request.after ?? 0;
|
|
122
|
+
let visited = 0;
|
|
123
|
+
while (position < through && visited < request.limit) {
|
|
124
|
+
position += 1;
|
|
125
|
+
visited += 1;
|
|
126
|
+
const id = idAt(position);
|
|
127
|
+
if (id !== undefined)
|
|
128
|
+
ids.push(id);
|
|
129
|
+
}
|
|
130
|
+
return position < through ? { ids, next: position, through } : { ids, through };
|
|
131
|
+
};
|
|
132
|
+
// SQL persistence breaks caller reference identity through serialization. The
|
|
133
|
+
// memory adapter must copy at the same boundaries or its test results diverge
|
|
134
|
+
// from the durable implementation when a caller mutates an input or result.
|
|
135
|
+
const snapshot = (value) => structuredClone(value);
|
|
136
|
+
const asStored = (plan) => ({
|
|
137
|
+
card: snapshot(plan.card),
|
|
138
|
+
decodedInput: snapshot(plan.decodedInput),
|
|
139
|
+
decision: plan.decision
|
|
140
|
+
});
|
|
141
|
+
// JSON tuple encoding keeps caller-chosen ids in separate fields, so a node's
|
|
142
|
+
// request id cannot alias another run's request or a plan id.
|
|
143
|
+
const approvalKey = (target) => target._tag === "Plan"
|
|
144
|
+
? JSON.stringify([target._tag, target.planId])
|
|
145
|
+
: JSON.stringify([target._tag, target.runId, target.requestId]);
|
|
146
|
+
const sameApprovalIdentity = (left, right) => left._tag === "Plan"
|
|
147
|
+
? right._tag === "Plan" && left.planId === right.planId
|
|
148
|
+
: right._tag === "Node" && left.runId === right.runId && left.requestId === right.requestId;
|
|
149
|
+
const approvalToken = (token) => ({
|
|
150
|
+
tokenId: token.tokenId,
|
|
151
|
+
target: snapshot(token.target),
|
|
152
|
+
...snapshot(token.decision)
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Deterministic in-memory runtime. It models the production fence and approval
|
|
156
|
+
* ordering seams but intentionally does not claim durable process survival;
|
|
157
|
+
* see `control-runtime-engine-integration`.
|
|
158
|
+
*
|
|
159
|
+
* @category layers
|
|
160
|
+
* @since 0.1.0
|
|
161
|
+
*/
|
|
162
|
+
export const layerMemory = (options = {}) => Layer.effect(ControlRuntime, Effect.gen(function* () {
|
|
163
|
+
const crypto = yield* Crypto.Crypto;
|
|
164
|
+
const now = options.now ?? Date.now;
|
|
165
|
+
// The in-memory adapter stamps `memory`/`test` on a caller that names no
|
|
166
|
+
// principal, so its default policy delegates that identity as well as
|
|
167
|
+
// the local operator. Production policies never carry it.
|
|
168
|
+
const approvalAuthority = options.approvalAuthority ?? (yield* ApprovalAuthority.make([
|
|
169
|
+
{
|
|
170
|
+
principal: { id: "local", kind: "operator" },
|
|
171
|
+
scopes: ["once", "run", "remembered"],
|
|
172
|
+
targets: ["Plan", "Node"]
|
|
173
|
+
},
|
|
174
|
+
{ principal: { id: "memory", kind: "test" }, scopes: ["once", "run", "remembered"], targets: ["Plan", "Node"] }
|
|
175
|
+
]).pipe(Effect.orDie));
|
|
176
|
+
const authorizeApproval = approvalAuthority.authorize.bind(approvalAuthority);
|
|
177
|
+
const configuredFlows = options.flows ?? plannable.map((entry) => ({
|
|
178
|
+
flowId: entry.flowId,
|
|
179
|
+
description: `Reserved ${entry.verb} system flow`,
|
|
180
|
+
deployClass: entry.deployClass,
|
|
181
|
+
envelope: emptyEnvelope
|
|
182
|
+
}));
|
|
183
|
+
const flows = new Map(configuredFlows.map((flow) => [
|
|
184
|
+
flow.flowId,
|
|
185
|
+
{ ...flow, envelope: snapshot(flow.envelope) }
|
|
186
|
+
]));
|
|
187
|
+
const plans = new Map();
|
|
188
|
+
const planKeys = new Map();
|
|
189
|
+
const tokens = new Map();
|
|
190
|
+
const runs = new Map();
|
|
191
|
+
const mutations = new Map();
|
|
192
|
+
const runKeys = new Map();
|
|
193
|
+
const installedGrants = [];
|
|
194
|
+
const installedGrantKeys = new Set();
|
|
195
|
+
let planSequence = 0;
|
|
196
|
+
const signalCommands = new Map();
|
|
197
|
+
let pendingSignalOffset = 0;
|
|
198
|
+
let runSequence = 0;
|
|
199
|
+
let fenceSequence = 0;
|
|
200
|
+
let resumeSequence = 0;
|
|
201
|
+
const requireRun = (runId) => Effect.fromOption(Option.fromNullishOr(runs.get(runId)), () => new RunNotFound({ runId }));
|
|
202
|
+
const updateSummary = (run, fields) => {
|
|
203
|
+
run.summary = snapshot({ ...run.summary, ...fields, updatedAt: now() });
|
|
204
|
+
return snapshot(run.summary);
|
|
205
|
+
};
|
|
206
|
+
const checkFence = (runId, run, fence) => run.fence === undefined || run.fence !== fence
|
|
207
|
+
? Effect.fail(new ClaimLost({ runId }))
|
|
208
|
+
: Effect.void;
|
|
209
|
+
/** `resume`, recording the identity `adopt` answers on the claimed run when given. */
|
|
210
|
+
const resumeRun = (runId, adopt) => Effect.gen(function* () {
|
|
211
|
+
const run = yield* requireRun(runId);
|
|
212
|
+
if (run.summary.status === "cancelled" ||
|
|
213
|
+
run.summary.status === "completed" ||
|
|
214
|
+
run.summary.status === "failed")
|
|
215
|
+
return snapshot(run.summary);
|
|
216
|
+
// Accepted claims are owned too; releasePending clears both fences.
|
|
217
|
+
if (run.fence !== undefined) {
|
|
218
|
+
/* v8 ignore next 3 -- one process holds this whole runtime, and it writes `fence` and `localFence` together; the peer this refuses exists only over a shared database, which is `SqlControlRuntime`'s fence */
|
|
219
|
+
if (run.localFence === undefined || run.fence !== run.localFence) {
|
|
220
|
+
return yield* new ClaimLost({ runId });
|
|
221
|
+
}
|
|
222
|
+
return snapshot(run.summary);
|
|
223
|
+
}
|
|
224
|
+
const adopted = adopt === undefined ? undefined : yield* adopt(run);
|
|
225
|
+
const fence = `fence-${++fenceSequence}`;
|
|
226
|
+
run.fence = fence;
|
|
227
|
+
run.localFence = fence;
|
|
228
|
+
// Claiming ends the park, so it ends the record of who wrote it.
|
|
229
|
+
return updateSummary(run, {
|
|
230
|
+
...adopted,
|
|
231
|
+
status: "accepted",
|
|
232
|
+
ownerId: "memory-owner",
|
|
233
|
+
parkedBy: undefined
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
const service = make({
|
|
237
|
+
authorizeApproval,
|
|
238
|
+
plan: Effect.fn("ControlRuntime.plan")(function* (input) {
|
|
239
|
+
const flow = flows.get(input.flowId);
|
|
240
|
+
if (flow === undefined)
|
|
241
|
+
return yield* new FlowNotFound({ flowId: input.flowId });
|
|
242
|
+
const requestFingerprint = yield* Effect.try({
|
|
243
|
+
// Validate before cloning. Canonicalization reports a throwing
|
|
244
|
+
// getter at its stable path, while `structuredClone` would invoke
|
|
245
|
+
// the getter first and erase that safe diagnostic.
|
|
246
|
+
try: () => planFingerprint(input),
|
|
247
|
+
catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
|
|
248
|
+
});
|
|
249
|
+
const submitted = yield* Effect.try({
|
|
250
|
+
try: () => snapshot(input),
|
|
251
|
+
catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
|
|
252
|
+
});
|
|
253
|
+
if (submitted.idempotencyKey !== undefined) {
|
|
254
|
+
const prior = planKeys.get(submitted.idempotencyKey);
|
|
255
|
+
if (prior !== undefined) {
|
|
256
|
+
if (prior.fingerprint !== requestFingerprint) {
|
|
257
|
+
return yield* new InvalidInput({
|
|
258
|
+
issue: `idempotency key ${submitted.idempotencyKey} was used for another plan`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const stored = plans.get(prior.planId);
|
|
262
|
+
/* v8 ignore next -- this map never loses a plan a key names; `SqlControlRuntime` covers the storage that can */
|
|
263
|
+
if (stored !== undefined)
|
|
264
|
+
return { card: snapshot(stored.card), created: false };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const decoded = yield* (flow.decode?.(submitted.input) ?? Effect.try({
|
|
268
|
+
try: () => {
|
|
269
|
+
canonical(submitted.input);
|
|
270
|
+
return submitted.input;
|
|
271
|
+
},
|
|
272
|
+
catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
|
|
273
|
+
}));
|
|
274
|
+
const planId = `plan-${++planSequence}`;
|
|
275
|
+
const handoff = flow.plan === undefined ? undefined : yield* flow.plan(decoded, planId);
|
|
276
|
+
const card = snapshot(yield* planCard({
|
|
277
|
+
planId,
|
|
278
|
+
flowId: submitted.flowId,
|
|
279
|
+
decodedInput: decoded,
|
|
280
|
+
envelope: budgeted(flow.envelope, submitted.budget),
|
|
281
|
+
deployClass: flow.deployClass,
|
|
282
|
+
executionDigest: flow.executionDigest,
|
|
283
|
+
handoff,
|
|
284
|
+
idempotencyKey: submitted.idempotencyKey
|
|
285
|
+
}).pipe(Effect.provideService(Crypto.Crypto, crypto)));
|
|
286
|
+
// Decoding, planning and hashing may yield to another keyed plan.
|
|
287
|
+
// Recheck immediately before publication, with no yield between a
|
|
288
|
+
// successful check and the three map writes below.
|
|
289
|
+
if (submitted.idempotencyKey !== undefined) {
|
|
290
|
+
const prior = planKeys.get(submitted.idempotencyKey);
|
|
291
|
+
if (prior !== undefined) {
|
|
292
|
+
if (prior.fingerprint !== requestFingerprint) {
|
|
293
|
+
return yield* new InvalidInput({
|
|
294
|
+
issue: `idempotency key ${submitted.idempotencyKey} was used for another plan`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const stored = plans.get(prior.planId);
|
|
298
|
+
/* v8 ignore next -- this map never loses a plan a key names; `SqlControlRuntime` covers the storage that can */
|
|
299
|
+
if (stored !== undefined)
|
|
300
|
+
return { card: snapshot(stored.card), created: false };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
plans.set(planId, { card, decodedInput: snapshot(decoded), decision: "pending" });
|
|
304
|
+
tokens.set(approvalKey(card.approval.target), {
|
|
305
|
+
tokenId: planId,
|
|
306
|
+
target: snapshot(card.approval.target),
|
|
307
|
+
decision: { _tag: "Pending" }
|
|
308
|
+
});
|
|
309
|
+
if (submitted.idempotencyKey !== undefined) {
|
|
310
|
+
planKeys.set(submitted.idempotencyKey, {
|
|
311
|
+
fingerprint: requestFingerprint,
|
|
312
|
+
planId
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return { card: snapshot(card), created: true };
|
|
316
|
+
}),
|
|
317
|
+
getPlan: Effect.fn("ControlRuntime.getPlan")((planId) => Effect.fromOption(Option.fromNullishOr(plans.get(planId)), () => new PlanNotFound({ planId })).pipe(Effect.map(asStored))),
|
|
318
|
+
pagePlanIds: Effect.fn("ControlRuntime.pagePlanIds")(function* (request) {
|
|
319
|
+
yield* idPageLimit(request.limit);
|
|
320
|
+
return pageByPosition(request, planSequence, (position) => plans.has(`plan-${position}`) ? `plan-${position}` : undefined);
|
|
321
|
+
}),
|
|
322
|
+
queryPlans: Effect.fn("ControlRuntime.queryPlans")(function* (request) {
|
|
323
|
+
yield* idPageLimit(request.limit);
|
|
324
|
+
const matched = [];
|
|
325
|
+
let position = request.after ?? 0;
|
|
326
|
+
while (position < planSequence && matched.length < request.limit) {
|
|
327
|
+
position += 1;
|
|
328
|
+
const plan = plans.get(`plan-${position}`);
|
|
329
|
+
if (plan !== undefined &&
|
|
330
|
+
(request.flowId === undefined || plan.card.flowId === request.flowId) &&
|
|
331
|
+
(request.decision === undefined || plan.decision === request.decision))
|
|
332
|
+
matched.push(asStored(plan));
|
|
333
|
+
}
|
|
334
|
+
return position < planSequence ? { plans: matched, next: position } : { plans: matched };
|
|
335
|
+
}),
|
|
336
|
+
lookupApproval: Effect.fn("ControlRuntime.lookupApproval")(function* (target) {
|
|
337
|
+
const requested = snapshot(target);
|
|
338
|
+
const tokenId = requested._tag === "Plan" ? requested.planId : requested.requestId;
|
|
339
|
+
const token = yield* Effect.fromOption(Option.fromNullishOr(tokens.get(approvalKey(requested))), () => requested._tag === "Node"
|
|
340
|
+
? new RunNotFound({ runId: requested.runId })
|
|
341
|
+
: new PlanNotFound({ planId: requested.planId }));
|
|
342
|
+
// A stored target that disagrees with its composite key is corrupted;
|
|
343
|
+
// accepting only its digest and envelope would recreate the alias the
|
|
344
|
+
// composite identity closes. In memory the key is DERIVED from the
|
|
345
|
+
// identity, so only a caller reaching into the map can produce the
|
|
346
|
+
// disagreement; `SqlControlRuntime` stores the two apart and covers
|
|
347
|
+
// the same refusal against a rewritten row.
|
|
348
|
+
/* v8 ignore next 6 -- unreachable while the map key is derived from the identity it is compared against */
|
|
349
|
+
if (!sameApprovalIdentity(token.target, requested)) {
|
|
350
|
+
return yield* new PersistenceError({
|
|
351
|
+
operation: "validate an approval token",
|
|
352
|
+
message: "The stored approval target does not match its identity"
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (token.target.digest !== requested.digest) {
|
|
356
|
+
return yield* new PlanDigestMismatch({
|
|
357
|
+
planId: tokenId,
|
|
358
|
+
expected: token.target.digest,
|
|
359
|
+
actual: requested.digest
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (!sameEnvelope(token.target.envelope, requested.envelope)) {
|
|
363
|
+
return yield* new EnvelopeMismatch({
|
|
364
|
+
planId: tokenId,
|
|
365
|
+
expected: canonical(token.target.envelope),
|
|
366
|
+
actual: canonical(requested.envelope)
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (token.decision._tag !== "Pending")
|
|
370
|
+
return yield* new AlreadyResolved({ requestId: tokenId });
|
|
371
|
+
return approvalToken(token);
|
|
372
|
+
}),
|
|
373
|
+
registerApproval: Effect.fn("ControlRuntime.registerApproval")(function* (target) {
|
|
374
|
+
const requested = snapshot(target);
|
|
375
|
+
yield* requireRun(requested.runId);
|
|
376
|
+
const key = approvalKey(requested);
|
|
377
|
+
const existing = tokens.get(key);
|
|
378
|
+
if (existing === undefined) {
|
|
379
|
+
const stored = {
|
|
380
|
+
tokenId: requested.requestId,
|
|
381
|
+
target: requested,
|
|
382
|
+
decision: { _tag: "Pending" }
|
|
383
|
+
};
|
|
384
|
+
tokens.set(key, stored);
|
|
385
|
+
return approvalToken(stored);
|
|
386
|
+
}
|
|
387
|
+
/* v8 ignore next 6 -- as in `lookupApproval`: the map key is derived from the identity, so a stored target can only disagree with it after a reach into the map */
|
|
388
|
+
if (!sameApprovalIdentity(existing.target, requested)) {
|
|
389
|
+
return yield* new PersistenceError({
|
|
390
|
+
operation: "validate an approval token",
|
|
391
|
+
message: "The stored approval target does not match its identity"
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (existing.target.digest !== requested.digest) {
|
|
395
|
+
return yield* new PlanDigestMismatch({
|
|
396
|
+
planId: requested.requestId,
|
|
397
|
+
expected: existing.target.digest,
|
|
398
|
+
actual: requested.digest
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (!sameEnvelope(existing.target.envelope, requested.envelope)) {
|
|
402
|
+
return yield* new EnvelopeMismatch({
|
|
403
|
+
planId: requested.requestId,
|
|
404
|
+
expected: canonical(existing.target.envelope),
|
|
405
|
+
actual: canonical(requested.envelope)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return approvalToken(existing);
|
|
409
|
+
}),
|
|
410
|
+
installBulkGrant: Effect.fn("ControlRuntime.installBulkGrant")((token, envelope, scope) => Effect.sync(() => {
|
|
411
|
+
const storedToken = snapshot(token);
|
|
412
|
+
const key = approvalKey(storedToken.target);
|
|
413
|
+
if (installedGrantKeys.has(key))
|
|
414
|
+
return;
|
|
415
|
+
installedGrantKeys.add(key);
|
|
416
|
+
installedGrants.push({
|
|
417
|
+
tokenId: storedToken.tokenId,
|
|
418
|
+
envelope: snapshot(envelope),
|
|
419
|
+
scope,
|
|
420
|
+
installedAt: now()
|
|
421
|
+
});
|
|
422
|
+
})),
|
|
423
|
+
resolveApproval: Effect.fn("ControlRuntime.resolveApproval")(function* (token, decision, principal, scope = "once") {
|
|
424
|
+
const requested = snapshot(token);
|
|
425
|
+
const requestedPrincipal = snapshot(principal);
|
|
426
|
+
const answer = yield* Schema.decodeUnknownEffect(ApprovalDecision)(decision === "approved"
|
|
427
|
+
? { _tag: "Approved", decisionPrincipal: requestedPrincipal, decidedAt: now(), scope }
|
|
428
|
+
: { _tag: "Denied", decisionPrincipal: requestedPrincipal, decidedAt: now() }).pipe(Effect.mapError(() => new PersistenceError({
|
|
429
|
+
operation: "record an approval decision",
|
|
430
|
+
message: "The approval decision metadata is invalid"
|
|
431
|
+
})));
|
|
432
|
+
yield* authorizeApproval({ principal: requestedPrincipal, target: requested.target, decision, scope });
|
|
433
|
+
const mutable = tokens.get(approvalKey(requested.target));
|
|
434
|
+
if (mutable === undefined || mutable.decision._tag !== "Pending") {
|
|
435
|
+
return yield* new AlreadyResolved({ requestId: requested.tokenId });
|
|
436
|
+
}
|
|
437
|
+
mutable.decision = answer;
|
|
438
|
+
if (requested.target._tag === "Plan") {
|
|
439
|
+
const plan = plans.get(requested.target.planId);
|
|
440
|
+
/* v8 ignore next -- a plan token is only ever registered by `plan`, which stores the plan in the same call */
|
|
441
|
+
if (plan !== undefined)
|
|
442
|
+
plan.decision = decision;
|
|
443
|
+
}
|
|
444
|
+
}),
|
|
445
|
+
launch: Effect.fn("ControlRuntime.launch")(function* (planId, requestedDigest, envelope, principal, reservedRunId) {
|
|
446
|
+
const plan = yield* Effect.fromOption(Option.fromNullishOr(plans.get(planId)), () => new PlanNotFound({ planId }));
|
|
447
|
+
if (plan.card.digest !== requestedDigest) {
|
|
448
|
+
return yield* new PlanDigestMismatch({
|
|
449
|
+
planId,
|
|
450
|
+
expected: plan.card.digest,
|
|
451
|
+
actual: requestedDigest
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (!sameEnvelope(plan.card.envelope, envelope)) {
|
|
455
|
+
return yield* new EnvelopeMismatch({
|
|
456
|
+
planId,
|
|
457
|
+
expected: canonical(plan.card.envelope),
|
|
458
|
+
actual: canonical(envelope)
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (plan.decision === "pending") {
|
|
462
|
+
return {
|
|
463
|
+
_tag: "Parked",
|
|
464
|
+
receipt: {
|
|
465
|
+
_tag: "Parked",
|
|
466
|
+
receiptId: `launch:${planId}`,
|
|
467
|
+
planId,
|
|
468
|
+
status: "waiting-approval"
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (plan.decision !== "approved") {
|
|
473
|
+
return yield* new PlanDenied({ planId });
|
|
474
|
+
}
|
|
475
|
+
const sequence = ++runSequence;
|
|
476
|
+
const runId = reservedRunId ?? `run-${sequence}`;
|
|
477
|
+
if (runs.has(runId))
|
|
478
|
+
return yield* new PersistenceError({ operation: "reserve a run", message: "Run identity already exists" });
|
|
479
|
+
const fence = `fence-${++fenceSequence}`;
|
|
480
|
+
const timestamp = now();
|
|
481
|
+
const summary = {
|
|
482
|
+
runId,
|
|
483
|
+
flowId: plan.card.flowId,
|
|
484
|
+
status: "accepted",
|
|
485
|
+
planId,
|
|
486
|
+
planDigest: plan.card.digest,
|
|
487
|
+
...(plan.card.executionDigest === undefined ? {} : { executionDigest: plan.card.executionDigest }),
|
|
488
|
+
...(options.engineVersion === undefined ? {} : { engineVersion: options.engineVersion }),
|
|
489
|
+
ownerId: "memory-owner",
|
|
490
|
+
...(principal === undefined ? {} : { launchedBy: { id: principal.id, kind: principal.kind } }),
|
|
491
|
+
...(plan.card.envelope.budget.deadline === undefined
|
|
492
|
+
? {}
|
|
493
|
+
: { deadlineAt: timestamp + plan.card.envelope.budget.deadline }),
|
|
494
|
+
createdAt: timestamp,
|
|
495
|
+
updatedAt: timestamp
|
|
496
|
+
};
|
|
497
|
+
runs.set(runId, {
|
|
498
|
+
summary: snapshot(summary),
|
|
499
|
+
fence,
|
|
500
|
+
localFence: fence,
|
|
501
|
+
sequence: runSequence,
|
|
502
|
+
signals: []
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
_tag: "Started",
|
|
506
|
+
receipt: accepted(`launch:${planId}:${runId}`, runId),
|
|
507
|
+
run: snapshot(summary)
|
|
508
|
+
};
|
|
509
|
+
}),
|
|
510
|
+
getRun: Effect.fn("ControlRuntime.getRun")((runId) => Effect.map(requireRun(runId), (run) => snapshot(run.summary))),
|
|
511
|
+
pageRunIds: Effect.fn("ControlRuntime.pageRunIds")(function* (request) {
|
|
512
|
+
yield* idPageLimit(request.limit);
|
|
513
|
+
return pageByPosition(request, runSequence, (position) => runs.has(`run-${position}`) ? `run-${position}` : undefined);
|
|
514
|
+
}),
|
|
515
|
+
queryRuns: Effect.fn("ControlRuntime.queryRuns")(function* (request) {
|
|
516
|
+
if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 500) {
|
|
517
|
+
return yield* new InvalidInput({ issue: "limit: must be an integer between 1 and 500" });
|
|
518
|
+
}
|
|
519
|
+
const selected = [];
|
|
520
|
+
const filters = request.filters;
|
|
521
|
+
const newest = request.order === "newest";
|
|
522
|
+
const oldest = request.order === "oldest";
|
|
523
|
+
const candidates = newest
|
|
524
|
+
? Array.from(runs.values()).sort((a, b) => b.summary.createdAt - a.summary.createdAt || b.sequence - a.sequence)
|
|
525
|
+
: oldest
|
|
526
|
+
? Array.from(runs.values()).sort((a, b) => a.summary.createdAt - b.summary.createdAt || a.sequence - b.sequence)
|
|
527
|
+
: runs.values();
|
|
528
|
+
for (const run of candidates) {
|
|
529
|
+
const after = request.cursor;
|
|
530
|
+
if (after !== undefined && (after.source !== 0 || (newest
|
|
531
|
+
? run.summary.createdAt > after.createdAt ||
|
|
532
|
+
(run.summary.createdAt === after.createdAt && run.sequence >= after.sequence)
|
|
533
|
+
: oldest
|
|
534
|
+
? run.summary.createdAt < after.createdAt ||
|
|
535
|
+
(run.summary.createdAt === after.createdAt && run.sequence <= after.sequence)
|
|
536
|
+
: run.sequence <= after.sequence)))
|
|
537
|
+
continue;
|
|
538
|
+
const summary = run.summary;
|
|
539
|
+
if (filters?.since !== undefined && summary.createdAt < filters.since)
|
|
540
|
+
continue;
|
|
541
|
+
if (filters?.until !== undefined && summary.createdAt >= filters.until)
|
|
542
|
+
continue;
|
|
543
|
+
if (filters?.runIds !== undefined && !filters.runIds.includes(summary.runId))
|
|
544
|
+
continue;
|
|
545
|
+
if (filters?.flowId !== undefined && summary.flowId !== filters.flowId)
|
|
546
|
+
continue;
|
|
547
|
+
if (filters?.status !== undefined && summary.status !== filters.status)
|
|
548
|
+
continue;
|
|
549
|
+
if (filters?.terminal !== undefined &&
|
|
550
|
+
["completed", "failed", "cancelled"].includes(summary.status) !== filters.terminal)
|
|
551
|
+
continue;
|
|
552
|
+
if (filters?.parentRunId !== undefined && summary.parentRunId !== filters.parentRunId)
|
|
553
|
+
continue;
|
|
554
|
+
if (filters?.lineageId !== undefined && summary.lineageId !== filters.lineageId)
|
|
555
|
+
continue;
|
|
556
|
+
if (filters?.launchedBy !== undefined && !launchedByMatches(summary, filters.launchedBy))
|
|
557
|
+
continue;
|
|
558
|
+
selected.push(run);
|
|
559
|
+
if (selected.length > request.limit)
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
const page = selected.slice(0, request.limit);
|
|
563
|
+
const last = page.at(-1);
|
|
564
|
+
return {
|
|
565
|
+
items: page.map((run) => snapshot(run.summary)),
|
|
566
|
+
...(selected.length <= request.limit || last === undefined ? {} : {
|
|
567
|
+
nextCursor: {
|
|
568
|
+
source: 0,
|
|
569
|
+
sequence: last.sequence,
|
|
570
|
+
createdAt: last.summary.createdAt,
|
|
571
|
+
runId: last.summary.runId
|
|
572
|
+
}
|
|
573
|
+
})
|
|
574
|
+
};
|
|
575
|
+
}),
|
|
576
|
+
listFlows: Effect.fn("ControlRuntime.listFlows")(() => Effect.sync(() => Array.from(flows.values(), (flow) => ({
|
|
577
|
+
flowId: flow.flowId,
|
|
578
|
+
description: flow.description
|
|
579
|
+
}))))(),
|
|
580
|
+
deliverSignal: Effect.fn("ControlRuntime.deliverSignal")((runId, signal) => Effect.tap(requireRun(runId), (run) => Effect.sync(() => void run.signals.push(snapshot(signal))))),
|
|
581
|
+
admitSignal: Effect.fn("ControlRuntime.admitSignal")(function* (commandId, runId, signal, principal) {
|
|
582
|
+
yield* requireRun(runId);
|
|
583
|
+
if (!signalCommands.has(commandId)) {
|
|
584
|
+
signalCommands.set(commandId, snapshot({
|
|
585
|
+
commandId,
|
|
586
|
+
runId,
|
|
587
|
+
signal,
|
|
588
|
+
token: null,
|
|
589
|
+
state: "pending",
|
|
590
|
+
...(principal === undefined ? {} : { principal })
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
}),
|
|
594
|
+
signalCommand: (commandId) => Effect.sync(() => snapshot(signalCommands.get(commandId))),
|
|
595
|
+
pendingSignals: Effect.sync(() => {
|
|
596
|
+
const pending = Array.from(signalCommands.values()).filter((command) => command.state === "pending");
|
|
597
|
+
if (pendingSignalOffset >= pending.length)
|
|
598
|
+
pendingSignalOffset = 0;
|
|
599
|
+
const page = pending.slice(pendingSignalOffset, pendingSignalOffset + 100);
|
|
600
|
+
pendingSignalOffset += page.length;
|
|
601
|
+
return snapshot(page);
|
|
602
|
+
}),
|
|
603
|
+
bindSignal: (commandId, token) => Effect.gen(function* () {
|
|
604
|
+
const command = signalCommands.get(commandId);
|
|
605
|
+
if (command === undefined) {
|
|
606
|
+
return yield* new PersistenceError({
|
|
607
|
+
operation: "bind signal",
|
|
608
|
+
message: `No pending signal command ${commandId}`
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (command.state !== "pending")
|
|
612
|
+
return command.token;
|
|
613
|
+
if (command.token === null &&
|
|
614
|
+
Array.from(signalCommands.values()).some((other) => other.commandId !== commandId && other.token === token))
|
|
615
|
+
return null;
|
|
616
|
+
const bound = command.token ?? token;
|
|
617
|
+
signalCommands.set(commandId, { ...command, token: bound });
|
|
618
|
+
return bound;
|
|
619
|
+
}),
|
|
620
|
+
settleSignal: (commandId, state) => Effect.sync(() => {
|
|
621
|
+
const command = signalCommands.get(commandId);
|
|
622
|
+
if (command !== undefined && command.state === "pending") {
|
|
623
|
+
signalCommands.set(commandId, { ...command, state });
|
|
624
|
+
}
|
|
625
|
+
}),
|
|
626
|
+
deliveredSignals: Effect.fn("ControlRuntime.deliveredSignals")((runId) => Effect.map(requireRun(runId), (run) => snapshot([
|
|
627
|
+
...run.signals,
|
|
628
|
+
...Array.from(signalCommands.values()).filter((command) => command.runId === runId && command.state !== "rejected").map((command) => command.signal)
|
|
629
|
+
]))),
|
|
630
|
+
requestResume: Effect.fn("ControlRuntime.requestResume")(function* (runId, options) {
|
|
631
|
+
const run = yield* requireRun(runId);
|
|
632
|
+
if (run.summary.status === "cancelled" ||
|
|
633
|
+
run.summary.status === "completed" ||
|
|
634
|
+
run.summary.status === "failed") {
|
|
635
|
+
return yield* new InvalidInput({
|
|
636
|
+
issue: `run ${runId} is ${run.summary.status} and cannot take a resume`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const sequence = ++resumeSequence;
|
|
640
|
+
run.pendingResume = sequence;
|
|
641
|
+
run.pendingResumeAtMs = now();
|
|
642
|
+
run.pendingConsent = options?.consent ?? run.pendingConsent;
|
|
643
|
+
updateSummary(run, { pendingResume: sequence });
|
|
644
|
+
return sequence;
|
|
645
|
+
}),
|
|
646
|
+
pendingResumes: Effect.sync(() => Array.from(runs.entries()).flatMap(([runId, run]) => run.pendingResume === undefined || run.pendingResumeAtMs === undefined ||
|
|
647
|
+
run.summary.status === "cancelled" ||
|
|
648
|
+
run.summary.status === "completed" || run.summary.status === "failed"
|
|
649
|
+
? []
|
|
650
|
+
: [{
|
|
651
|
+
runId,
|
|
652
|
+
sequence: run.pendingResume,
|
|
653
|
+
requestedAtMs: run.pendingResumeAtMs,
|
|
654
|
+
...(run.pendingConsent === undefined ? {} : { consent: run.pendingConsent })
|
|
655
|
+
}])),
|
|
656
|
+
clearResume: Effect.fn("ControlRuntime.clearResume")((runId, sequence) => Effect.sync(() => {
|
|
657
|
+
const run = runs.get(runId);
|
|
658
|
+
if (run === undefined || run.pendingResume !== sequence)
|
|
659
|
+
return;
|
|
660
|
+
run.pendingResume = undefined;
|
|
661
|
+
run.pendingResumeAtMs = undefined;
|
|
662
|
+
run.pendingConsent = undefined;
|
|
663
|
+
updateSummary(run, { pendingResume: undefined });
|
|
664
|
+
})),
|
|
665
|
+
registerFiber: Effect.fn("ControlRuntime.registerFiber")((runId, fiber) => Effect.tap(requireRun(runId), (run) => Effect.sync(() => {
|
|
666
|
+
run.fiber = fiber;
|
|
667
|
+
fiber.addObserver(() => {
|
|
668
|
+
if (run.fiber === fiber)
|
|
669
|
+
run.fiber = undefined;
|
|
670
|
+
});
|
|
671
|
+
}))),
|
|
672
|
+
interrupt: Effect.fn("ControlRuntime.interrupt")(function* (runId, settle = (effect) => effect) {
|
|
673
|
+
const run = yield* requireRun(runId);
|
|
674
|
+
// Terminal first, as `resume` does: a settled run released its fence,
|
|
675
|
+
// and answering `ClaimLost` there would name the wrong problem.
|
|
676
|
+
if (run.summary.status === "cancelled" ||
|
|
677
|
+
run.summary.status === "completed" ||
|
|
678
|
+
run.summary.status === "failed")
|
|
679
|
+
return snapshot(run.summary);
|
|
680
|
+
if (run.localFence === undefined)
|
|
681
|
+
return yield* new ClaimLost({ runId });
|
|
682
|
+
const fence = run.localFence;
|
|
683
|
+
yield* checkFence(runId, run, fence);
|
|
684
|
+
if (run.fiber !== undefined)
|
|
685
|
+
yield* Fiber.interrupt(run.fiber);
|
|
686
|
+
return yield* settle(Effect.gen(function* () {
|
|
687
|
+
// Cleanup may settle the run or release and replace its owner.
|
|
688
|
+
if (run.summary.status === "cancelled" ||
|
|
689
|
+
run.summary.status === "completed" ||
|
|
690
|
+
run.summary.status === "failed")
|
|
691
|
+
return snapshot(run.summary);
|
|
692
|
+
yield* checkFence(runId, run, fence);
|
|
693
|
+
run.fence = undefined;
|
|
694
|
+
run.localFence = undefined;
|
|
695
|
+
return updateSummary(run, { status: "cancelled", ownerId: undefined, parkedBy: undefined });
|
|
696
|
+
}));
|
|
697
|
+
}),
|
|
698
|
+
codeDrift: Effect.fn("ControlRuntime.codeDrift")(function* (runId) {
|
|
699
|
+
const run = yield* requireRun(runId);
|
|
700
|
+
return codeDriftOf(run.summary, flows.get(run.summary.flowId), options.engineVersion);
|
|
701
|
+
}),
|
|
702
|
+
recordedCode: Effect.fn("ControlRuntime.recordedCode")(function* (runId) {
|
|
703
|
+
const { executionDigest, engineVersion } = (yield* requireRun(runId)).summary;
|
|
704
|
+
return { executionDigest, engineVersion };
|
|
705
|
+
}),
|
|
706
|
+
resume: Effect.fn("ControlRuntime.resume")((runId) => resumeRun(runId)),
|
|
707
|
+
// `plan` refuses a flow this catalog does not hold, and the catalog is
|
|
708
|
+
// fixed at construction, so a run's flow is never gone here and there
|
|
709
|
+
// is always code to adopt. `SqlControlRuntime` reads a catalog that can
|
|
710
|
+
// lose a flow, and refuses that case before the claim.
|
|
711
|
+
resumeAdopting: Effect.fn("ControlRuntime.resumeAdopting")((runId) => resumeRun(runId, (run) => Effect.succeed(adoptedCode(run.summary, flows.get(run.summary.flowId), options.engineVersion)))),
|
|
712
|
+
claimFence: Effect.fn("ControlRuntime.claimFence")(function* (runId) {
|
|
713
|
+
const run = yield* requireRun(runId);
|
|
714
|
+
if (run.localFence === undefined)
|
|
715
|
+
return yield* new ClaimLost({ runId });
|
|
716
|
+
return run.localFence;
|
|
717
|
+
}),
|
|
718
|
+
releasePending: Effect.fn("ControlRuntime.releasePending")(function* (runId, fence) {
|
|
719
|
+
const run = yield* requireRun(runId);
|
|
720
|
+
yield* checkFence(runId, run, fence);
|
|
721
|
+
run.fence = undefined;
|
|
722
|
+
run.localFence = undefined;
|
|
723
|
+
return updateSummary(run, {
|
|
724
|
+
status: "accepted",
|
|
725
|
+
ownerId: undefined,
|
|
726
|
+
parkedBy: undefined
|
|
727
|
+
});
|
|
728
|
+
}),
|
|
729
|
+
writeStatus: Effect.fn("ControlRuntime.writeStatus")(function* (runId, fence, status) {
|
|
730
|
+
const run = yield* requireRun(runId);
|
|
731
|
+
yield* checkFence(runId, run, fence);
|
|
732
|
+
// Ownership is released by any status that is not being driven, so the
|
|
733
|
+
// fence that wrote a terminal or parked status is spent by writing it.
|
|
734
|
+
if (status === "accepted" || status === "running") {
|
|
735
|
+
return updateSummary(run, { status, parkedBy: undefined });
|
|
736
|
+
}
|
|
737
|
+
run.fence = undefined;
|
|
738
|
+
run.localFence = undefined;
|
|
739
|
+
// The spent fence is kept on a park, and only on a park: it is the
|
|
740
|
+
// only thing left on the row that says which host parked it.
|
|
741
|
+
return updateSummary(run, {
|
|
742
|
+
status,
|
|
743
|
+
ownerId: undefined,
|
|
744
|
+
parkedBy: status === "parked" || status === "waiting-approval" ? fence : undefined
|
|
745
|
+
});
|
|
746
|
+
}),
|
|
747
|
+
// Precedence matches `SqlControlRuntime`: the submitted identity is
|
|
748
|
+
// the one a server authenticated, and the configured one is this
|
|
749
|
+
// composition's fallback for a caller that named none.
|
|
750
|
+
stampPrincipal: Effect.fn("ControlRuntime.stampPrincipal")((submitted) => Effect.sync(() => ({
|
|
751
|
+
id: submitted?.id ?? options.principal?.id ?? "memory",
|
|
752
|
+
kind: submitted?.kind ?? options.principal?.kind ?? "test",
|
|
753
|
+
stampedAt: now()
|
|
754
|
+
}))),
|
|
755
|
+
lookupMutation: Effect.fn("ControlRuntime.lookupMutation")((key, fingerprint) => Effect.sync(() => {
|
|
756
|
+
const record = mutations.get(key);
|
|
757
|
+
if (record === undefined)
|
|
758
|
+
return undefined;
|
|
759
|
+
return record.fingerprint === fingerprint
|
|
760
|
+
? replayReceipt(key, record.receipt)
|
|
761
|
+
: { _tag: "Conflict", message: `idempotency key ${key} was used for another mutation` };
|
|
762
|
+
})),
|
|
763
|
+
recordMutation: Effect.fn("ControlRuntime.recordMutation")((key, fingerprint, receipt) => Effect.gen(function* () {
|
|
764
|
+
const prior = mutations.get(key);
|
|
765
|
+
if (prior !== undefined &&
|
|
766
|
+
(prior.fingerprint !== fingerprint || canonical(prior.receipt) !== canonical(receipt))) {
|
|
767
|
+
return yield* Effect.fail(new PersistenceError({
|
|
768
|
+
operation: "record a mutation",
|
|
769
|
+
message: `Idempotency key ${key} was already settled by another mutation`
|
|
770
|
+
}));
|
|
771
|
+
}
|
|
772
|
+
mutations.set(key, { fingerprint, receipt: snapshot(receipt) });
|
|
773
|
+
})),
|
|
774
|
+
claimRunKey: Effect.fn("ControlRuntime.claimRunKey")(function* (key, fingerprint) {
|
|
775
|
+
const holder = runKeys.get(key);
|
|
776
|
+
if (holder !== undefined) {
|
|
777
|
+
if (holder.fingerprint !== fingerprint) {
|
|
778
|
+
return yield* new InvalidInput({
|
|
779
|
+
issue: `idempotency key ${key} was used for another run`
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
const record = mutations.get(key);
|
|
783
|
+
if (record === undefined) {
|
|
784
|
+
// One process owns this runtime, so reaching here takes a caller
|
|
785
|
+
// that claimed and never recorded: the cross-process winner this
|
|
786
|
+
// branch really models commits both in one transaction, which is
|
|
787
|
+
// `SqlControlRuntime`'s seam.
|
|
788
|
+
return yield* new PersistenceError({
|
|
789
|
+
operation: "read a mutation",
|
|
790
|
+
message: `run key ${key} was claimed by a mutation that recorded no receipt`
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return { _tag: "Raced", receipt: snapshot(record.receipt) };
|
|
794
|
+
}
|
|
795
|
+
runKeys.set(key, { fingerprint });
|
|
796
|
+
return { _tag: "Claimed" };
|
|
797
|
+
}),
|
|
798
|
+
releaseRunKey: Effect.fn("ControlRuntime.releaseRunKey")((key) => Effect.sync(() => {
|
|
799
|
+
runKeys.delete(key);
|
|
800
|
+
})),
|
|
801
|
+
grants: Effect.fn("ControlRuntime.grants")(() => Effect.sync(() => snapshot(installedGrants)))()
|
|
802
|
+
});
|
|
803
|
+
return service;
|
|
804
|
+
}));
|
|
805
|
+
Fault.register("/control/ApprovalPending", "wait");
|
|
806
|
+
Fault.register("/control/ApprovalDenied", "user");
|
|
807
|
+
//# sourceMappingURL=ControlRuntime.js.map
|