@smthrs/engine-store 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 +247 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/dist/cjs/ArtifactGc.d.ts +235 -0
- package/dist/cjs/ArtifactGc.d.ts.map +1 -0
- package/dist/cjs/ArtifactGc.js +183 -0
- package/dist/cjs/ArtifactGc.js.map +7 -0
- package/dist/cjs/ArtifactSync.d.ts +164 -0
- package/dist/cjs/ArtifactSync.d.ts.map +1 -0
- package/dist/cjs/ArtifactSync.js +144 -0
- package/dist/cjs/ArtifactSync.js.map +7 -0
- package/dist/cjs/CacheSync.d.ts +112 -0
- package/dist/cjs/CacheSync.d.ts.map +1 -0
- package/dist/cjs/CacheSync.js +64 -0
- package/dist/cjs/CacheSync.js.map +7 -0
- package/dist/cjs/DeferredClockFold.d.ts +354 -0
- package/dist/cjs/DeferredClockFold.d.ts.map +1 -0
- package/dist/cjs/DeferredClockFold.js +335 -0
- package/dist/cjs/DeferredClockFold.js.map +7 -0
- package/dist/cjs/DisasterRecovery.d.ts +431 -0
- package/dist/cjs/DisasterRecovery.d.ts.map +1 -0
- package/dist/cjs/DisasterRecovery.js +453 -0
- package/dist/cjs/DisasterRecovery.js.map +7 -0
- package/dist/cjs/DurableEngineState.d.ts +776 -0
- package/dist/cjs/DurableEngineState.d.ts.map +1 -0
- package/dist/cjs/DurableEngineState.js +1536 -0
- package/dist/cjs/DurableEngineState.js.map +7 -0
- package/dist/cjs/EngineStore.d.ts +158 -0
- package/dist/cjs/EngineStore.d.ts.map +1 -0
- package/dist/cjs/EngineStore.js +398 -0
- package/dist/cjs/EngineStore.js.map +7 -0
- package/dist/cjs/EngineStoreMetrics.d.ts +276 -0
- package/dist/cjs/EngineStoreMetrics.d.ts.map +1 -0
- package/dist/cjs/EngineStoreMetrics.js +135 -0
- package/dist/cjs/EngineStoreMetrics.js.map +7 -0
- package/dist/cjs/Errors.d.ts +22 -0
- package/dist/cjs/Errors.d.ts.map +1 -0
- package/dist/cjs/Errors.js +39 -0
- package/dist/cjs/Errors.js.map +7 -0
- package/dist/cjs/EventTypes.d.ts +27 -0
- package/dist/cjs/EventTypes.d.ts.map +1 -0
- package/dist/cjs/EventTypes.js +37 -0
- package/dist/cjs/EventTypes.js.map +7 -0
- package/dist/cjs/ExecutionFacts.d.ts +59 -0
- package/dist/cjs/ExecutionFacts.d.ts.map +1 -0
- package/dist/cjs/ExecutionFacts.js +123 -0
- package/dist/cjs/ExecutionFacts.js.map +7 -0
- package/dist/cjs/ExecutionSnapshot.d.ts +117 -0
- package/dist/cjs/ExecutionSnapshot.d.ts.map +1 -0
- package/dist/cjs/ExecutionSnapshot.js +111 -0
- package/dist/cjs/ExecutionSnapshot.js.map +7 -0
- package/dist/cjs/Inconsistency.d.ts +154 -0
- package/dist/cjs/Inconsistency.d.ts.map +1 -0
- package/dist/cjs/Inconsistency.js +135 -0
- package/dist/cjs/Inconsistency.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +56 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +78 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/OwnerIdentity.d.ts +91 -0
- package/dist/cjs/OwnerIdentity.d.ts.map +1 -0
- package/dist/cjs/OwnerIdentity.js +65 -0
- package/dist/cjs/OwnerIdentity.js.map +7 -0
- package/dist/cjs/PlanInputStore.d.ts +110 -0
- package/dist/cjs/PlanInputStore.d.ts.map +1 -0
- package/dist/cjs/PlanInputStore.js +224 -0
- package/dist/cjs/PlanInputStore.js.map +7 -0
- package/dist/cjs/PlanMergeStore.d.ts +113 -0
- package/dist/cjs/PlanMergeStore.d.ts.map +1 -0
- package/dist/cjs/PlanMergeStore.js +255 -0
- package/dist/cjs/PlanMergeStore.js.map +7 -0
- package/dist/cjs/PlanScheduler.d.ts +363 -0
- package/dist/cjs/PlanScheduler.d.ts.map +1 -0
- package/dist/cjs/PlanScheduler.js +1090 -0
- package/dist/cjs/PlanScheduler.js.map +7 -0
- package/dist/cjs/Reconciliation.d.ts +175 -0
- package/dist/cjs/Reconciliation.d.ts.map +1 -0
- package/dist/cjs/Reconciliation.js +119 -0
- package/dist/cjs/Reconciliation.js.map +7 -0
- package/dist/cjs/ReplayOnly.d.ts +91 -0
- package/dist/cjs/ReplayOnly.d.ts.map +1 -0
- package/dist/cjs/ReplayOnly.js +52 -0
- package/dist/cjs/ReplayOnly.js.map +7 -0
- package/dist/cjs/Retention.d.ts +113 -0
- package/dist/cjs/Retention.d.ts.map +1 -0
- package/dist/cjs/Retention.js +99 -0
- package/dist/cjs/Retention.js.map +7 -0
- package/dist/cjs/RunCatalogRead.d.ts +17 -0
- package/dist/cjs/RunCatalogRead.d.ts.map +1 -0
- package/dist/cjs/RunCatalogRead.js +33 -0
- package/dist/cjs/RunCatalogRead.js.map +7 -0
- package/dist/cjs/RunChangeFeed.d.ts +75 -0
- package/dist/cjs/RunChangeFeed.d.ts.map +1 -0
- package/dist/cjs/RunChangeFeed.js +96 -0
- package/dist/cjs/RunChangeFeed.js.map +7 -0
- package/dist/cjs/RunState.d.ts +116 -0
- package/dist/cjs/RunState.d.ts.map +1 -0
- package/dist/cjs/RunState.js +76 -0
- package/dist/cjs/RunState.js.map +7 -0
- package/dist/cjs/Selection.d.ts +495 -0
- package/dist/cjs/Selection.d.ts.map +1 -0
- package/dist/cjs/Selection.js +288 -0
- package/dist/cjs/Selection.js.map +7 -0
- package/dist/cjs/SelectionStore.d.ts +141 -0
- package/dist/cjs/SelectionStore.d.ts.map +1 -0
- package/dist/cjs/SelectionStore.js +229 -0
- package/dist/cjs/SelectionStore.js.map +7 -0
- package/dist/cjs/StepBoundary.d.ts +501 -0
- package/dist/cjs/StepBoundary.d.ts.map +1 -0
- package/dist/cjs/StepBoundary.js +619 -0
- package/dist/cjs/StepBoundary.js.map +7 -0
- package/dist/cjs/StepCacheFold.d.ts +148 -0
- package/dist/cjs/StepCacheFold.d.ts.map +1 -0
- package/dist/cjs/StepCacheFold.js +131 -0
- package/dist/cjs/StepCacheFold.js.map +7 -0
- package/dist/cjs/StepSandbox.d.ts +78 -0
- package/dist/cjs/StepSandbox.d.ts.map +1 -0
- package/dist/cjs/StepSandbox.js +75 -0
- package/dist/cjs/StepSandbox.js.map +7 -0
- package/dist/cjs/WakeBus.d.ts +116 -0
- package/dist/cjs/WakeBus.d.ts.map +1 -0
- package/dist/cjs/WakeBus.js +89 -0
- package/dist/cjs/WakeBus.js.map +7 -0
- package/dist/cjs/WorkspaceSandbox.d.ts +619 -0
- package/dist/cjs/WorkspaceSandbox.d.ts.map +1 -0
- package/dist/cjs/WorkspaceSandbox.js +711 -0
- package/dist/cjs/WorkspaceSandbox.js.map +7 -0
- package/dist/cjs/index.d.ts +157 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +95 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ActionPersistence.d.ts +276 -0
- package/dist/cjs/internal/ActionPersistence.d.ts.map +1 -0
- package/dist/cjs/internal/ActionPersistence.js +1395 -0
- package/dist/cjs/internal/ActionPersistence.js.map +7 -0
- package/dist/cjs/internal/ArtifactRoots.d.ts +38 -0
- package/dist/cjs/internal/ArtifactRoots.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactRoots.js +89 -0
- package/dist/cjs/internal/ArtifactRoots.js.map +7 -0
- package/dist/cjs/internal/AttemptAdmission.d.ts +53 -0
- package/dist/cjs/internal/AttemptAdmission.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptAdmission.js +52 -0
- package/dist/cjs/internal/AttemptAdmission.js.map +7 -0
- package/dist/cjs/internal/AttemptLifecycle.d.ts +245 -0
- package/dist/cjs/internal/AttemptLifecycle.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptLifecycle.js +112 -0
- package/dist/cjs/internal/AttemptLifecycle.js.map +7 -0
- package/dist/cjs/internal/AttemptProbe.d.ts +53 -0
- package/dist/cjs/internal/AttemptProbe.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptProbe.js +66 -0
- package/dist/cjs/internal/AttemptProbe.js.map +7 -0
- package/dist/cjs/internal/CacheAdmission.d.ts +79 -0
- package/dist/cjs/internal/CacheAdmission.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAdmission.js +71 -0
- package/dist/cjs/internal/CacheAdmission.js.map +7 -0
- package/dist/cjs/internal/CacheAgeHistory.d.ts +27 -0
- package/dist/cjs/internal/CacheAgeHistory.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAgeHistory.js +61 -0
- package/dist/cjs/internal/CacheAgeHistory.js.map +7 -0
- package/dist/cjs/internal/CacheAgeVerdicts.d.ts +15 -0
- package/dist/cjs/internal/CacheAgeVerdicts.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAgeVerdicts.js +73 -0
- package/dist/cjs/internal/CacheAgeVerdicts.js.map +7 -0
- package/dist/cjs/internal/CacheOutputPolicy.d.ts +24 -0
- package/dist/cjs/internal/CacheOutputPolicy.d.ts.map +1 -0
- package/dist/cjs/internal/CacheOutputPolicy.js +75 -0
- package/dist/cjs/internal/CacheOutputPolicy.js.map +7 -0
- package/dist/cjs/internal/CachePublication.d.ts +97 -0
- package/dist/cjs/internal/CachePublication.d.ts.map +1 -0
- package/dist/cjs/internal/CachePublication.js +80 -0
- package/dist/cjs/internal/CachePublication.js.map +7 -0
- package/dist/cjs/internal/CallFacts.d.ts +15 -0
- package/dist/cjs/internal/CallFacts.d.ts.map +1 -0
- package/dist/cjs/internal/CallFacts.js +97 -0
- package/dist/cjs/internal/CallFacts.js.map +7 -0
- package/dist/cjs/internal/CopiedRecord.d.ts +61 -0
- package/dist/cjs/internal/CopiedRecord.d.ts.map +1 -0
- package/dist/cjs/internal/CopiedRecord.js +134 -0
- package/dist/cjs/internal/CopiedRecord.js.map +7 -0
- package/dist/cjs/internal/DeferredPersistence.d.ts +108 -0
- package/dist/cjs/internal/DeferredPersistence.d.ts.map +1 -0
- package/dist/cjs/internal/DeferredPersistence.js +270 -0
- package/dist/cjs/internal/DeferredPersistence.js.map +7 -0
- package/dist/cjs/internal/EffectRecords.d.ts +92 -0
- package/dist/cjs/internal/EffectRecords.d.ts.map +1 -0
- package/dist/cjs/internal/EffectRecords.js +63 -0
- package/dist/cjs/internal/EffectRecords.js.map +7 -0
- package/dist/cjs/internal/EngineJj.d.ts +30 -0
- package/dist/cjs/internal/EngineJj.d.ts.map +1 -0
- package/dist/cjs/internal/EngineJj.js +42 -0
- package/dist/cjs/internal/EngineJj.js.map +7 -0
- package/dist/cjs/internal/EngineStateSchema.d.ts +68 -0
- package/dist/cjs/internal/EngineStateSchema.d.ts.map +1 -0
- package/dist/cjs/internal/EngineStateSchema.js +58 -0
- package/dist/cjs/internal/EngineStateSchema.js.map +7 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.d.ts +162 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.d.ts.map +1 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.js +199 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.js.map +7 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts +39 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.js +105 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.js.map +7 -0
- package/dist/cjs/internal/ExitEncoding.d.ts +180 -0
- package/dist/cjs/internal/ExitEncoding.d.ts.map +1 -0
- package/dist/cjs/internal/ExitEncoding.js +236 -0
- package/dist/cjs/internal/ExitEncoding.js.map +7 -0
- package/dist/cjs/internal/FileBoundarySnapshot.d.ts +14 -0
- package/dist/cjs/internal/FileBoundarySnapshot.d.ts.map +1 -0
- package/dist/cjs/internal/FileBoundarySnapshot.js +51 -0
- package/dist/cjs/internal/FileBoundarySnapshot.js.map +7 -0
- package/dist/cjs/internal/FileEnumeration.d.ts +127 -0
- package/dist/cjs/internal/FileEnumeration.d.ts.map +1 -0
- package/dist/cjs/internal/FileEnumeration.js +194 -0
- package/dist/cjs/internal/FileEnumeration.js.map +7 -0
- package/dist/cjs/internal/HostReflection.d.ts +80 -0
- package/dist/cjs/internal/HostReflection.d.ts.map +1 -0
- package/dist/cjs/internal/HostReflection.js +43 -0
- package/dist/cjs/internal/HostReflection.js.map +7 -0
- package/dist/cjs/internal/JournalRecords.d.ts +325 -0
- package/dist/cjs/internal/JournalRecords.d.ts.map +1 -0
- package/dist/cjs/internal/JournalRecords.js +122 -0
- package/dist/cjs/internal/JournalRecords.js.map +7 -0
- package/dist/cjs/internal/NodeJournal.d.ts +157 -0
- package/dist/cjs/internal/NodeJournal.d.ts.map +1 -0
- package/dist/cjs/internal/NodeJournal.js +192 -0
- package/dist/cjs/internal/NodeJournal.js.map +7 -0
- package/dist/cjs/internal/Ordering.d.ts +14 -0
- package/dist/cjs/internal/Ordering.d.ts.map +1 -0
- package/dist/cjs/internal/Ordering.js +25 -0
- package/dist/cjs/internal/Ordering.js.map +7 -0
- package/dist/cjs/internal/PlanInputs.d.ts +14 -0
- package/dist/cjs/internal/PlanInputs.d.ts.map +1 -0
- package/dist/cjs/internal/PlanInputs.js +78 -0
- package/dist/cjs/internal/PlanInputs.js.map +7 -0
- package/dist/cjs/internal/PlanMerges.d.ts +178 -0
- package/dist/cjs/internal/PlanMerges.d.ts.map +1 -0
- package/dist/cjs/internal/PlanMerges.js +125 -0
- package/dist/cjs/internal/PlanMerges.js.map +7 -0
- package/dist/cjs/internal/ProvenanceSlot.d.ts +30 -0
- package/dist/cjs/internal/ProvenanceSlot.d.ts.map +1 -0
- package/dist/cjs/internal/ProvenanceSlot.js +53 -0
- package/dist/cjs/internal/ProvenanceSlot.js.map +7 -0
- package/dist/cjs/internal/ResultEnvelope.d.ts +88 -0
- package/dist/cjs/internal/ResultEnvelope.d.ts.map +1 -0
- package/dist/cjs/internal/ResultEnvelope.js +90 -0
- package/dist/cjs/internal/ResultEnvelope.js.map +7 -0
- package/dist/cjs/internal/RetentionOps.d.ts +414 -0
- package/dist/cjs/internal/RetentionOps.d.ts.map +1 -0
- package/dist/cjs/internal/RetentionOps.js +354 -0
- package/dist/cjs/internal/RetentionOps.js.map +7 -0
- package/dist/cjs/internal/RunCatalogOps.d.ts +118 -0
- package/dist/cjs/internal/RunCatalogOps.d.ts.map +1 -0
- package/dist/cjs/internal/RunCatalogOps.js +85 -0
- package/dist/cjs/internal/RunCatalogOps.js.map +7 -0
- package/dist/cjs/internal/RunCoordinator.d.ts +78 -0
- package/dist/cjs/internal/RunCoordinator.d.ts.map +1 -0
- package/dist/cjs/internal/RunCoordinator.js +123 -0
- package/dist/cjs/internal/RunCoordinator.js.map +7 -0
- package/dist/cjs/internal/RunDriver.d.ts +192 -0
- package/dist/cjs/internal/RunDriver.d.ts.map +1 -0
- package/dist/cjs/internal/RunDriver.js +1584 -0
- package/dist/cjs/internal/RunDriver.js.map +7 -0
- package/dist/cjs/internal/RunLifecycle.d.ts +22 -0
- package/dist/cjs/internal/RunLifecycle.d.ts.map +1 -0
- package/dist/cjs/internal/RunLifecycle.js +63 -0
- package/dist/cjs/internal/RunLifecycle.js.map +7 -0
- package/dist/cjs/internal/RunListing.d.ts +78 -0
- package/dist/cjs/internal/RunListing.d.ts.map +1 -0
- package/dist/cjs/internal/RunListing.js +171 -0
- package/dist/cjs/internal/RunListing.js.map +7 -0
- package/dist/cjs/internal/RunListingError.d.ts +35 -0
- package/dist/cjs/internal/RunListingError.d.ts.map +1 -0
- package/dist/cjs/internal/RunListingError.js +47 -0
- package/dist/cjs/internal/RunListingError.js.map +7 -0
- package/dist/cjs/internal/RuntimeGraph.d.ts +73 -0
- package/dist/cjs/internal/RuntimeGraph.d.ts.map +1 -0
- package/dist/cjs/internal/RuntimeGraph.js +180 -0
- package/dist/cjs/internal/RuntimeGraph.js.map +7 -0
- package/dist/cjs/internal/SandboxedExecution.d.ts +80 -0
- package/dist/cjs/internal/SandboxedExecution.d.ts.map +1 -0
- package/dist/cjs/internal/SandboxedExecution.js +104 -0
- package/dist/cjs/internal/SandboxedExecution.js.map +7 -0
- package/dist/cjs/internal/StateTransaction.d.ts +44 -0
- package/dist/cjs/internal/StateTransaction.d.ts.map +1 -0
- package/dist/cjs/internal/StateTransaction.js +52 -0
- package/dist/cjs/internal/StateTransaction.js.map +7 -0
- package/dist/cjs/internal/StepFacts.d.ts +14 -0
- package/dist/cjs/internal/StepFacts.d.ts.map +1 -0
- package/dist/cjs/internal/StepFacts.js +58 -0
- package/dist/cjs/internal/StepFacts.js.map +7 -0
- package/dist/cjs/internal/TypedEvents.d.ts +14 -0
- package/dist/cjs/internal/TypedEvents.d.ts.map +1 -0
- package/dist/cjs/internal/TypedEvents.js +37 -0
- package/dist/cjs/internal/TypedEvents.js.map +7 -0
- package/dist/cjs/migrations/0001_initial.d.ts +17 -0
- package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_initial.js +61 -0
- package/dist/cjs/migrations/0001_initial.js.map +7 -0
- package/dist/cjs/migrations/0002_selection_store.d.ts +15 -0
- package/dist/cjs/migrations/0002_selection_store.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_selection_store.js +56 -0
- package/dist/cjs/migrations/0002_selection_store.js.map +7 -0
- package/dist/cjs/migrations/0003_plan_inputs.d.ts +13 -0
- package/dist/cjs/migrations/0003_plan_inputs.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_plan_inputs.js +90 -0
- package/dist/cjs/migrations/0003_plan_inputs.js.map +7 -0
- package/dist/cjs/migrations/0004_plan_environment.d.ts +14 -0
- package/dist/cjs/migrations/0004_plan_environment.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_plan_environment.js +56 -0
- package/dist/cjs/migrations/0004_plan_environment.js.map +7 -0
- package/dist/cjs/migrations/0005_plan_merges.d.ts +14 -0
- package/dist/cjs/migrations/0005_plan_merges.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_plan_merges.js +82 -0
- package/dist/cjs/migrations/0005_plan_merges.js.map +7 -0
- package/dist/cjs/migrations/0006_execution_listing.d.ts +15 -0
- package/dist/cjs/migrations/0006_execution_listing.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_execution_listing.js +101 -0
- package/dist/cjs/migrations/0006_execution_listing.js.map +7 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.d.ts +15 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.js +40 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.js.map +7 -0
- package/dist/cjs/migrations/0008_deferred_consumption.d.ts +16 -0
- package/dist/cjs/migrations/0008_deferred_consumption.d.ts.map +1 -0
- package/dist/cjs/migrations/0008_deferred_consumption.js +45 -0
- package/dist/cjs/migrations/0008_deferred_consumption.js.map +7 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts +24 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.js +141 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.js.map +7 -0
- package/dist/cjs/migrations/0010_pending_cancellation.d.ts +15 -0
- package/dist/cjs/migrations/0010_pending_cancellation.d.ts.map +1 -0
- package/dist/cjs/migrations/0010_pending_cancellation.js +41 -0
- package/dist/cjs/migrations/0010_pending_cancellation.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestStores.d.ts +89 -0
- package/dist/cjs/test/TestStores.d.ts.map +1 -0
- package/dist/cjs/test/TestStores.js +98 -0
- package/dist/cjs/test/TestStores.js.map +7 -0
- package/dist/esm/ArtifactGc.d.ts +235 -0
- package/dist/esm/ArtifactGc.d.ts.map +1 -0
- package/dist/esm/ArtifactGc.js +268 -0
- package/dist/esm/ArtifactGc.js.map +1 -0
- package/dist/esm/ArtifactSync.d.ts +164 -0
- package/dist/esm/ArtifactSync.d.ts.map +1 -0
- package/dist/esm/ArtifactSync.js +176 -0
- package/dist/esm/ArtifactSync.js.map +1 -0
- package/dist/esm/CacheSync.d.ts +112 -0
- package/dist/esm/CacheSync.d.ts.map +1 -0
- package/dist/esm/CacheSync.js +94 -0
- package/dist/esm/CacheSync.js.map +1 -0
- package/dist/esm/DeferredClockFold.d.ts +354 -0
- package/dist/esm/DeferredClockFold.d.ts.map +1 -0
- package/dist/esm/DeferredClockFold.js +472 -0
- package/dist/esm/DeferredClockFold.js.map +1 -0
- package/dist/esm/DisasterRecovery.d.ts +431 -0
- package/dist/esm/DisasterRecovery.d.ts.map +1 -0
- package/dist/esm/DisasterRecovery.js +523 -0
- package/dist/esm/DisasterRecovery.js.map +1 -0
- package/dist/esm/DurableEngineState.d.ts +776 -0
- package/dist/esm/DurableEngineState.d.ts.map +1 -0
- package/dist/esm/DurableEngineState.js +1648 -0
- package/dist/esm/DurableEngineState.js.map +1 -0
- package/dist/esm/EngineStore.d.ts +158 -0
- package/dist/esm/EngineStore.d.ts.map +1 -0
- package/dist/esm/EngineStore.js +403 -0
- package/dist/esm/EngineStore.js.map +1 -0
- package/dist/esm/EngineStoreMetrics.d.ts +276 -0
- package/dist/esm/EngineStoreMetrics.d.ts.map +1 -0
- package/dist/esm/EngineStoreMetrics.js +260 -0
- package/dist/esm/EngineStoreMetrics.js.map +1 -0
- package/dist/esm/Errors.d.ts +22 -0
- package/dist/esm/Errors.d.ts.map +1 -0
- package/dist/esm/Errors.js +22 -0
- package/dist/esm/Errors.js.map +1 -0
- package/dist/esm/EventTypes.d.ts +27 -0
- package/dist/esm/EventTypes.d.ts.map +1 -0
- package/dist/esm/EventTypes.js +27 -0
- package/dist/esm/EventTypes.js.map +1 -0
- package/dist/esm/ExecutionFacts.d.ts +59 -0
- package/dist/esm/ExecutionFacts.d.ts.map +1 -0
- package/dist/esm/ExecutionFacts.js +96 -0
- package/dist/esm/ExecutionFacts.js.map +1 -0
- package/dist/esm/ExecutionSnapshot.d.ts +117 -0
- package/dist/esm/ExecutionSnapshot.d.ts.map +1 -0
- package/dist/esm/ExecutionSnapshot.js +95 -0
- package/dist/esm/ExecutionSnapshot.js.map +1 -0
- package/dist/esm/Inconsistency.d.ts +154 -0
- package/dist/esm/Inconsistency.d.ts.map +1 -0
- package/dist/esm/Inconsistency.js +133 -0
- package/dist/esm/Inconsistency.js.map +1 -0
- package/dist/esm/Migrations.d.ts +56 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +91 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/OwnerIdentity.d.ts +91 -0
- package/dist/esm/OwnerIdentity.d.ts.map +1 -0
- package/dist/esm/OwnerIdentity.js +99 -0
- package/dist/esm/OwnerIdentity.js.map +1 -0
- package/dist/esm/PlanInputStore.d.ts +110 -0
- package/dist/esm/PlanInputStore.d.ts.map +1 -0
- package/dist/esm/PlanInputStore.js +207 -0
- package/dist/esm/PlanInputStore.js.map +1 -0
- package/dist/esm/PlanMergeStore.d.ts +113 -0
- package/dist/esm/PlanMergeStore.d.ts.map +1 -0
- package/dist/esm/PlanMergeStore.js +244 -0
- package/dist/esm/PlanMergeStore.js.map +1 -0
- package/dist/esm/PlanScheduler.d.ts +363 -0
- package/dist/esm/PlanScheduler.d.ts.map +1 -0
- package/dist/esm/PlanScheduler.js +1321 -0
- package/dist/esm/PlanScheduler.js.map +1 -0
- package/dist/esm/Reconciliation.d.ts +175 -0
- package/dist/esm/Reconciliation.d.ts.map +1 -0
- package/dist/esm/Reconciliation.js +175 -0
- package/dist/esm/Reconciliation.js.map +1 -0
- package/dist/esm/ReplayOnly.d.ts +91 -0
- package/dist/esm/ReplayOnly.d.ts.map +1 -0
- package/dist/esm/ReplayOnly.js +59 -0
- package/dist/esm/ReplayOnly.js.map +1 -0
- package/dist/esm/Retention.d.ts +113 -0
- package/dist/esm/Retention.d.ts.map +1 -0
- package/dist/esm/Retention.js +139 -0
- package/dist/esm/Retention.js.map +1 -0
- package/dist/esm/RunCatalogRead.d.ts +17 -0
- package/dist/esm/RunCatalogRead.d.ts.map +1 -0
- package/dist/esm/RunCatalogRead.js +17 -0
- package/dist/esm/RunCatalogRead.js.map +1 -0
- package/dist/esm/RunChangeFeed.d.ts +75 -0
- package/dist/esm/RunChangeFeed.d.ts.map +1 -0
- package/dist/esm/RunChangeFeed.js +81 -0
- package/dist/esm/RunChangeFeed.js.map +1 -0
- package/dist/esm/RunState.d.ts +116 -0
- package/dist/esm/RunState.d.ts.map +1 -0
- package/dist/esm/RunState.js +109 -0
- package/dist/esm/RunState.js.map +1 -0
- package/dist/esm/Selection.d.ts +495 -0
- package/dist/esm/Selection.d.ts.map +1 -0
- package/dist/esm/Selection.js +482 -0
- package/dist/esm/Selection.js.map +1 -0
- package/dist/esm/SelectionStore.d.ts +141 -0
- package/dist/esm/SelectionStore.d.ts.map +1 -0
- package/dist/esm/SelectionStore.js +215 -0
- package/dist/esm/SelectionStore.js.map +1 -0
- package/dist/esm/StepBoundary.d.ts +501 -0
- package/dist/esm/StepBoundary.d.ts.map +1 -0
- package/dist/esm/StepBoundary.js +919 -0
- package/dist/esm/StepBoundary.js.map +1 -0
- package/dist/esm/StepCacheFold.d.ts +148 -0
- package/dist/esm/StepCacheFold.d.ts.map +1 -0
- package/dist/esm/StepCacheFold.js +201 -0
- package/dist/esm/StepCacheFold.js.map +1 -0
- package/dist/esm/StepSandbox.d.ts +78 -0
- package/dist/esm/StepSandbox.d.ts.map +1 -0
- package/dist/esm/StepSandbox.js +70 -0
- package/dist/esm/StepSandbox.js.map +1 -0
- package/dist/esm/WakeBus.d.ts +116 -0
- package/dist/esm/WakeBus.d.ts.map +1 -0
- package/dist/esm/WakeBus.js +121 -0
- package/dist/esm/WakeBus.js.map +1 -0
- package/dist/esm/WorkspaceSandbox.d.ts +619 -0
- package/dist/esm/WorkspaceSandbox.d.ts.map +1 -0
- package/dist/esm/WorkspaceSandbox.js +985 -0
- package/dist/esm/WorkspaceSandbox.js.map +1 -0
- package/dist/esm/index.d.ts +157 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +157 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ActionPersistence.d.ts +276 -0
- package/dist/esm/internal/ActionPersistence.d.ts.map +1 -0
- package/dist/esm/internal/ActionPersistence.js +2306 -0
- package/dist/esm/internal/ActionPersistence.js.map +1 -0
- package/dist/esm/internal/ArtifactRoots.d.ts +38 -0
- package/dist/esm/internal/ArtifactRoots.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactRoots.js +76 -0
- package/dist/esm/internal/ArtifactRoots.js.map +1 -0
- package/dist/esm/internal/AttemptAdmission.d.ts +53 -0
- package/dist/esm/internal/AttemptAdmission.d.ts.map +1 -0
- package/dist/esm/internal/AttemptAdmission.js +52 -0
- package/dist/esm/internal/AttemptAdmission.js.map +1 -0
- package/dist/esm/internal/AttemptLifecycle.d.ts +245 -0
- package/dist/esm/internal/AttemptLifecycle.d.ts.map +1 -0
- package/dist/esm/internal/AttemptLifecycle.js +104 -0
- package/dist/esm/internal/AttemptLifecycle.js.map +1 -0
- package/dist/esm/internal/AttemptProbe.d.ts +53 -0
- package/dist/esm/internal/AttemptProbe.d.ts.map +1 -0
- package/dist/esm/internal/AttemptProbe.js +83 -0
- package/dist/esm/internal/AttemptProbe.js.map +1 -0
- package/dist/esm/internal/CacheAdmission.d.ts +79 -0
- package/dist/esm/internal/CacheAdmission.d.ts.map +1 -0
- package/dist/esm/internal/CacheAdmission.js +72 -0
- package/dist/esm/internal/CacheAdmission.js.map +1 -0
- package/dist/esm/internal/CacheAgeHistory.d.ts +27 -0
- package/dist/esm/internal/CacheAgeHistory.d.ts.map +1 -0
- package/dist/esm/internal/CacheAgeHistory.js +49 -0
- package/dist/esm/internal/CacheAgeHistory.js.map +1 -0
- package/dist/esm/internal/CacheAgeVerdicts.d.ts +15 -0
- package/dist/esm/internal/CacheAgeVerdicts.d.ts.map +1 -0
- package/dist/esm/internal/CacheAgeVerdicts.js +54 -0
- package/dist/esm/internal/CacheAgeVerdicts.js.map +1 -0
- package/dist/esm/internal/CacheOutputPolicy.d.ts +24 -0
- package/dist/esm/internal/CacheOutputPolicy.d.ts.map +1 -0
- package/dist/esm/internal/CacheOutputPolicy.js +73 -0
- package/dist/esm/internal/CacheOutputPolicy.js.map +1 -0
- package/dist/esm/internal/CachePublication.d.ts +97 -0
- package/dist/esm/internal/CachePublication.d.ts.map +1 -0
- package/dist/esm/internal/CachePublication.js +116 -0
- package/dist/esm/internal/CachePublication.js.map +1 -0
- package/dist/esm/internal/CallFacts.d.ts +15 -0
- package/dist/esm/internal/CallFacts.d.ts.map +1 -0
- package/dist/esm/internal/CallFacts.js +83 -0
- package/dist/esm/internal/CallFacts.js.map +1 -0
- package/dist/esm/internal/CopiedRecord.d.ts +61 -0
- package/dist/esm/internal/CopiedRecord.d.ts.map +1 -0
- package/dist/esm/internal/CopiedRecord.js +165 -0
- package/dist/esm/internal/CopiedRecord.js.map +1 -0
- package/dist/esm/internal/DeferredPersistence.d.ts +108 -0
- package/dist/esm/internal/DeferredPersistence.d.ts.map +1 -0
- package/dist/esm/internal/DeferredPersistence.js +240 -0
- package/dist/esm/internal/DeferredPersistence.js.map +1 -0
- package/dist/esm/internal/EffectRecords.d.ts +92 -0
- package/dist/esm/internal/EffectRecords.d.ts.map +1 -0
- package/dist/esm/internal/EffectRecords.js +94 -0
- package/dist/esm/internal/EffectRecords.js.map +1 -0
- package/dist/esm/internal/EngineJj.d.ts +30 -0
- package/dist/esm/internal/EngineJj.d.ts.map +1 -0
- package/dist/esm/internal/EngineJj.js +29 -0
- package/dist/esm/internal/EngineJj.js.map +1 -0
- package/dist/esm/internal/EngineStateSchema.d.ts +68 -0
- package/dist/esm/internal/EngineStateSchema.d.ts.map +1 -0
- package/dist/esm/internal/EngineStateSchema.js +53 -0
- package/dist/esm/internal/EngineStateSchema.js.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRead.d.ts +162 -0
- package/dist/esm/internal/ExecutionSnapshotRead.d.ts.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRead.js +218 -0
- package/dist/esm/internal/ExecutionSnapshotRead.js.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.d.ts +39 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.js +81 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.js.map +1 -0
- package/dist/esm/internal/ExitEncoding.d.ts +180 -0
- package/dist/esm/internal/ExitEncoding.d.ts.map +1 -0
- package/dist/esm/internal/ExitEncoding.js +361 -0
- package/dist/esm/internal/ExitEncoding.js.map +1 -0
- package/dist/esm/internal/FileBoundarySnapshot.d.ts +14 -0
- package/dist/esm/internal/FileBoundarySnapshot.d.ts.map +1 -0
- package/dist/esm/internal/FileBoundarySnapshot.js +29 -0
- package/dist/esm/internal/FileBoundarySnapshot.js.map +1 -0
- package/dist/esm/internal/FileEnumeration.d.ts +127 -0
- package/dist/esm/internal/FileEnumeration.d.ts.map +1 -0
- package/dist/esm/internal/FileEnumeration.js +236 -0
- package/dist/esm/internal/FileEnumeration.js.map +1 -0
- package/dist/esm/internal/HostReflection.d.ts +80 -0
- package/dist/esm/internal/HostReflection.d.ts.map +1 -0
- package/dist/esm/internal/HostReflection.js +98 -0
- package/dist/esm/internal/HostReflection.js.map +1 -0
- package/dist/esm/internal/JournalRecords.d.ts +325 -0
- package/dist/esm/internal/JournalRecords.d.ts.map +1 -0
- package/dist/esm/internal/JournalRecords.js +306 -0
- package/dist/esm/internal/JournalRecords.js.map +1 -0
- package/dist/esm/internal/NodeJournal.d.ts +157 -0
- package/dist/esm/internal/NodeJournal.d.ts.map +1 -0
- package/dist/esm/internal/NodeJournal.js +298 -0
- package/dist/esm/internal/NodeJournal.js.map +1 -0
- package/dist/esm/internal/Ordering.d.ts +14 -0
- package/dist/esm/internal/Ordering.d.ts.map +1 -0
- package/dist/esm/internal/Ordering.js +14 -0
- package/dist/esm/internal/Ordering.js.map +1 -0
- package/dist/esm/internal/PlanInputs.d.ts +14 -0
- package/dist/esm/internal/PlanInputs.d.ts.map +1 -0
- package/dist/esm/internal/PlanInputs.js +57 -0
- package/dist/esm/internal/PlanInputs.js.map +1 -0
- package/dist/esm/internal/PlanMerges.d.ts +178 -0
- package/dist/esm/internal/PlanMerges.d.ts.map +1 -0
- package/dist/esm/internal/PlanMerges.js +113 -0
- package/dist/esm/internal/PlanMerges.js.map +1 -0
- package/dist/esm/internal/ProvenanceSlot.d.ts +30 -0
- package/dist/esm/internal/ProvenanceSlot.d.ts.map +1 -0
- package/dist/esm/internal/ProvenanceSlot.js +39 -0
- package/dist/esm/internal/ProvenanceSlot.js.map +1 -0
- package/dist/esm/internal/ResultEnvelope.d.ts +88 -0
- package/dist/esm/internal/ResultEnvelope.d.ts.map +1 -0
- package/dist/esm/internal/ResultEnvelope.js +83 -0
- package/dist/esm/internal/ResultEnvelope.js.map +1 -0
- package/dist/esm/internal/RetentionOps.d.ts +414 -0
- package/dist/esm/internal/RetentionOps.d.ts.map +1 -0
- package/dist/esm/internal/RetentionOps.js +576 -0
- package/dist/esm/internal/RetentionOps.js.map +1 -0
- package/dist/esm/internal/RunCatalogOps.d.ts +118 -0
- package/dist/esm/internal/RunCatalogOps.d.ts.map +1 -0
- package/dist/esm/internal/RunCatalogOps.js +120 -0
- package/dist/esm/internal/RunCatalogOps.js.map +1 -0
- package/dist/esm/internal/RunCoordinator.d.ts +78 -0
- package/dist/esm/internal/RunCoordinator.d.ts.map +1 -0
- package/dist/esm/internal/RunCoordinator.js +115 -0
- package/dist/esm/internal/RunCoordinator.js.map +1 -0
- package/dist/esm/internal/RunDriver.d.ts +192 -0
- package/dist/esm/internal/RunDriver.d.ts.map +1 -0
- package/dist/esm/internal/RunDriver.js +2334 -0
- package/dist/esm/internal/RunDriver.js.map +1 -0
- package/dist/esm/internal/RunLifecycle.d.ts +22 -0
- package/dist/esm/internal/RunLifecycle.d.ts.map +1 -0
- package/dist/esm/internal/RunLifecycle.js +49 -0
- package/dist/esm/internal/RunLifecycle.js.map +1 -0
- package/dist/esm/internal/RunListing.d.ts +78 -0
- package/dist/esm/internal/RunListing.d.ts.map +1 -0
- package/dist/esm/internal/RunListing.js +146 -0
- package/dist/esm/internal/RunListing.js.map +1 -0
- package/dist/esm/internal/RunListingError.d.ts +35 -0
- package/dist/esm/internal/RunListingError.d.ts.map +1 -0
- package/dist/esm/internal/RunListingError.js +27 -0
- package/dist/esm/internal/RunListingError.js.map +1 -0
- package/dist/esm/internal/RuntimeGraph.d.ts +73 -0
- package/dist/esm/internal/RuntimeGraph.d.ts.map +1 -0
- package/dist/esm/internal/RuntimeGraph.js +182 -0
- package/dist/esm/internal/RuntimeGraph.js.map +1 -0
- package/dist/esm/internal/SandboxedExecution.d.ts +80 -0
- package/dist/esm/internal/SandboxedExecution.d.ts.map +1 -0
- package/dist/esm/internal/SandboxedExecution.js +107 -0
- package/dist/esm/internal/SandboxedExecution.js.map +1 -0
- package/dist/esm/internal/StateTransaction.d.ts +44 -0
- package/dist/esm/internal/StateTransaction.d.ts.map +1 -0
- package/dist/esm/internal/StateTransaction.js +54 -0
- package/dist/esm/internal/StateTransaction.js.map +1 -0
- package/dist/esm/internal/StepFacts.d.ts +14 -0
- package/dist/esm/internal/StepFacts.d.ts.map +1 -0
- package/dist/esm/internal/StepFacts.js +35 -0
- package/dist/esm/internal/StepFacts.js.map +1 -0
- package/dist/esm/internal/TypedEvents.d.ts +14 -0
- package/dist/esm/internal/TypedEvents.d.ts.map +1 -0
- package/dist/esm/internal/TypedEvents.js +14 -0
- package/dist/esm/internal/TypedEvents.js.map +1 -0
- package/dist/esm/migrations/0001_initial.d.ts +17 -0
- package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
- package/dist/esm/migrations/0001_initial.js +41 -0
- package/dist/esm/migrations/0001_initial.js.map +1 -0
- package/dist/esm/migrations/0002_selection_store.d.ts +15 -0
- package/dist/esm/migrations/0002_selection_store.d.ts.map +1 -0
- package/dist/esm/migrations/0002_selection_store.js +34 -0
- package/dist/esm/migrations/0002_selection_store.js.map +1 -0
- package/dist/esm/migrations/0003_plan_inputs.d.ts +13 -0
- package/dist/esm/migrations/0003_plan_inputs.d.ts.map +1 -0
- package/dist/esm/migrations/0003_plan_inputs.js +70 -0
- package/dist/esm/migrations/0003_plan_inputs.js.map +1 -0
- package/dist/esm/migrations/0004_plan_environment.d.ts +14 -0
- package/dist/esm/migrations/0004_plan_environment.d.ts.map +1 -0
- package/dist/esm/migrations/0004_plan_environment.js +33 -0
- package/dist/esm/migrations/0004_plan_environment.js.map +1 -0
- package/dist/esm/migrations/0005_plan_merges.d.ts +14 -0
- package/dist/esm/migrations/0005_plan_merges.d.ts.map +1 -0
- package/dist/esm/migrations/0005_plan_merges.js +59 -0
- package/dist/esm/migrations/0005_plan_merges.js.map +1 -0
- package/dist/esm/migrations/0006_execution_listing.d.ts +15 -0
- package/dist/esm/migrations/0006_execution_listing.d.ts.map +1 -0
- package/dist/esm/migrations/0006_execution_listing.js +81 -0
- package/dist/esm/migrations/0006_execution_listing.js.map +1 -0
- package/dist/esm/migrations/0007_run_parent_sequence.d.ts +15 -0
- package/dist/esm/migrations/0007_run_parent_sequence.d.ts.map +1 -0
- package/dist/esm/migrations/0007_run_parent_sequence.js +18 -0
- package/dist/esm/migrations/0007_run_parent_sequence.js.map +1 -0
- package/dist/esm/migrations/0008_deferred_consumption.d.ts +16 -0
- package/dist/esm/migrations/0008_deferred_consumption.d.ts.map +1 -0
- package/dist/esm/migrations/0008_deferred_consumption.js +24 -0
- package/dist/esm/migrations/0008_deferred_consumption.js.map +1 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.d.ts +24 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.js +127 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.js.map +1 -0
- package/dist/esm/migrations/0010_pending_cancellation.d.ts +15 -0
- package/dist/esm/migrations/0010_pending_cancellation.d.ts.map +1 -0
- package/dist/esm/migrations/0010_pending_cancellation.js +19 -0
- package/dist/esm/migrations/0010_pending_cancellation.js.map +1 -0
- package/dist/esm/test/TestStores.d.ts +89 -0
- package/dist/esm/test/TestStores.d.ts.map +1 -0
- package/dist/esm/test/TestStores.js +103 -0
- package/dist/esm/test/TestStores.js.map +1 -0
- package/docs/README.md +113 -0
- package/docs/api.md +1494 -0
- package/docs/concepts/attempts-and-replay.md +149 -0
- package/docs/concepts/cache-admission.md +265 -0
- package/docs/concepts/call-facts.md +86 -0
- package/docs/concepts/durable-waits.md +175 -0
- package/docs/concepts/execution-facts.md +90 -0
- package/docs/concepts/ownership-and-fencing.md +128 -0
- package/docs/concepts/step-boundaries.md +148 -0
- package/docs/concepts/workspace-transactions.md +199 -0
- package/docs/event-types.md +14 -0
- package/docs/guides/back-up-and-restore.md +164 -0
- package/docs/guides/collect-unreferenced-artifacts.md +128 -0
- package/docs/guides/compose-a-durable-engine.md +146 -0
- package/docs/guides/coordinate-two-processes.md +120 -0
- package/docs/guides/defer-work-with-selection.md +171 -0
- package/docs/guides/delete-old-run-history.md +154 -0
- package/docs/guides/drive-a-plan.md +314 -0
- package/docs/guides/observe-engine-metrics.md +98 -0
- package/docs/guides/observe-executions.md +187 -0
- package/docs/guides/reclaim-runs-from-a-dead-host.md +133 -0
- package/docs/guides/share-a-cache-across-machines.md +134 -0
- package/docs/guides/testing.md +123 -0
- package/docs/installation.md +102 -0
- package/docs/quickstart.md +215 -0
- package/docs/troubleshooting.md +347 -0
- package/package.json +438 -3
- package/src/ArtifactGc.ts +408 -0
- package/src/ArtifactSync.ts +278 -0
- package/src/CacheSync.ts +131 -0
- package/src/DeferredClockFold.ts +586 -0
- package/src/DisasterRecovery.ts +888 -0
- package/src/DurableEngineState.ts +2656 -0
- package/src/EngineStore.ts +656 -0
- package/src/EngineStoreMetrics.ts +331 -0
- package/src/Errors.ts +28 -0
- package/src/EventTypes.ts +27 -0
- package/src/ExecutionFacts.ts +125 -0
- package/src/ExecutionSnapshot.ts +186 -0
- package/src/Inconsistency.ts +253 -0
- package/src/Migrations.ts +95 -0
- package/src/OwnerIdentity.ts +125 -0
- package/src/PlanInputStore.ts +292 -0
- package/src/PlanMergeStore.ts +330 -0
- package/src/PlanScheduler.ts +1806 -0
- package/src/Reconciliation.ts +224 -0
- package/src/ReplayOnly.ts +95 -0
- package/src/Retention.ts +214 -0
- package/src/RunCatalogRead.ts +27 -0
- package/src/RunChangeFeed.ts +125 -0
- package/src/RunState.ts +137 -0
- package/src/Selection.ts +683 -0
- package/src/SelectionStore.ts +353 -0
- package/src/StepBoundary.ts +1165 -0
- package/src/StepCacheFold.ts +244 -0
- package/src/StepSandbox.ts +110 -0
- package/src/WakeBus.ts +167 -0
- package/src/WorkspaceSandbox.ts +1604 -0
- package/src/index.ts +185 -0
- package/src/internal/ActionPersistence.ts +2687 -0
- package/src/internal/ArtifactRoots.ts +92 -0
- package/src/internal/AttemptAdmission.ts +76 -0
- package/src/internal/AttemptLifecycle.ts +163 -0
- package/src/internal/AttemptProbe.ts +90 -0
- package/src/internal/CacheAdmission.ts +133 -0
- package/src/internal/CacheAgeHistory.ts +58 -0
- package/src/internal/CacheAgeVerdicts.ts +61 -0
- package/src/internal/CacheOutputPolicy.ts +82 -0
- package/src/internal/CachePublication.ts +153 -0
- package/src/internal/CallFacts.ts +87 -0
- package/src/internal/CopiedRecord.ts +201 -0
- package/src/internal/DeferredPersistence.ts +459 -0
- package/src/internal/EffectRecords.ts +136 -0
- package/src/internal/EngineJj.ts +30 -0
- package/src/internal/EngineStateSchema.ts +92 -0
- package/src/internal/ExecutionSnapshotRead.ts +298 -0
- package/src/internal/ExecutionSnapshotRelations.ts +121 -0
- package/src/internal/ExitEncoding.ts +453 -0
- package/src/internal/FileBoundarySnapshot.ts +39 -0
- package/src/internal/FileEnumeration.ts +334 -0
- package/src/internal/HostReflection.ts +128 -0
- package/src/internal/JournalRecords.ts +396 -0
- package/src/internal/NodeJournal.ts +340 -0
- package/src/internal/Ordering.ts +14 -0
- package/src/internal/PlanInputs.ts +62 -0
- package/src/internal/PlanMerges.ts +123 -0
- package/src/internal/ProvenanceSlot.ts +52 -0
- package/src/internal/ResultEnvelope.ts +118 -0
- package/src/internal/RetentionOps.ts +865 -0
- package/src/internal/RunCatalogOps.ts +174 -0
- package/src/internal/RunCoordinator.ts +215 -0
- package/src/internal/RunDriver.ts +3152 -0
- package/src/internal/RunLifecycle.ts +53 -0
- package/src/internal/RunListing.ts +210 -0
- package/src/internal/RunListingError.ts +37 -0
- package/src/internal/RuntimeGraph.ts +253 -0
- package/src/internal/SandboxedExecution.ts +165 -0
- package/src/internal/StateTransaction.ts +71 -0
- package/src/internal/StepFacts.ts +36 -0
- package/src/internal/TypedEvents.ts +29 -0
- package/src/migrations/0001_initial.ts +51 -0
- package/src/migrations/0002_selection_store.ts +38 -0
- package/src/migrations/0003_plan_inputs.ts +75 -0
- package/src/migrations/0004_plan_environment.ts +34 -0
- package/src/migrations/0005_plan_merges.ts +60 -0
- package/src/migrations/0006_execution_listing.ts +86 -0
- package/src/migrations/0007_run_parent_sequence.ts +19 -0
- package/src/migrations/0008_deferred_consumption.ts +27 -0
- package/src/migrations/0009_deferred_clock_facts.ts +163 -0
- package/src/migrations/0010_pending_cancellation.ts +20 -0
- package/src/test/TestStores.ts +145 -0
|
@@ -0,0 +1,2656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable deferred-completion and clock-deadline state used by the flow
|
|
3
|
+
* engine adapter.
|
|
4
|
+
*
|
|
5
|
+
* A parked run keeps the `suspended` status the run store admits; there is
|
|
6
|
+
* no `waiting` status. The waiting taxonomy lives beside it on the run row
|
|
7
|
+
* in the `waiting_reason`, `waiting_wake_at_ms`, `waiting_token`, and
|
|
8
|
+
* `waiting_request` columns, written by `park` and cleared by `wake`. See
|
|
9
|
+
* `docs/concepts/durable-waits.md`.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as Dialect from "@smthrs/database/Dialect"
|
|
15
|
+
import { afterCommit, DatabaseError, DurableWriter } from "@smthrs/database/DurableWriter"
|
|
16
|
+
import { contentDigest } from "@smthrs/journal/JournalEvent"
|
|
17
|
+
import type { OwnerId } from "@smthrs/run-store/Ownership"
|
|
18
|
+
import * as Cause from "effect/Cause"
|
|
19
|
+
import * as Context from "effect/Context"
|
|
20
|
+
import * as Effect from "effect/Effect"
|
|
21
|
+
import * as Exit from "effect/Exit"
|
|
22
|
+
import * as Layer from "effect/Layer"
|
|
23
|
+
import * as Option from "effect/Option"
|
|
24
|
+
import * as Result from "effect/Result"
|
|
25
|
+
import * as Schema from "effect/Schema"
|
|
26
|
+
import * as Semaphore from "effect/Semaphore"
|
|
27
|
+
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
|
28
|
+
import * as EngineStateSchema from "./internal/EngineStateSchema.ts"
|
|
29
|
+
import { compareText } from "./internal/Ordering.ts"
|
|
30
|
+
import type { OnParentExit } from "./RunState.ts"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A position in pending admission creation order.
|
|
34
|
+
*
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
* @category models
|
|
37
|
+
*/
|
|
38
|
+
export interface PendingRunCursor {
|
|
39
|
+
readonly runId: string
|
|
40
|
+
readonly createdAtMs: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pendingCancellationLimit = (limit: number): Effect.Effect<void> =>
|
|
44
|
+
Number.isSafeInteger(limit) && limit >= 0
|
|
45
|
+
? Effect.void
|
|
46
|
+
: Effect.die(new RangeError("Pending cancellation limit must be a non-negative safe integer"))
|
|
47
|
+
|
|
48
|
+
/** JSON text carrying an arbitrary decoded value. */
|
|
49
|
+
const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown)
|
|
50
|
+
|
|
51
|
+
/** @private */
|
|
52
|
+
const NonNegativeSafeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The durable address of a deferred result.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
* @category models
|
|
59
|
+
*/
|
|
60
|
+
export const DeferredAddress = Schema.Struct({
|
|
61
|
+
flowName: Schema.NonEmptyString,
|
|
62
|
+
executionId: Schema.NonEmptyString,
|
|
63
|
+
deferredName: Schema.NonEmptyString
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The durable address of a deferred result.
|
|
68
|
+
*
|
|
69
|
+
* @category models
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
export type DeferredAddress = typeof DeferredAddress.Type
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The first durable completion recorded for a deferred.
|
|
76
|
+
*
|
|
77
|
+
* Correlation data remains opaque at this boundary. A future external-event
|
|
78
|
+
* layer owns its schema and authorization policy.
|
|
79
|
+
*
|
|
80
|
+
* @since 0.1.0
|
|
81
|
+
* @category models
|
|
82
|
+
*/
|
|
83
|
+
export const DeferredRow = Schema.Struct({
|
|
84
|
+
...DeferredAddress.fields,
|
|
85
|
+
exit: Schema.Unknown,
|
|
86
|
+
metadata: Schema.optionalKey(Schema.Unknown),
|
|
87
|
+
completedAtMs: NonNegativeSafeInt
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The first durable completion recorded for a deferred.
|
|
92
|
+
*
|
|
93
|
+
* @category models
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export type DeferredRow = typeof DeferredRow.Type
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Result of a first-writer-wins deferred completion.
|
|
100
|
+
*
|
|
101
|
+
* @since 0.1.0
|
|
102
|
+
* @category models
|
|
103
|
+
*/
|
|
104
|
+
export type CompleteDeferredOutcome =
|
|
105
|
+
| { readonly _tag: "Completed"; readonly row: DeferredRow; readonly digest: DeferredDigest }
|
|
106
|
+
| { readonly _tag: "Existing"; readonly row: DeferredRow; readonly digest: DeferredDigest }
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The `JournalEvent.contentDigest` of a stored completion's encoded `exit`
|
|
110
|
+
* and `metadata` text. The journal fact that announces a completion carries
|
|
111
|
+
* these digests instead of the values, which stay in the row.
|
|
112
|
+
*
|
|
113
|
+
* @since 1.0.0
|
|
114
|
+
* @category models
|
|
115
|
+
*/
|
|
116
|
+
export interface DeferredDigest {
|
|
117
|
+
readonly exit: string
|
|
118
|
+
readonly metadata: string | null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The durable address of a clock.
|
|
123
|
+
*
|
|
124
|
+
* @since 0.1.0
|
|
125
|
+
* @category models
|
|
126
|
+
*/
|
|
127
|
+
export const ClockAddress = Schema.Struct({
|
|
128
|
+
flowName: Schema.NonEmptyString,
|
|
129
|
+
executionId: Schema.NonEmptyString,
|
|
130
|
+
clockName: Schema.NonEmptyString
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The durable address of a clock.
|
|
135
|
+
*
|
|
136
|
+
* @category models
|
|
137
|
+
* @since 0.1.0
|
|
138
|
+
*/
|
|
139
|
+
export type ClockAddress = typeof ClockAddress.Type
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A durable absolute clock deadline.
|
|
143
|
+
*
|
|
144
|
+
* @since 0.1.0
|
|
145
|
+
* @category models
|
|
146
|
+
*/
|
|
147
|
+
export const ClockRow = Schema.Struct({
|
|
148
|
+
...ClockAddress.fields,
|
|
149
|
+
deferredName: Schema.NonEmptyString,
|
|
150
|
+
dueAtMs: NonNegativeSafeInt,
|
|
151
|
+
completedAtMs: Schema.NullOr(NonNegativeSafeInt)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A durable absolute clock deadline.
|
|
156
|
+
*
|
|
157
|
+
* @category models
|
|
158
|
+
* @since 0.1.0
|
|
159
|
+
*/
|
|
160
|
+
export type ClockRow = typeof ClockRow.Type
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Result of scheduling a durable clock.
|
|
164
|
+
*
|
|
165
|
+
* @since 0.1.0
|
|
166
|
+
* @category models
|
|
167
|
+
*/
|
|
168
|
+
export type ScheduleClockOutcome =
|
|
169
|
+
| { readonly _tag: "Scheduled"; readonly row: ClockRow }
|
|
170
|
+
| { readonly _tag: "Existing"; readonly row: ClockRow }
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Result of completing a durable clock.
|
|
174
|
+
*
|
|
175
|
+
* @since 0.1.0
|
|
176
|
+
* @category models
|
|
177
|
+
*/
|
|
178
|
+
export type CompleteClockOutcome =
|
|
179
|
+
| { readonly _tag: "Completed"; readonly row: ClockRow }
|
|
180
|
+
| { readonly _tag: "AlreadyCompleted"; readonly row: ClockRow }
|
|
181
|
+
| { readonly _tag: "NotFound" }
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The core wait-reason vocabulary a supervisor understands for wake policy.
|
|
185
|
+
*
|
|
186
|
+
* - `approval` — parked until a human (or policy) approves; woken by token.
|
|
187
|
+
* - `event` — parked on an external event / deferred completion.
|
|
188
|
+
* - `timer` — parked until `wakeAt`; woken by the durable clock sweep.
|
|
189
|
+
* - `quota` — parked on provider quota; woken at `wakeAt` when known.
|
|
190
|
+
* - `released` — the owning process released the run without settling it
|
|
191
|
+
* (shutdown, heartbeat self-interrupt; issue #39). A released row has no
|
|
192
|
+
* held lease and no `wakeAt`: any supervisor/sweeper MUST scan for this
|
|
193
|
+
* reason explicitly and re-drive the run through the claim path, or
|
|
194
|
+
* released runs are stranded forever (issue #67).
|
|
195
|
+
* - `quarantine` — parked because a SUCCEEDED attempt row's recorded
|
|
196
|
+
* evidence measured corrupt under the strict verdict (issue #171). The
|
|
197
|
+
* row cannot be evicted/re-executed like a cache row (its side effects
|
|
198
|
+
* already ran), so the corrupt boundary evidence is quarantined off the
|
|
199
|
+
* row while its durable outcome stays intact. No sweeper auto-wakes this
|
|
200
|
+
* reason: the strict integrity verdict remains visible until an explicit
|
|
201
|
+
* resume, which completes from the recorded outcome without re-execution.
|
|
202
|
+
*
|
|
203
|
+
* Left open (`string & {}`) so a plugin can park on a reason the core
|
|
204
|
+
* taxonomy has not named yet — the store persists whatever it is given
|
|
205
|
+
* rather than rejecting unknown reasons.
|
|
206
|
+
*
|
|
207
|
+
* @since 0.1.0
|
|
208
|
+
* @category models
|
|
209
|
+
*/
|
|
210
|
+
export const WaitingReason = Schema.NonEmptyString
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The open wait-reason vocabulary.
|
|
214
|
+
*
|
|
215
|
+
* @category models
|
|
216
|
+
* @since 0.1.0
|
|
217
|
+
*/
|
|
218
|
+
export type WaitingReason = typeof WaitingReason.Type
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The payload recorded when a run parks.
|
|
222
|
+
*
|
|
223
|
+
* `reason` and `wakeAt` earn columns because a supervisor sweeper queries
|
|
224
|
+
* them (`WHERE waiting_reason = 'quota' AND waiting_wake_at_ms <= ?`);
|
|
225
|
+
* `token` is compare-and-swap/lookup material a wake handler matches
|
|
226
|
+
* against.
|
|
227
|
+
*
|
|
228
|
+
* @since 0.1.0
|
|
229
|
+
* @category models
|
|
230
|
+
*/
|
|
231
|
+
export const Waiting = Schema.Struct({
|
|
232
|
+
reason: WaitingReason,
|
|
233
|
+
wakeAt: Schema.optional(NonNegativeSafeInt),
|
|
234
|
+
token: Schema.optional(Schema.NonEmptyString),
|
|
235
|
+
/**
|
|
236
|
+
* What the wait declared about itself: for a `HumanTask` park, the kind of
|
|
237
|
+
* answer it wants, the prompt, and the attempt budget.
|
|
238
|
+
*
|
|
239
|
+
* `reason` and `token` say that a person owes the run an answer and which
|
|
240
|
+
* wait point holds it. Neither says what was asked, and the question lives
|
|
241
|
+
* in an action payload of an execution that has released its owner, so a
|
|
242
|
+
* reader had nothing to render but the wait's own name. This is the question
|
|
243
|
+
* itself, as the JSON TEXT the declaring flow wrote, stored and cleared with
|
|
244
|
+
* the other waiting columns.
|
|
245
|
+
*/
|
|
246
|
+
request: Schema.optional(Schema.String)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The payload recorded when a run parks.
|
|
251
|
+
*
|
|
252
|
+
* @category models
|
|
253
|
+
* @since 0.1.0
|
|
254
|
+
*/
|
|
255
|
+
export type Waiting = typeof Waiting.Type
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A decoded waiting row for a parked run.
|
|
259
|
+
*
|
|
260
|
+
* @since 0.1.0
|
|
261
|
+
* @category models
|
|
262
|
+
*/
|
|
263
|
+
export const WaitingRow = Schema.Struct({
|
|
264
|
+
runId: Schema.NonEmptyString,
|
|
265
|
+
reason: WaitingReason,
|
|
266
|
+
wakeAt: Schema.NullOr(NonNegativeSafeInt),
|
|
267
|
+
token: Schema.NullOr(Schema.NonEmptyString),
|
|
268
|
+
/** The question {@link Waiting} declared, absent on a park that declared none. */
|
|
269
|
+
request: Schema.optionalKey(Schema.Json)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A decoded waiting row for a parked run.
|
|
274
|
+
*
|
|
275
|
+
* @category models
|
|
276
|
+
* @since 0.1.0
|
|
277
|
+
*/
|
|
278
|
+
export type WaitingRow = typeof WaitingRow.Type
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A `running` row whose owner stopped heartbeating, and the lease it froze on.
|
|
282
|
+
*
|
|
283
|
+
* The heartbeat identifies the lease: a sweep that remembers a refusal against
|
|
284
|
+
* one heartbeat must not apply it to a later stall of the same run.
|
|
285
|
+
*
|
|
286
|
+
* @category models
|
|
287
|
+
* @since 1.0.0
|
|
288
|
+
*/
|
|
289
|
+
export interface StaleRunningRow {
|
|
290
|
+
readonly runId: string
|
|
291
|
+
readonly heartbeatAtMs: number
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A predicate over `waitingRuns` — omitted fields are unconstrained.
|
|
296
|
+
*
|
|
297
|
+
* @since 0.1.0
|
|
298
|
+
* @category models
|
|
299
|
+
*/
|
|
300
|
+
export interface WaitingRunsFilter {
|
|
301
|
+
readonly reason?: string
|
|
302
|
+
readonly dueBeforeMs?: number
|
|
303
|
+
/**
|
|
304
|
+
* When `true`, only parked runs whose cancellation was durably requested
|
|
305
|
+
* (`flows_runs.cancel_requested_at_ms IS NOT NULL`). Lets the parked-run
|
|
306
|
+
* sweep fetch actionable rows instead of scanning every parked run and
|
|
307
|
+
* probing each with a `store.get` (issue #68). The in-memory
|
|
308
|
+
* implementation reads the flag from the `runs` view
|
|
309
|
+
* (`MemoryRunView.cancelRequestedAtMs`); without a view it stays
|
|
310
|
+
* permissive and the sweeper's own per-row guard decides.
|
|
311
|
+
*/
|
|
312
|
+
readonly cancelRequested?: boolean
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Result of recording that a run parked on a waiting reason.
|
|
317
|
+
*
|
|
318
|
+
* @since 0.1.0
|
|
319
|
+
* @category models
|
|
320
|
+
*/
|
|
321
|
+
export type ParkOutcome =
|
|
322
|
+
| { readonly _tag: "Parked"; readonly row: WaitingRow }
|
|
323
|
+
| { readonly _tag: "NotFound" }
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Result of clearing a run's waiting payload on wake.
|
|
327
|
+
*
|
|
328
|
+
* @since 0.1.0
|
|
329
|
+
* @category models
|
|
330
|
+
*/
|
|
331
|
+
export type WakeOutcome =
|
|
332
|
+
| { readonly _tag: "Woken"; readonly row: WaitingRow }
|
|
333
|
+
| { readonly _tag: "NotWaiting" }
|
|
334
|
+
| { readonly _tag: "NotFound" }
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A durable parent edge in the run DAG.
|
|
338
|
+
*
|
|
339
|
+
* The run row's `state_json` carries only the first (creating) parent; a
|
|
340
|
+
* diamond gives a run a second parent the row cannot express. Cycle
|
|
341
|
+
* detection must see every edge from every owner process and across
|
|
342
|
+
* restarts (issues #40/#41), so edges are persisted here. `seq` is a
|
|
343
|
+
* store-global insertion order used only to list a run's parents oldest
|
|
344
|
+
* first (and to make the cycle walk deterministic); it no longer arbitrates
|
|
345
|
+
* anything, so duplicate values would be harmless (issues #54/#66).
|
|
346
|
+
*
|
|
347
|
+
* @since 0.1.0
|
|
348
|
+
* @category models
|
|
349
|
+
*/
|
|
350
|
+
export const RunParentEdge = Schema.Struct({
|
|
351
|
+
childId: Schema.NonEmptyString,
|
|
352
|
+
parentId: Schema.NonEmptyString,
|
|
353
|
+
seq: NonNegativeSafeInt
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A durable parent edge in the run DAG.
|
|
358
|
+
*
|
|
359
|
+
* @category models
|
|
360
|
+
* @since 0.1.0
|
|
361
|
+
*/
|
|
362
|
+
export type RunParentEdge = typeof RunParentEdge.Type
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The surviving attempt rows of one action key: the earliest surviving
|
|
366
|
+
* row's start time and the highest attempt number contiguous from it.
|
|
367
|
+
*
|
|
368
|
+
* @since 0.1.0
|
|
369
|
+
* @category models
|
|
370
|
+
*/
|
|
371
|
+
export const AttemptSurvivors = Schema.Struct({
|
|
372
|
+
/**
|
|
373
|
+
* The lowest surviving attempt number. Anything above 1 means the prune
|
|
374
|
+
* job removed a leading run of attempts; the engine store applies the
|
|
375
|
+
* pruned-prefix tolerance to it so both survivor paths agree (issue #96).
|
|
376
|
+
*/
|
|
377
|
+
earliestAttempt: NonNegativeSafeInt,
|
|
378
|
+
earliestStartedAtMs: NonNegativeSafeInt,
|
|
379
|
+
latest: NonNegativeSafeInt
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The surviving attempt range of one action key.
|
|
384
|
+
*
|
|
385
|
+
* @category models
|
|
386
|
+
* @since 0.1.0
|
|
387
|
+
*/
|
|
388
|
+
export type AttemptSurvivors = typeof AttemptSurvivors.Type
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Recording a parent edge would close a cycle in the run DAG.
|
|
392
|
+
*
|
|
393
|
+
* Raised from inside the same transaction that inserted the edge, which is
|
|
394
|
+
* rolled back: a rejected edge leaves no durable trace anywhere (issues
|
|
395
|
+
* #54/#55/#56). `path` lists execution ids from the child back to itself
|
|
396
|
+
* through its would-be ancestors, matching the engine's
|
|
397
|
+
* `FlowCycleDetected.path` shape.
|
|
398
|
+
*
|
|
399
|
+
* @since 0.1.0
|
|
400
|
+
* @category errors
|
|
401
|
+
*/
|
|
402
|
+
export class RunParentCycleError extends Schema.TaggedError<RunParentCycleError>()(
|
|
403
|
+
"@smthrs/engine-store/RunParentCycleError",
|
|
404
|
+
{
|
|
405
|
+
path: Schema.Array(Schema.String)
|
|
406
|
+
}
|
|
407
|
+
) {}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* A durable engine-state invariant broke: a defect (a bug), never a typed
|
|
411
|
+
* failure.
|
|
412
|
+
*
|
|
413
|
+
* `value_not_serializable` and `value_not_decodable` name the JSON column
|
|
414
|
+
* (`field`) whose value could not be written or read back.
|
|
415
|
+
* `deferred_completion_missing` and `run_parent_edge_missing` mean a row a
|
|
416
|
+
* first-writer transaction just lost to was gone when read.
|
|
417
|
+
*
|
|
418
|
+
* @since 1.0.0
|
|
419
|
+
* @category errors
|
|
420
|
+
*/
|
|
421
|
+
export class EngineStateFault extends Schema.TaggedError<EngineStateFault>()(
|
|
422
|
+
"@smthrs/engine-store/EngineStateFault",
|
|
423
|
+
{
|
|
424
|
+
reason: Schema.Literals([
|
|
425
|
+
"value_not_serializable",
|
|
426
|
+
"value_not_decodable",
|
|
427
|
+
"deferred_completion_missing",
|
|
428
|
+
"run_parent_edge_missing"
|
|
429
|
+
]),
|
|
430
|
+
field: Schema.optional(Schema.String),
|
|
431
|
+
cause: Schema.optional(Schema.Defect())
|
|
432
|
+
}
|
|
433
|
+
) {
|
|
434
|
+
override get message(): string {
|
|
435
|
+
switch (this.reason) {
|
|
436
|
+
case "value_not_serializable":
|
|
437
|
+
return `${this.field} must be JSON-serializable`
|
|
438
|
+
case "value_not_decodable":
|
|
439
|
+
return `could not decode ${this.field}`
|
|
440
|
+
case "deferred_completion_missing":
|
|
441
|
+
return "deferred completion disappeared during first-writer transaction"
|
|
442
|
+
case "run_parent_edge_missing":
|
|
443
|
+
return "run parent edge disappeared during first-writer transaction"
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Result of recording a durable parent edge (idempotent).
|
|
450
|
+
*
|
|
451
|
+
* @since 0.1.0
|
|
452
|
+
* @category models
|
|
453
|
+
*/
|
|
454
|
+
export type RecordRunParentOutcome =
|
|
455
|
+
| { readonly _tag: "Recorded"; readonly edge: RunParentEdge }
|
|
456
|
+
| { readonly _tag: "Existing"; readonly edge: RunParentEdge }
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Minimal durable state missing from the current `@smthrs/journal` contract.
|
|
460
|
+
*
|
|
461
|
+
* A successful mutation means the row is durable. Callers may therefore
|
|
462
|
+
* journal and schedule a wake only after the mutation returns. Deferred and
|
|
463
|
+
* clock state belong to this package for 1.0.
|
|
464
|
+
*
|
|
465
|
+
* @since 0.1.0
|
|
466
|
+
* @category models
|
|
467
|
+
*/
|
|
468
|
+
export interface Service {
|
|
469
|
+
readonly deferred: (address: DeferredAddress) => Effect.Effect<Option.Option<DeferredRow>>
|
|
470
|
+
/**
|
|
471
|
+
* Marks an existing result observed without removing its replay evidence.
|
|
472
|
+
* Returns whether this call consumed it: false when there is no result or
|
|
473
|
+
* it was already consumed.
|
|
474
|
+
*/
|
|
475
|
+
readonly consumeDeferred: (address: DeferredAddress, consumedAtMs: number) => Effect.Effect<boolean>
|
|
476
|
+
readonly completeDeferred: (row: DeferredRow) => Effect.Effect<CompleteDeferredOutcome>
|
|
477
|
+
readonly clock: (address: ClockAddress) => Effect.Effect<Option.Option<ClockRow>>
|
|
478
|
+
/**
|
|
479
|
+
* Schedules a clock while the execution is running under the supplied owner.
|
|
480
|
+
* An existing clock wins unchanged, even after ownership is lost. If no
|
|
481
|
+
* clock exists and the ownership fence fails, the effect self-interrupts.
|
|
482
|
+
*/
|
|
483
|
+
readonly scheduleClock: (row: ClockRow, owner: OwnerId) => Effect.Effect<ScheduleClockOutcome>
|
|
484
|
+
readonly completeClock: (
|
|
485
|
+
address: ClockAddress,
|
|
486
|
+
completedAtMs: number
|
|
487
|
+
) => Effect.Effect<CompleteClockOutcome>
|
|
488
|
+
readonly dueClocks: (nowMs: number) => Effect.Effect<ReadonlyArray<ClockRow>>
|
|
489
|
+
/**
|
|
490
|
+
* Completes every uncompleted clock row of one run in a single statement.
|
|
491
|
+
*
|
|
492
|
+
* A run that settled has no future: a timer that fires against it can only
|
|
493
|
+
* schedule a resume nothing will drive. Terminal transitions call this in
|
|
494
|
+
* their own transaction, so the rows stop being pending with the run rather
|
|
495
|
+
* than lingering as durable work no sweep will ever finish.
|
|
496
|
+
*
|
|
497
|
+
* It takes the run id rather than a clock address because the caller is
|
|
498
|
+
* closing a RUN, and it does not read the run row, so it composes with a
|
|
499
|
+
* terminal transition in either order.
|
|
500
|
+
*/
|
|
501
|
+
readonly completeRunClocks: (
|
|
502
|
+
executionId: string,
|
|
503
|
+
completedAtMs: number
|
|
504
|
+
) => Effect.Effect<ReadonlyArray<ClockRow>>
|
|
505
|
+
/**
|
|
506
|
+
* Lists uncompleted clock rows scoped to an execution or flow, with no
|
|
507
|
+
* due-time bound. Suspension-reason derivation and registration-time
|
|
508
|
+
* recovery use this instead of abusing `dueClocks` as an all-clocks
|
|
509
|
+
* listing (issue #35): `dueClocks` is a due-timer sweeper query and may
|
|
510
|
+
* grow a result cap without affecting park correctness.
|
|
511
|
+
*
|
|
512
|
+
* Rows belonging to a settled run are never listed. Registration sweeps
|
|
513
|
+
* this to re-arm timers, so without the run-status join every restart armed
|
|
514
|
+
* one timer per clock row a flow ever wrote, for runs that ended long ago.
|
|
515
|
+
*/
|
|
516
|
+
readonly pendingClocks: (scope: {
|
|
517
|
+
readonly executionId?: string
|
|
518
|
+
readonly flowName?: string
|
|
519
|
+
}) => Effect.Effect<ReadonlyArray<ClockRow>>
|
|
520
|
+
/**
|
|
521
|
+
* Lists unconsumed completed deferred addresses for registration-time wake recovery.
|
|
522
|
+
* Observed results remain readable through `deferred` but cannot re-drive a
|
|
523
|
+
* run that has moved on to a later wait.
|
|
524
|
+
*
|
|
525
|
+
* Rows belonging to a settled run are never listed, for the reason
|
|
526
|
+
* {@link Service.pendingClocks} gives: this is swept on every registration,
|
|
527
|
+
* and one resume per completion in a flow's whole history is unbounded work
|
|
528
|
+
* that can wake nothing.
|
|
529
|
+
*/
|
|
530
|
+
readonly completedDeferreds: (
|
|
531
|
+
flowName: string
|
|
532
|
+
) => Effect.Effect<ReadonlyArray<DeferredAddress>>
|
|
533
|
+
/**
|
|
534
|
+
* Records the waiting-reason payload for a parked run, fenced to the
|
|
535
|
+
* current owner so a stale process cannot park a run it no longer runs.
|
|
536
|
+
*/
|
|
537
|
+
readonly park: (
|
|
538
|
+
runId: string,
|
|
539
|
+
waiting: Waiting,
|
|
540
|
+
owner: OwnerId
|
|
541
|
+
) => Effect.Effect<ParkOutcome>
|
|
542
|
+
/**
|
|
543
|
+
* Clears a run's waiting payload on wake or resume. Idempotent: waking a
|
|
544
|
+
* run that is not waiting reports `NotWaiting` rather than failing.
|
|
545
|
+
*/
|
|
546
|
+
readonly wake: (runId: string) => Effect.Effect<WakeOutcome>
|
|
547
|
+
/**
|
|
548
|
+
* Reads the current waiting payload for a run, if any.
|
|
549
|
+
*/
|
|
550
|
+
readonly waiting: (runId: string) => Effect.Effect<Option.Option<WaitingRow>>
|
|
551
|
+
/**
|
|
552
|
+
* Reads the waiting payloads of a run AND of every execution beneath it.
|
|
553
|
+
*
|
|
554
|
+
* A flow that calls another flow parks the CHILD execution, not the run an
|
|
555
|
+
* operator named: `coding/request` sat at `waiting_reason = 'event'` while
|
|
556
|
+
* the `HumanTask` a person actually had to answer was three executions down,
|
|
557
|
+
* on `coding/PreparePlan`. Reading one row therefore answered "nothing is
|
|
558
|
+
* waiting on you" about a run tree that was waiting on a person, which is
|
|
559
|
+
* how run-3 parked forever with an empty approvals inbox.
|
|
560
|
+
*
|
|
561
|
+
* The walk follows every `flows_run_parents` spawn edge and `parent_run_id`
|
|
562
|
+
* link, so it sees shared children, forks, and trampoline rounds alike.
|
|
563
|
+
* Each run appears once, ordered by minimum depth, creation time, then run
|
|
564
|
+
* ID; the named run comes first. Rows of a settled execution are never
|
|
565
|
+
* listed — a wait a finished execution left behind is not one a person
|
|
566
|
+
* can answer.
|
|
567
|
+
*
|
|
568
|
+
* A DETACHED child and everything under it are skipped. `onParentExit:
|
|
569
|
+
* "detach"` is a fire-and-forget spawn whose whole point is to outlive the
|
|
570
|
+
* run that started it, so its question is not one the run that started it is
|
|
571
|
+
* waiting on, and reporting it here would tell a reader that a run which can
|
|
572
|
+
* proceed cannot. Every other edge is attached: the run is waiting for that
|
|
573
|
+
* child's value, whether its own row has flipped to `suspended` yet or not.
|
|
574
|
+
* Trampoline links continue the same run, so an inherited detach policy
|
|
575
|
+
* does not hide later rounds from that run's own tree.
|
|
576
|
+
*
|
|
577
|
+
* `{ detached: "include" }` walks detached subtrees too, for a caller asking
|
|
578
|
+
* what an explicit resume of the run would restart rather than what the run
|
|
579
|
+
* itself waits on.
|
|
580
|
+
*/
|
|
581
|
+
readonly waitingTree: (
|
|
582
|
+
runId: string,
|
|
583
|
+
options?: WaitingTreeOptions | undefined
|
|
584
|
+
) => Effect.Effect<ReadonlyArray<WaitingRow>>
|
|
585
|
+
/**
|
|
586
|
+
* Lists parked runs matching an optional reason/due-before filter, ordered
|
|
587
|
+
* for sweeper consumption (earliest wake first, untimed waits last, then
|
|
588
|
+
* run id to break ties).
|
|
589
|
+
*/
|
|
590
|
+
readonly waitingRuns: (
|
|
591
|
+
filter?: WaitingRunsFilter
|
|
592
|
+
) => Effect.Effect<ReadonlyArray<WaitingRow>>
|
|
593
|
+
/** Pending admissions ordered by creation, strictly after and through optional cursors. */
|
|
594
|
+
readonly pendingRuns: (
|
|
595
|
+
flowName: string,
|
|
596
|
+
limit?: number | undefined,
|
|
597
|
+
after?: PendingRunCursor | undefined,
|
|
598
|
+
through?: PendingRunCursor | undefined,
|
|
599
|
+
eligibleBeforeMs?: number | undefined
|
|
600
|
+
) => Effect.Effect<ReadonlyArray<PendingRunCursor>>
|
|
601
|
+
/** Latest pending admission, bounding one finite recovery scan cycle. */
|
|
602
|
+
readonly pendingRunTail: (
|
|
603
|
+
flowName: string,
|
|
604
|
+
eligibleBeforeMs?: number | undefined
|
|
605
|
+
) => Effect.Effect<Option.Option<PendingRunCursor>>
|
|
606
|
+
/** Pending cancellation requests in bounded creation order; limit is zero or a positive safe integer. */
|
|
607
|
+
readonly pendingCancellationRuns: (
|
|
608
|
+
limit: number,
|
|
609
|
+
after?: PendingRunCursor | undefined,
|
|
610
|
+
through?: PendingRunCursor | undefined
|
|
611
|
+
) => Effect.Effect<ReadonlyArray<PendingRunCursor>>
|
|
612
|
+
/** Latest pending cancellation, bounding one finite cleanup scan cycle. */
|
|
613
|
+
readonly pendingCancellationTail: () => Effect.Effect<Option.Option<PendingRunCursor>>
|
|
614
|
+
/**
|
|
615
|
+
* Lists the runs whose row is `running` with a heartbeat strictly older
|
|
616
|
+
* than `staleBeforeMs`, each with that heartbeat — an owner that stopped heartbeating without
|
|
617
|
+
* releasing the run (SIGKILL, OOM, power loss). Nothing else ever
|
|
618
|
+
* revisits such a run: it has no waiting row for the parked-run sweep,
|
|
619
|
+
* no pending clock, and no future deferred completion. A periodic
|
|
620
|
+
* sweeper re-drives these through the ordinary claim/steal path — the
|
|
621
|
+
* analog of Temporal's task-timeout re-dispatch (issue #53).
|
|
622
|
+
*
|
|
623
|
+
* `limit` caps one enumeration (issue #79): the rows come back oldest
|
|
624
|
+
* heartbeat first, so a capped sweep drains a mass owner death across
|
|
625
|
+
* ticks instead of waking every stale run in every driver every tick.
|
|
626
|
+
*/
|
|
627
|
+
readonly staleRunningRuns: (
|
|
628
|
+
staleBeforeMs: number,
|
|
629
|
+
limit?: number | undefined
|
|
630
|
+
) => Effect.Effect<ReadonlyArray<StaleRunningRow>>
|
|
631
|
+
/**
|
|
632
|
+
* The surviving attempt rows for an action key, in one range read: the
|
|
633
|
+
* earliest surviving row's start time (the durable retry origin when
|
|
634
|
+
* attempt 1 itself was pruned, issue #69) and the highest attempt number
|
|
635
|
+
* contiguous from it (the resumed attempt counter, issue #59).
|
|
636
|
+
* `Option.none()` means no attempt row survives.
|
|
637
|
+
*
|
|
638
|
+
* Optional because only storage that can range-scan `flows_attempts`
|
|
639
|
+
* implements it; when absent the engine store falls back to per-attempt
|
|
640
|
+
* point reads against `AttemptStore` (issue #77).
|
|
641
|
+
*/
|
|
642
|
+
readonly attemptSurvivors?:
|
|
643
|
+
| ((
|
|
644
|
+
runId: string,
|
|
645
|
+
stepKeyDigest: string
|
|
646
|
+
) => Effect.Effect<Option.Option<AttemptSurvivors>>)
|
|
647
|
+
| undefined
|
|
648
|
+
/**
|
|
649
|
+
* Durably records a parent edge in the run DAG, first-writer-wins per
|
|
650
|
+
* `(child, parent)` pair.
|
|
651
|
+
*
|
|
652
|
+
* The cycle check happens inside the same transaction as the insert: the
|
|
653
|
+
* edge is inserted, the child's ancestor chain is walked over the durable
|
|
654
|
+
* edges, and on a hit the transaction rolls back and the call fails with
|
|
655
|
+
* `RunParentCycleError`. There is no window between insert and check, no
|
|
656
|
+
* arbitration, and no withdrawal protocol — concurrent writers that
|
|
657
|
+
* jointly close a cycle serialize on the write transaction and exactly
|
|
658
|
+
* one of them fails (issues #29/#40/#54/#55/#56).
|
|
659
|
+
*/
|
|
660
|
+
readonly recordRunParent: (
|
|
661
|
+
childId: string,
|
|
662
|
+
parentId: string
|
|
663
|
+
) => Effect.Effect<RecordRunParentOutcome, RunParentCycleError>
|
|
664
|
+
/**
|
|
665
|
+
* Removes every parent edge touching a run — both the edges naming it as
|
|
666
|
+
* child and the edges naming it as parent. The cleanup half of run
|
|
667
|
+
* deletion (issue #66): a lane that deletes run rows (time-travel,
|
|
668
|
+
* retention) must call this so `runParents` and the cycle walk stop
|
|
669
|
+
* seeing edges of runs that no longer exist.
|
|
670
|
+
*/
|
|
671
|
+
readonly removeRunParentsForRun: (runId: string) => Effect.Effect<void>
|
|
672
|
+
/**
|
|
673
|
+
* Lists the durably recorded parent edges of a run, oldest first.
|
|
674
|
+
*/
|
|
675
|
+
readonly runParents: (childId: string) => Effect.Effect<ReadonlyArray<RunParentEdge>>
|
|
676
|
+
/**
|
|
677
|
+
* Lists the durably recorded child edges of a run, oldest first.
|
|
678
|
+
*
|
|
679
|
+
* The reverse direction of `runParents`, and the only instance-independent
|
|
680
|
+
* way to find the runs a cancelled parent linked to itself. Cancellation
|
|
681
|
+
* cascade reads it rather than an in-process instance map, so a
|
|
682
|
+
* cross-process cancel observed by a driver that never spawned the children
|
|
683
|
+
* still reaches them (`docs/concepts/durable-waits.md`, "The run DAG").
|
|
684
|
+
* Served by the
|
|
685
|
+
* `flows_run_parents_parent_idx` index.
|
|
686
|
+
*/
|
|
687
|
+
readonly runChildren: (parentId: string) => Effect.Effect<ReadonlyArray<RunParentEdge>>
|
|
688
|
+
/**
|
|
689
|
+
* Runs `effect` inside one storage write transaction, so a caller can make
|
|
690
|
+
* several store operations atomic — the run driver wraps the parent-edge
|
|
691
|
+
* record and the run-row creation it guards, closing the crash window that
|
|
692
|
+
* left a durable orphan edge for a run that was never created (issue #80).
|
|
693
|
+
* Nested store writes become savepoints of this transaction. Storage
|
|
694
|
+
* failures are defects, matching the store methods' own posture.
|
|
695
|
+
*
|
|
696
|
+
* The in-memory twin honors the same contract: it serializes every store
|
|
697
|
+
* operation behind one permit, snapshots its state maps on entry, restores
|
|
698
|
+
* them when the effect fails or dies, and treats a nested call on the same
|
|
699
|
+
* fiber as a savepoint of the outer one. What it cannot offer is
|
|
700
|
+
* durability: a process crash mid-transaction loses the whole state, not
|
|
701
|
+
* only the uncommitted part.
|
|
702
|
+
*
|
|
703
|
+
* **Lock order.** A caller that needs this transaction AND the journal's
|
|
704
|
+
* write transaction opens this one OUTSIDE. The SQL implementation cannot
|
|
705
|
+
* tell the difference, because both are the same `DurableWriter`
|
|
706
|
+
* transaction and either nests as a savepoint of the other; the memory twin
|
|
707
|
+
* has a permit of its own, and a fiber that holds the journal's writer
|
|
708
|
+
* while waiting for that permit deadlocks against a fiber doing the
|
|
709
|
+
* opposite. Interruption is not a way out of that cycle: both boundaries
|
|
710
|
+
* are acquired at the caller's own interruptibility, so a settlement that
|
|
711
|
+
* masked interruption to finish its write keeps waiting. The same
|
|
712
|
+
* rule applies to every single operation above, each of which takes the
|
|
713
|
+
* permit for itself: inside an open journal transaction, call them only
|
|
714
|
+
* under an enclosing `transaction` on the same fiber.
|
|
715
|
+
*/
|
|
716
|
+
readonly transaction: <A, E, R>(
|
|
717
|
+
effect: Effect.Effect<A, E, R>,
|
|
718
|
+
options?: {
|
|
719
|
+
/**
|
|
720
|
+
* Receives an idempotent memory commit effect. A coordinating durable
|
|
721
|
+
* transaction may run it after COMMIT, before publishing or capturing
|
|
722
|
+
* state. This accepts the snapshot and releases its gate; subsequent
|
|
723
|
+
* publication failures cannot restore the old snapshot. Without an
|
|
724
|
+
* external boundary, returning successfully commits as usual. SQL owns
|
|
725
|
+
* its commit boundary and does not need this callback.
|
|
726
|
+
*/
|
|
727
|
+
readonly onCommit: (commit: Effect.Effect<void>) => void
|
|
728
|
+
}
|
|
729
|
+
) => Effect.Effect<A, E, R>
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Service tag for durable deferred completions and absolute clock deadlines.
|
|
734
|
+
*
|
|
735
|
+
* @since 0.1.0
|
|
736
|
+
* @category services
|
|
737
|
+
*/
|
|
738
|
+
export class DurableEngineState extends Context.Service<DurableEngineState, Service>()(
|
|
739
|
+
"@smthrs/engine-store/DurableEngineState"
|
|
740
|
+
) {}
|
|
741
|
+
|
|
742
|
+
/** @private */
|
|
743
|
+
const deferredKey = (address: DeferredAddress): string =>
|
|
744
|
+
JSON.stringify([address.flowName, address.executionId, address.deferredName])
|
|
745
|
+
|
|
746
|
+
/** @private */
|
|
747
|
+
const clockKey = (address: ClockAddress): string =>
|
|
748
|
+
JSON.stringify([address.flowName, address.executionId, address.clockName])
|
|
749
|
+
|
|
750
|
+
/** @private */
|
|
751
|
+
const DeferredDatabaseRow = Schema.Struct({
|
|
752
|
+
...DeferredAddress.fields,
|
|
753
|
+
exitJson: Schema.String,
|
|
754
|
+
metadataJson: Schema.NullOr(Schema.String),
|
|
755
|
+
completedAtMs: NonNegativeSafeInt
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
type DeferredDatabaseRow = typeof DeferredDatabaseRow.Type
|
|
759
|
+
|
|
760
|
+
/** @private */
|
|
761
|
+
const ClockDatabaseRow = Schema.Struct({
|
|
762
|
+
...ClockAddress.fields,
|
|
763
|
+
deferredName: Schema.NonEmptyString,
|
|
764
|
+
dueAtMs: NonNegativeSafeInt,
|
|
765
|
+
completedAtMs: Schema.NullOr(NonNegativeSafeInt)
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
type ClockDatabaseRow = typeof ClockDatabaseRow.Type
|
|
769
|
+
|
|
770
|
+
/** @private */
|
|
771
|
+
const WaitingDatabaseRow = Schema.Struct({
|
|
772
|
+
runId: Schema.NonEmptyString,
|
|
773
|
+
waitingReason: WaitingReason,
|
|
774
|
+
waitingWakeAtMs: Schema.NullOr(NonNegativeSafeInt),
|
|
775
|
+
waitingToken: Schema.NullOr(Schema.NonEmptyString),
|
|
776
|
+
/** Absent rather than null on a database written before `0004_waiting_request`. */
|
|
777
|
+
waitingRequest: Schema.optional(Schema.NullOr(Schema.String))
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
type WaitingDatabaseRow = typeof WaitingDatabaseRow.Type
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* The declared question of a waiting row, or absence.
|
|
784
|
+
*
|
|
785
|
+
* The column is the wait's own JSON, so text that does not parse is text the
|
|
786
|
+
* writer never wrote: it is reported as no declared question rather than
|
|
787
|
+
* failing the read of a park that is otherwise intact.
|
|
788
|
+
*/
|
|
789
|
+
const decodeWaitingRequest = (value: string | null | undefined): { readonly request?: unknown } => {
|
|
790
|
+
if (value === null || value === undefined) return {}
|
|
791
|
+
try {
|
|
792
|
+
return { request: JSON.parse(value) as unknown }
|
|
793
|
+
} catch {
|
|
794
|
+
return {}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const decodeWaitingRow = (input: unknown): Effect.Effect<WaitingRow> => decodeWaitingRowResult(input).pipe(Effect.orDie)
|
|
799
|
+
|
|
800
|
+
/** {@link decodeWaitingRow} with its parse failure left in the error channel. */
|
|
801
|
+
const decodeWaitingRowResult = (input: unknown): Effect.Effect<WaitingRow, unknown> =>
|
|
802
|
+
Schema.decodeUnknownEffect(WaitingDatabaseRow)(input).pipe(
|
|
803
|
+
Effect.map((row) => ({
|
|
804
|
+
runId: row.runId,
|
|
805
|
+
reason: row.waitingReason,
|
|
806
|
+
wakeAt: row.waitingWakeAtMs,
|
|
807
|
+
token: row.waitingToken,
|
|
808
|
+
...decodeWaitingRequest(row.waitingRequest)
|
|
809
|
+
} as WaitingRow))
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
/** @private */
|
|
813
|
+
const RunParentDatabaseRow = Schema.Struct({
|
|
814
|
+
childId: Schema.NonEmptyString,
|
|
815
|
+
parentId: Schema.NonEmptyString,
|
|
816
|
+
seq: NonNegativeSafeInt
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
type RunParentDatabaseRow = typeof RunParentDatabaseRow.Type
|
|
820
|
+
|
|
821
|
+
const decodeRunParentEdge = (input: unknown): Effect.Effect<RunParentEdge> =>
|
|
822
|
+
decodeRunParentEdgeResult(input).pipe(Effect.orDie)
|
|
823
|
+
|
|
824
|
+
/** {@link decodeRunParentEdge} with its parse failure left in the error channel. */
|
|
825
|
+
const decodeRunParentEdgeResult = (input: unknown): Effect.Effect<RunParentEdge, unknown> =>
|
|
826
|
+
Schema.decodeUnknownEffect(RunParentDatabaseRow)(input)
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Walks the parent chain upward from `parentId` over the durable edges,
|
|
830
|
+
* looking for `childId`. Returns the cycle path the new `(childId,
|
|
831
|
+
* parentId)` edge would close — ids from the child back to itself, matching
|
|
832
|
+
* the engine's `FlowCycleDetected.path` shape — or `undefined` when the
|
|
833
|
+
* edge is safe. A repeated id (pre-existing corrupt cycle written outside
|
|
834
|
+
* this API) terminates that branch instead of looping forever.
|
|
835
|
+
*/
|
|
836
|
+
const findCyclePath = (
|
|
837
|
+
childId: string,
|
|
838
|
+
parentId: string,
|
|
839
|
+
parentsOf: (id: string) => Effect.Effect<ReadonlyArray<string>>
|
|
840
|
+
): Effect.Effect<ReadonlyArray<string> | undefined> =>
|
|
841
|
+
Effect.suspend(() => {
|
|
842
|
+
if (parentId === childId) {
|
|
843
|
+
return Effect.succeed<ReadonlyArray<string> | undefined>([childId])
|
|
844
|
+
}
|
|
845
|
+
const seen = new Set<string>([parentId])
|
|
846
|
+
const walk = (
|
|
847
|
+
current: string,
|
|
848
|
+
chain: ReadonlyArray<string>
|
|
849
|
+
): Effect.Effect<ReadonlyArray<string> | undefined> =>
|
|
850
|
+
Effect.gen(function*() {
|
|
851
|
+
const parents = yield* parentsOf(current)
|
|
852
|
+
for (const parent of parents) {
|
|
853
|
+
if (parent === childId) return [...chain, parent].reverse()
|
|
854
|
+
if (seen.has(parent)) continue
|
|
855
|
+
seen.add(parent)
|
|
856
|
+
const found = yield* walk(parent, [...chain, parent])
|
|
857
|
+
if (found !== undefined) return found
|
|
858
|
+
}
|
|
859
|
+
return undefined
|
|
860
|
+
})
|
|
861
|
+
return walk(parentId, [parentId])
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
/** @private */
|
|
865
|
+
const DeferredAddressDatabaseRow = DeferredAddress
|
|
866
|
+
|
|
867
|
+
/** The digests of a stored completion row's encoded text. */
|
|
868
|
+
const deferredDigest = (row: {
|
|
869
|
+
readonly exitJson: string
|
|
870
|
+
readonly metadataJson: string | null
|
|
871
|
+
}): DeferredDigest => ({
|
|
872
|
+
exit: contentDigest(row.exitJson),
|
|
873
|
+
metadata: row.metadataJson === null ? null : contentDigest(row.metadataJson)
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
/** The digests of a memory completion row, over the text the SQL store would keep. */
|
|
877
|
+
const memoryDigest = (row: DeferredRow): Effect.Effect<DeferredDigest> =>
|
|
878
|
+
Effect.gen(function*() {
|
|
879
|
+
const exitJson = yield* encodeJson(row.exit, "exit")
|
|
880
|
+
const metadataJson = row.metadata === undefined ? null : yield* encodeJson(row.metadata, "metadata")
|
|
881
|
+
return deferredDigest({ exitJson, metadataJson })
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
const encodeJson = (value: unknown, field: string): Effect.Effect<string> =>
|
|
885
|
+
Schema.encodeEffect(UnknownFromJsonString)(value).pipe(
|
|
886
|
+
Effect.mapError((cause) => new EngineStateFault({ reason: "value_not_serializable", field, cause })),
|
|
887
|
+
Effect.orDie
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
const decodeJson = (value: string, field: string): Effect.Effect<unknown> =>
|
|
891
|
+
Schema.decodeUnknownEffect(UnknownFromJsonString)(value).pipe(
|
|
892
|
+
Effect.mapError((cause) => new EngineStateFault({ reason: "value_not_decodable", field, cause })),
|
|
893
|
+
Effect.orDie
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Rehydrates Effect's JSON representation after a storage round-trip.
|
|
898
|
+
*
|
|
899
|
+
* `Exit` and `Cause` deliberately serialize as plain JSON objects. Leaving a
|
|
900
|
+
* failure in that form is unsafe: the success branch happens to be rebuilt by
|
|
901
|
+
* `Exit.map`, while a plain failure is not an Exit and the deferred's schema
|
|
902
|
+
* rejects it after restart. Unknown non-Exit payloads remain untouched for
|
|
903
|
+
* the generic durable-state contract.
|
|
904
|
+
*/
|
|
905
|
+
const restoreJsonExit = (value: unknown): unknown => {
|
|
906
|
+
if (typeof value !== "object" || value === null) return value
|
|
907
|
+
const record = value as Record<string, unknown>
|
|
908
|
+
if (record["_id"] !== "Exit") return value
|
|
909
|
+
if (record["_tag"] === "Success") return Exit.succeed(record["value"])
|
|
910
|
+
if (record["_tag"] !== "Failure") return value
|
|
911
|
+
const cause = record["cause"]
|
|
912
|
+
if (typeof cause !== "object" || cause === null) return value
|
|
913
|
+
const failures = (cause as Record<string, unknown>)["failures"]
|
|
914
|
+
if (!Array.isArray(failures)) return value
|
|
915
|
+
const reasons: Array<Cause.Reason<unknown>> = []
|
|
916
|
+
for (const failure of failures) {
|
|
917
|
+
if (typeof failure !== "object" || failure === null) return value
|
|
918
|
+
const reason = failure as Record<string, unknown>
|
|
919
|
+
switch (reason["_tag"]) {
|
|
920
|
+
case "Fail":
|
|
921
|
+
reasons.push(Cause.makeFailReason(reason["error"]))
|
|
922
|
+
break
|
|
923
|
+
case "Die":
|
|
924
|
+
reasons.push(Cause.makeDieReason(reason["defect"]))
|
|
925
|
+
break
|
|
926
|
+
case "Interrupt":
|
|
927
|
+
reasons.push(Cause.makeInterruptReason(
|
|
928
|
+
typeof reason["fiberId"] === "number" ? reason["fiberId"] : undefined
|
|
929
|
+
))
|
|
930
|
+
break
|
|
931
|
+
default:
|
|
932
|
+
return value
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return Exit.failCause(Cause.fromReasons(reasons))
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const decodeDeferredRow = (input: unknown): Effect.Effect<DeferredRow> =>
|
|
939
|
+
Schema.decodeUnknownEffect(DeferredDatabaseRow)(input).pipe(
|
|
940
|
+
Effect.orDie,
|
|
941
|
+
Effect.flatMap((row) =>
|
|
942
|
+
Effect.all({
|
|
943
|
+
exit: decodeJson(row.exitJson, "exit_json").pipe(Effect.map(restoreJsonExit)),
|
|
944
|
+
metadata: row.metadataJson === null
|
|
945
|
+
? Effect.succeed(undefined)
|
|
946
|
+
: decodeJson(row.metadataJson, "metadata_json")
|
|
947
|
+
}).pipe(
|
|
948
|
+
Effect.map(({ exit, metadata }) => ({
|
|
949
|
+
flowName: row.flowName,
|
|
950
|
+
executionId: row.executionId,
|
|
951
|
+
deferredName: row.deferredName,
|
|
952
|
+
exit,
|
|
953
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
954
|
+
completedAtMs: row.completedAtMs
|
|
955
|
+
}))
|
|
956
|
+
)
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
const decodeClockRow = (input: unknown): Effect.Effect<ClockRow> => decodeClockRowResult(input).pipe(Effect.orDie)
|
|
961
|
+
|
|
962
|
+
/** {@link decodeClockRow} with its parse failure left in the error channel. */
|
|
963
|
+
const decodeClockRowResult = (input: unknown): Effect.Effect<ClockRow, unknown> =>
|
|
964
|
+
Schema.decodeUnknownEffect(ClockDatabaseRow)(input).pipe(
|
|
965
|
+
Effect.map((row) => ({
|
|
966
|
+
flowName: row.flowName,
|
|
967
|
+
executionId: row.executionId,
|
|
968
|
+
clockName: row.clockName,
|
|
969
|
+
deferredName: row.deferredName,
|
|
970
|
+
dueAtMs: row.dueAtMs,
|
|
971
|
+
completedAtMs: row.completedAtMs
|
|
972
|
+
}))
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
/** The primary key of a clock row, read defensively off a row that may not decode. */
|
|
976
|
+
const clockRowKey = (row: Record<string, unknown>): string =>
|
|
977
|
+
JSON.stringify([row["flowName"], row["executionId"], row["clockName"]])
|
|
978
|
+
|
|
979
|
+
/** The primary key of a deferred completion, read the same way. */
|
|
980
|
+
const deferredAddressRowKey = (row: Record<string, unknown>): string =>
|
|
981
|
+
JSON.stringify([row["flowName"], row["executionId"], row["deferredName"]])
|
|
982
|
+
|
|
983
|
+
/** The primary key of a run-parent edge, read the same way. */
|
|
984
|
+
const runParentRowKey = (row: Record<string, unknown>): string => JSON.stringify([row["childId"], row["parentId"]])
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* How far {@link Service.waitingTree} walks: detached subtrees are skipped
|
|
988
|
+
* unless `detached` is `"include"`.
|
|
989
|
+
*
|
|
990
|
+
* @since 1.0.0
|
|
991
|
+
* @category models
|
|
992
|
+
*/
|
|
993
|
+
export interface WaitingTreeOptions {
|
|
994
|
+
readonly detached?: "skip" | "include" | undefined
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* The deepest nesting {@link Service.waitingTree} walks.
|
|
999
|
+
*
|
|
1000
|
+
* A run tree is authored, not generated: `agent/run` → `coding/request` →
|
|
1001
|
+
* `coding/Request` → `coding/PrepareWithWiki` → `coding/PreparePlan` is five,
|
|
1002
|
+
* and the deepest composition in this repository is well inside this. The cap
|
|
1003
|
+
* exists so a tree whose edges were corrupted outside the engine costs a
|
|
1004
|
+
* bounded read rather than the whole table.
|
|
1005
|
+
*
|
|
1006
|
+
* @since 1.0.0
|
|
1007
|
+
* @category constants
|
|
1008
|
+
*/
|
|
1009
|
+
export const waitingTreeMaxDepth = 64
|
|
1010
|
+
|
|
1011
|
+
/** The primary key of a parked run's waiting payload, read the same way. */
|
|
1012
|
+
const waitingRowKey = (row: Record<string, unknown>): string => JSON.stringify([row["runId"]])
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Decodes the rows one durable-state sweep read, skipping — with a
|
|
1016
|
+
* storage-integrity warning naming the row's key — any row that will not
|
|
1017
|
+
* decode.
|
|
1018
|
+
*
|
|
1019
|
+
* Decoding the batch through `Effect.orDie` made one malformed row fatal for
|
|
1020
|
+
* every row beside it: `completedDeferreds` and `pendingClocks` are what
|
|
1021
|
+
* `register` sweeps, so a single unreadable row anywhere in a flow's history
|
|
1022
|
+
* killed every registration of that flow, and no run of it ever resumed again.
|
|
1023
|
+
* The blast radius of a corrupt row must be that row. The key is logged
|
|
1024
|
+
* because it is what an operator needs to find and repair it, and it is read
|
|
1025
|
+
* field by field off the raw row so a row that failed to decode can still name
|
|
1026
|
+
* itself.
|
|
1027
|
+
*
|
|
1028
|
+
* Every enumeration goes through here for that reason, not just the two the
|
|
1029
|
+
* outage named: `dueClocks`, `waitingRuns`, `runParents`, and `runChildren`
|
|
1030
|
+
* each had their own version of it — no timer in the store fires, no parked
|
|
1031
|
+
* run is ever swept, no cycle check can read a run's ancestry, no
|
|
1032
|
+
* cancellation reaches a child.
|
|
1033
|
+
*
|
|
1034
|
+
* The point reads (`deferred`, `clock`, `waiting`) deliberately stay on
|
|
1035
|
+
* `Effect.orDie`. They answer a question about ONE row, and their callers
|
|
1036
|
+
* read "no row" as "this never happened": silently reporting that for a
|
|
1037
|
+
* completion or deadline that is durably recorded but unreadable would
|
|
1038
|
+
* re-run work whose side effects already ran. There the corrupt row IS the
|
|
1039
|
+
* blast radius, so dying is the correct-sized failure.
|
|
1040
|
+
*/
|
|
1041
|
+
const decodeSweep = <A>(
|
|
1042
|
+
rows: ReadonlyArray<Record<string, unknown>>,
|
|
1043
|
+
kind: string,
|
|
1044
|
+
keyOf: (row: Record<string, unknown>) => string,
|
|
1045
|
+
decode: (row: Record<string, unknown>) => Effect.Effect<A, unknown>
|
|
1046
|
+
): Effect.Effect<ReadonlyArray<A>> =>
|
|
1047
|
+
Effect.gen(function*() {
|
|
1048
|
+
const decoded: Array<A> = []
|
|
1049
|
+
for (const row of rows) {
|
|
1050
|
+
const result = yield* Effect.result(decode(row))
|
|
1051
|
+
if (Result.isSuccess(result)) {
|
|
1052
|
+
decoded.push(result.success)
|
|
1053
|
+
continue
|
|
1054
|
+
}
|
|
1055
|
+
yield* Effect.logWarning(
|
|
1056
|
+
`engine-store: skipping a malformed ${kind} row ${keyOf(row)}`,
|
|
1057
|
+
result.failure
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
return decoded
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Demotes a write boundary's storage failures to defects, reason by reason.
|
|
1065
|
+
*
|
|
1066
|
+
* `transaction` declares the BODY's error type, and the shared writer adds
|
|
1067
|
+
* `DatabaseError` to it. A sole storage failure is already a defect; a
|
|
1068
|
+
* composite one used to be cast back through the declared channel, which let
|
|
1069
|
+
* a raw `DatabaseError` surface as a typed failure of a type that never
|
|
1070
|
+
* admits one. Re-channelling keeps the error object, its annotations, the
|
|
1071
|
+
* other reasons and the interruptions intact while making the declared type
|
|
1072
|
+
* true. Every other reason is passed through UNCHANGED rather than rebuilt,
|
|
1073
|
+
* so a body failure keeps its own identity and spans.
|
|
1074
|
+
*/
|
|
1075
|
+
const dieOnDatabaseError = <E>(cause: Cause.Cause<E | DatabaseError>): Cause.Cause<E> =>
|
|
1076
|
+
Cause.fromReasons(
|
|
1077
|
+
cause.reasons.map((reason) =>
|
|
1078
|
+
Cause.isFailReason(reason) && reason.error instanceof DatabaseError
|
|
1079
|
+
? Cause.makeDieReason(reason.error).annotate(Cause.reasonAnnotations(reason))
|
|
1080
|
+
// Narrowing a generic union by `instanceof` is beyond the checker, but
|
|
1081
|
+
// the guard above is the proof: what is left is a defect, an
|
|
1082
|
+
// interruption, or a failure whose error is not a `DatabaseError`.
|
|
1083
|
+
: reason as Cause.Reason<E>
|
|
1084
|
+
)
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Constructs the database-backed durable-state implementation.
|
|
1089
|
+
*
|
|
1090
|
+
* Clock creation is fenced against the current run owner. Deferred completion
|
|
1091
|
+
* and clock firing are external trigger admissions protected by first-writer
|
|
1092
|
+
* and compare-and-set semantics; execution remains claim-gated by `RunStore`.
|
|
1093
|
+
*
|
|
1094
|
+
* @since 0.1.0
|
|
1095
|
+
* @category constructors
|
|
1096
|
+
*/
|
|
1097
|
+
export const make: Effect.Effect<Service, never, DurableWriter | SqlClient.SqlClient> = Effect.gen(function*() {
|
|
1098
|
+
const sql = yield* Effect.service(SqlClient.SqlClient)
|
|
1099
|
+
const writer = yield* DurableWriter
|
|
1100
|
+
|
|
1101
|
+
// Engine-store-owned storage created outside the journal migration
|
|
1102
|
+
// (issues #40/#41/#79/#81). The statements, their rationale, and
|
|
1103
|
+
// the dialects each is known to accept live in one machine-readable
|
|
1104
|
+
// inventory so the pg-porting plan cannot omit them (issue #92).
|
|
1105
|
+
yield* EngineStateSchema.apply(sql, writer)
|
|
1106
|
+
|
|
1107
|
+
const selectDeferred = (address: DeferredAddress) =>
|
|
1108
|
+
sql<DeferredDatabaseRow>`
|
|
1109
|
+
SELECT
|
|
1110
|
+
flow_name AS "flowName",
|
|
1111
|
+
execution_id AS "executionId",
|
|
1112
|
+
deferred_name AS "deferredName",
|
|
1113
|
+
exit_json AS "exitJson",
|
|
1114
|
+
metadata_json AS "metadataJson",
|
|
1115
|
+
completed_at_ms AS "completedAtMs"
|
|
1116
|
+
FROM flows_deferred_completions
|
|
1117
|
+
WHERE flow_name = ${address.flowName}
|
|
1118
|
+
AND execution_id = ${address.executionId}
|
|
1119
|
+
AND deferred_name = ${address.deferredName}
|
|
1120
|
+
`
|
|
1121
|
+
|
|
1122
|
+
const selectClock = (address: ClockAddress) =>
|
|
1123
|
+
sql<ClockDatabaseRow>`
|
|
1124
|
+
SELECT
|
|
1125
|
+
flow_name AS "flowName",
|
|
1126
|
+
execution_id AS "executionId",
|
|
1127
|
+
clock_name AS "clockName",
|
|
1128
|
+
deferred_name AS "deferredName",
|
|
1129
|
+
due_at_ms AS "dueAtMs",
|
|
1130
|
+
completed_at_ms AS "completedAtMs"
|
|
1131
|
+
FROM flows_clock_deadlines
|
|
1132
|
+
WHERE flow_name = ${address.flowName}
|
|
1133
|
+
AND execution_id = ${address.executionId}
|
|
1134
|
+
AND clock_name = ${address.clockName}
|
|
1135
|
+
`
|
|
1136
|
+
|
|
1137
|
+
const deferred: Service["deferred"] = Effect.fn("DurableEngineState.deferred")((address) =>
|
|
1138
|
+
selectDeferred(address).pipe(
|
|
1139
|
+
Effect.orDie,
|
|
1140
|
+
Effect.flatMap((rows) =>
|
|
1141
|
+
rows[0] === undefined
|
|
1142
|
+
? Effect.succeedNone
|
|
1143
|
+
: Effect.map(decodeDeferredRow(rows[0]), Option.some)
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
const consumeDeferred: Service["consumeDeferred"] = Effect.fn("DurableEngineState.consumeDeferred")((
|
|
1149
|
+
address,
|
|
1150
|
+
consumedAtMs
|
|
1151
|
+
) =>
|
|
1152
|
+
writer.write(sql<{ readonly flowName: string }>`
|
|
1153
|
+
UPDATE flows_deferred_completions
|
|
1154
|
+
SET consumed_at_ms = ${consumedAtMs}
|
|
1155
|
+
WHERE flow_name = ${address.flowName}
|
|
1156
|
+
AND execution_id = ${address.executionId}
|
|
1157
|
+
AND deferred_name = ${address.deferredName}
|
|
1158
|
+
AND consumed_at_ms IS NULL
|
|
1159
|
+
RETURNING flow_name AS "flowName"
|
|
1160
|
+
`).pipe(Effect.orDie, Effect.map((rows) => rows.length > 0))
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
const completeDeferred: Service["completeDeferred"] = Effect.fn(
|
|
1164
|
+
"DurableEngineState.completeDeferred"
|
|
1165
|
+
)((row) =>
|
|
1166
|
+
Effect.gen(function*() {
|
|
1167
|
+
const exitJson = yield* encodeJson(row.exit, "exit")
|
|
1168
|
+
const metadataJson = row.metadata === undefined
|
|
1169
|
+
? null
|
|
1170
|
+
: yield* encodeJson(row.metadata, "metadata")
|
|
1171
|
+
return yield* writer.write(
|
|
1172
|
+
Effect.gen(function*() {
|
|
1173
|
+
const inserted = yield* sql<DeferredDatabaseRow>`
|
|
1174
|
+
INSERT INTO flows_deferred_completions (
|
|
1175
|
+
flow_name,
|
|
1176
|
+
execution_id,
|
|
1177
|
+
deferred_name,
|
|
1178
|
+
exit_json,
|
|
1179
|
+
metadata_json,
|
|
1180
|
+
completed_at_ms
|
|
1181
|
+
) VALUES (
|
|
1182
|
+
${row.flowName},
|
|
1183
|
+
${row.executionId},
|
|
1184
|
+
${row.deferredName},
|
|
1185
|
+
${exitJson},
|
|
1186
|
+
${metadataJson},
|
|
1187
|
+
${row.completedAtMs}
|
|
1188
|
+
)
|
|
1189
|
+
ON CONFLICT (flow_name, execution_id, deferred_name) DO NOTHING
|
|
1190
|
+
RETURNING
|
|
1191
|
+
flow_name AS "flowName",
|
|
1192
|
+
execution_id AS "executionId",
|
|
1193
|
+
deferred_name AS "deferredName",
|
|
1194
|
+
exit_json AS "exitJson",
|
|
1195
|
+
metadata_json AS "metadataJson",
|
|
1196
|
+
completed_at_ms AS "completedAtMs"
|
|
1197
|
+
`
|
|
1198
|
+
if (inserted[0] !== undefined) {
|
|
1199
|
+
return {
|
|
1200
|
+
_tag: "Completed" as const,
|
|
1201
|
+
row: yield* decodeDeferredRow(inserted[0]),
|
|
1202
|
+
digest: deferredDigest(inserted[0])
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const existing = yield* selectDeferred(row)
|
|
1206
|
+
if (existing[0] === undefined) {
|
|
1207
|
+
return yield* Effect.die(new EngineStateFault({ reason: "deferred_completion_missing" }))
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
_tag: "Existing" as const,
|
|
1211
|
+
row: yield* decodeDeferredRow(existing[0]),
|
|
1212
|
+
digest: deferredDigest(existing[0])
|
|
1213
|
+
}
|
|
1214
|
+
})
|
|
1215
|
+
).pipe(Effect.orDie)
|
|
1216
|
+
})
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
const clock: Service["clock"] = Effect.fn("DurableEngineState.clock")((address) =>
|
|
1220
|
+
selectClock(address).pipe(
|
|
1221
|
+
Effect.orDie,
|
|
1222
|
+
Effect.flatMap((rows) =>
|
|
1223
|
+
rows[0] === undefined
|
|
1224
|
+
? Effect.succeedNone
|
|
1225
|
+
: Effect.map(decodeClockRow(rows[0]), Option.some)
|
|
1226
|
+
)
|
|
1227
|
+
)
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
const scheduleClock: Service["scheduleClock"] = Effect.fn("DurableEngineState.scheduleClock")((row, owner) =>
|
|
1231
|
+
writer.write(
|
|
1232
|
+
Effect.gen(function*() {
|
|
1233
|
+
const inserted = yield* sql<ClockDatabaseRow>`
|
|
1234
|
+
INSERT INTO flows_clock_deadlines (
|
|
1235
|
+
flow_name,
|
|
1236
|
+
execution_id,
|
|
1237
|
+
clock_name,
|
|
1238
|
+
deferred_name,
|
|
1239
|
+
due_at_ms,
|
|
1240
|
+
completed_at_ms
|
|
1241
|
+
)
|
|
1242
|
+
SELECT
|
|
1243
|
+
${row.flowName},
|
|
1244
|
+
${row.executionId},
|
|
1245
|
+
${row.clockName},
|
|
1246
|
+
${row.deferredName},
|
|
1247
|
+
${row.dueAtMs},
|
|
1248
|
+
${row.completedAtMs}
|
|
1249
|
+
WHERE EXISTS (
|
|
1250
|
+
SELECT 1
|
|
1251
|
+
FROM flows_runs
|
|
1252
|
+
WHERE run_id = ${row.executionId}
|
|
1253
|
+
AND status = 'running'
|
|
1254
|
+
AND owner_host_id = ${owner.hostId}
|
|
1255
|
+
AND owner_pid = ${owner.pid}
|
|
1256
|
+
AND owner_nonce = ${owner.nonce}
|
|
1257
|
+
)
|
|
1258
|
+
ON CONFLICT (flow_name, execution_id, clock_name) DO NOTHING
|
|
1259
|
+
RETURNING
|
|
1260
|
+
flow_name AS "flowName",
|
|
1261
|
+
execution_id AS "executionId",
|
|
1262
|
+
clock_name AS "clockName",
|
|
1263
|
+
deferred_name AS "deferredName",
|
|
1264
|
+
due_at_ms AS "dueAtMs",
|
|
1265
|
+
completed_at_ms AS "completedAtMs"
|
|
1266
|
+
`
|
|
1267
|
+
if (inserted[0] !== undefined) {
|
|
1268
|
+
return {
|
|
1269
|
+
_tag: "Scheduled" as const,
|
|
1270
|
+
row: yield* decodeClockRow(inserted[0])
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const existing = yield* selectClock(row)
|
|
1274
|
+
if (existing[0] !== undefined) {
|
|
1275
|
+
return {
|
|
1276
|
+
_tag: "Existing" as const,
|
|
1277
|
+
row: yield* decodeClockRow(existing[0])
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return yield* Effect.interrupt
|
|
1281
|
+
})
|
|
1282
|
+
).pipe(Effect.orDie)
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
const completeClock: Service["completeClock"] = Effect.fn("DurableEngineState.completeClock")((
|
|
1286
|
+
address,
|
|
1287
|
+
completedAtMs
|
|
1288
|
+
) =>
|
|
1289
|
+
writer.write(
|
|
1290
|
+
Effect.gen(function*() {
|
|
1291
|
+
const updated = yield* sql<ClockDatabaseRow>`
|
|
1292
|
+
UPDATE flows_clock_deadlines
|
|
1293
|
+
SET completed_at_ms = ${completedAtMs}
|
|
1294
|
+
WHERE flow_name = ${address.flowName}
|
|
1295
|
+
AND execution_id = ${address.executionId}
|
|
1296
|
+
AND clock_name = ${address.clockName}
|
|
1297
|
+
AND completed_at_ms IS NULL
|
|
1298
|
+
RETURNING
|
|
1299
|
+
flow_name AS "flowName",
|
|
1300
|
+
execution_id AS "executionId",
|
|
1301
|
+
clock_name AS "clockName",
|
|
1302
|
+
deferred_name AS "deferredName",
|
|
1303
|
+
due_at_ms AS "dueAtMs",
|
|
1304
|
+
completed_at_ms AS "completedAtMs"
|
|
1305
|
+
`
|
|
1306
|
+
if (updated[0] !== undefined) {
|
|
1307
|
+
return {
|
|
1308
|
+
_tag: "Completed" as const,
|
|
1309
|
+
row: yield* decodeClockRow(updated[0])
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const existing = yield* selectClock(address)
|
|
1313
|
+
return existing[0] === undefined
|
|
1314
|
+
? { _tag: "NotFound" as const }
|
|
1315
|
+
: {
|
|
1316
|
+
_tag: "AlreadyCompleted" as const,
|
|
1317
|
+
row: yield* decodeClockRow(existing[0])
|
|
1318
|
+
}
|
|
1319
|
+
})
|
|
1320
|
+
).pipe(Effect.orDie)
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
const dueClocks: Service["dueClocks"] = Effect.fn("DurableEngineState.dueClocks")((nowMs) =>
|
|
1324
|
+
sql<ClockDatabaseRow>`
|
|
1325
|
+
SELECT
|
|
1326
|
+
flow_name AS "flowName",
|
|
1327
|
+
execution_id AS "executionId",
|
|
1328
|
+
clock_name AS "clockName",
|
|
1329
|
+
deferred_name AS "deferredName",
|
|
1330
|
+
due_at_ms AS "dueAtMs",
|
|
1331
|
+
completed_at_ms AS "completedAtMs"
|
|
1332
|
+
FROM flows_clock_deadlines
|
|
1333
|
+
WHERE completed_at_ms IS NULL
|
|
1334
|
+
AND due_at_ms <= ${nowMs}
|
|
1335
|
+
ORDER BY due_at_ms, execution_id, clock_name
|
|
1336
|
+
`.pipe(
|
|
1337
|
+
Effect.orDie,
|
|
1338
|
+
// This is the whole store's due-timer sweep: dying on one unreadable
|
|
1339
|
+
// deadline stopped every timer in every flow from firing.
|
|
1340
|
+
Effect.flatMap((rows) => decodeSweep(rows, "clock deadline", clockRowKey, decodeClockRowResult))
|
|
1341
|
+
)
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
const pendingClocks: Service["pendingClocks"] = Effect.fn("DurableEngineState.pendingClocks")((scope) =>
|
|
1345
|
+
(scope.executionId !== undefined
|
|
1346
|
+
? sql<ClockDatabaseRow>`
|
|
1347
|
+
SELECT
|
|
1348
|
+
flow_name AS "flowName",
|
|
1349
|
+
execution_id AS "executionId",
|
|
1350
|
+
clock_name AS "clockName",
|
|
1351
|
+
deferred_name AS "deferredName",
|
|
1352
|
+
due_at_ms AS "dueAtMs",
|
|
1353
|
+
completed_at_ms AS "completedAtMs"
|
|
1354
|
+
FROM flows_clock_deadlines
|
|
1355
|
+
WHERE completed_at_ms IS NULL
|
|
1356
|
+
AND execution_id = ${scope.executionId}
|
|
1357
|
+
AND ${scope.flowName === undefined ? sql`1 = 1` : sql`flow_name = ${scope.flowName}`}
|
|
1358
|
+
AND EXISTS (
|
|
1359
|
+
SELECT 1 FROM flows_runs
|
|
1360
|
+
WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
|
|
1361
|
+
AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
|
|
1362
|
+
)
|
|
1363
|
+
ORDER BY due_at_ms, execution_id, clock_name
|
|
1364
|
+
`
|
|
1365
|
+
: scope.flowName !== undefined
|
|
1366
|
+
? sql<ClockDatabaseRow>`
|
|
1367
|
+
SELECT
|
|
1368
|
+
flow_name AS "flowName",
|
|
1369
|
+
execution_id AS "executionId",
|
|
1370
|
+
clock_name AS "clockName",
|
|
1371
|
+
deferred_name AS "deferredName",
|
|
1372
|
+
due_at_ms AS "dueAtMs",
|
|
1373
|
+
completed_at_ms AS "completedAtMs"
|
|
1374
|
+
FROM flows_clock_deadlines
|
|
1375
|
+
WHERE completed_at_ms IS NULL
|
|
1376
|
+
AND flow_name = ${scope.flowName}
|
|
1377
|
+
AND EXISTS (
|
|
1378
|
+
SELECT 1 FROM flows_runs
|
|
1379
|
+
WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
|
|
1380
|
+
AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
|
|
1381
|
+
)
|
|
1382
|
+
ORDER BY due_at_ms, execution_id, clock_name
|
|
1383
|
+
`
|
|
1384
|
+
: sql<ClockDatabaseRow>`
|
|
1385
|
+
SELECT
|
|
1386
|
+
flow_name AS "flowName",
|
|
1387
|
+
execution_id AS "executionId",
|
|
1388
|
+
clock_name AS "clockName",
|
|
1389
|
+
deferred_name AS "deferredName",
|
|
1390
|
+
due_at_ms AS "dueAtMs",
|
|
1391
|
+
completed_at_ms AS "completedAtMs"
|
|
1392
|
+
FROM flows_clock_deadlines
|
|
1393
|
+
WHERE completed_at_ms IS NULL
|
|
1394
|
+
AND EXISTS (
|
|
1395
|
+
SELECT 1 FROM flows_runs
|
|
1396
|
+
WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
|
|
1397
|
+
AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
|
|
1398
|
+
)
|
|
1399
|
+
ORDER BY due_at_ms, execution_id, clock_name
|
|
1400
|
+
`).pipe(
|
|
1401
|
+
Effect.orDie,
|
|
1402
|
+
// A row that will not decode is a storage-integrity event about that
|
|
1403
|
+
// row. Dying here made one of them fatal for the whole sweep, and so
|
|
1404
|
+
// for `register` itself (issue B-08).
|
|
1405
|
+
Effect.flatMap((rows) => decodeSweep(rows, "clock deadline", clockRowKey, decodeClockRowResult))
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
const completedDeferreds: Service["completedDeferreds"] = Effect.fn(
|
|
1410
|
+
"DurableEngineState.completedDeferreds"
|
|
1411
|
+
)((flowName) =>
|
|
1412
|
+
sql<Record<string, unknown>>`
|
|
1413
|
+
SELECT
|
|
1414
|
+
flow_name AS "flowName",
|
|
1415
|
+
execution_id AS "executionId",
|
|
1416
|
+
deferred_name AS "deferredName"
|
|
1417
|
+
FROM flows_deferred_completions
|
|
1418
|
+
WHERE flow_name = ${flowName}
|
|
1419
|
+
AND consumed_at_ms IS NULL
|
|
1420
|
+
AND EXISTS (
|
|
1421
|
+
SELECT 1 FROM flows_runs
|
|
1422
|
+
WHERE flows_runs.run_id = flows_deferred_completions.execution_id
|
|
1423
|
+
AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
|
|
1424
|
+
)
|
|
1425
|
+
ORDER BY execution_id, deferred_name
|
|
1426
|
+
`.pipe(
|
|
1427
|
+
Effect.orDie,
|
|
1428
|
+
Effect.flatMap((rows) =>
|
|
1429
|
+
decodeSweep(
|
|
1430
|
+
rows,
|
|
1431
|
+
"deferred completion",
|
|
1432
|
+
deferredAddressRowKey,
|
|
1433
|
+
(row) => Schema.decodeUnknownEffect(DeferredAddressDatabaseRow)(row)
|
|
1434
|
+
)
|
|
1435
|
+
)
|
|
1436
|
+
)
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
const completeRunClocks: Service["completeRunClocks"] = Effect.fn(
|
|
1440
|
+
"DurableEngineState.completeRunClocks"
|
|
1441
|
+
)((executionId, completedAtMs) =>
|
|
1442
|
+
writer.write(sql<ClockDatabaseRow>`
|
|
1443
|
+
UPDATE flows_clock_deadlines
|
|
1444
|
+
SET completed_at_ms = ${completedAtMs}
|
|
1445
|
+
WHERE execution_id = ${executionId}
|
|
1446
|
+
AND completed_at_ms IS NULL
|
|
1447
|
+
RETURNING
|
|
1448
|
+
flow_name AS "flowName",
|
|
1449
|
+
execution_id AS "executionId",
|
|
1450
|
+
clock_name AS "clockName",
|
|
1451
|
+
deferred_name AS "deferredName",
|
|
1452
|
+
due_at_ms AS "dueAtMs",
|
|
1453
|
+
completed_at_ms AS "completedAtMs"
|
|
1454
|
+
`).pipe(
|
|
1455
|
+
Effect.orDie,
|
|
1456
|
+
Effect.flatMap((rows) => Effect.forEach(rows, decodeClockRow)),
|
|
1457
|
+
Effect.map((rows) =>
|
|
1458
|
+
[...rows].sort((left, right) =>
|
|
1459
|
+
compareText(left.flowName, right.flowName) || compareText(left.clockName, right.clockName)
|
|
1460
|
+
)
|
|
1461
|
+
)
|
|
1462
|
+
)
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
const selectWaiting = (runId: string) =>
|
|
1466
|
+
sql<WaitingDatabaseRow>`
|
|
1467
|
+
SELECT
|
|
1468
|
+
run_id AS "runId",
|
|
1469
|
+
waiting_reason AS "waitingReason",
|
|
1470
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1471
|
+
waiting_token AS "waitingToken",
|
|
1472
|
+
waiting_request AS "waitingRequest"
|
|
1473
|
+
FROM flows_runs
|
|
1474
|
+
WHERE run_id = ${runId}
|
|
1475
|
+
AND waiting_reason IS NOT NULL
|
|
1476
|
+
`
|
|
1477
|
+
|
|
1478
|
+
const park: Service["park"] = Effect.fn("DurableEngineState.park")((runId, waiting, owner) =>
|
|
1479
|
+
writer.write(
|
|
1480
|
+
Effect.gen(function*() {
|
|
1481
|
+
const updated = yield* sql<WaitingDatabaseRow>`
|
|
1482
|
+
UPDATE flows_runs
|
|
1483
|
+
SET
|
|
1484
|
+
waiting_reason = ${waiting.reason},
|
|
1485
|
+
waiting_wake_at_ms = ${waiting.wakeAt ?? null},
|
|
1486
|
+
waiting_token = ${waiting.token ?? null},
|
|
1487
|
+
waiting_request = ${waiting.request ?? null}
|
|
1488
|
+
WHERE run_id = ${runId}
|
|
1489
|
+
AND owner_host_id = ${owner.hostId}
|
|
1490
|
+
AND owner_pid = ${owner.pid}
|
|
1491
|
+
AND owner_nonce = ${owner.nonce}
|
|
1492
|
+
RETURNING
|
|
1493
|
+
run_id AS "runId",
|
|
1494
|
+
waiting_reason AS "waitingReason",
|
|
1495
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1496
|
+
waiting_token AS "waitingToken",
|
|
1497
|
+
waiting_request AS "waitingRequest"
|
|
1498
|
+
`
|
|
1499
|
+
if (updated[0] === undefined) {
|
|
1500
|
+
return { _tag: "NotFound" as const }
|
|
1501
|
+
}
|
|
1502
|
+
return { _tag: "Parked" as const, row: yield* decodeWaitingRow(updated[0]) }
|
|
1503
|
+
})
|
|
1504
|
+
).pipe(Effect.orDie)
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
const wake: Service["wake"] = Effect.fn("DurableEngineState.wake")((runId) =>
|
|
1508
|
+
writer.write(
|
|
1509
|
+
Effect.gen(function*() {
|
|
1510
|
+
const before = yield* selectWaiting(runId)
|
|
1511
|
+
if (before[0] === undefined) {
|
|
1512
|
+
const existing = yield* sql<{ runId: string }>`
|
|
1513
|
+
SELECT run_id AS "runId" FROM flows_runs WHERE run_id = ${runId}
|
|
1514
|
+
`
|
|
1515
|
+
return existing[0] === undefined ? { _tag: "NotFound" as const } : { _tag: "NotWaiting" as const }
|
|
1516
|
+
}
|
|
1517
|
+
const row = yield* decodeWaitingRow(before[0])
|
|
1518
|
+
yield* sql`
|
|
1519
|
+
UPDATE flows_runs
|
|
1520
|
+
SET
|
|
1521
|
+
waiting_reason = NULL,
|
|
1522
|
+
waiting_wake_at_ms = NULL,
|
|
1523
|
+
waiting_token = NULL,
|
|
1524
|
+
waiting_request = NULL
|
|
1525
|
+
WHERE run_id = ${runId}
|
|
1526
|
+
AND waiting_reason IS NOT NULL
|
|
1527
|
+
`
|
|
1528
|
+
return { _tag: "Woken" as const, row }
|
|
1529
|
+
})
|
|
1530
|
+
).pipe(Effect.orDie)
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
const waiting: Service["waiting"] = Effect.fn("DurableEngineState.waiting")((runId) =>
|
|
1534
|
+
selectWaiting(runId).pipe(
|
|
1535
|
+
Effect.orDie,
|
|
1536
|
+
Effect.flatMap((rows) =>
|
|
1537
|
+
rows[0] === undefined
|
|
1538
|
+
? Effect.succeedNone
|
|
1539
|
+
: Effect.map(decodeWaitingRow(rows[0]), Option.some)
|
|
1540
|
+
)
|
|
1541
|
+
)
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Every open wait in one run tree, the named run's own first.
|
|
1546
|
+
*
|
|
1547
|
+
* Walk the authoritative spawn and trampoline edges. UNION visits each
|
|
1548
|
+
* (run, depth) pair once; the depth cap bounds even externally corrupted
|
|
1549
|
+
* cycles. Grouping by run afterwards collapses unequal-depth DAG paths
|
|
1550
|
+
* and orders each wait at its shortest attached distance from the root.
|
|
1551
|
+
* Correlated edge lookups seek each parent's indexes instead of building
|
|
1552
|
+
* an edge set from the database's entire execution history.
|
|
1553
|
+
*/
|
|
1554
|
+
const waitingTree: Service["waitingTree"] = Effect.fn("DurableEngineState.waitingTree")((runId, options) =>
|
|
1555
|
+
sql<WaitingDatabaseRow>`
|
|
1556
|
+
WITH RECURSIVE tree(run_id, depth) AS (
|
|
1557
|
+
SELECT run_id, 0 FROM flows_runs WHERE run_id = ${runId}
|
|
1558
|
+
UNION
|
|
1559
|
+
SELECT child.run_id, tree.depth + 1
|
|
1560
|
+
FROM tree
|
|
1561
|
+
JOIN flows_runs child ON child.run_id IN (
|
|
1562
|
+
SELECT child_id FROM flows_run_parents WHERE parent_id = tree.run_id
|
|
1563
|
+
UNION ALL
|
|
1564
|
+
SELECT run_id FROM flows_runs WHERE parent_run_id = tree.run_id
|
|
1565
|
+
)
|
|
1566
|
+
WHERE tree.depth < ${waitingTreeMaxDepth}
|
|
1567
|
+
AND (${options?.detached === "include" ? 1 : 0} = 1
|
|
1568
|
+
OR child.parent_run_id = tree.run_id
|
|
1569
|
+
OR COALESCE(${Dialect.jsonText(sql, sql`child.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
|
|
1570
|
+
), reachable(run_id, depth) AS (
|
|
1571
|
+
SELECT run_id, MIN(depth) FROM tree GROUP BY run_id
|
|
1572
|
+
)
|
|
1573
|
+
SELECT
|
|
1574
|
+
parked.run_id AS "runId",
|
|
1575
|
+
parked.waiting_reason AS "waitingReason",
|
|
1576
|
+
parked.waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1577
|
+
parked.waiting_token AS "waitingToken",
|
|
1578
|
+
parked.waiting_request AS "waitingRequest"
|
|
1579
|
+
FROM reachable
|
|
1580
|
+
JOIN flows_runs parked ON parked.run_id = reachable.run_id
|
|
1581
|
+
WHERE parked.waiting_reason IS NOT NULL
|
|
1582
|
+
AND parked.status NOT IN ('completed', 'failed', 'cancelled')
|
|
1583
|
+
ORDER BY reachable.depth, parked.created_at_ms, parked.run_id
|
|
1584
|
+
`.pipe(
|
|
1585
|
+
Effect.orDie,
|
|
1586
|
+
Effect.flatMap((rows) => decodeSweep(rows, "waiting run", waitingRowKey, decodeWaitingRowResult))
|
|
1587
|
+
)
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
const waitingRuns: Service["waitingRuns"] = Effect.fn("DurableEngineState.waitingRuns")((filter) => {
|
|
1591
|
+
// `1 = 0` short-circuits the cancel predicate away when the caller did
|
|
1592
|
+
// not ask for it, so the reason/wake filters (and the 0004 index) stay
|
|
1593
|
+
// the leading predicates (issue #68).
|
|
1594
|
+
const cancelRequestedOnly = filter?.cancelRequested === true ? 1 : 0
|
|
1595
|
+
return (filter?.reason !== undefined && filter.dueBeforeMs !== undefined
|
|
1596
|
+
? sql<WaitingDatabaseRow>`
|
|
1597
|
+
SELECT
|
|
1598
|
+
run_id AS "runId",
|
|
1599
|
+
waiting_reason AS "waitingReason",
|
|
1600
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1601
|
+
waiting_token AS "waitingToken",
|
|
1602
|
+
waiting_request AS "waitingRequest"
|
|
1603
|
+
FROM flows_runs
|
|
1604
|
+
WHERE waiting_reason = ${filter.reason}
|
|
1605
|
+
AND waiting_wake_at_ms IS NOT NULL
|
|
1606
|
+
AND waiting_wake_at_ms <= ${filter.dueBeforeMs}
|
|
1607
|
+
AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
|
|
1608
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1609
|
+
ORDER BY waiting_wake_at_ms, run_id
|
|
1610
|
+
`
|
|
1611
|
+
: filter?.reason !== undefined
|
|
1612
|
+
? sql<WaitingDatabaseRow>`
|
|
1613
|
+
SELECT
|
|
1614
|
+
run_id AS "runId",
|
|
1615
|
+
waiting_reason AS "waitingReason",
|
|
1616
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1617
|
+
waiting_token AS "waitingToken",
|
|
1618
|
+
waiting_request AS "waitingRequest"
|
|
1619
|
+
FROM flows_runs
|
|
1620
|
+
WHERE waiting_reason = ${filter.reason}
|
|
1621
|
+
AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
|
|
1622
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1623
|
+
ORDER BY waiting_wake_at_ms IS NULL, waiting_wake_at_ms, run_id
|
|
1624
|
+
`
|
|
1625
|
+
: filter?.dueBeforeMs !== undefined
|
|
1626
|
+
? sql<WaitingDatabaseRow>`
|
|
1627
|
+
SELECT
|
|
1628
|
+
run_id AS "runId",
|
|
1629
|
+
waiting_reason AS "waitingReason",
|
|
1630
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1631
|
+
waiting_token AS "waitingToken",
|
|
1632
|
+
waiting_request AS "waitingRequest"
|
|
1633
|
+
FROM flows_runs
|
|
1634
|
+
WHERE waiting_reason IS NOT NULL
|
|
1635
|
+
AND waiting_wake_at_ms IS NOT NULL
|
|
1636
|
+
AND waiting_wake_at_ms <= ${filter.dueBeforeMs}
|
|
1637
|
+
AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
|
|
1638
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1639
|
+
ORDER BY waiting_wake_at_ms, run_id
|
|
1640
|
+
`
|
|
1641
|
+
: sql<WaitingDatabaseRow>`
|
|
1642
|
+
SELECT
|
|
1643
|
+
run_id AS "runId",
|
|
1644
|
+
waiting_reason AS "waitingReason",
|
|
1645
|
+
waiting_wake_at_ms AS "waitingWakeAtMs",
|
|
1646
|
+
waiting_token AS "waitingToken",
|
|
1647
|
+
waiting_request AS "waitingRequest"
|
|
1648
|
+
FROM flows_runs
|
|
1649
|
+
WHERE waiting_reason IS NOT NULL
|
|
1650
|
+
AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
|
|
1651
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')
|
|
1652
|
+
ORDER BY waiting_wake_at_ms IS NULL, waiting_wake_at_ms, run_id
|
|
1653
|
+
`).pipe(
|
|
1654
|
+
Effect.orDie,
|
|
1655
|
+
// The parked-run sweeper's only enumeration: one unreadable waiting
|
|
1656
|
+
// payload stranded every parked run, including the ones whose
|
|
1657
|
+
// cancellation had already been requested.
|
|
1658
|
+
Effect.flatMap((rows) => decodeSweep(rows, "waiting run", waitingRowKey, decodeWaitingRowResult))
|
|
1659
|
+
)
|
|
1660
|
+
})
|
|
1661
|
+
|
|
1662
|
+
const pendingRows = (
|
|
1663
|
+
flowName: string,
|
|
1664
|
+
limit?: number,
|
|
1665
|
+
after?: PendingRunCursor,
|
|
1666
|
+
through?: PendingRunCursor,
|
|
1667
|
+
descending = false,
|
|
1668
|
+
eligibleBeforeMs?: number
|
|
1669
|
+
): Effect.Effect<ReadonlyArray<PendingRunCursor>> =>
|
|
1670
|
+
sql<{ readonly runId: string; readonly createdAtMs: number | bigint | string }>`
|
|
1671
|
+
SELECT run_id AS "runId", created_at_ms AS "createdAtMs" FROM flows_runs
|
|
1672
|
+
${Dialect.isPostgres(sql) ? sql`` : sql`INDEXED BY flows_runs_listing_3`}
|
|
1673
|
+
WHERE status = 'pending'
|
|
1674
|
+
AND ${eligibleBeforeMs === undefined ? sql`TRUE` : sql`created_at_ms < ${eligibleBeforeMs}`}
|
|
1675
|
+
AND execution_flow = ${flowName}
|
|
1676
|
+
AND ${
|
|
1677
|
+
after === undefined ? sql`TRUE` : sql`(
|
|
1678
|
+
created_at_ms > ${after.createdAtMs}
|
|
1679
|
+
OR (created_at_ms = ${after.createdAtMs} AND run_id > ${after.runId})
|
|
1680
|
+
)`
|
|
1681
|
+
}
|
|
1682
|
+
AND ${
|
|
1683
|
+
through === undefined ? sql`TRUE` : sql`(
|
|
1684
|
+
created_at_ms < ${through.createdAtMs}
|
|
1685
|
+
OR (created_at_ms = ${through.createdAtMs} AND run_id <= ${through.runId})
|
|
1686
|
+
)`
|
|
1687
|
+
}
|
|
1688
|
+
ORDER BY ${descending ? sql`created_at_ms DESC, run_id DESC` : sql`created_at_ms, run_id`}
|
|
1689
|
+
LIMIT ${limit ?? (Dialect.isPostgres(sql) ? null : -1)}
|
|
1690
|
+
`.pipe(
|
|
1691
|
+
Effect.orDie,
|
|
1692
|
+
Effect.map((rows) => rows.map((row) => ({ runId: row.runId, createdAtMs: Number(row.createdAtMs) })))
|
|
1693
|
+
)
|
|
1694
|
+
const pendingRuns: Service["pendingRuns"] = Effect.fn("DurableEngineState.pendingRuns")((
|
|
1695
|
+
flowName,
|
|
1696
|
+
limit,
|
|
1697
|
+
after,
|
|
1698
|
+
through,
|
|
1699
|
+
eligibleBeforeMs
|
|
1700
|
+
) => pendingRows(flowName, limit, after, through, false, eligibleBeforeMs))
|
|
1701
|
+
const pendingRunTail: Service["pendingRunTail"] = Effect.fn("DurableEngineState.pendingRunTail")((
|
|
1702
|
+
flowName,
|
|
1703
|
+
eligibleBeforeMs
|
|
1704
|
+
) =>
|
|
1705
|
+
pendingRows(flowName, 1, undefined, undefined, true, eligibleBeforeMs).pipe(
|
|
1706
|
+
Effect.map((rows) => Option.fromNullishOr(rows[0]))
|
|
1707
|
+
)
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
const pendingCancellationRows = (
|
|
1711
|
+
limit: number,
|
|
1712
|
+
after?: PendingRunCursor,
|
|
1713
|
+
through?: PendingRunCursor,
|
|
1714
|
+
descending = false
|
|
1715
|
+
): Effect.Effect<ReadonlyArray<PendingRunCursor>> =>
|
|
1716
|
+
pendingCancellationLimit(limit).pipe(
|
|
1717
|
+
Effect.andThen(sql<{ readonly runId: string; readonly createdAtMs: number | bigint | string }>`
|
|
1718
|
+
SELECT run_id AS "runId", created_at_ms AS "createdAtMs" FROM flows_runs
|
|
1719
|
+
${Dialect.isPostgres(sql) ? sql`` : sql`INDEXED BY flows_runs_pending_cancel_idx`}
|
|
1720
|
+
WHERE status = 'pending' AND cancel_requested_at_ms IS NOT NULL
|
|
1721
|
+
AND ${after === undefined ? sql`TRUE` : sql`(created_at_ms, run_id) > (${after.createdAtMs}, ${after.runId})`}
|
|
1722
|
+
AND ${
|
|
1723
|
+
through === undefined ? sql`TRUE` : sql`(created_at_ms, run_id) <= (${through.createdAtMs}, ${through.runId})`
|
|
1724
|
+
}
|
|
1725
|
+
ORDER BY ${descending ? sql`created_at_ms DESC, run_id DESC` : sql`created_at_ms, run_id`}
|
|
1726
|
+
LIMIT ${limit}
|
|
1727
|
+
`.pipe(
|
|
1728
|
+
Effect.orDie,
|
|
1729
|
+
Effect.map((rows) => rows.map((row) => ({ runId: row.runId, createdAtMs: Number(row.createdAtMs) })))
|
|
1730
|
+
))
|
|
1731
|
+
)
|
|
1732
|
+
const pendingCancellationRuns: Service["pendingCancellationRuns"] = Effect.fn(
|
|
1733
|
+
"DurableEngineState.pendingCancellationRuns"
|
|
1734
|
+
)((limit, after, through) => pendingCancellationRows(limit, after, through))
|
|
1735
|
+
const pendingCancellationTail: Service["pendingCancellationTail"] = Effect.fn(
|
|
1736
|
+
"DurableEngineState.pendingCancellationTail"
|
|
1737
|
+
)(() =>
|
|
1738
|
+
pendingCancellationRows(1, undefined, undefined, true).pipe(
|
|
1739
|
+
Effect.map((rows) => Option.fromNullishOr(rows[0]))
|
|
1740
|
+
)
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
const staleRunningRuns: Service["staleRunningRuns"] = Effect.fn(
|
|
1744
|
+
"DurableEngineState.staleRunningRuns"
|
|
1745
|
+
)((staleBeforeMs, limit) =>
|
|
1746
|
+
// `LIMIT -1` is SQLite's explicit "no limit"; the partial index created
|
|
1747
|
+
// in `make` serves the predicate, so the per-tick sweep is an index
|
|
1748
|
+
// range read instead of a full table scan (issue #79).
|
|
1749
|
+
sql<{ runId: string; heartbeatAtMs: number | bigint | string }>`
|
|
1750
|
+
SELECT run_id AS "runId", heartbeat_at_ms AS "heartbeatAtMs"
|
|
1751
|
+
FROM flows_runs
|
|
1752
|
+
WHERE status = 'running'
|
|
1753
|
+
AND heartbeat_at_ms IS NOT NULL
|
|
1754
|
+
AND heartbeat_at_ms < ${staleBeforeMs}
|
|
1755
|
+
ORDER BY heartbeat_at_ms, run_id
|
|
1756
|
+
LIMIT ${limit ?? (Dialect.isPostgres(sql) ? null : -1)}
|
|
1757
|
+
`.pipe(
|
|
1758
|
+
Effect.orDie,
|
|
1759
|
+
Effect.map((rows) => rows.map((row) => ({ runId: String(row.runId), heartbeatAtMs: Number(row.heartbeatAtMs) })))
|
|
1760
|
+
)
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* One ordered range read replaces the engine store's per-attempt point
|
|
1765
|
+
* probes (issue #77): a fresh key costs one empty SELECT instead of 32
|
|
1766
|
+
* sequential gets, and a resumed key one SELECT instead of one per
|
|
1767
|
+
* surviving attempt. The earliest surviving row is the durable retry
|
|
1768
|
+
* origin (issue #69) and the highest attempt contiguous from it the
|
|
1769
|
+
* resumed counter (issue #59). This read is deliberately unbounded; the
|
|
1770
|
+
* pruned-prefix tolerance that makes it agree with the point-probe
|
|
1771
|
+
* fallback is applied once, by the caller, in
|
|
1772
|
+
* `internal/AttemptProbe.probeAttempts` (issue #96).
|
|
1773
|
+
*/
|
|
1774
|
+
const attemptSurvivors: NonNullable<Service["attemptSurvivors"]> = Effect.fn(
|
|
1775
|
+
"DurableEngineState.attemptSurvivors"
|
|
1776
|
+
)((runId, stepKeyDigest) =>
|
|
1777
|
+
sql<{ readonly attempt: number; readonly startedAtMs: number }>`
|
|
1778
|
+
SELECT attempt AS "attempt", started_at_ms AS "startedAtMs"
|
|
1779
|
+
FROM flows_attempts
|
|
1780
|
+
WHERE run_id = ${runId}
|
|
1781
|
+
AND step_key_digest = ${stepKeyDigest}
|
|
1782
|
+
ORDER BY attempt
|
|
1783
|
+
`.pipe(
|
|
1784
|
+
Effect.orDie,
|
|
1785
|
+
Effect.map((rows) => {
|
|
1786
|
+
const first = rows[0]
|
|
1787
|
+
if (first === undefined) return Option.none()
|
|
1788
|
+
let latest = Number(first.attempt)
|
|
1789
|
+
for (let index = 1; index < rows.length; index++) {
|
|
1790
|
+
const next = Number(rows[index]!.attempt)
|
|
1791
|
+
if (next !== latest + 1) break
|
|
1792
|
+
latest = next
|
|
1793
|
+
}
|
|
1794
|
+
return Option.some({
|
|
1795
|
+
earliestAttempt: Number(first.attempt),
|
|
1796
|
+
earliestStartedAtMs: Number(first.startedAtMs),
|
|
1797
|
+
latest
|
|
1798
|
+
})
|
|
1799
|
+
})
|
|
1800
|
+
)
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
const selectRunParent = (childId: string, parentId: string) =>
|
|
1804
|
+
sql<RunParentDatabaseRow>`
|
|
1805
|
+
SELECT
|
|
1806
|
+
child_id AS "childId",
|
|
1807
|
+
parent_id AS "parentId",
|
|
1808
|
+
seq AS "seq"
|
|
1809
|
+
FROM flows_run_parents
|
|
1810
|
+
WHERE child_id = ${childId}
|
|
1811
|
+
AND parent_id = ${parentId}
|
|
1812
|
+
`
|
|
1813
|
+
|
|
1814
|
+
const parentIdsOf = (id: string): Effect.Effect<ReadonlyArray<string>> =>
|
|
1815
|
+
sql<RunParentDatabaseRow>`
|
|
1816
|
+
SELECT
|
|
1817
|
+
child_id AS "childId",
|
|
1818
|
+
parent_id AS "parentId",
|
|
1819
|
+
seq AS "seq"
|
|
1820
|
+
FROM flows_run_parents
|
|
1821
|
+
WHERE child_id = ${id}
|
|
1822
|
+
ORDER BY seq
|
|
1823
|
+
`.pipe(
|
|
1824
|
+
Effect.orDie,
|
|
1825
|
+
Effect.map((rows) => rows.map((row) => String(row.parentId)))
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
const recordRunParent: Service["recordRunParent"] = Effect.fn(
|
|
1829
|
+
"DurableEngineState.recordRunParent"
|
|
1830
|
+
)((childId, parentId) =>
|
|
1831
|
+
writer.write(
|
|
1832
|
+
Effect.gen(function*() {
|
|
1833
|
+
// MAX(seq)+1 and the insert share one write transaction, so the
|
|
1834
|
+
// assigned seq is a total insertion order over the store.
|
|
1835
|
+
const inserted = yield* sql<RunParentDatabaseRow>`
|
|
1836
|
+
INSERT INTO flows_run_parents (child_id, parent_id, seq)
|
|
1837
|
+
SELECT ${childId}, ${parentId}, COALESCE(MAX(seq), 0) + 1
|
|
1838
|
+
FROM flows_run_parents
|
|
1839
|
+
WHERE TRUE
|
|
1840
|
+
ON CONFLICT (child_id, parent_id) DO NOTHING
|
|
1841
|
+
RETURNING
|
|
1842
|
+
child_id AS "childId",
|
|
1843
|
+
parent_id AS "parentId",
|
|
1844
|
+
seq AS "seq"
|
|
1845
|
+
`
|
|
1846
|
+
if (inserted[0] !== undefined) {
|
|
1847
|
+
// The cycle check shares the insert's transaction: a hit fails the
|
|
1848
|
+
// effect, rolling the insert back, so a rejected edge is never
|
|
1849
|
+
// durable and there is no check-then-record window for another
|
|
1850
|
+
// writer to slip through (issues #54/#55/#56). The insert
|
|
1851
|
+
// precedes the walk, so the transaction holds the write lock
|
|
1852
|
+
// while walking: under `DurableWriter.write`'s documented contract —
|
|
1853
|
+
// write transactions are mutually serialized (issue #74) — of two
|
|
1854
|
+
// edges that jointly close a cycle, exactly the later one fails.
|
|
1855
|
+
// This is a contract requirement, not a SQLite artifact; a
|
|
1856
|
+
// backend that ran writes at READ COMMITTED would break it and is
|
|
1857
|
+
// excluded by the DurableWriter contract.
|
|
1858
|
+
const cycle = yield* findCyclePath(childId, parentId, parentIdsOf)
|
|
1859
|
+
if (cycle !== undefined) {
|
|
1860
|
+
return yield* Effect.fail(new RunParentCycleError({ path: cycle }))
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
_tag: "Recorded" as const,
|
|
1864
|
+
edge: yield* decodeRunParentEdge(inserted[0])
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const existing = yield* selectRunParent(childId, parentId)
|
|
1868
|
+
if (existing[0] === undefined) {
|
|
1869
|
+
return yield* Effect.die(new EngineStateFault({ reason: "run_parent_edge_missing" }))
|
|
1870
|
+
}
|
|
1871
|
+
return {
|
|
1872
|
+
_tag: "Existing" as const,
|
|
1873
|
+
edge: yield* decodeRunParentEdge(existing[0])
|
|
1874
|
+
}
|
|
1875
|
+
})
|
|
1876
|
+
).pipe(
|
|
1877
|
+
Effect.catch((error) => error instanceof RunParentCycleError ? Effect.fail(error) : Effect.die(error))
|
|
1878
|
+
)
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
const removeRunParentsForRun: Service["removeRunParentsForRun"] = Effect.fn(
|
|
1882
|
+
"DurableEngineState.removeRunParentsForRun"
|
|
1883
|
+
)((runId) =>
|
|
1884
|
+
writer.write(sql`
|
|
1885
|
+
DELETE FROM flows_run_parents
|
|
1886
|
+
WHERE child_id = ${runId}
|
|
1887
|
+
OR parent_id = ${runId}
|
|
1888
|
+
`).pipe(Effect.orDie, Effect.asVoid)
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
// Keep state-first ordering. This gate and DurableWriter's shared permit are
|
|
1892
|
+
// both taken at the caller's interruptibility, so an interruptible caller can
|
|
1893
|
+
// still be cancelled while queued, including behind a journal-only write that
|
|
1894
|
+
// skips this gate.
|
|
1895
|
+
// A nested SQL transaction already owns the connection and bypasses this gate.
|
|
1896
|
+
const transactionGate = Semaphore.makeUnsafe(1)
|
|
1897
|
+
const transaction: Service["transaction"] = <A, E, R>(
|
|
1898
|
+
effect: Effect.Effect<A, E, R>
|
|
1899
|
+
): Effect.Effect<A, E, R> =>
|
|
1900
|
+
Effect.flatMap(Effect.serviceOption(sql.transactionService), (enclosing) => {
|
|
1901
|
+
const write = (body: Effect.Effect<A, E, R>) =>
|
|
1902
|
+
writer.write(body).pipe(
|
|
1903
|
+
Effect.catchCause((cause) => {
|
|
1904
|
+
// Only a sole storage failure is normalized. A composite keeps every
|
|
1905
|
+
// reason for the caller to diagnose, and its storage failures are
|
|
1906
|
+
// demoted to defects one by one rather than cast through the
|
|
1907
|
+
// declared error type: `transaction` promises the BODY's failures,
|
|
1908
|
+
// so a `DatabaseError` arriving as a typed failure would be a lie
|
|
1909
|
+
// the compiler could not catch. Defect identity is unchanged.
|
|
1910
|
+
if (cause.reasons.length === 1) {
|
|
1911
|
+
const reason = cause.reasons[0]!
|
|
1912
|
+
if (Cause.isFailReason(reason) && reason.error instanceof DatabaseError) {
|
|
1913
|
+
return Effect.die(reason.error)
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return Effect.failCause(dieOnDatabaseError<E>(cause))
|
|
1917
|
+
})
|
|
1918
|
+
)
|
|
1919
|
+
if (Option.isSome(enclosing)) return write(effect)
|
|
1920
|
+
return Effect.uninterruptibleMask((restore) =>
|
|
1921
|
+
Effect.gen(function*() {
|
|
1922
|
+
yield* restore(transactionGate.take(1))
|
|
1923
|
+
let released = false
|
|
1924
|
+
const release = Effect.suspend(() => {
|
|
1925
|
+
if (released) return Effect.void
|
|
1926
|
+
released = true
|
|
1927
|
+
return transactionGate.release(1).pipe(Effect.asVoid)
|
|
1928
|
+
})
|
|
1929
|
+
return yield* restore(write(Effect.gen(function*() {
|
|
1930
|
+
// Release at COMMIT, before publication can read through another
|
|
1931
|
+
// transaction. A failed write instead releases in ensuring below.
|
|
1932
|
+
yield* afterCommit(release, sql)
|
|
1933
|
+
return yield* effect
|
|
1934
|
+
}))).pipe(Effect.ensuring(release))
|
|
1935
|
+
})
|
|
1936
|
+
)
|
|
1937
|
+
})
|
|
1938
|
+
|
|
1939
|
+
const runParents: Service["runParents"] = Effect.fn("DurableEngineState.runParents")((childId) =>
|
|
1940
|
+
sql<RunParentDatabaseRow>`
|
|
1941
|
+
SELECT
|
|
1942
|
+
child_id AS "childId",
|
|
1943
|
+
parent_id AS "parentId",
|
|
1944
|
+
seq AS "seq"
|
|
1945
|
+
FROM flows_run_parents
|
|
1946
|
+
WHERE child_id = ${childId}
|
|
1947
|
+
ORDER BY seq
|
|
1948
|
+
`.pipe(
|
|
1949
|
+
Effect.orDie,
|
|
1950
|
+
// One unreadable edge made a run's whole ancestry unreadable, so no
|
|
1951
|
+
// subflow under it could be linked at all.
|
|
1952
|
+
Effect.flatMap((rows) => decodeSweep(rows, "run parent", runParentRowKey, decodeRunParentEdgeResult))
|
|
1953
|
+
)
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
const runChildren: Service["runChildren"] = Effect.fn("DurableEngineState.runChildren")((parentId) =>
|
|
1957
|
+
sql<RunParentDatabaseRow>`
|
|
1958
|
+
SELECT
|
|
1959
|
+
child_id AS "childId",
|
|
1960
|
+
parent_id AS "parentId",
|
|
1961
|
+
seq AS "seq"
|
|
1962
|
+
FROM flows_run_parents
|
|
1963
|
+
WHERE parent_id = ${parentId}
|
|
1964
|
+
ORDER BY seq
|
|
1965
|
+
`.pipe(
|
|
1966
|
+
Effect.orDie,
|
|
1967
|
+
// The cancellation cascade's only cross-process way to find a run's
|
|
1968
|
+
// children: one unreadable sibling edge left every child running after
|
|
1969
|
+
// its parent was cancelled.
|
|
1970
|
+
Effect.flatMap((rows) => decodeSweep(rows, "run parent", runParentRowKey, decodeRunParentEdgeResult))
|
|
1971
|
+
)
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
return DurableEngineState.of({
|
|
1975
|
+
deferred,
|
|
1976
|
+
consumeDeferred,
|
|
1977
|
+
completeDeferred,
|
|
1978
|
+
clock,
|
|
1979
|
+
scheduleClock,
|
|
1980
|
+
completeClock,
|
|
1981
|
+
completeRunClocks,
|
|
1982
|
+
dueClocks,
|
|
1983
|
+
pendingClocks,
|
|
1984
|
+
completedDeferreds,
|
|
1985
|
+
park,
|
|
1986
|
+
wake,
|
|
1987
|
+
waiting,
|
|
1988
|
+
waitingTree,
|
|
1989
|
+
waitingRuns,
|
|
1990
|
+
pendingRuns,
|
|
1991
|
+
pendingRunTail,
|
|
1992
|
+
pendingCancellationRuns,
|
|
1993
|
+
pendingCancellationTail,
|
|
1994
|
+
staleRunningRuns,
|
|
1995
|
+
attemptSurvivors,
|
|
1996
|
+
recordRunParent,
|
|
1997
|
+
removeRunParentsForRun,
|
|
1998
|
+
runParents,
|
|
1999
|
+
runChildren,
|
|
2000
|
+
transaction
|
|
2001
|
+
})
|
|
2002
|
+
})
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Provides database-backed durable engine state.
|
|
2006
|
+
*
|
|
2007
|
+
* @since 0.1.0
|
|
2008
|
+
* @category layers
|
|
2009
|
+
*/
|
|
2010
|
+
export const layer: Layer.Layer<DurableEngineState, never, DurableWriter | SqlClient.SqlClient> = Layer.effect(
|
|
2011
|
+
DurableEngineState,
|
|
2012
|
+
make
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* A run's ownership view as the in-memory implementation needs it for the
|
|
2017
|
+
* same fences the SQL implementation reads from `flows_runs`.
|
|
2018
|
+
*
|
|
2019
|
+
* @since 0.1.0
|
|
2020
|
+
* @category models
|
|
2021
|
+
*/
|
|
2022
|
+
export interface MemoryRunView {
|
|
2023
|
+
readonly flowName?: string | undefined
|
|
2024
|
+
readonly createdAtMs?: number | undefined
|
|
2025
|
+
readonly status: "pending" | "running" | "suspended" | "completed" | "failed" | "cancelled"
|
|
2026
|
+
readonly owner: OwnerId | null
|
|
2027
|
+
/**
|
|
2028
|
+
* The run's last heartbeat instant, mirroring `flows_runs.heartbeat_at_ms`.
|
|
2029
|
+
* `undefined` means unknown — such a run never surfaces from
|
|
2030
|
+
* `staleRunningRuns` (issue #53).
|
|
2031
|
+
*/
|
|
2032
|
+
readonly heartbeatAtMs?: number | null | undefined
|
|
2033
|
+
/**
|
|
2034
|
+
* The run's durable cancel request, mirroring
|
|
2035
|
+
* `flows_runs.cancel_requested_at_ms`. `undefined`/`null` means not
|
|
2036
|
+
* requested — such a run never surfaces from a
|
|
2037
|
+
* `waitingRuns({ cancelRequested: true })` query (issue #68).
|
|
2038
|
+
*/
|
|
2039
|
+
readonly cancelRequestedAtMs?: number | null | undefined
|
|
2040
|
+
/**
|
|
2041
|
+
* What this run does when the run that spawned it ends, mirroring
|
|
2042
|
+
* `RunState.onParentExit`. `undefined` reads as `cancel`, the default and
|
|
2043
|
+
* the shape a `.child()` boundary takes, so a view that does not record it
|
|
2044
|
+
* describes an attached child.
|
|
2045
|
+
*/
|
|
2046
|
+
readonly onParentExit?: OnParentExit | undefined
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* Options for the in-memory durable-state implementation.
|
|
2051
|
+
*
|
|
2052
|
+
* @since 0.1.0
|
|
2053
|
+
* @category models
|
|
2054
|
+
*/
|
|
2055
|
+
export interface MemoryOptions {
|
|
2056
|
+
/**
|
|
2057
|
+
* Resolves a run's existence, status, and owner — the in-memory analogue
|
|
2058
|
+
* of the `flows_runs` lookups the SQL implementation performs for its
|
|
2059
|
+
* `park`/`wake`/`scheduleClock` fences. `Option.none()` means the run does
|
|
2060
|
+
* not exist. When omitted, every run is treated as running and owned by
|
|
2061
|
+
* whichever owner is presented (a deliberately permissive default for tests
|
|
2062
|
+
* that exercise only deferred/clock state without a run table).
|
|
2063
|
+
*/
|
|
2064
|
+
readonly runs?: (runId: string) => Option.Option<MemoryRunView>
|
|
2065
|
+
/**
|
|
2066
|
+
* Enumerates every known run — the in-memory analogue of scanning
|
|
2067
|
+
* `flows_runs`, which `staleRunningRuns` needs (the `runs` lookup alone
|
|
2068
|
+
* cannot enumerate). When omitted, `staleRunningRuns` reports no stale
|
|
2069
|
+
* rows.
|
|
2070
|
+
*/
|
|
2071
|
+
readonly listRuns?: () => Iterable<readonly [string, MemoryRunView]>
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Constructs a deterministic in-memory durable-state implementation.
|
|
2076
|
+
*
|
|
2077
|
+
* The returned service can be shared by multiple fresh engine instances in a
|
|
2078
|
+
* test to model process restart over the same storage. With `runs` supplied
|
|
2079
|
+
* it enforces the same ownership fences as the SQL implementation and is
|
|
2080
|
+
* held to the same contract suite
|
|
2081
|
+
* (`test/contract/DurableEngineStateContract.ts`).
|
|
2082
|
+
*
|
|
2083
|
+
* @since 0.1.0
|
|
2084
|
+
* @category constructors
|
|
2085
|
+
*/
|
|
2086
|
+
export const makeMemory = (options: MemoryOptions = {}): Service => {
|
|
2087
|
+
const deferreds = new Map<string, DeferredRow>()
|
|
2088
|
+
const consumedDeferreds = new Map<string, number>()
|
|
2089
|
+
const clocks = new Map<string, ClockRow>()
|
|
2090
|
+
const waitingRows = new Map<string, WaitingRow>()
|
|
2091
|
+
// childId -> (parentId -> seq); `parentSeq` mirrors the SQL MAX(seq)+1.
|
|
2092
|
+
const parentEdges = new Map<string, Map<string, number>>()
|
|
2093
|
+
let parentSeq = 0
|
|
2094
|
+
|
|
2095
|
+
const snapshotDeferred = (row: DeferredRow): Effect.Effect<DeferredRow> =>
|
|
2096
|
+
Schema.decodeUnknownEffect(DeferredRow)(row).pipe(
|
|
2097
|
+
Effect.orDie,
|
|
2098
|
+
Effect.flatMap((decoded) =>
|
|
2099
|
+
Effect.all({
|
|
2100
|
+
exitJson: encodeJson(decoded.exit, "exit"),
|
|
2101
|
+
metadataJson: decoded.metadata === undefined
|
|
2102
|
+
? Effect.succeed(null)
|
|
2103
|
+
: encodeJson(decoded.metadata, "metadata")
|
|
2104
|
+
}).pipe(
|
|
2105
|
+
Effect.flatMap(({ exitJson, metadataJson }) =>
|
|
2106
|
+
decodeDeferredRow({
|
|
2107
|
+
flowName: decoded.flowName,
|
|
2108
|
+
executionId: decoded.executionId,
|
|
2109
|
+
deferredName: decoded.deferredName,
|
|
2110
|
+
exitJson,
|
|
2111
|
+
metadataJson,
|
|
2112
|
+
completedAtMs: decoded.completedAtMs
|
|
2113
|
+
})
|
|
2114
|
+
)
|
|
2115
|
+
)
|
|
2116
|
+
)
|
|
2117
|
+
)
|
|
2118
|
+
|
|
2119
|
+
const snapshotClock = (row: ClockRow): Effect.Effect<ClockRow> =>
|
|
2120
|
+
Schema.decodeUnknownEffect(ClockRow)(row).pipe(
|
|
2121
|
+
Effect.orDie,
|
|
2122
|
+
Effect.map((decoded) => ({ ...decoded }))
|
|
2123
|
+
)
|
|
2124
|
+
|
|
2125
|
+
const snapshotWaiting = (row: WaitingRow): WaitingRow => ({ ...row })
|
|
2126
|
+
|
|
2127
|
+
/** The synchronous twin of `findCyclePath` over the in-memory edge map. */
|
|
2128
|
+
const findCyclePathSync = (
|
|
2129
|
+
childId: string,
|
|
2130
|
+
parentId: string
|
|
2131
|
+
): ReadonlyArray<string> | undefined => {
|
|
2132
|
+
if (parentId === childId) return [childId]
|
|
2133
|
+
const seen = new Set<string>([parentId])
|
|
2134
|
+
const walk = (
|
|
2135
|
+
current: string,
|
|
2136
|
+
chain: ReadonlyArray<string>
|
|
2137
|
+
): ReadonlyArray<string> | undefined => {
|
|
2138
|
+
for (const parent of parentEdges.get(current)?.keys() ?? []) {
|
|
2139
|
+
if (parent === childId) return [...chain, parent].reverse()
|
|
2140
|
+
if (seen.has(parent)) continue
|
|
2141
|
+
seen.add(parent)
|
|
2142
|
+
const found = walk(parent, [...chain, parent])
|
|
2143
|
+
if (found !== undefined) return found
|
|
2144
|
+
}
|
|
2145
|
+
return undefined
|
|
2146
|
+
}
|
|
2147
|
+
return walk(parentId, [parentId])
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const sameOwner = (left: OwnerId, right: OwnerId): boolean =>
|
|
2151
|
+
left.hostId === right.hostId && left.pid === right.pid && left.nonce === right.nonce
|
|
2152
|
+
|
|
2153
|
+
/** Mirrors the SQL `flows_runs` owner-match predicate for a run. */
|
|
2154
|
+
const runView = (runId: string, presented: OwnerId | undefined): {
|
|
2155
|
+
readonly exists: boolean
|
|
2156
|
+
readonly running: boolean
|
|
2157
|
+
readonly owned: boolean
|
|
2158
|
+
} => {
|
|
2159
|
+
if (options.runs === undefined) {
|
|
2160
|
+
return { exists: true, running: true, owned: true }
|
|
2161
|
+
}
|
|
2162
|
+
const view = options.runs(runId)
|
|
2163
|
+
if (Option.isNone(view)) {
|
|
2164
|
+
return { exists: false, running: false, owned: false }
|
|
2165
|
+
}
|
|
2166
|
+
const owned = presented !== undefined &&
|
|
2167
|
+
view.value.owner !== null &&
|
|
2168
|
+
sameOwner(view.value.owner, presented)
|
|
2169
|
+
return { exists: true, running: view.value.status === "running", owned }
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Mirrors the SQL sweeps' run-status join: a row whose run has settled is
|
|
2174
|
+
* never swept, and `Option.none()` from a supplied `runs` view means the run
|
|
2175
|
+
* does not exist, so its orphaned engine-state rows stay hidden. The
|
|
2176
|
+
* permissive default (no `runs` view) treats every run as live because a
|
|
2177
|
+
* standalone memory store has no run table to consult.
|
|
2178
|
+
*/
|
|
2179
|
+
const isLive = (runId: string): boolean => {
|
|
2180
|
+
if (options.runs === undefined) return true
|
|
2181
|
+
const view = options.runs(runId)
|
|
2182
|
+
return Option.isSome(view) && (
|
|
2183
|
+
view.value.status === "pending" ||
|
|
2184
|
+
view.value.status === "running" ||
|
|
2185
|
+
view.value.status === "suspended"
|
|
2186
|
+
)
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
const pendingMemoryRows = (
|
|
2190
|
+
flowName: string | undefined,
|
|
2191
|
+
after?: PendingRunCursor,
|
|
2192
|
+
through?: PendingRunCursor,
|
|
2193
|
+
eligibleBeforeMs?: number,
|
|
2194
|
+
cancellationOnly = false
|
|
2195
|
+
) =>
|
|
2196
|
+
Array.from(options.listRuns?.() ?? [])
|
|
2197
|
+
.filter(([runId, view]) =>
|
|
2198
|
+
view.status === "pending" && (flowName === undefined || view.flowName === flowName) &&
|
|
2199
|
+
(!cancellationOnly || view.cancelRequestedAtMs != null) &&
|
|
2200
|
+
(eligibleBeforeMs === undefined || (view.createdAtMs ?? 0) < eligibleBeforeMs) &&
|
|
2201
|
+
(after === undefined || (view.createdAtMs ?? 0) > after.createdAtMs ||
|
|
2202
|
+
((view.createdAtMs ?? 0) === after.createdAtMs && compareText(runId, after.runId) > 0)) &&
|
|
2203
|
+
(through === undefined || (view.createdAtMs ?? 0) < through.createdAtMs ||
|
|
2204
|
+
((view.createdAtMs ?? 0) === through.createdAtMs && compareText(runId, through.runId) <= 0))
|
|
2205
|
+
)
|
|
2206
|
+
.sort(([leftId, left], [rightId, right]) =>
|
|
2207
|
+
(left.createdAtMs ?? 0) - (right.createdAtMs ?? 0) || compareText(leftId, rightId)
|
|
2208
|
+
)
|
|
2209
|
+
.map(([runId, view]) => ({ runId, createdAtMs: view.createdAtMs ?? 0 }))
|
|
2210
|
+
|
|
2211
|
+
const unguarded: Omit<Service, "transaction"> = {
|
|
2212
|
+
deferred: Effect.fn("DurableEngineState.deferred")((address) =>
|
|
2213
|
+
Effect.gen(function*() {
|
|
2214
|
+
const row = deferreds.get(deferredKey(address))
|
|
2215
|
+
return row === undefined ? Option.none() : Option.some(yield* snapshotDeferred(row))
|
|
2216
|
+
})
|
|
2217
|
+
),
|
|
2218
|
+
consumeDeferred: Effect.fn("DurableEngineState.consumeDeferred")((address, consumedAtMs) =>
|
|
2219
|
+
Effect.sync(() => {
|
|
2220
|
+
const key = deferredKey(address)
|
|
2221
|
+
if (!deferreds.has(key) || consumedDeferreds.has(key)) return false
|
|
2222
|
+
consumedDeferreds.set(key, consumedAtMs)
|
|
2223
|
+
return true
|
|
2224
|
+
})
|
|
2225
|
+
),
|
|
2226
|
+
completeDeferred: Effect.fn("DurableEngineState.completeDeferred")((row) =>
|
|
2227
|
+
Effect.gen(function*() {
|
|
2228
|
+
const stored = yield* snapshotDeferred(row)
|
|
2229
|
+
const key = deferredKey(stored)
|
|
2230
|
+
const existing = deferreds.get(key)
|
|
2231
|
+
if (existing !== undefined) {
|
|
2232
|
+
return {
|
|
2233
|
+
_tag: "Existing" as const,
|
|
2234
|
+
row: yield* snapshotDeferred(existing),
|
|
2235
|
+
digest: yield* memoryDigest(existing)
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
deferreds.set(key, stored)
|
|
2239
|
+
return {
|
|
2240
|
+
_tag: "Completed" as const,
|
|
2241
|
+
row: yield* snapshotDeferred(stored),
|
|
2242
|
+
digest: yield* memoryDigest(stored)
|
|
2243
|
+
}
|
|
2244
|
+
})
|
|
2245
|
+
),
|
|
2246
|
+
clock: Effect.fn("DurableEngineState.clock")((address) =>
|
|
2247
|
+
Effect.gen(function*() {
|
|
2248
|
+
const row = clocks.get(clockKey(address))
|
|
2249
|
+
return row === undefined ? Option.none() : Option.some(yield* snapshotClock(row))
|
|
2250
|
+
})
|
|
2251
|
+
),
|
|
2252
|
+
scheduleClock: Effect.fn("DurableEngineState.scheduleClock")((row, owner) =>
|
|
2253
|
+
Effect.gen(function*(): Generator<Effect.Effect<unknown>, ScheduleClockOutcome, never> {
|
|
2254
|
+
// Mirrors the SQL fence: creation requires the presented owner to
|
|
2255
|
+
// currently run the execution; a lost fence surfaces as
|
|
2256
|
+
// self-interruption, an existing row wins regardless.
|
|
2257
|
+
const stored = yield* snapshotClock(row)
|
|
2258
|
+
const key = clockKey(stored)
|
|
2259
|
+
const existing = clocks.get(key)
|
|
2260
|
+
if (existing !== undefined) {
|
|
2261
|
+
return { _tag: "Existing" as const, row: yield* snapshotClock(existing) }
|
|
2262
|
+
}
|
|
2263
|
+
const view = runView(stored.executionId, owner)
|
|
2264
|
+
if (!view.exists || !view.running || !view.owned) {
|
|
2265
|
+
return yield* Effect.interrupt
|
|
2266
|
+
}
|
|
2267
|
+
clocks.set(key, stored)
|
|
2268
|
+
return { _tag: "Scheduled" as const, row: yield* snapshotClock(stored) }
|
|
2269
|
+
})
|
|
2270
|
+
),
|
|
2271
|
+
completeClock: Effect.fn("DurableEngineState.completeClock")((address, completedAtMs) =>
|
|
2272
|
+
Effect.sync(() => {
|
|
2273
|
+
const key = clockKey(address)
|
|
2274
|
+
const existing = clocks.get(key)
|
|
2275
|
+
if (existing === undefined) {
|
|
2276
|
+
return { _tag: "NotFound" as const }
|
|
2277
|
+
}
|
|
2278
|
+
if (existing.completedAtMs !== null) {
|
|
2279
|
+
return { _tag: "AlreadyCompleted" as const, row: { ...existing } }
|
|
2280
|
+
}
|
|
2281
|
+
const row = { ...existing, completedAtMs }
|
|
2282
|
+
clocks.set(key, row)
|
|
2283
|
+
return { _tag: "Completed" as const, row: { ...row } }
|
|
2284
|
+
})
|
|
2285
|
+
),
|
|
2286
|
+
dueClocks: Effect.fn("DurableEngineState.dueClocks")((nowMs) =>
|
|
2287
|
+
Effect.forEach(
|
|
2288
|
+
Array.from(clocks.values())
|
|
2289
|
+
.filter((row) => row.completedAtMs === null && row.dueAtMs <= nowMs)
|
|
2290
|
+
.sort((left, right) =>
|
|
2291
|
+
left.dueAtMs - right.dueAtMs ||
|
|
2292
|
+
compareText(left.executionId, right.executionId) ||
|
|
2293
|
+
compareText(left.clockName, right.clockName)
|
|
2294
|
+
),
|
|
2295
|
+
snapshotClock
|
|
2296
|
+
)
|
|
2297
|
+
),
|
|
2298
|
+
completeRunClocks: Effect.fn("DurableEngineState.completeRunClocks")((executionId, completedAtMs) =>
|
|
2299
|
+
Effect.sync(() => {
|
|
2300
|
+
const completed: Array<ClockRow> = []
|
|
2301
|
+
for (const [key, row] of clocks) {
|
|
2302
|
+
if (row.executionId !== executionId || row.completedAtMs !== null) continue
|
|
2303
|
+
const next = { ...row, completedAtMs }
|
|
2304
|
+
clocks.set(key, next)
|
|
2305
|
+
completed.push({ ...next })
|
|
2306
|
+
}
|
|
2307
|
+
return completed.sort((left, right) =>
|
|
2308
|
+
compareText(left.flowName, right.flowName) || compareText(left.clockName, right.clockName)
|
|
2309
|
+
)
|
|
2310
|
+
})
|
|
2311
|
+
),
|
|
2312
|
+
pendingClocks: Effect.fn("DurableEngineState.pendingClocks")((scope) =>
|
|
2313
|
+
Effect.flatMap(
|
|
2314
|
+
Effect.sync(() =>
|
|
2315
|
+
Array.from(clocks.values())
|
|
2316
|
+
.filter((row) =>
|
|
2317
|
+
row.completedAtMs === null &&
|
|
2318
|
+
isLive(row.executionId) &&
|
|
2319
|
+
(scope.executionId === undefined || row.executionId === scope.executionId) &&
|
|
2320
|
+
(scope.flowName === undefined || row.flowName === scope.flowName)
|
|
2321
|
+
)
|
|
2322
|
+
.sort((left, right) =>
|
|
2323
|
+
left.dueAtMs - right.dueAtMs ||
|
|
2324
|
+
compareText(left.executionId, right.executionId) ||
|
|
2325
|
+
compareText(left.clockName, right.clockName)
|
|
2326
|
+
)
|
|
2327
|
+
),
|
|
2328
|
+
(rows) => Effect.forEach(rows, snapshotClock)
|
|
2329
|
+
)
|
|
2330
|
+
),
|
|
2331
|
+
completedDeferreds: Effect.fn("DurableEngineState.completedDeferreds")((flowName) =>
|
|
2332
|
+
Effect.sync(() =>
|
|
2333
|
+
Array.from(deferreds.values())
|
|
2334
|
+
.filter((row) =>
|
|
2335
|
+
row.flowName === flowName && isLive(row.executionId) && !consumedDeferreds.has(deferredKey(row))
|
|
2336
|
+
)
|
|
2337
|
+
.map(({ flowName, executionId, deferredName }) => ({
|
|
2338
|
+
flowName,
|
|
2339
|
+
executionId,
|
|
2340
|
+
deferredName
|
|
2341
|
+
}))
|
|
2342
|
+
.sort((left, right) =>
|
|
2343
|
+
compareText(left.executionId, right.executionId) ||
|
|
2344
|
+
compareText(left.deferredName, right.deferredName)
|
|
2345
|
+
)
|
|
2346
|
+
)
|
|
2347
|
+
),
|
|
2348
|
+
park: Effect.fn("DurableEngineState.park")((runId, waitingPayload, owner) =>
|
|
2349
|
+
Effect.sync(() => {
|
|
2350
|
+
// Mirrors the SQL fence: only the current owner of an existing run
|
|
2351
|
+
// may park it; anything else reports NotFound, exactly like the
|
|
2352
|
+
// owner-guarded UPDATE matching no row.
|
|
2353
|
+
const view = runView(runId, owner)
|
|
2354
|
+
if (!view.exists || !view.owned) {
|
|
2355
|
+
return { _tag: "NotFound" as const }
|
|
2356
|
+
}
|
|
2357
|
+
// The declared question is stored as JSON TEXT and read back decoded,
|
|
2358
|
+
// exactly as the column and its SELECT do it.
|
|
2359
|
+
const row: WaitingRow = {
|
|
2360
|
+
runId,
|
|
2361
|
+
reason: waitingPayload.reason,
|
|
2362
|
+
wakeAt: waitingPayload.wakeAt ?? null,
|
|
2363
|
+
token: waitingPayload.token ?? null,
|
|
2364
|
+
...decodeWaitingRequest(waitingPayload.request)
|
|
2365
|
+
} as WaitingRow
|
|
2366
|
+
waitingRows.set(runId, row)
|
|
2367
|
+
return { _tag: "Parked" as const, row: snapshotWaiting(row) }
|
|
2368
|
+
})
|
|
2369
|
+
),
|
|
2370
|
+
wake: Effect.fn("DurableEngineState.wake")((runId) =>
|
|
2371
|
+
Effect.sync(() => {
|
|
2372
|
+
const row = waitingRows.get(runId)
|
|
2373
|
+
if (row === undefined) {
|
|
2374
|
+
// Mirrors SQL: an unknown run is NotFound, an existing unparked
|
|
2375
|
+
// run is NotWaiting.
|
|
2376
|
+
return runView(runId, undefined).exists
|
|
2377
|
+
? { _tag: "NotWaiting" as const }
|
|
2378
|
+
: { _tag: "NotFound" as const }
|
|
2379
|
+
}
|
|
2380
|
+
waitingRows.delete(runId)
|
|
2381
|
+
return { _tag: "Woken" as const, row: snapshotWaiting(row) }
|
|
2382
|
+
})
|
|
2383
|
+
),
|
|
2384
|
+
waiting: Effect.fn("DurableEngineState.waiting")((runId) =>
|
|
2385
|
+
Effect.sync(() => {
|
|
2386
|
+
const row = waitingRows.get(runId)
|
|
2387
|
+
return row === undefined ? Option.none() : Option.some(snapshotWaiting(row))
|
|
2388
|
+
})
|
|
2389
|
+
),
|
|
2390
|
+
waitingTree: Effect.fn("DurableEngineState.waitingTree")((runId, walk) =>
|
|
2391
|
+
Effect.sync(() => {
|
|
2392
|
+
// Breadth-first over recorded spawn edges: the named run first, then
|
|
2393
|
+
// each generation beneath it. This memory view does not model the
|
|
2394
|
+
// parent_run_id trampoline links that the SQL walk also follows.
|
|
2395
|
+
const found: Array<WaitingRow> = []
|
|
2396
|
+
const seen = new Set<string>([runId])
|
|
2397
|
+
let frontier: ReadonlyArray<string> = [runId]
|
|
2398
|
+
for (let depth = 0; depth <= waitingTreeMaxDepth && frontier.length > 0; depth++) {
|
|
2399
|
+
const next: Array<string> = []
|
|
2400
|
+
for (const current of frontier) {
|
|
2401
|
+
const row = waitingRows.get(current)
|
|
2402
|
+
if (row !== undefined && isLive(current)) found.push(snapshotWaiting(row))
|
|
2403
|
+
for (const [childId, parents] of parentEdges) {
|
|
2404
|
+
if (!parents.has(current) || seen.has(childId)) continue
|
|
2405
|
+
// The SQL walk skips a detached subtree; so does this one, for a
|
|
2406
|
+
// view that records the policy. A view that does not describes an
|
|
2407
|
+
// attached child, which is the default.
|
|
2408
|
+
const view = options.runs?.(childId)
|
|
2409
|
+
if (
|
|
2410
|
+
walk?.detached !== "include" && view !== undefined && Option.isSome(view) &&
|
|
2411
|
+
view.value.onParentExit === "detach"
|
|
2412
|
+
) continue
|
|
2413
|
+
seen.add(childId)
|
|
2414
|
+
next.push(childId)
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
frontier = next
|
|
2418
|
+
}
|
|
2419
|
+
return found
|
|
2420
|
+
})
|
|
2421
|
+
),
|
|
2422
|
+
waitingRuns: Effect.fn("DurableEngineState.waitingRuns")((filter) =>
|
|
2423
|
+
Effect.sync(() =>
|
|
2424
|
+
Array.from(waitingRows.values())
|
|
2425
|
+
// Mirrors the SQL status predicate: a terminally closed run's
|
|
2426
|
+
// stale waiting row never surfaces to a sweeper (issue #28). The
|
|
2427
|
+
// permissive default (no `runs` view) treats every run as live.
|
|
2428
|
+
.filter((row) => isLive(row.runId))
|
|
2429
|
+
.filter((row) => filter?.reason === undefined || row.reason === filter.reason)
|
|
2430
|
+
.filter((row) =>
|
|
2431
|
+
filter?.dueBeforeMs === undefined ||
|
|
2432
|
+
(row.wakeAt !== null && row.wakeAt <= filter.dueBeforeMs)
|
|
2433
|
+
)
|
|
2434
|
+
// Mirrors the SQL cancel predicate over
|
|
2435
|
+
// `flows_runs.cancel_requested_at_ms` (issue #68). Without a
|
|
2436
|
+
// `runs` view the flag is unknowable, so the query stays
|
|
2437
|
+
// permissive and the sweeper's own per-row guard decides.
|
|
2438
|
+
.filter((row) => {
|
|
2439
|
+
if (filter?.cancelRequested !== true) return true
|
|
2440
|
+
if (options.runs === undefined) return true
|
|
2441
|
+
const view = options.runs(row.runId)
|
|
2442
|
+
return Option.isSome(view) && view.value.cancelRequestedAtMs != null
|
|
2443
|
+
})
|
|
2444
|
+
.sort((left, right) =>
|
|
2445
|
+
Number(left.wakeAt === null) - Number(right.wakeAt === null) ||
|
|
2446
|
+
(left.wakeAt ?? 0) - (right.wakeAt ?? 0) ||
|
|
2447
|
+
compareText(left.runId, right.runId)
|
|
2448
|
+
)
|
|
2449
|
+
.map(snapshotWaiting)
|
|
2450
|
+
)
|
|
2451
|
+
),
|
|
2452
|
+
pendingRuns: Effect.fn("DurableEngineState.pendingRuns")((flowName, limit, after, through, eligibleBeforeMs) =>
|
|
2453
|
+
Effect.sync(() => {
|
|
2454
|
+
const ordered = pendingMemoryRows(flowName, after, through, eligibleBeforeMs)
|
|
2455
|
+
return limit === undefined ? ordered : ordered.slice(0, limit)
|
|
2456
|
+
})
|
|
2457
|
+
),
|
|
2458
|
+
pendingRunTail: Effect.fn("DurableEngineState.pendingRunTail")((flowName, eligibleBeforeMs) =>
|
|
2459
|
+
Effect.sync(() =>
|
|
2460
|
+
Option.fromNullishOr(pendingMemoryRows(flowName, undefined, undefined, eligibleBeforeMs).at(-1))
|
|
2461
|
+
)
|
|
2462
|
+
),
|
|
2463
|
+
pendingCancellationRuns: Effect.fn("DurableEngineState.pendingCancellationRuns")((limit, after, through) =>
|
|
2464
|
+
pendingCancellationLimit(limit).pipe(Effect.andThen(
|
|
2465
|
+
Effect.sync(() => pendingMemoryRows(undefined, after, through, undefined, true).slice(0, limit))
|
|
2466
|
+
))
|
|
2467
|
+
),
|
|
2468
|
+
pendingCancellationTail: Effect.fn("DurableEngineState.pendingCancellationTail")(() =>
|
|
2469
|
+
Effect.sync(() =>
|
|
2470
|
+
Option.fromNullishOr(
|
|
2471
|
+
pendingMemoryRows(undefined, undefined, undefined, undefined, true).at(-1)
|
|
2472
|
+
)
|
|
2473
|
+
)
|
|
2474
|
+
),
|
|
2475
|
+
staleRunningRuns: Effect.fn("DurableEngineState.staleRunningRuns")((staleBeforeMs, limit) =>
|
|
2476
|
+
Effect.sync(() => {
|
|
2477
|
+
// Mirrors the SQL scan of `flows_runs`: without an enumerator there
|
|
2478
|
+
// is nothing to scan, so no stale rows exist.
|
|
2479
|
+
if (options.listRuns === undefined) return []
|
|
2480
|
+
const stale: Array<StaleRunningRow> = []
|
|
2481
|
+
for (const [runId, view] of options.listRuns()) {
|
|
2482
|
+
const heartbeatAtMs = view.heartbeatAtMs
|
|
2483
|
+
if (
|
|
2484
|
+
view.status === "running" &&
|
|
2485
|
+
typeof heartbeatAtMs === "number" &&
|
|
2486
|
+
heartbeatAtMs < staleBeforeMs
|
|
2487
|
+
) {
|
|
2488
|
+
stale.push({ runId, heartbeatAtMs })
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
const ordered = stale.sort((left, right) =>
|
|
2492
|
+
left.heartbeatAtMs - right.heartbeatAtMs ||
|
|
2493
|
+
compareText(left.runId, right.runId)
|
|
2494
|
+
)
|
|
2495
|
+
// Mirrors the SQL LIMIT: oldest heartbeats first, capped per sweep
|
|
2496
|
+
// (issue #79).
|
|
2497
|
+
return limit === undefined ? ordered : ordered.slice(0, limit)
|
|
2498
|
+
})
|
|
2499
|
+
),
|
|
2500
|
+
recordRunParent: Effect.fn("DurableEngineState.recordRunParent")((childId, parentId) =>
|
|
2501
|
+
// A single synchronous step mirrors the SQL transaction: the cycle
|
|
2502
|
+
// check and the insert are atomic — no fiber can interleave between
|
|
2503
|
+
// them, and a rejected edge is never observable (issues #54/#55/#56).
|
|
2504
|
+
Effect.suspend((): Effect.Effect<RecordRunParentOutcome, RunParentCycleError> => {
|
|
2505
|
+
const parents = parentEdges.get(childId) ?? new Map<string, number>()
|
|
2506
|
+
const existing = parents.get(parentId)
|
|
2507
|
+
if (existing !== undefined) {
|
|
2508
|
+
return Effect.succeed({
|
|
2509
|
+
_tag: "Existing" as const,
|
|
2510
|
+
edge: { childId, parentId, seq: existing }
|
|
2511
|
+
})
|
|
2512
|
+
}
|
|
2513
|
+
const cycle = findCyclePathSync(childId, parentId)
|
|
2514
|
+
if (cycle !== undefined) {
|
|
2515
|
+
return Effect.fail(new RunParentCycleError({ path: cycle }))
|
|
2516
|
+
}
|
|
2517
|
+
parentSeq += 1
|
|
2518
|
+
parents.set(parentId, parentSeq)
|
|
2519
|
+
parentEdges.set(childId, parents)
|
|
2520
|
+
return Effect.succeed({
|
|
2521
|
+
_tag: "Recorded" as const,
|
|
2522
|
+
edge: { childId, parentId, seq: parentSeq }
|
|
2523
|
+
})
|
|
2524
|
+
})
|
|
2525
|
+
),
|
|
2526
|
+
removeRunParentsForRun: Effect.fn("DurableEngineState.removeRunParentsForRun")((runId) =>
|
|
2527
|
+
Effect.sync(() => {
|
|
2528
|
+
parentEdges.delete(runId)
|
|
2529
|
+
for (const [childId, parents] of parentEdges) {
|
|
2530
|
+
parents.delete(runId)
|
|
2531
|
+
if (parents.size === 0) parentEdges.delete(childId)
|
|
2532
|
+
}
|
|
2533
|
+
})
|
|
2534
|
+
),
|
|
2535
|
+
runParents: Effect.fn("DurableEngineState.runParents")((childId) =>
|
|
2536
|
+
Effect.sync(() =>
|
|
2537
|
+
Array.from(parentEdges.get(childId) ?? [])
|
|
2538
|
+
.map(([parentId, seq]) => ({ childId, parentId, seq }))
|
|
2539
|
+
.sort((left, right) => left.seq - right.seq)
|
|
2540
|
+
)
|
|
2541
|
+
),
|
|
2542
|
+
runChildren: Effect.fn("DurableEngineState.runChildren")((parentId) =>
|
|
2543
|
+
Effect.sync(() => {
|
|
2544
|
+
const edges: Array<RunParentEdge> = []
|
|
2545
|
+
for (const [childId, parents] of parentEdges) {
|
|
2546
|
+
const seq = parents.get(parentId)
|
|
2547
|
+
if (seq !== undefined) edges.push({ childId, parentId, seq })
|
|
2548
|
+
}
|
|
2549
|
+
return edges.sort((left, right) => left.seq - right.seq)
|
|
2550
|
+
})
|
|
2551
|
+
)
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const gate = Semaphore.makeUnsafe(1)
|
|
2555
|
+
const transactionDepth = new Map<number, number>()
|
|
2556
|
+
const guard = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
2557
|
+
Effect.flatMap(
|
|
2558
|
+
Effect.fiberId,
|
|
2559
|
+
(fiberId) => (transactionDepth.get(fiberId) ?? 0) > 0 ? effect : gate.withPermit(effect)
|
|
2560
|
+
)
|
|
2561
|
+
|
|
2562
|
+
const restoreMap = <K, V>(target: Map<K, V>, source: ReadonlyMap<K, V>): void => {
|
|
2563
|
+
target.clear()
|
|
2564
|
+
for (const [key, value] of source) target.set(key, value)
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const transaction: Service["transaction"] = <A, E, R>(
|
|
2568
|
+
effect: Effect.Effect<A, E, R>,
|
|
2569
|
+
options?: { readonly onCommit: (commit: Effect.Effect<void>) => void }
|
|
2570
|
+
): Effect.Effect<A, E, R> =>
|
|
2571
|
+
Effect.flatMap(Effect.fiberId, (fiberId) =>
|
|
2572
|
+
Effect.uninterruptibleMask((restore) =>
|
|
2573
|
+
Effect.gen(function*() {
|
|
2574
|
+
const nested = (transactionDepth.get(fiberId) ?? 0) > 0
|
|
2575
|
+
if (!nested) yield* restore(gate.take(1))
|
|
2576
|
+
const before = {
|
|
2577
|
+
deferreds: new Map(deferreds),
|
|
2578
|
+
consumedDeferreds: new Map(consumedDeferreds),
|
|
2579
|
+
clocks: new Map(clocks),
|
|
2580
|
+
waitingRows: new Map(waitingRows),
|
|
2581
|
+
parentEdges: new Map([...parentEdges].map(([child, parents]) => [child, new Map(parents)])),
|
|
2582
|
+
parentSeq
|
|
2583
|
+
}
|
|
2584
|
+
transactionDepth.set(fiberId, (transactionDepth.get(fiberId) ?? 0) + 1)
|
|
2585
|
+
let committed = false
|
|
2586
|
+
let closed = false
|
|
2587
|
+
const close = Effect.suspend(() => {
|
|
2588
|
+
if (closed) return Effect.void
|
|
2589
|
+
closed = true
|
|
2590
|
+
const depth = transactionDepth.get(fiberId)! - 1
|
|
2591
|
+
if (depth === 0) transactionDepth.delete(fiberId)
|
|
2592
|
+
else transactionDepth.set(fiberId, depth)
|
|
2593
|
+
return nested ? Effect.void : gate.release(1).pipe(Effect.asVoid)
|
|
2594
|
+
})
|
|
2595
|
+
const commit = Effect.sync(() => {
|
|
2596
|
+
committed = true
|
|
2597
|
+
}).pipe(Effect.andThen(close))
|
|
2598
|
+
return yield* Effect.sync(() => options?.onCommit(commit)).pipe(
|
|
2599
|
+
Effect.andThen(restore(effect)),
|
|
2600
|
+
Effect.catchCause((cause) =>
|
|
2601
|
+
Effect.sync(() => {
|
|
2602
|
+
if (committed) return
|
|
2603
|
+
restoreMap(deferreds, before.deferreds)
|
|
2604
|
+
restoreMap(consumedDeferreds, before.consumedDeferreds)
|
|
2605
|
+
restoreMap(clocks, before.clocks)
|
|
2606
|
+
restoreMap(waitingRows, before.waitingRows)
|
|
2607
|
+
restoreMap(parentEdges, before.parentEdges)
|
|
2608
|
+
parentSeq = before.parentSeq
|
|
2609
|
+
}).pipe(Effect.andThen(Effect.failCause(cause)))
|
|
2610
|
+
),
|
|
2611
|
+
Effect.ensuring(close)
|
|
2612
|
+
)
|
|
2613
|
+
})
|
|
2614
|
+
))
|
|
2615
|
+
|
|
2616
|
+
return DurableEngineState.of({
|
|
2617
|
+
deferred: (address) => guard(unguarded.deferred(address)),
|
|
2618
|
+
consumeDeferred: (address, consumedAtMs) => guard(unguarded.consumeDeferred(address, consumedAtMs)),
|
|
2619
|
+
completeDeferred: (row) => guard(unguarded.completeDeferred(row)),
|
|
2620
|
+
clock: (address) => guard(unguarded.clock(address)),
|
|
2621
|
+
scheduleClock: (row, owner) => guard(unguarded.scheduleClock(row, owner)),
|
|
2622
|
+
completeClock: (address, completedAtMs) => guard(unguarded.completeClock(address, completedAtMs)),
|
|
2623
|
+
dueClocks: (nowMs) => guard(unguarded.dueClocks(nowMs)),
|
|
2624
|
+
completeRunClocks: (executionId, completedAtMs) => guard(unguarded.completeRunClocks(executionId, completedAtMs)),
|
|
2625
|
+
pendingClocks: (scope) => guard(unguarded.pendingClocks(scope)),
|
|
2626
|
+
completedDeferreds: (flowName) => guard(unguarded.completedDeferreds(flowName)),
|
|
2627
|
+
park: (runId, waiting, owner) => guard(unguarded.park(runId, waiting, owner)),
|
|
2628
|
+
wake: (runId) => guard(unguarded.wake(runId)),
|
|
2629
|
+
waiting: (runId) => guard(unguarded.waiting(runId)),
|
|
2630
|
+
waitingTree: (runId, options) => guard(unguarded.waitingTree(runId, options)),
|
|
2631
|
+
waitingRuns: (filter) => guard(unguarded.waitingRuns(filter)),
|
|
2632
|
+
pendingRuns: (flowName, limit, after, through, eligibleBeforeMs) =>
|
|
2633
|
+
guard(unguarded.pendingRuns(flowName, limit, after, through, eligibleBeforeMs)),
|
|
2634
|
+
pendingRunTail: (flowName, eligibleBeforeMs) => guard(unguarded.pendingRunTail(flowName, eligibleBeforeMs)),
|
|
2635
|
+
pendingCancellationRuns: (limit, after, through) => guard(unguarded.pendingCancellationRuns(limit, after, through)),
|
|
2636
|
+
pendingCancellationTail: () => guard(unguarded.pendingCancellationTail()),
|
|
2637
|
+
staleRunningRuns: (staleBeforeMs, limit) => guard(unguarded.staleRunningRuns(staleBeforeMs, limit)),
|
|
2638
|
+
attemptSurvivors: undefined,
|
|
2639
|
+
recordRunParent: (childId, parentId) => guard(unguarded.recordRunParent(childId, parentId)),
|
|
2640
|
+
removeRunParentsForRun: (runId) => guard(unguarded.removeRunParentsForRun(runId)),
|
|
2641
|
+
runParents: (childId) => guard(unguarded.runParents(childId)),
|
|
2642
|
+
runChildren: (parentId) => guard(unguarded.runChildren(parentId)),
|
|
2643
|
+
transaction
|
|
2644
|
+
})
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* Provides deterministic in-memory durable engine state.
|
|
2649
|
+
*
|
|
2650
|
+
* @since 0.1.0
|
|
2651
|
+
* @category layers
|
|
2652
|
+
*/
|
|
2653
|
+
export const layerMemory: Layer.Layer<DurableEngineState> = Layer.sync(
|
|
2654
|
+
DurableEngineState,
|
|
2655
|
+
makeMemory
|
|
2656
|
+
)
|