@smthrs/control 0.0.0-stage → 1.0.0-rc.4
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 +1293 -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 +744 -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 +1522 -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 +1589 -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 +808 -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 +7 -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 +2127 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1601 -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 +2478 -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
package/src/Channels.ts
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel contracts and the in-memory channel coordinator.
|
|
3
|
+
*
|
|
4
|
+
* A channel verifies opaque transport data before it decodes or maps it. The
|
|
5
|
+
* resulting request is then dispatched through the Control service; channels
|
|
6
|
+
* do not acquire an execution path of their own.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Sha256 from "@smthrs/crypto/Sha256"
|
|
12
|
+
import { Clock, Context, Effect, Layer, Ref, type Schema, Semaphore } from "effect"
|
|
13
|
+
import { Control } from "./Control.ts"
|
|
14
|
+
import { type ControlError, InvalidInput, type Unauthorized, Unavailable } from "./ControlError.ts"
|
|
15
|
+
import { ControlRuntime } from "./ControlRuntime.ts"
|
|
16
|
+
import type { FlowId, IdempotencyKey, Receipt, RunId, RunSummary, SignalPayload } from "./ControlSchema.ts"
|
|
17
|
+
import { alreadyApplied } from "./internal/planning.ts"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Opaque request data passed to a channel before decoding.
|
|
21
|
+
*
|
|
22
|
+
* @category models
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
export interface RawInbound {
|
|
26
|
+
readonly body: Uint8Array
|
|
27
|
+
readonly headers: Readonly<Record<string, string | undefined>>
|
|
28
|
+
readonly idempotencyKey: IdempotencyKey
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The channel mapping after a verified payload has been decoded.
|
|
33
|
+
*
|
|
34
|
+
* @category models
|
|
35
|
+
* @since 0.1.0
|
|
36
|
+
*/
|
|
37
|
+
export type InboundResult =
|
|
38
|
+
| { readonly _tag: "Start"; readonly flowId: FlowId; readonly input: unknown }
|
|
39
|
+
| { readonly _tag: "Signal"; readonly runId: RunId; readonly signal: SignalPayload }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A process-local per-channel delivery record. `messageId` identifies an existing
|
|
43
|
+
* platform message, so a reconnect can update it instead of posting again.
|
|
44
|
+
*
|
|
45
|
+
* @category models
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
export interface Delivery {
|
|
49
|
+
readonly cursor: string
|
|
50
|
+
readonly messageId?: string | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A side-effect-free outbound projection. Network delivery belongs to the
|
|
55
|
+
* transport adapter, which consumes this value after it is journaled.
|
|
56
|
+
*
|
|
57
|
+
* @category models
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export interface DeliveryProjection {
|
|
61
|
+
readonly cursor: string
|
|
62
|
+
readonly messageId?: string | undefined
|
|
63
|
+
readonly operation: "post" | "edit" | "noop"
|
|
64
|
+
readonly message: unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A bidirectional platform adapter.
|
|
69
|
+
*
|
|
70
|
+
* `verify` must only inspect opaque bytes and headers. It always precedes
|
|
71
|
+
* `decode`, preventing untrusted public requests from reaching planning.
|
|
72
|
+
*
|
|
73
|
+
* @category services
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
*/
|
|
76
|
+
export interface Channel<A = unknown> {
|
|
77
|
+
readonly name: string
|
|
78
|
+
readonly schema: Schema.Schema<A>
|
|
79
|
+
/**
|
|
80
|
+
* Non-secret HTTP header names whose values change the decoded command.
|
|
81
|
+
* Names are matched case-insensitively and folded into durable idempotency;
|
|
82
|
+
* signature, authorization, and credential headers must not be declared.
|
|
83
|
+
*/
|
|
84
|
+
readonly fingerprintHeaders?: ReadonlyArray<string> | undefined
|
|
85
|
+
readonly verify: (raw: RawInbound) => Effect.Effect<void, Unauthorized>
|
|
86
|
+
/** Deterministic, side-effect-free decoding; retries may evaluate it again. */
|
|
87
|
+
readonly decode: (raw: RawInbound) => Effect.Effect<A, InvalidInput>
|
|
88
|
+
/** Deterministic, side-effect-free mapping; retries may evaluate it again. */
|
|
89
|
+
readonly map: (payload: A) => Effect.Effect<InboundResult, InvalidInput>
|
|
90
|
+
readonly project: (run: RunSummary, delivery: Delivery | undefined) => DeliveryProjection
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A registered channel with its payload type hidden. Decoding and mapping stay
|
|
95
|
+
* paired, so lookup never exposes a mapper accepting arbitrary unknown data.
|
|
96
|
+
*
|
|
97
|
+
* @category models
|
|
98
|
+
* @since 1.0.0
|
|
99
|
+
*/
|
|
100
|
+
export interface RegisteredChannel extends Omit<Channel, "decode" | "map"> {
|
|
101
|
+
readonly decodeAndMap: (raw: RawInbound) => Effect.Effect<InboundResult, InvalidInput>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const eraseChannel = <A>(channel: Channel<A>): RegisteredChannel => ({
|
|
105
|
+
name: channel.name,
|
|
106
|
+
schema: channel.schema,
|
|
107
|
+
fingerprintHeaders: channel.fingerprintHeaders,
|
|
108
|
+
verify: (raw) => channel.verify(raw),
|
|
109
|
+
decodeAndMap: (raw) => Effect.flatMap(channel.decode(raw), (payload) => channel.map(payload)),
|
|
110
|
+
project: (run, delivery) => channel.project(run, delivery)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Arguments for ingesting one authenticated channel request.
|
|
115
|
+
*
|
|
116
|
+
* @category models
|
|
117
|
+
* @since 0.1.0
|
|
118
|
+
*/
|
|
119
|
+
export interface IngestRequest {
|
|
120
|
+
readonly channel: string
|
|
121
|
+
readonly raw: RawInbound
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Arguments for projecting one run onto a channel.
|
|
126
|
+
*
|
|
127
|
+
* @category models
|
|
128
|
+
* @since 0.1.0
|
|
129
|
+
*/
|
|
130
|
+
export interface ProjectRequest {
|
|
131
|
+
readonly channel: string
|
|
132
|
+
readonly run: RunSummary
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The channel coordinator.
|
|
137
|
+
*
|
|
138
|
+
* @category services
|
|
139
|
+
* @since 0.1.0
|
|
140
|
+
*/
|
|
141
|
+
export interface Channels {
|
|
142
|
+
readonly register: <A>(channel: Channel<A>) => Effect.Effect<void>
|
|
143
|
+
readonly lookup: (name: string) => Effect.Effect<RegisteredChannel, Unavailable>
|
|
144
|
+
readonly ingest: (request: IngestRequest) => Effect.Effect<Receipt, ControlError>
|
|
145
|
+
readonly project: (request: ProjectRequest) => Effect.Effect<DeliveryProjection, Unavailable>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Service tag for channel registration, ingestion, and pure projection.
|
|
150
|
+
*
|
|
151
|
+
* @category services
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
export const Channels: Context.Service<Channels, Channels> = Context.Service("/control/Channels")
|
|
155
|
+
|
|
156
|
+
const unavailable = (feature: string): Unavailable =>
|
|
157
|
+
new Unavailable({
|
|
158
|
+
feature,
|
|
159
|
+
ticket: "control-channel-registry"
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const deliveryKey = (channel: string, run: RunSummary): string => `${channel.length}:${channel}:${run.runId}`
|
|
163
|
+
|
|
164
|
+
/** Collision-free control-plane key for one channel-owned external key. */
|
|
165
|
+
const scopedKey = (channel: string, key: IdempotencyKey): IdempotencyKey =>
|
|
166
|
+
`channel:${channel.length}:${channel}:${key}`
|
|
167
|
+
|
|
168
|
+
const mutationKey = (channel: string, key: IdempotencyKey): IdempotencyKey =>
|
|
169
|
+
`channel.ingest:${scopedKey(channel, key)}`
|
|
170
|
+
|
|
171
|
+
const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype) as object
|
|
172
|
+
const typedArrayBuffer = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer")?.get
|
|
173
|
+
const typedArrayByteOffset = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset")?.get
|
|
174
|
+
const typedArrayByteLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength")?.get
|
|
175
|
+
|
|
176
|
+
class InboundBoundaryError extends TypeError {
|
|
177
|
+
readonly issue: string
|
|
178
|
+
constructor(issue: string) {
|
|
179
|
+
super(issue)
|
|
180
|
+
this.issue = issue
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const boundary = (issue: string): never => {
|
|
185
|
+
throw new InboundBoundaryError(issue)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const plainRecord = (input: unknown, path: string): Record<PropertyKey, unknown> => {
|
|
189
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
190
|
+
return boundary(`${path}: must be a plain record`)
|
|
191
|
+
}
|
|
192
|
+
const prototype = Object.getPrototypeOf(input)
|
|
193
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
194
|
+
return boundary(`${path}: must have a plain prototype`)
|
|
195
|
+
}
|
|
196
|
+
return input as Record<PropertyKey, unknown>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ownData = (input: Record<PropertyKey, unknown>, key: PropertyKey, path: string): unknown => {
|
|
200
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key)
|
|
201
|
+
if (descriptor === undefined || !("value" in descriptor)) return boundary(`${path}: must be an own data property`)
|
|
202
|
+
return descriptor.value
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const copyBody = (input: unknown): Uint8Array => {
|
|
206
|
+
/* v8 ignore next 4 -- every host that has `Uint8Array` has its prototype accessors; the refusal exists so a host without them fails closed rather than reading a caller-owned buffer */
|
|
207
|
+
if (
|
|
208
|
+
typedArrayBuffer === undefined || typedArrayByteOffset === undefined ||
|
|
209
|
+
typedArrayByteLength === undefined
|
|
210
|
+
) return boundary("raw.body: this host cannot inspect typed arrays")
|
|
211
|
+
try {
|
|
212
|
+
const buffer = Reflect.apply(typedArrayBuffer, input, []) as ArrayBufferLike
|
|
213
|
+
const byteOffset = Reflect.apply(typedArrayByteOffset, input, []) as number
|
|
214
|
+
const byteLength = Reflect.apply(typedArrayByteLength, input, []) as number
|
|
215
|
+
const source = new Uint8Array(buffer, byteOffset, byteLength)
|
|
216
|
+
const snapshot = new Uint8Array(byteLength)
|
|
217
|
+
snapshot.set(source)
|
|
218
|
+
return snapshot
|
|
219
|
+
} catch {
|
|
220
|
+
return boundary("raw.body: must be a Uint8Array")
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const snapshotHeaders = (input: unknown): Readonly<Record<string, string | undefined>> => {
|
|
225
|
+
const record = plainRecord(input, "raw.headers")
|
|
226
|
+
const snapshot = Object.create(null) as Record<string, string | undefined>
|
|
227
|
+
for (const key of Reflect.ownKeys(record)) {
|
|
228
|
+
if (typeof key !== "string") return boundary("raw.headers: symbol keys are not supported")
|
|
229
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key)
|
|
230
|
+
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
231
|
+
return boundary(`raw.headers.${key}: must be an enumerable data property`)
|
|
232
|
+
}
|
|
233
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
234
|
+
return boundary(`raw.headers.${key}: must be a string or undefined`)
|
|
235
|
+
}
|
|
236
|
+
const normalized = key.toLowerCase()
|
|
237
|
+
if (Object.hasOwn(snapshot, normalized)) {
|
|
238
|
+
return boundary(`raw.headers.${normalized}: duplicate case-insensitive name`)
|
|
239
|
+
}
|
|
240
|
+
snapshot[normalized] = descriptor.value as string | undefined
|
|
241
|
+
}
|
|
242
|
+
return Object.freeze(snapshot)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const snapshotRequest = (
|
|
246
|
+
input: IngestRequest
|
|
247
|
+
): Effect.Effect<{ readonly channel: string; readonly raw: RawInbound }, InvalidInput> =>
|
|
248
|
+
Effect.try({
|
|
249
|
+
try: () => {
|
|
250
|
+
const request = plainRecord(input, "request")
|
|
251
|
+
const channel = ownData(request, "channel", "request.channel")
|
|
252
|
+
const candidate = plainRecord(ownData(request, "raw", "request.raw"), "request.raw")
|
|
253
|
+
const idempotencyKey = ownData(candidate, "idempotencyKey", "raw.idempotencyKey")
|
|
254
|
+
if (typeof channel !== "string" || channel.length === 0) return boundary("request.channel: must be non-empty")
|
|
255
|
+
if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
|
|
256
|
+
return boundary("raw.idempotencyKey: must be non-empty")
|
|
257
|
+
}
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
channel,
|
|
260
|
+
raw: Object.freeze({
|
|
261
|
+
body: copyBody(ownData(candidate, "body", "raw.body")),
|
|
262
|
+
headers: snapshotHeaders(ownData(candidate, "headers", "raw.headers")),
|
|
263
|
+
idempotencyKey
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
},
|
|
267
|
+
catch: (cause) =>
|
|
268
|
+
new InvalidInput({
|
|
269
|
+
issue: cause instanceof InboundBoundaryError ? cause.issue : "channel request could not be inspected safely"
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const normalizedFingerprintHeaders = (channel: RegisteredChannel): ReadonlyArray<string> =>
|
|
274
|
+
Array.from(new Set((channel.fingerprintHeaders ?? []).map((name) => name.toLowerCase()))).sort()
|
|
275
|
+
|
|
276
|
+
/** Digests the body plus only the adapter-declared, non-secret semantic headers. */
|
|
277
|
+
const fingerprint = (
|
|
278
|
+
body: Uint8Array,
|
|
279
|
+
headers: Readonly<Record<string, string | undefined>>,
|
|
280
|
+
names: ReadonlyArray<string>
|
|
281
|
+
): string => {
|
|
282
|
+
const document = JSON.stringify([
|
|
283
|
+
Sha256.digestSync(body),
|
|
284
|
+
names.map((name) => [name, headers[name] ?? null])
|
|
285
|
+
])
|
|
286
|
+
return `channel-ingress:v2:${Sha256.digestSync(new TextEncoder().encode(document))}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const externalReceipt = (receipt: Receipt, key: IdempotencyKey): Receipt => {
|
|
290
|
+
switch (receipt._tag) {
|
|
291
|
+
case "Accepted":
|
|
292
|
+
case "AlreadyApplied":
|
|
293
|
+
case "Parked":
|
|
294
|
+
return { ...receipt, receiptId: key }
|
|
295
|
+
default:
|
|
296
|
+
return receipt
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The slice of the control runtime's mutation store that inbound idempotency uses. */
|
|
301
|
+
interface InboundReceiptStore {
|
|
302
|
+
readonly lookupMutation: (
|
|
303
|
+
key: IdempotencyKey,
|
|
304
|
+
fingerprint: string
|
|
305
|
+
) => Effect.Effect<Receipt | undefined, ControlError>
|
|
306
|
+
readonly recordMutation: (
|
|
307
|
+
key: IdempotencyKey,
|
|
308
|
+
fingerprint: string,
|
|
309
|
+
receipt: Receipt
|
|
310
|
+
) => Effect.Effect<void, ControlError>
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Builds an in-memory channel registry and delivery map over Control's durable
|
|
315
|
+
* mutation store. Inbound idempotency therefore survives coordinator restarts;
|
|
316
|
+
* only registration and outbound projection cursors are process-local.
|
|
317
|
+
*/
|
|
318
|
+
const makeWith = (runtime: InboundReceiptStore) =>
|
|
319
|
+
Effect.gen(function*() {
|
|
320
|
+
const channels = yield* Ref.make(new Map<string, RegisteredChannel>())
|
|
321
|
+
const deliveries = new Map<string, Delivery>()
|
|
322
|
+
// Live identities are never evicted. Terminal identities have a FIFO window
|
|
323
|
+
// for reconnects; retaining one per historical run would grow without bound.
|
|
324
|
+
const terminalDeliveries = new Set<string>()
|
|
325
|
+
const maximumTerminalDeliveries = 1024
|
|
326
|
+
const ingestion = yield* Semaphore.make(1)
|
|
327
|
+
const control = yield* Control
|
|
328
|
+
|
|
329
|
+
const lookup = Effect.fn("Channels.lookup")(
|
|
330
|
+
function*(name: string): Effect.fn.Return<RegisteredChannel, Unavailable> {
|
|
331
|
+
return yield* Effect.flatMap(Ref.get(channels), (registered) => {
|
|
332
|
+
const channel = registered.get(name)
|
|
333
|
+
return channel === undefined
|
|
334
|
+
? Effect.fail(unavailable(`channel "${name}" is not registered`))
|
|
335
|
+
: Effect.succeed(channel)
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return Channels.of({
|
|
341
|
+
register: Effect.fn("Channels.register")(<A>(channel: Channel<A>) =>
|
|
342
|
+
Ref.update(channels, (registered) => {
|
|
343
|
+
const next = new Map(registered)
|
|
344
|
+
next.set(channel.name, eraseChannel(channel))
|
|
345
|
+
return next
|
|
346
|
+
})
|
|
347
|
+
),
|
|
348
|
+
lookup,
|
|
349
|
+
ingest: Effect.fn("Channels.ingest")((request) =>
|
|
350
|
+
Effect.flatMap(snapshotRequest(request), (snapshot) =>
|
|
351
|
+
ingestion.withPermits(1)(Effect.gen(function*() {
|
|
352
|
+
const channel = yield* lookup(snapshot.channel)
|
|
353
|
+
const fingerprintHeaders = normalizedFingerprintHeaders(channel)
|
|
354
|
+
const bodyFingerprint = fingerprint(snapshot.raw.body, snapshot.raw.headers, fingerprintHeaders)
|
|
355
|
+
// This ordering is intentional: signature verification is the
|
|
356
|
+
// amplification guard and must happen before decode or Control access.
|
|
357
|
+
// The verifier receives its own body copy, so even a verifier that
|
|
358
|
+
// edits bytes cannot change what the decoder sees after approval.
|
|
359
|
+
yield* channel.verify({ ...snapshot.raw, body: copyBody(snapshot.raw.body) })
|
|
360
|
+
const externalKey = snapshot.raw.idempotencyKey
|
|
361
|
+
const durableKey = mutationKey(snapshot.channel, externalKey)
|
|
362
|
+
const prior = yield* runtime.lookupMutation(durableKey, bodyFingerprint)
|
|
363
|
+
if (prior !== undefined) return externalReceipt(prior, externalKey)
|
|
364
|
+
|
|
365
|
+
const mapped = yield* channel.decodeAndMap(snapshot.raw)
|
|
366
|
+
const key = scopedKey(snapshot.channel, externalKey)
|
|
367
|
+
let receipt: Receipt
|
|
368
|
+
if (mapped._tag === "Signal") {
|
|
369
|
+
// The channel is the caller, not the local operator: a webhook
|
|
370
|
+
// body names the run and wait, so a signal it maps to answers a
|
|
371
|
+
// human wait only if the host delegates approval to this channel.
|
|
372
|
+
receipt = yield* control.signal({
|
|
373
|
+
runId: mapped.runId,
|
|
374
|
+
signal: mapped.signal,
|
|
375
|
+
idempotencyKey: key,
|
|
376
|
+
principal: { id: snapshot.channel, kind: "channel", stampedAt: yield* Clock.currentTimeMillis }
|
|
377
|
+
})
|
|
378
|
+
} else {
|
|
379
|
+
const plan = yield* control.plan({
|
|
380
|
+
flowId: mapped.flowId,
|
|
381
|
+
input: mapped.input,
|
|
382
|
+
idempotencyKey: key
|
|
383
|
+
})
|
|
384
|
+
receipt = yield* control.run({
|
|
385
|
+
_tag: "Plan",
|
|
386
|
+
planId: plan.planId,
|
|
387
|
+
digest: plan.digest,
|
|
388
|
+
envelope: plan.envelope,
|
|
389
|
+
idempotencyKey: key
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
// A parked start has not launched. Leave ingress unsettled so a
|
|
393
|
+
// redelivery can retry the same Control key after plan approval.
|
|
394
|
+
if (receipt._tag !== "Conflict" && receipt._tag !== "Parked") {
|
|
395
|
+
yield* runtime.recordMutation(durableKey, bodyFingerprint, receipt)
|
|
396
|
+
}
|
|
397
|
+
return externalReceipt(receipt, externalKey)
|
|
398
|
+
})))
|
|
399
|
+
),
|
|
400
|
+
project: Effect.fn("Channels.project")(function*(request) {
|
|
401
|
+
const channel = yield* lookup(request.channel)
|
|
402
|
+
const key = deliveryKey(request.channel, request.run)
|
|
403
|
+
// Keep the read, projection and update synchronous so concurrent fibers
|
|
404
|
+
// cannot project from the same stale delivery identity.
|
|
405
|
+
return yield* Effect.sync(() => {
|
|
406
|
+
const prior = deliveries.get(key)
|
|
407
|
+
const projection = channel.project(request.run, prior)
|
|
408
|
+
if (
|
|
409
|
+
projection.operation !== "noop" &&
|
|
410
|
+
(prior === undefined || prior.cursor !== projection.cursor || prior.messageId !== projection.messageId)
|
|
411
|
+
) {
|
|
412
|
+
deliveries.set(key, { cursor: projection.cursor, messageId: projection.messageId })
|
|
413
|
+
}
|
|
414
|
+
if (deliveries.has(key)) {
|
|
415
|
+
const status = request.run.status
|
|
416
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
417
|
+
terminalDeliveries.add(key)
|
|
418
|
+
if (terminalDeliveries.size > maximumTerminalDeliveries) {
|
|
419
|
+
// The set is nonempty because it just exceeded the limit.
|
|
420
|
+
const oldest = terminalDeliveries.values().next().value!
|
|
421
|
+
terminalDeliveries.delete(oldest)
|
|
422
|
+
deliveries.delete(oldest)
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
terminalDeliveries.delete(key)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return projection
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Builds the coordinator over Control's durable mutation store.
|
|
436
|
+
*
|
|
437
|
+
* @category constructors
|
|
438
|
+
* @since 1.0.0
|
|
439
|
+
*/
|
|
440
|
+
export const make = Effect.gen(function*() {
|
|
441
|
+
const runtime = yield* ControlRuntime
|
|
442
|
+
return yield* makeWith(runtime)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Builds a process-local coordinator for adapter unit tests.
|
|
447
|
+
*
|
|
448
|
+
* Production hosts use {@link layer}; this constructor deliberately makes no
|
|
449
|
+
* restart guarantee.
|
|
450
|
+
*
|
|
451
|
+
* @category constructors
|
|
452
|
+
* @since 1.0.0
|
|
453
|
+
*/
|
|
454
|
+
export const makeMemory = Effect.gen(function*() {
|
|
455
|
+
const records = yield* Ref.make(
|
|
456
|
+
new Map<IdempotencyKey, {
|
|
457
|
+
readonly fingerprint: string
|
|
458
|
+
readonly receipt: Receipt
|
|
459
|
+
}>()
|
|
460
|
+
)
|
|
461
|
+
return yield* makeWith({
|
|
462
|
+
lookupMutation: (key, candidate) =>
|
|
463
|
+
Ref.get(records).pipe(Effect.map((stored) => {
|
|
464
|
+
const prior = stored.get(key)
|
|
465
|
+
if (prior === undefined) return undefined
|
|
466
|
+
return prior.fingerprint === candidate
|
|
467
|
+
? alreadyApplied(key, prior.receipt)
|
|
468
|
+
: { _tag: "Conflict", message: `idempotency key ${key} was used for another mutation` }
|
|
469
|
+
})),
|
|
470
|
+
recordMutation: (key, candidate, receipt) =>
|
|
471
|
+
Ref.update(records, (stored) => {
|
|
472
|
+
const next = new Map(stored)
|
|
473
|
+
next.set(key, { fingerprint: candidate, receipt })
|
|
474
|
+
return next
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Channel layer with durable inbound receipts supplied by `ControlRuntime`.
|
|
481
|
+
*
|
|
482
|
+
* @category layers
|
|
483
|
+
* @since 0.1.0
|
|
484
|
+
*/
|
|
485
|
+
export const layer = Layer.effect(Channels, make)
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Process-local channel layer for adapter unit tests only.
|
|
489
|
+
*
|
|
490
|
+
* @category layers
|
|
491
|
+
* @since 1.0.0
|
|
492
|
+
*/
|
|
493
|
+
export const layerMemory: Layer.Layer<Channels, never, Control> = Layer.effect(Channels, makeMemory)
|