@effect/platform-cloudflare 0.0.0-stage → 4.1.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/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +505 -2
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +74 -3
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow Durable Object runtime. One object holds one workflow
|
|
3
|
+
* execution: its payload, run result, activity results keyed
|
|
4
|
+
* `${name}/${attempt}`, durable deferred exits, and the clock due table behind
|
|
5
|
+
* the single alarm. Handlers are looked up in the module-level workflow
|
|
6
|
+
* registry and built once per wake; a crash or hibernation wipes RAM and the
|
|
7
|
+
* next contact replays the execution from SQLite.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
import type { SqlStorage } from "@cloudflare/workers-types"
|
|
12
|
+
import * as Cause from "effect/Cause"
|
|
13
|
+
import * as Context from "effect/Context"
|
|
14
|
+
import * as Duration from "effect/Duration"
|
|
15
|
+
import * as Effect from "effect/Effect"
|
|
16
|
+
import * as Exit from "effect/Exit"
|
|
17
|
+
import * as Fiber from "effect/Fiber"
|
|
18
|
+
import * as Schedule from "effect/Schedule"
|
|
19
|
+
import * as Scope from "effect/Scope"
|
|
20
|
+
import * as DurableClock from "effect/workflow/DurableClock"
|
|
21
|
+
import * as Workflow from "effect/workflow/Workflow"
|
|
22
|
+
import * as WorkflowEngine from "effect/workflow/WorkflowEngine"
|
|
23
|
+
import { decodeName, encodeName } from "./clusterName.ts"
|
|
24
|
+
import { armAlarm, type EntityAlarm } from "./entityStorage.ts"
|
|
25
|
+
import {
|
|
26
|
+
CurrentExecutionHandle,
|
|
27
|
+
deferredState,
|
|
28
|
+
getWorkflowRegistration,
|
|
29
|
+
type WorkflowRunOptions,
|
|
30
|
+
type WorkflowStub
|
|
31
|
+
} from "./workflowRegistry.ts"
|
|
32
|
+
import * as WorkflowStorage from "./workflowStorage.ts"
|
|
33
|
+
import { decodePayload, encodeDefectResult, encodeExit, encodeResult } from "./workflowWire.ts"
|
|
34
|
+
|
|
35
|
+
/** @internal */
|
|
36
|
+
export const InterruptSignalName = "Workflow/InterruptSignal"
|
|
37
|
+
|
|
38
|
+
const resumeGuardMillis = 60_000
|
|
39
|
+
|
|
40
|
+
let voidExitCache: Promise<string> | undefined
|
|
41
|
+
const voidExitText = (): Promise<
|
|
42
|
+
string
|
|
43
|
+
> => (voidExitCache ??= Effect.runPromise(encodeExit(Exit.void, Context.empty())))
|
|
44
|
+
|
|
45
|
+
const mergeWake = (
|
|
46
|
+
current: WorkflowStorage.WakeMode | undefined,
|
|
47
|
+
mode: WorkflowStorage.WakeMode
|
|
48
|
+
): WorkflowStorage.WakeMode => current === "explicit" ? current : mode
|
|
49
|
+
|
|
50
|
+
/** @internal */
|
|
51
|
+
export interface WorkflowRuntimeOptions {
|
|
52
|
+
readonly name: string
|
|
53
|
+
readonly sql: SqlStorage
|
|
54
|
+
readonly alarm: EntityAlarm
|
|
55
|
+
readonly now: () => number
|
|
56
|
+
readonly waitUntil: (promise: Promise<unknown>) => void
|
|
57
|
+
readonly getStub: (name: string) => WorkflowStub
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @internal */
|
|
61
|
+
export interface WorkflowRuntime extends WorkflowStub {
|
|
62
|
+
readonly executionId: string
|
|
63
|
+
readonly loadActivity: (key: string) => string | undefined
|
|
64
|
+
readonly saveActivity: (key: string, exit: string) => void
|
|
65
|
+
readonly loadDeferred: (name: string) => string | undefined
|
|
66
|
+
readonly completeOwnDeferred: (name: string, exit: string) => Effect.Effect<void>
|
|
67
|
+
readonly runAlarm: () => Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Inflight {
|
|
71
|
+
readonly instance: WorkflowEngine.WorkflowInstance["Service"]
|
|
72
|
+
readonly fiber: Fiber.Fiber<{
|
|
73
|
+
readonly result: Workflow.Result<unknown, unknown>
|
|
74
|
+
readonly text: string
|
|
75
|
+
}, unknown>
|
|
76
|
+
readonly promise: Promise<string>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @internal */
|
|
80
|
+
export const makeWorkflowRuntime = (options: WorkflowRuntimeOptions): WorkflowRuntime => {
|
|
81
|
+
const name = decodeName(options.name)
|
|
82
|
+
if (name === undefined) {
|
|
83
|
+
throw new Error("ClusterWorkflow requires a canonical workflow Durable Object name")
|
|
84
|
+
}
|
|
85
|
+
const workflowName = name.type
|
|
86
|
+
const executionId = name.id
|
|
87
|
+
const sql = options.sql
|
|
88
|
+
|
|
89
|
+
WorkflowStorage.ensureWorkflowStorage(sql)
|
|
90
|
+
|
|
91
|
+
let inflight: Inflight | undefined
|
|
92
|
+
let resumeRequested: WorkflowStorage.WakeMode | undefined
|
|
93
|
+
// Parent of every attempt's scope. As in the memory engine, scopes of
|
|
94
|
+
// attempts that suspended stay open under it, so their finalizers and
|
|
95
|
+
// resources last until the execution completes or an attempt defects.
|
|
96
|
+
let executionScope = Scope.makeUnsafe()
|
|
97
|
+
|
|
98
|
+
// The final attempt's scope has already closed with the real exit. Closing
|
|
99
|
+
// the parent closes earlier attempts' scopes in reverse order with
|
|
100
|
+
// `Exit.void`, so their superseded compensations are skipped. A fresh
|
|
101
|
+
// parent is swapped in first: a later replay must not fork from a closed
|
|
102
|
+
// scope, which would run its finalizers as soon as they are registered.
|
|
103
|
+
const closeExecutionScope = Effect.suspend(() => {
|
|
104
|
+
const closing = executionScope
|
|
105
|
+
executionScope = Scope.makeUnsafe()
|
|
106
|
+
return Scope.close(closing, Exit.void)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const detach = (promise: Promise<unknown>): void => {
|
|
110
|
+
options.waitUntil(promise.then(() => undefined, () => undefined))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parseResult = (result: string) => JSON.parse(result) as { readonly _tag?: unknown; readonly failed?: unknown }
|
|
114
|
+
|
|
115
|
+
const isComplete = (result: string | undefined): boolean =>
|
|
116
|
+
result !== undefined && parseResult(result)._tag === "Complete"
|
|
117
|
+
|
|
118
|
+
// As in the memory engine, only an explicit wake replays a failure suspension.
|
|
119
|
+
const isUnwakeable = (result: string | undefined, mode: WorkflowStorage.WakeMode): boolean => {
|
|
120
|
+
if (result === undefined) return false
|
|
121
|
+
const parsed = parseResult(result)
|
|
122
|
+
return parsed._tag === "Complete" || (mode === "automatic" && parsed.failed === true)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Records a wake durably. A running attempt queues it and decides when it
|
|
127
|
+
* settles, since its result may still become a failure suspension. Returns
|
|
128
|
+
* whether a replay is now pending.
|
|
129
|
+
*/
|
|
130
|
+
const requestWake = (mode: WorkflowStorage.WakeMode): boolean => {
|
|
131
|
+
const row = WorkflowStorage.loadExecution(sql)
|
|
132
|
+
if (row === undefined || isComplete(row.result)) return false
|
|
133
|
+
if (inflight === undefined && isUnwakeable(row.result, mode)) return false
|
|
134
|
+
const pending = mergeWake(row.resumePending, mode)
|
|
135
|
+
if (pending !== row.resumePending) WorkflowStorage.setResumePending(sql, pending)
|
|
136
|
+
if (inflight !== undefined) resumeRequested = mergeWake(resumeRequested, mode)
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const armResumeGuard = Effect.suspend(() => armAlarm(options.alarm, options.now() + resumeGuardMillis))
|
|
141
|
+
|
|
142
|
+
const resumeParent = (parent: { readonly workflowName: string; readonly executionId: string }) =>
|
|
143
|
+
Effect.promise(() => options.getStub(encodeName(parent.workflowName, parent.executionId)).resume("automatic")).pipe(
|
|
144
|
+
Effect.sandbox,
|
|
145
|
+
Effect.retry({ times: 5, schedule: Schedule.exponential(200) })
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const logResumeParentFailure = (cause: Cause.Cause<unknown>) =>
|
|
149
|
+
Effect.logError("Workflow parent resume failed", cause)
|
|
150
|
+
|
|
151
|
+
const deliverParentWake = Effect.suspend(() => {
|
|
152
|
+
const row = WorkflowStorage.loadExecution(sql)
|
|
153
|
+
if (row === undefined || row.resumePending === undefined || !isComplete(row.result) || row.parent === undefined) {
|
|
154
|
+
return Effect.void
|
|
155
|
+
}
|
|
156
|
+
// Re-arm before sending so a crash or exhausted retries leave another attempt.
|
|
157
|
+
return armResumeGuard.pipe(
|
|
158
|
+
Effect.andThen(
|
|
159
|
+
resumeParent(row.parent).pipe(
|
|
160
|
+
Effect.tap(() => Effect.sync(() => WorkflowStorage.setResumePending(sql, undefined))),
|
|
161
|
+
Effect.catchCause(logResumeParentFailure)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// An attempt the machinery cannot finish (a defect with `CaptureDefects`
|
|
168
|
+
// off, a payload that no longer decodes, a result that fails to encode, a
|
|
169
|
+
// dying finalizer, a missing registration) completes the execution with the
|
|
170
|
+
// defect, as `ClusterWorkflowEngine` stores an exhausted defect retry. The
|
|
171
|
+
// object never rejects `run` for such a reason, so a caller can tell the
|
|
172
|
+
// child's own defect from a transport failure, and a parent replaying a
|
|
173
|
+
// discarded child finds the result instead of restarting it.
|
|
174
|
+
const failAttempt = (cause: Cause.Cause<unknown>): Promise<string> =>
|
|
175
|
+
Effect.runPromise(Effect.gen(function*() {
|
|
176
|
+
yield* Effect.logError("Workflow execution failed", cause)
|
|
177
|
+
const text = yield* encodeDefectResult(Cause.squash(cause))
|
|
178
|
+
const parent = WorkflowStorage.loadExecution(sql)?.parent
|
|
179
|
+
if (parent !== undefined) yield* armResumeGuard
|
|
180
|
+
WorkflowStorage.saveCompletion(sql, text, parent !== undefined)
|
|
181
|
+
inflight = undefined
|
|
182
|
+
resumeRequested = undefined
|
|
183
|
+
if (parent !== undefined) options.waitUntil(Effect.runPromise(deliverParentWake))
|
|
184
|
+
return text
|
|
185
|
+
}))
|
|
186
|
+
|
|
187
|
+
const startAttempt = (row: WorkflowStorage.ExecutionRow): Promise<string> => {
|
|
188
|
+
const registration = getWorkflowRegistration(workflowName)
|
|
189
|
+
if (registration === undefined) {
|
|
190
|
+
return failAttempt(Cause.die(new Error(`No workflow registered for name: ${workflowName}`)))
|
|
191
|
+
}
|
|
192
|
+
const workflow = registration.workflow
|
|
193
|
+
const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId, {
|
|
194
|
+
scope: Scope.forkUnsafe(executionScope)
|
|
195
|
+
})
|
|
196
|
+
const execute = decodePayload(workflow, row.payload, registration.context).pipe(
|
|
197
|
+
Effect.flatMap((payload) => registration.execute(payload, executionId) as Effect.Effect<unknown, unknown>),
|
|
198
|
+
Effect.onExit((exit) => {
|
|
199
|
+
const suspendOnFailure = Context.get(workflow.annotations, Workflow.SuspendOnFailure)
|
|
200
|
+
if (!instance.suspended && !(suspendOnFailure && exit._tag === "Failure")) {
|
|
201
|
+
return Effect.void
|
|
202
|
+
}
|
|
203
|
+
if (WorkflowStorage.loadDeferred(sql, InterruptSignalName) === undefined) {
|
|
204
|
+
return Effect.void
|
|
205
|
+
}
|
|
206
|
+
instance.suspended = false
|
|
207
|
+
instance.interrupted = true
|
|
208
|
+
return Effect.withFiber<void>((fiber) => Effect.interruptible(Fiber.interrupt(fiber)))
|
|
209
|
+
}),
|
|
210
|
+
Workflow.intoResult,
|
|
211
|
+
(effect) => deferredState.trackRun(instance, effect),
|
|
212
|
+
Effect.flatMap((result) =>
|
|
213
|
+
Effect.map(encodeResult(workflow, result, registration.context), (text) => ({ result, text }))
|
|
214
|
+
),
|
|
215
|
+
Effect.provideService(DurableClock.InMemoryThreshold, Duration.zero),
|
|
216
|
+
Effect.provideService(CurrentExecutionHandle, runtime)
|
|
217
|
+
)
|
|
218
|
+
const persist = ({ result, text }: {
|
|
219
|
+
readonly result: Workflow.Result<unknown, unknown>
|
|
220
|
+
readonly text: string
|
|
221
|
+
}): Promise<string> =>
|
|
222
|
+
Effect.runPromise(
|
|
223
|
+
Effect.gen(function*() {
|
|
224
|
+
const parent = WorkflowStorage.loadExecution(sql)?.parent
|
|
225
|
+
if (result._tag === "Complete" && parent !== undefined) {
|
|
226
|
+
// Arm before the atomic result/pending save so a persisted
|
|
227
|
+
// completion always has a delivery wake.
|
|
228
|
+
yield* armResumeGuard
|
|
229
|
+
}
|
|
230
|
+
// Stay in flight until the result is saved so a concurrent resume
|
|
231
|
+
// queues behind this attempt instead of replaying alongside it.
|
|
232
|
+
if (result._tag === "Complete") {
|
|
233
|
+
WorkflowStorage.saveCompletion(sql, text, parent !== undefined)
|
|
234
|
+
inflight = undefined
|
|
235
|
+
resumeRequested = undefined
|
|
236
|
+
if (parent !== undefined) options.waitUntil(Effect.runPromise(deliverParentWake))
|
|
237
|
+
return text
|
|
238
|
+
}
|
|
239
|
+
const replay = resumeRequested !== undefined && !isUnwakeable(text, resumeRequested)
|
|
240
|
+
? resumeRequested
|
|
241
|
+
: undefined
|
|
242
|
+
WorkflowStorage.saveSuspension(sql, text, replay)
|
|
243
|
+
inflight = undefined
|
|
244
|
+
resumeRequested = undefined
|
|
245
|
+
if (replay !== undefined) detach(startAttempt(row))
|
|
246
|
+
return text
|
|
247
|
+
}).pipe(Effect.tapCause(() =>
|
|
248
|
+
Effect.sync(() => {
|
|
249
|
+
inflight = undefined
|
|
250
|
+
})
|
|
251
|
+
))
|
|
252
|
+
)
|
|
253
|
+
const finish = (attempt: {
|
|
254
|
+
readonly result: Workflow.Result<unknown, unknown>
|
|
255
|
+
readonly text: string
|
|
256
|
+
}): Promise<string> => {
|
|
257
|
+
if (attempt.result._tag !== "Complete") return persist(attempt)
|
|
258
|
+
// Close retained scopes before saving, so a durable completion never
|
|
259
|
+
// has unrun finalizers. A dying finalizer fails the attempt instead.
|
|
260
|
+
return Effect.runPromise(Effect.exit(closeExecutionScope)).then((closed) =>
|
|
261
|
+
Exit.isSuccess(closed) ? persist(attempt) : failAttempt(closed.cause)
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
const fiber = Effect.runFork(execute)
|
|
265
|
+
const promise = Effect.runPromise(Fiber.await(fiber)).then((exit) => {
|
|
266
|
+
if (Exit.isSuccess(exit)) return finish(exit.value)
|
|
267
|
+
if (Cause.hasInterruptsOnly(exit.cause) && (instance.interrupted || instance.suspended)) {
|
|
268
|
+
// A hard interrupt (or a completion preempting the fiber after it
|
|
269
|
+
// already produced its result) kills the fiber before it can encode;
|
|
270
|
+
// synthesize the result from the instance state so it still persists.
|
|
271
|
+
const result: Workflow.Result<unknown, unknown> = instance.interrupted
|
|
272
|
+
? new Workflow.Complete({ exit: exit as Exit.Exit<unknown, unknown> })
|
|
273
|
+
: new Workflow.Suspended({})
|
|
274
|
+
return Effect.runPromise(encodeResult(workflow, result, registration.context))
|
|
275
|
+
.then((text) => finish({ result, text }))
|
|
276
|
+
}
|
|
277
|
+
// A defect ends every retained attempt; a replay starts in fresh scopes.
|
|
278
|
+
// Stay in flight while closing so cleanup precedes any replay.
|
|
279
|
+
return Effect.runPromise(Effect.exit(closeExecutionScope)).then((closed) =>
|
|
280
|
+
failAttempt(Exit.isSuccess(closed) ? exit.cause : Cause.combine(exit.cause, closed.cause))
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
inflight = { instance, fiber, promise }
|
|
284
|
+
return promise
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const run = (payload: string, opts: WorkflowRunOptions): Promise<string> => {
|
|
288
|
+
let row = WorkflowStorage.loadExecution(sql)
|
|
289
|
+
if (row === undefined) {
|
|
290
|
+
WorkflowStorage.createExecution(sql, workflowName, executionId, payload, opts.parent)
|
|
291
|
+
row = { workflowName, executionId, payload, parent: opts.parent, result: undefined, resumePending: undefined }
|
|
292
|
+
} else if (opts.parent !== undefined && row.parent === undefined) {
|
|
293
|
+
// An execution started standalone can gain a parent later; keep the
|
|
294
|
+
// first parent so its completion still wakes that parent.
|
|
295
|
+
WorkflowStorage.setParent(sql, opts.parent)
|
|
296
|
+
row = { ...row, parent: opts.parent }
|
|
297
|
+
}
|
|
298
|
+
if (inflight !== undefined) return opts.discard ? Promise.resolve("") : inflight.promise
|
|
299
|
+
if (row.result !== undefined) return Promise.resolve(opts.discard ? "" : row.result)
|
|
300
|
+
// The attempt stays pending under the guard alarm until it persists a
|
|
301
|
+
// result, so an attempt lost with this object is replayed even when no
|
|
302
|
+
// caller awaits it.
|
|
303
|
+
WorkflowStorage.setResumePending(sql, mergeWake(row.resumePending, "automatic"))
|
|
304
|
+
const attempt = startAttempt(row)
|
|
305
|
+
const armed = Effect.runPromise(armResumeGuard)
|
|
306
|
+
if (opts.discard) {
|
|
307
|
+
detach(attempt)
|
|
308
|
+
return armed.then(() => "")
|
|
309
|
+
}
|
|
310
|
+
return Promise.all([attempt, armed]).then(([text]) => text)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const startPending = (): void => {
|
|
314
|
+
const row = WorkflowStorage.loadExecution(sql)
|
|
315
|
+
if (row === undefined || row.resumePending === undefined || inflight !== undefined) return
|
|
316
|
+
if (isUnwakeable(row.result, row.resumePending)) return
|
|
317
|
+
detach(startAttempt(row))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const resume = (mode: WorkflowStorage.WakeMode = "explicit"): Promise<void> => {
|
|
321
|
+
if (!requestWake(mode)) return Promise.resolve()
|
|
322
|
+
// Persist and guard the resume before acknowledging it, so an abort after
|
|
323
|
+
// the RPC returns is still recovered.
|
|
324
|
+
return Effect.runPromise(armResumeGuard).then(startPending)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const completeDeferred = (
|
|
328
|
+
deferredName: string,
|
|
329
|
+
exitText: string,
|
|
330
|
+
mode: WorkflowStorage.WakeMode
|
|
331
|
+
): Promise<void> => {
|
|
332
|
+
if (!WorkflowStorage.saveDeferred(sql, deferredName, exitText)) return Promise.resolve()
|
|
333
|
+
// Persisted with the exit in the same write batch: if this wake is lost
|
|
334
|
+
// before the replay finishes, the next wake (or the guard alarm) replays
|
|
335
|
+
// the execution. The in-flight request is set synchronously so a settling
|
|
336
|
+
// attempt cannot clear the pending resume without a restart.
|
|
337
|
+
if (!requestWake(mode)) return Promise.resolve()
|
|
338
|
+
return Effect.runPromise(
|
|
339
|
+
armResumeGuard.pipe(Effect.andThen(deferredState.deferredDone(executionId, deferredName)))
|
|
340
|
+
).then(() => resume(mode))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const deferredDone = (deferredName: string, exitText: string): Promise<void> =>
|
|
344
|
+
completeDeferred(deferredName, exitText, "automatic")
|
|
345
|
+
|
|
346
|
+
// The caller belongs to the run this wake interrupts, so awaiting that run
|
|
347
|
+
// would wait on the caller's own cleanup. As in the memory engine, the wake
|
|
348
|
+
// starts on a separate fiber after the caller continues.
|
|
349
|
+
const completeOwnDeferred = (deferredName: string, exitText: string): Effect.Effect<void> =>
|
|
350
|
+
Effect.suspend(() => {
|
|
351
|
+
if (!WorkflowStorage.saveDeferred(sql, deferredName, exitText) || !requestWake("automatic")) {
|
|
352
|
+
return Effect.void
|
|
353
|
+
}
|
|
354
|
+
const wake = deferredState.deferredDone(executionId, deferredName).pipe(
|
|
355
|
+
Effect.andThen(
|
|
356
|
+
Effect.suspend(() => inflight === undefined ? Effect.promise(() => resume("automatic")) : Effect.void)
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
return armResumeGuard.pipe(
|
|
360
|
+
Effect.andThen(Effect.forkDetach(wake)),
|
|
361
|
+
Effect.flatMap((fiber) => Effect.sync(() => detach(Effect.runPromise(Fiber.await(fiber)))))
|
|
362
|
+
)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const interrupt = (): Promise<void> => {
|
|
366
|
+
const row = WorkflowStorage.loadExecution(sql)
|
|
367
|
+
if (row === undefined || isComplete(row.result)) return Promise.resolve()
|
|
368
|
+
return voidExitText().then((exitText) => completeDeferred(InterruptSignalName, exitText, "explicit"))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const interruptUnsafe = (): Promise<void> => {
|
|
372
|
+
const current = inflight
|
|
373
|
+
const signalled = interrupt()
|
|
374
|
+
if (current === undefined) return signalled
|
|
375
|
+
current.instance.interrupted = true
|
|
376
|
+
return signalled
|
|
377
|
+
.then(() => Effect.runPromise(Fiber.interrupt(current.fiber)))
|
|
378
|
+
.then(() => undefined)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const scheduleClock = (clockName: string, deferredName: string, wakeUp: number): Promise<void> => {
|
|
382
|
+
WorkflowStorage.saveClock(sql, clockName, deferredName, wakeUp)
|
|
383
|
+
const earliest = WorkflowStorage.earliestClockWakeUp(sql)
|
|
384
|
+
return earliest === undefined ? Promise.resolve() : Effect.runPromise(armAlarm(options.alarm, earliest))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const runAlarm = (): Promise<void> =>
|
|
388
|
+
voidExitText().then((voidExit) => {
|
|
389
|
+
const completed = WorkflowStorage.dueClocks(sql, options.now()).filter((clock) => {
|
|
390
|
+
WorkflowStorage.markClockFired(sql, clock.name)
|
|
391
|
+
return WorkflowStorage.saveDeferred(sql, clock.deferredName, voidExit)
|
|
392
|
+
})
|
|
393
|
+
if (completed.length > 0) requestWake("automatic")
|
|
394
|
+
return Effect.runPromise(Effect.forEach(
|
|
395
|
+
completed,
|
|
396
|
+
(clock) => deferredState.deferredDone(executionId, clock.deferredName),
|
|
397
|
+
{ discard: true }
|
|
398
|
+
)).then(() => {
|
|
399
|
+
const row = WorkflowStorage.loadExecution(sql)
|
|
400
|
+
const mode = row?.resumePending
|
|
401
|
+
const pending = mode !== undefined && !isUnwakeable(row?.result, mode)
|
|
402
|
+
// A live attempt is not lost: only re-arm the guard. Requesting a wake
|
|
403
|
+
// would replay the attempt once it suspends, for no reason.
|
|
404
|
+
return (pending && inflight === undefined ? resume(mode) : Promise.resolve()).then(() => {
|
|
405
|
+
// While a resume is pending a guard alarm stays armed, so a replay
|
|
406
|
+
// lost with this isolate is retried instead of sleeping forever.
|
|
407
|
+
const targets = [
|
|
408
|
+
WorkflowStorage.earliestClockWakeUp(sql),
|
|
409
|
+
pending ? options.now() + resumeGuardMillis : undefined
|
|
410
|
+
].filter((target) => target !== undefined)
|
|
411
|
+
if (targets.length === 0) return undefined
|
|
412
|
+
// The alarm that fired is spent, so set the next one outright:
|
|
413
|
+
// `armAlarm` only ever moves an alarm earlier.
|
|
414
|
+
return options.alarm.setAlarm(Math.min(...targets))
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
}).then(() => Effect.runPromise(deliverParentWake)).then(() => undefined)
|
|
418
|
+
|
|
419
|
+
const runtime: WorkflowRuntime = {
|
|
420
|
+
executionId,
|
|
421
|
+
run,
|
|
422
|
+
poll: () => Promise.resolve(WorkflowStorage.loadExecution(sql)?.result),
|
|
423
|
+
resume,
|
|
424
|
+
interrupt,
|
|
425
|
+
interruptUnsafe,
|
|
426
|
+
deferredDone,
|
|
427
|
+
scheduleClock,
|
|
428
|
+
runAlarm,
|
|
429
|
+
loadActivity: (key) => WorkflowStorage.loadActivity(sql, key),
|
|
430
|
+
saveActivity: (key, exit) => WorkflowStorage.saveActivity(sql, key, exit),
|
|
431
|
+
loadDeferred: (deferredName) => WorkflowStorage.loadDeferred(sql, deferredName),
|
|
432
|
+
completeOwnDeferred
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Self-heal on wake: a resume recorded by deferredDone but lost with the
|
|
436
|
+
// previous isolate replays now instead of waiting for external contact.
|
|
437
|
+
const pending = WorkflowStorage.loadExecution(sql)?.resumePending
|
|
438
|
+
if (pending !== undefined) {
|
|
439
|
+
void resume(pending)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return runtime
|
|
443
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage glue for the workflow Durable Object. The constructor must stay
|
|
3
|
+
* cheap: open SQLite, ensure the tables, and re-arm the single alarm from the
|
|
4
|
+
* earliest pending clock. No workflow handlers are built here.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
import type { SqlStorage } from "@cloudflare/workers-types"
|
|
9
|
+
|
|
10
|
+
const ddl = [
|
|
11
|
+
`CREATE TABLE IF NOT EXISTS workflow_execution (
|
|
12
|
+
id INTEGER PRIMARY KEY CHECK (id = 0),
|
|
13
|
+
workflow_name TEXT NOT NULL,
|
|
14
|
+
execution_id TEXT NOT NULL,
|
|
15
|
+
payload TEXT NOT NULL,
|
|
16
|
+
parent_name TEXT,
|
|
17
|
+
parent_execution_id TEXT,
|
|
18
|
+
result TEXT,
|
|
19
|
+
resume_pending INTEGER NOT NULL DEFAULT 0
|
|
20
|
+
)`,
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS workflow_activities (
|
|
22
|
+
key TEXT PRIMARY KEY,
|
|
23
|
+
exit TEXT NOT NULL
|
|
24
|
+
)`,
|
|
25
|
+
`CREATE TABLE IF NOT EXISTS workflow_deferreds (
|
|
26
|
+
name TEXT PRIMARY KEY,
|
|
27
|
+
exit TEXT NOT NULL
|
|
28
|
+
)`,
|
|
29
|
+
`CREATE TABLE IF NOT EXISTS workflow_clocks (
|
|
30
|
+
name TEXT PRIMARY KEY,
|
|
31
|
+
deferred_name TEXT NOT NULL,
|
|
32
|
+
wake_up INTEGER NOT NULL,
|
|
33
|
+
fired INTEGER NOT NULL DEFAULT 0
|
|
34
|
+
)`
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export const ensureWorkflowStorage = (sql: SqlStorage): void => {
|
|
39
|
+
for (const statement of ddl) {
|
|
40
|
+
sql.exec(statement)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How a replay was requested. An explicit wake (`resume`, `interrupt`) always
|
|
46
|
+
* replays; an automatic wake (deferred or clock completion, a child's
|
|
47
|
+
* completion) leaves a failure-suspended execution parked.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export type WakeMode = "explicit" | "automatic"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The stored `(workflowName, executionId)` also serve to recover the object
|
|
55
|
+
* name on an alarm wake, where `ctx.id.name` is undefined.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export interface ExecutionRow {
|
|
60
|
+
readonly workflowName: string
|
|
61
|
+
readonly executionId: string
|
|
62
|
+
readonly payload: string
|
|
63
|
+
readonly parent: { readonly workflowName: string; readonly executionId: string } | undefined
|
|
64
|
+
readonly result: string | undefined
|
|
65
|
+
/**
|
|
66
|
+
* The pending replay of an incomplete execution. Once complete, any value
|
|
67
|
+
* means the parent completion wake is still undelivered.
|
|
68
|
+
*/
|
|
69
|
+
readonly resumePending: WakeMode | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type StoredExecutionRow = {
|
|
73
|
+
readonly workflow_name: string
|
|
74
|
+
readonly execution_id: string
|
|
75
|
+
readonly payload: string
|
|
76
|
+
readonly parent_name: string | null
|
|
77
|
+
readonly parent_execution_id: string | null
|
|
78
|
+
readonly result: string | null
|
|
79
|
+
readonly resume_pending: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ExitRow = {
|
|
83
|
+
readonly exit: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ClockWakeUpRow = {
|
|
87
|
+
readonly wake_up: number | null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ClockRow = {
|
|
91
|
+
readonly name: string
|
|
92
|
+
readonly deferred_name: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 1 predates wake modes, so a row written before them replays as explicit.
|
|
96
|
+
const encodeWakeMode = (mode: WakeMode | undefined): number => mode === undefined ? 0 : mode === "explicit" ? 1 : 2
|
|
97
|
+
|
|
98
|
+
const decodeWakeMode = (value: number): WakeMode | undefined =>
|
|
99
|
+
value === 1 ? "explicit" : value === 2 ? "automatic" : undefined
|
|
100
|
+
|
|
101
|
+
/** @internal */
|
|
102
|
+
export const loadExecution = (sql: SqlStorage): ExecutionRow | undefined => {
|
|
103
|
+
const row = sql.exec<StoredExecutionRow>(
|
|
104
|
+
`SELECT workflow_name, execution_id, payload, parent_name, parent_execution_id, result, resume_pending
|
|
105
|
+
FROM workflow_execution WHERE id = 0`
|
|
106
|
+
).toArray()[0]
|
|
107
|
+
if (row === undefined) return undefined
|
|
108
|
+
return {
|
|
109
|
+
workflowName: row.workflow_name,
|
|
110
|
+
executionId: row.execution_id,
|
|
111
|
+
payload: row.payload,
|
|
112
|
+
parent: row.parent_name !== null && row.parent_execution_id !== null
|
|
113
|
+
? { workflowName: row.parent_name, executionId: row.parent_execution_id }
|
|
114
|
+
: undefined,
|
|
115
|
+
result: row.result ?? undefined,
|
|
116
|
+
resumePending: decodeWakeMode(row.resume_pending)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @internal */
|
|
121
|
+
export const createExecution = (
|
|
122
|
+
sql: SqlStorage,
|
|
123
|
+
workflowName: string,
|
|
124
|
+
executionId: string,
|
|
125
|
+
payload: string,
|
|
126
|
+
parent: { readonly workflowName: string; readonly executionId: string } | undefined
|
|
127
|
+
): void => {
|
|
128
|
+
sql.exec(
|
|
129
|
+
`INSERT OR IGNORE INTO workflow_execution
|
|
130
|
+
(id, workflow_name, execution_id, payload, parent_name, parent_execution_id, result)
|
|
131
|
+
VALUES (0, ?, ?, ?, ?, ?, NULL)`,
|
|
132
|
+
workflowName,
|
|
133
|
+
executionId,
|
|
134
|
+
payload,
|
|
135
|
+
parent?.workflowName ?? null,
|
|
136
|
+
parent?.executionId ?? null
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @internal */
|
|
141
|
+
export const setParent = (
|
|
142
|
+
sql: SqlStorage,
|
|
143
|
+
parent: { readonly workflowName: string; readonly executionId: string }
|
|
144
|
+
): void => {
|
|
145
|
+
sql.exec(
|
|
146
|
+
"UPDATE workflow_execution SET parent_name = ?, parent_execution_id = ? WHERE id = 0 AND parent_name IS NULL",
|
|
147
|
+
parent.workflowName,
|
|
148
|
+
parent.executionId
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @internal */
|
|
153
|
+
export const saveSuspension = (sql: SqlStorage, result: string, pending: WakeMode | undefined): void => {
|
|
154
|
+
sql.exec(
|
|
155
|
+
"UPDATE workflow_execution SET result = ?, resume_pending = ? WHERE id = 0",
|
|
156
|
+
result,
|
|
157
|
+
encodeWakeMode(pending)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @internal */
|
|
162
|
+
export const saveCompletion = (sql: SqlStorage, result: string, wakeParent: boolean): void => {
|
|
163
|
+
sql.exec("UPDATE workflow_execution SET result = ?, resume_pending = ? WHERE id = 0", result, wakeParent ? 1 : 0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @internal */
|
|
167
|
+
export const setResumePending = (sql: SqlStorage, pending: WakeMode | undefined): void => {
|
|
168
|
+
sql.exec("UPDATE workflow_execution SET resume_pending = ? WHERE id = 0", encodeWakeMode(pending))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @internal */
|
|
172
|
+
export const loadActivity = (sql: SqlStorage, key: string): string | undefined => {
|
|
173
|
+
const row = sql.exec<ExitRow>("SELECT exit FROM workflow_activities WHERE key = ?", key).toArray()[0]
|
|
174
|
+
return row?.exit
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @internal */
|
|
178
|
+
export const saveActivity = (sql: SqlStorage, key: string, exit: string): void => {
|
|
179
|
+
sql.exec("INSERT OR IGNORE INTO workflow_activities (key, exit) VALUES (?, ?)", key, exit)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** @internal */
|
|
183
|
+
export const loadDeferred = (sql: SqlStorage, name: string): string | undefined => {
|
|
184
|
+
const row = sql.exec<ExitRow>("SELECT exit FROM workflow_deferreds WHERE name = ?", name).toArray()[0]
|
|
185
|
+
return row?.exit
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* First write wins; safe without a conflict clause because a Durable Object's
|
|
190
|
+
* SQLite access is single-threaded.
|
|
191
|
+
*
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
export const saveDeferred = (sql: SqlStorage, name: string, exit: string): boolean => {
|
|
195
|
+
if (loadDeferred(sql, name) !== undefined) return false
|
|
196
|
+
sql.exec("INSERT INTO workflow_deferreds (name, exit) VALUES (?, ?)", name, exit)
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @internal */
|
|
201
|
+
export const saveClock = (sql: SqlStorage, name: string, deferredName: string, wakeUp: number): void => {
|
|
202
|
+
sql.exec(
|
|
203
|
+
"INSERT OR IGNORE INTO workflow_clocks (name, deferred_name, wake_up, fired) VALUES (?, ?, ?, 0)",
|
|
204
|
+
name,
|
|
205
|
+
deferredName,
|
|
206
|
+
wakeUp
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** @internal */
|
|
211
|
+
export const earliestClockWakeUp = (sql: SqlStorage): number | undefined => {
|
|
212
|
+
const row = sql.exec<ClockWakeUpRow>(
|
|
213
|
+
"SELECT min(wake_up) AS wake_up FROM workflow_clocks WHERE fired = 0"
|
|
214
|
+
).toArray()[0]
|
|
215
|
+
return row?.wake_up ?? undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @internal */
|
|
219
|
+
export const dueClocks = (
|
|
220
|
+
sql: SqlStorage,
|
|
221
|
+
now: number
|
|
222
|
+
): Array<{ readonly name: string; readonly deferredName: string }> =>
|
|
223
|
+
sql.exec<ClockRow>(
|
|
224
|
+
"SELECT name, deferred_name FROM workflow_clocks WHERE fired = 0 AND wake_up <= ?",
|
|
225
|
+
now
|
|
226
|
+
).toArray().map((row) => ({
|
|
227
|
+
name: row.name,
|
|
228
|
+
deferredName: row.deferred_name
|
|
229
|
+
}))
|
|
230
|
+
|
|
231
|
+
/** @internal */
|
|
232
|
+
export const markClockFired = (sql: SqlStorage, name: string): void => {
|
|
233
|
+
sql.exec("UPDATE workflow_clocks SET fired = 1 WHERE name = ?", name)
|
|
234
|
+
}
|