@voyantjs/workflows-orchestrator-cloudflare 0.6.7 → 0.6.9
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/dist/cf-container-runner.d.ts +102 -0
- package/dist/cf-container-runner.d.ts.map +1 -0
- package/dist/cf-container-runner.js +153 -0
- package/dist/dispatch-handler.d.ts +20 -0
- package/dist/dispatch-handler.d.ts.map +1 -0
- package/dist/dispatch-handler.js +31 -0
- package/dist/do-store.d.ts +4 -0
- package/dist/do-store.d.ts.map +1 -0
- package/dist/do-store.js +34 -0
- package/dist/durable-object.d.ts +24 -0
- package/dist/durable-object.d.ts.map +1 -0
- package/dist/durable-object.js +173 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/r2-sign.d.ts +18 -0
- package/dist/r2-sign.d.ts.map +1 -0
- package/dist/r2-sign.js +98 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/worker.d.ts +27 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +165 -0
- package/package.json +25 -19
- package/src/cf-container-runner.ts +63 -71
- package/src/dispatch-handler.ts +12 -14
- package/src/do-store.ts +15 -17
- package/src/durable-object.ts +83 -83
- package/src/index.ts +19 -19
- package/src/r2-sign.ts +53 -56
- package/src/types.ts +31 -28
- package/src/worker.ts +61 -59
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
// workflow registry already loaded. See
|
|
25
25
|
// `apps/workflows-node-step-container/` for the reference image.
|
|
26
26
|
|
|
27
|
-
import type { StepJournalEntry } from "@voyantjs/workflows/handler"
|
|
28
|
-
import type { StepRunner } from "@voyantjs/workflows/handler";
|
|
27
|
+
import type { StepJournalEntry, StepRunner } from "@voyantjs/workflows/handler"
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Minimal subset of `DurableObjectNamespace` that the runner actually
|
|
@@ -34,8 +33,8 @@ import type { StepRunner } from "@voyantjs/workflows/handler";
|
|
|
34
33
|
* tests can pass a stub.
|
|
35
34
|
*/
|
|
36
35
|
export interface ContainerNamespaceLike {
|
|
37
|
-
idFromName(name: string): { toString(): string }
|
|
38
|
-
get(id: { toString(): string }): { fetch(request: Request): Promise<Response> }
|
|
36
|
+
idFromName(name: string): { toString(): string }
|
|
37
|
+
get(id: { toString(): string }): { fetch(request: Request): Promise<Response> }
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export interface BundleLocation {
|
|
@@ -44,7 +43,7 @@ export interface BundleLocation {
|
|
|
44
43
|
* from R2. Expected TTL is minutes, not hours — scoped tightly to
|
|
45
44
|
* the specific `<projectId>/<workflowVersion>/container.mjs` key.
|
|
46
45
|
*/
|
|
47
|
-
url: string
|
|
46
|
+
url: string
|
|
48
47
|
/**
|
|
49
48
|
* SHA-256 hex of the bundle bytes, computed at deploy time and
|
|
50
49
|
* stored alongside the bundle. The container verifies the
|
|
@@ -52,7 +51,7 @@ export interface BundleLocation {
|
|
|
52
51
|
* integrity check and as a pin preventing stale-cache confusion.
|
|
53
52
|
* Accepts both plain hex and `sha256:<hex>` formats.
|
|
54
53
|
*/
|
|
55
|
-
hash: string
|
|
54
|
+
hash: string
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
export interface CfContainerRunnerDeps {
|
|
@@ -61,7 +60,7 @@ export interface CfContainerRunnerDeps {
|
|
|
61
60
|
* `env.NODE_STEP_POOL` wired in wrangler.jsonc to a `Container`-
|
|
62
61
|
* extending class (from `@cloudflare/containers`).
|
|
63
62
|
*/
|
|
64
|
-
namespace: ContainerNamespaceLike
|
|
63
|
+
namespace: ContainerNamespaceLike
|
|
65
64
|
/**
|
|
66
65
|
* Resolve a signed R2 URL + manifest hash for the bundle the
|
|
67
66
|
* container should import for this dispatch. Called on every
|
|
@@ -73,26 +72,26 @@ export interface CfContainerRunnerDeps {
|
|
|
73
72
|
* resolver; single-tenant / dev images can skip it.
|
|
74
73
|
*/
|
|
75
74
|
resolveBundle?: (args: {
|
|
76
|
-
runId: string
|
|
77
|
-
workflowId: string
|
|
78
|
-
workflowVersion: string
|
|
79
|
-
projectId: string
|
|
80
|
-
organizationId: string
|
|
81
|
-
}) => Promise<BundleLocation> | BundleLocation
|
|
75
|
+
runId: string
|
|
76
|
+
workflowId: string
|
|
77
|
+
workflowVersion: string
|
|
78
|
+
projectId: string
|
|
79
|
+
organizationId: string
|
|
80
|
+
}) => Promise<BundleLocation> | BundleLocation
|
|
82
81
|
/**
|
|
83
82
|
* Base URL presented to the container. The container's Worker
|
|
84
83
|
* proxy only inspects the path, so this is cosmetic — defaults to
|
|
85
84
|
* `https://node-step.voyant.internal`.
|
|
86
85
|
*/
|
|
87
|
-
baseUrl?: string
|
|
86
|
+
baseUrl?: string
|
|
88
87
|
/**
|
|
89
88
|
* Optional HMAC signer for the `X-Voyant-Step-Auth` header so the
|
|
90
89
|
* container can verify the request came from a Voyant orchestrator.
|
|
91
90
|
* Shape matches `createHmacSigner` from `@voyantjs/workflows/auth`.
|
|
92
91
|
*/
|
|
93
|
-
sign?: (body: string) => Promise<string> | string
|
|
92
|
+
sign?: (body: string) => Promise<string> | string
|
|
94
93
|
/** Optional structured logger. */
|
|
95
|
-
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
94
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
96
95
|
/**
|
|
97
96
|
* Build the container-addressing id for a given step invocation.
|
|
98
97
|
* Default: `"<runId>:<attempt>:<stepId>"` — deterministic and
|
|
@@ -100,37 +99,37 @@ export interface CfContainerRunnerDeps {
|
|
|
100
99
|
* containers, so the CF addressing model isolates them).
|
|
101
100
|
*/
|
|
102
101
|
containerId?: (args: {
|
|
103
|
-
runId: string
|
|
104
|
-
workflowId: string
|
|
105
|
-
workflowVersion: string
|
|
106
|
-
stepId: string
|
|
107
|
-
attempt: number
|
|
108
|
-
}) => string
|
|
102
|
+
runId: string
|
|
103
|
+
workflowId: string
|
|
104
|
+
workflowVersion: string
|
|
105
|
+
stepId: string
|
|
106
|
+
attempt: number
|
|
107
|
+
}) => string
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
interface StepDispatchPayload {
|
|
112
|
-
runId: string
|
|
113
|
-
workflowId: string
|
|
114
|
-
workflowVersion: string
|
|
115
|
-
projectId: string
|
|
116
|
-
organizationId: string
|
|
117
|
-
stepId: string
|
|
118
|
-
attempt: number
|
|
119
|
-
input: unknown
|
|
111
|
+
runId: string
|
|
112
|
+
workflowId: string
|
|
113
|
+
workflowVersion: string
|
|
114
|
+
projectId: string
|
|
115
|
+
organizationId: string
|
|
116
|
+
stepId: string
|
|
117
|
+
attempt: number
|
|
118
|
+
input: unknown
|
|
120
119
|
options: {
|
|
121
|
-
machine?: string
|
|
122
|
-
timeout?: string | number
|
|
123
|
-
}
|
|
120
|
+
machine?: string
|
|
121
|
+
timeout?: string | number
|
|
122
|
+
}
|
|
124
123
|
/** Signed R2 URL + hash for the bundle to import. Absent when the
|
|
125
124
|
* container is single-tenant (bundle baked into the image). */
|
|
126
|
-
bundle?: BundleLocation
|
|
125
|
+
bundle?: BundleLocation
|
|
127
126
|
/**
|
|
128
127
|
* Journal slice at dispatch time. The container uses it to
|
|
129
128
|
* short-circuit already-completed steps on body replay and to stop
|
|
130
129
|
* the drive cleanly after the target step. Passed as opaque JSON
|
|
131
130
|
* here; the container knows how to consume it.
|
|
132
131
|
*/
|
|
133
|
-
journal?: unknown
|
|
132
|
+
journal?: unknown
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
/**
|
|
@@ -149,10 +148,8 @@ interface StepDispatchPayload {
|
|
|
149
148
|
* `runtime: "edge"` (or unset) continues to run inline.
|
|
150
149
|
*/
|
|
151
150
|
export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRunner {
|
|
152
|
-
const baseUrl = deps.baseUrl ?? "https://node-step.voyant.internal"
|
|
153
|
-
const idOf =
|
|
154
|
-
deps.containerId
|
|
155
|
-
?? (({ runId, attempt, stepId }) => `${runId}:${attempt}:${stepId}`);
|
|
151
|
+
const baseUrl = deps.baseUrl ?? "https://node-step.voyant.internal"
|
|
152
|
+
const idOf = deps.containerId ?? (({ runId, attempt, stepId }) => `${runId}:${attempt}:${stepId}`)
|
|
156
153
|
|
|
157
154
|
return async ({
|
|
158
155
|
stepId,
|
|
@@ -167,8 +164,8 @@ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRu
|
|
|
167
164
|
options,
|
|
168
165
|
journal,
|
|
169
166
|
}): Promise<StepJournalEntry> => {
|
|
170
|
-
const startedAt = Date.now()
|
|
171
|
-
let bundle: BundleLocation | undefined
|
|
167
|
+
const startedAt = Date.now()
|
|
168
|
+
let bundle: BundleLocation | undefined
|
|
172
169
|
if (deps.resolveBundle) {
|
|
173
170
|
try {
|
|
174
171
|
bundle = await deps.resolveBundle({
|
|
@@ -177,14 +174,14 @@ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRu
|
|
|
177
174
|
workflowVersion,
|
|
178
175
|
projectId,
|
|
179
176
|
organizationId,
|
|
180
|
-
})
|
|
177
|
+
})
|
|
181
178
|
} catch (err) {
|
|
182
179
|
deps.logger?.("error", "cf-container: resolveBundle threw", {
|
|
183
180
|
runId,
|
|
184
181
|
stepId,
|
|
185
182
|
error: err instanceof Error ? err.message : String(err),
|
|
186
|
-
})
|
|
187
|
-
return failed(attempt, startedAt, "BUNDLE_RESOLVE_FAILED", err)
|
|
183
|
+
})
|
|
184
|
+
return failed(attempt, startedAt, "BUNDLE_RESOLVE_FAILED", err)
|
|
188
185
|
}
|
|
189
186
|
}
|
|
190
187
|
const payload: StepDispatchPayload = {
|
|
@@ -205,83 +202,78 @@ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRu
|
|
|
205
202
|
},
|
|
206
203
|
bundle,
|
|
207
204
|
journal,
|
|
208
|
-
}
|
|
209
|
-
const body = JSON.stringify(payload)
|
|
205
|
+
}
|
|
206
|
+
const body = JSON.stringify(payload)
|
|
210
207
|
const headers: Record<string, string> = {
|
|
211
208
|
"content-type": "application/json; charset=utf-8",
|
|
212
|
-
}
|
|
209
|
+
}
|
|
213
210
|
if (deps.sign) {
|
|
214
|
-
headers["x-voyant-step-auth"] = await deps.sign(body)
|
|
211
|
+
headers["x-voyant-step-auth"] = await deps.sign(body)
|
|
215
212
|
}
|
|
216
213
|
|
|
217
214
|
const id = deps.namespace.idFromName(
|
|
218
215
|
idOf({ runId, workflowId, workflowVersion, stepId, attempt }),
|
|
219
|
-
)
|
|
220
|
-
const stub = deps.namespace.get(id)
|
|
216
|
+
)
|
|
217
|
+
const stub = deps.namespace.get(id)
|
|
221
218
|
const request = new Request(`${baseUrl}/step`, {
|
|
222
219
|
method: "POST",
|
|
223
220
|
headers,
|
|
224
221
|
body,
|
|
225
222
|
signal: stepCtx.signal,
|
|
226
|
-
})
|
|
223
|
+
})
|
|
227
224
|
|
|
228
225
|
deps.logger?.("info", "cf-container: dispatching step", {
|
|
229
226
|
runId,
|
|
230
227
|
workflowId,
|
|
231
228
|
stepId,
|
|
232
229
|
attempt,
|
|
233
|
-
})
|
|
230
|
+
})
|
|
234
231
|
|
|
235
|
-
let response: Response
|
|
232
|
+
let response: Response
|
|
236
233
|
try {
|
|
237
|
-
response = await stub.fetch(request)
|
|
234
|
+
response = await stub.fetch(request)
|
|
238
235
|
} catch (err) {
|
|
239
236
|
deps.logger?.("error", "cf-container: fetch threw", {
|
|
240
237
|
runId,
|
|
241
238
|
stepId,
|
|
242
239
|
error: err instanceof Error ? err.message : String(err),
|
|
243
|
-
})
|
|
244
|
-
return failed(attempt, startedAt, "CONTAINER_DISPATCH_FAILED", err)
|
|
240
|
+
})
|
|
241
|
+
return failed(attempt, startedAt, "CONTAINER_DISPATCH_FAILED", err)
|
|
245
242
|
}
|
|
246
243
|
|
|
247
|
-
const text = await response.text()
|
|
244
|
+
const text = await response.text()
|
|
248
245
|
if (response.status !== 200) {
|
|
249
246
|
deps.logger?.("warn", "cf-container: non-200 response", {
|
|
250
247
|
runId,
|
|
251
248
|
stepId,
|
|
252
249
|
status: response.status,
|
|
253
250
|
body: text.slice(0, 500),
|
|
254
|
-
})
|
|
251
|
+
})
|
|
255
252
|
return failed(
|
|
256
253
|
attempt,
|
|
257
254
|
startedAt,
|
|
258
255
|
"CONTAINER_HTTP_ERROR",
|
|
259
256
|
new Error(`container returned HTTP ${response.status}: ${text}`),
|
|
260
|
-
)
|
|
257
|
+
)
|
|
261
258
|
}
|
|
262
259
|
try {
|
|
263
|
-
const entry = JSON.parse(text) as StepJournalEntry
|
|
260
|
+
const entry = JSON.parse(text) as StepJournalEntry
|
|
264
261
|
// Trust the container's own timestamps; they reflect the actual
|
|
265
262
|
// step body execution, not the dispatch round-trip.
|
|
266
|
-
return entry
|
|
263
|
+
return entry
|
|
267
264
|
} catch (err) {
|
|
268
265
|
return failed(
|
|
269
266
|
attempt,
|
|
270
267
|
startedAt,
|
|
271
268
|
"CONTAINER_INVALID_RESPONSE",
|
|
272
269
|
new Error(`container returned non-JSON body: ${String(err)}`),
|
|
273
|
-
)
|
|
270
|
+
)
|
|
274
271
|
}
|
|
275
|
-
}
|
|
272
|
+
}
|
|
276
273
|
}
|
|
277
274
|
|
|
278
|
-
function failed(
|
|
279
|
-
|
|
280
|
-
startedAt: number,
|
|
281
|
-
code: string,
|
|
282
|
-
err: unknown,
|
|
283
|
-
): StepJournalEntry {
|
|
284
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
275
|
+
function failed(attempt: number, startedAt: number, code: string, err: unknown): StepJournalEntry {
|
|
276
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
285
277
|
return {
|
|
286
278
|
attempt,
|
|
287
279
|
status: "err",
|
|
@@ -294,5 +286,5 @@ function failed(
|
|
|
294
286
|
name: e.name,
|
|
295
287
|
stack: e.stack,
|
|
296
288
|
},
|
|
297
|
-
}
|
|
289
|
+
}
|
|
298
290
|
}
|
package/src/dispatch-handler.ts
CHANGED
|
@@ -9,17 +9,17 @@ import {
|
|
|
9
9
|
createHttpStepHandler,
|
|
10
10
|
type StepHandler,
|
|
11
11
|
type WorkflowStepRequest,
|
|
12
|
-
} from "@voyantjs/workflows-orchestrator"
|
|
13
|
-
import type { DispatchNamespaceLike } from "./types.js"
|
|
12
|
+
} from "@voyantjs/workflows-orchestrator"
|
|
13
|
+
import type { DispatchNamespaceLike } from "./types.js"
|
|
14
14
|
|
|
15
15
|
export interface DispatchHandlerDeps {
|
|
16
|
-
dispatcher: DispatchNamespaceLike
|
|
16
|
+
dispatcher: DispatchNamespaceLike
|
|
17
17
|
/** Optional HMAC signer for the X-Voyant-Dispatch-Auth header. */
|
|
18
|
-
sign?: (body: string) => Promise<string> | string
|
|
18
|
+
sign?: (body: string) => Promise<string> | string
|
|
19
19
|
/** Optional logger for step-level observability. */
|
|
20
|
-
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
20
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
21
21
|
/** Base URL to present to the tenant. Defaults to `https://tenant.voyant.internal`. */
|
|
22
|
-
baseUrl?: string
|
|
22
|
+
baseUrl?: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -33,21 +33,19 @@ export function createDispatchStepHandler(
|
|
|
33
33
|
tenantScript: string,
|
|
34
34
|
deps: DispatchHandlerDeps,
|
|
35
35
|
): StepHandler {
|
|
36
|
-
const baseUrl = deps.baseUrl ?? "https://tenant.voyant.internal"
|
|
36
|
+
const baseUrl = deps.baseUrl ?? "https://tenant.voyant.internal"
|
|
37
37
|
return createHttpStepHandler({
|
|
38
|
-
sign: deps.sign
|
|
39
|
-
? (body) => deps.sign!(body)
|
|
40
|
-
: undefined,
|
|
38
|
+
sign: deps.sign ? (body) => deps.sign!(body) : undefined,
|
|
41
39
|
logger: deps.logger,
|
|
42
40
|
resolveTarget(_req: WorkflowStepRequest) {
|
|
43
|
-
const binding = deps.dispatcher.get(tenantScript)
|
|
41
|
+
const binding = deps.dispatcher.get(tenantScript)
|
|
44
42
|
return {
|
|
45
43
|
url: `${baseUrl}/__voyant/workflow-step`,
|
|
46
44
|
label: tenantScript,
|
|
47
45
|
fetch(request: Request) {
|
|
48
|
-
return binding.fetch(request)
|
|
46
|
+
return binding.fetch(request)
|
|
49
47
|
},
|
|
50
|
-
}
|
|
48
|
+
}
|
|
51
49
|
},
|
|
52
|
-
})
|
|
50
|
+
})
|
|
53
51
|
}
|
package/src/do-store.ts
CHANGED
|
@@ -8,32 +8,30 @@
|
|
|
8
8
|
// through the global run index (lives in voyant-cloud's Postgres,
|
|
9
9
|
// not here).
|
|
10
10
|
|
|
11
|
-
import type { RunRecord, RunRecordStore } from "@voyantjs/workflows-orchestrator"
|
|
12
|
-
import type { DurableObjectStorageLike } from "./types.js"
|
|
11
|
+
import type { RunRecord, RunRecordStore } from "@voyantjs/workflows-orchestrator"
|
|
12
|
+
import type { DurableObjectStorageLike } from "./types.js"
|
|
13
13
|
|
|
14
|
-
const RECORD_KEY = "record"
|
|
14
|
+
const RECORD_KEY = "record"
|
|
15
15
|
|
|
16
|
-
export function createDurableObjectRunStore(
|
|
17
|
-
storage: DurableObjectStorageLike,
|
|
18
|
-
): RunRecordStore {
|
|
16
|
+
export function createDurableObjectRunStore(storage: DurableObjectStorageLike): RunRecordStore {
|
|
19
17
|
return {
|
|
20
18
|
async get(id) {
|
|
21
|
-
const r = await storage.get<RunRecord>(RECORD_KEY)
|
|
22
|
-
if (!r || r.id !== id) return undefined
|
|
23
|
-
return r
|
|
19
|
+
const r = await storage.get<RunRecord>(RECORD_KEY)
|
|
20
|
+
if (!r || r.id !== id) return undefined
|
|
21
|
+
return r
|
|
24
22
|
},
|
|
25
23
|
|
|
26
24
|
async save(record) {
|
|
27
|
-
await storage.put<RunRecord>(RECORD_KEY, record)
|
|
28
|
-
return record
|
|
25
|
+
await storage.put<RunRecord>(RECORD_KEY, record)
|
|
26
|
+
return record
|
|
29
27
|
},
|
|
30
28
|
|
|
31
29
|
async list(filter = {}) {
|
|
32
|
-
const r = await storage.get<RunRecord>(RECORD_KEY)
|
|
33
|
-
if (!r) return []
|
|
34
|
-
if (filter.workflowId && r.workflowId !== filter.workflowId) return []
|
|
35
|
-
if (filter.status && r.status !== filter.status) return []
|
|
36
|
-
return [r]
|
|
30
|
+
const r = await storage.get<RunRecord>(RECORD_KEY)
|
|
31
|
+
if (!r) return []
|
|
32
|
+
if (filter.workflowId && r.workflowId !== filter.workflowId) return []
|
|
33
|
+
if (filter.status && r.status !== filter.status) return []
|
|
34
|
+
return [r]
|
|
37
35
|
},
|
|
38
|
-
}
|
|
36
|
+
}
|
|
39
37
|
}
|
package/src/durable-object.ts
CHANGED
|
@@ -19,44 +19,44 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
applyWaitpointInjection,
|
|
22
|
-
cancel as orchestratorCancel,
|
|
23
22
|
driveUntilPaused,
|
|
23
|
+
cancel as orchestratorCancel,
|
|
24
24
|
resume as orchestratorResume,
|
|
25
25
|
trigger as orchestratorTrigger,
|
|
26
26
|
type PendingWaitpoint,
|
|
27
27
|
type RunRecord,
|
|
28
28
|
type StepHandler,
|
|
29
|
-
} from "@voyantjs/workflows-orchestrator"
|
|
29
|
+
} from "@voyantjs/workflows-orchestrator"
|
|
30
|
+
import { createDurableObjectRunStore } from "./do-store.js"
|
|
30
31
|
import type {
|
|
31
32
|
CancelPayload,
|
|
32
33
|
DurableObjectStorageLike,
|
|
33
34
|
ResumePayload,
|
|
34
35
|
TriggerPayload,
|
|
35
|
-
} from "./types.js"
|
|
36
|
-
import { createDurableObjectRunStore } from "./do-store.js";
|
|
36
|
+
} from "./types.js"
|
|
37
37
|
|
|
38
38
|
export interface DurableObjectDeps {
|
|
39
|
-
storage: DurableObjectStorageLike
|
|
39
|
+
storage: DurableObjectStorageLike
|
|
40
40
|
/**
|
|
41
41
|
* Resolve the StepHandler to use for a given tenant script. Called
|
|
42
42
|
* with the tenantScript (from the trigger payload) so the DO can
|
|
43
43
|
* route to the correct tenant Worker. In production this closes
|
|
44
44
|
* over the dispatch namespace; in tests it returns a mock.
|
|
45
45
|
*/
|
|
46
|
-
resolveStepHandler: (tenantScript: string) => StepHandler
|
|
47
|
-
now?: () => number
|
|
46
|
+
resolveStepHandler: (tenantScript: string) => StepHandler
|
|
47
|
+
now?: () => number
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function handleDurableObjectRequest(
|
|
51
51
|
req: Request,
|
|
52
52
|
deps: DurableObjectDeps,
|
|
53
53
|
): Promise<Response> {
|
|
54
|
-
const url = new URL(req.url)
|
|
55
|
-
const store = createDurableObjectRunStore(deps.storage)
|
|
54
|
+
const url = new URL(req.url)
|
|
55
|
+
const store = createDurableObjectRunStore(deps.storage)
|
|
56
56
|
|
|
57
57
|
if (req.method === "POST" && url.pathname === "/trigger") {
|
|
58
|
-
const payload = (await req.json()) as TriggerPayload
|
|
59
|
-
const handler = deps.resolveStepHandler(payload.tenantMeta.tenantScript)
|
|
58
|
+
const payload = (await req.json()) as TriggerPayload
|
|
59
|
+
const handler = deps.resolveStepHandler(payload.tenantMeta.tenantScript)
|
|
60
60
|
const record = await orchestratorTrigger(
|
|
61
61
|
{
|
|
62
62
|
workflowId: payload.workflowId,
|
|
@@ -68,52 +68,52 @@ export async function handleDurableObjectRequest(
|
|
|
68
68
|
runId: payload.runId,
|
|
69
69
|
},
|
|
70
70
|
{ store, handler, now: deps.now },
|
|
71
|
-
)
|
|
72
|
-
await reconcileAlarm(record, store, deps)
|
|
73
|
-
return json(200, record)
|
|
71
|
+
)
|
|
72
|
+
await reconcileAlarm(record, store, deps)
|
|
73
|
+
return json(200, record)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (req.method === "POST" && url.pathname === "/resume") {
|
|
77
|
-
const payload = (await req.json()) as ResumePayload
|
|
78
|
-
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
79
|
-
if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" })
|
|
80
|
-
const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "")
|
|
77
|
+
const payload = (await req.json()) as ResumePayload
|
|
78
|
+
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
79
|
+
if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" })
|
|
80
|
+
const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "")
|
|
81
81
|
const out = await orchestratorResume(
|
|
82
82
|
{ runId: existing.id, injection: payload.injection },
|
|
83
83
|
{ store, handler, now: deps.now },
|
|
84
|
-
)
|
|
84
|
+
)
|
|
85
85
|
if (!out.ok) {
|
|
86
|
-
const status = out.status === "not_found" ? 404 : out.status === "no_match" ? 400 : 409
|
|
87
|
-
return json(status, { error: out.status, message: out.message })
|
|
86
|
+
const status = out.status === "not_found" ? 404 : out.status === "no_match" ? 400 : 409
|
|
87
|
+
return json(status, { error: out.status, message: out.message })
|
|
88
88
|
}
|
|
89
|
-
await reconcileAlarm(out.record, store, deps)
|
|
90
|
-
return json(200, out.record)
|
|
89
|
+
await reconcileAlarm(out.record, store, deps)
|
|
90
|
+
return json(200, out.record)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (req.method === "POST" && url.pathname === "/cancel") {
|
|
94
|
-
const payload = (await req.json()) as CancelPayload
|
|
95
|
-
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
96
|
-
if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" })
|
|
97
|
-
const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "")
|
|
94
|
+
const payload = (await req.json()) as CancelPayload
|
|
95
|
+
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
96
|
+
if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" })
|
|
97
|
+
const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "")
|
|
98
98
|
const out = await orchestratorCancel(
|
|
99
99
|
{ runId: existing.id, reason: payload.reason },
|
|
100
100
|
{ store, handler, now: deps.now },
|
|
101
|
-
)
|
|
101
|
+
)
|
|
102
102
|
if (!out.ok) {
|
|
103
|
-
const status = out.status === "not_found" ? 404 : 409
|
|
104
|
-
return json(status, { error: out.status, message: out.message })
|
|
103
|
+
const status = out.status === "not_found" ? 404 : 409
|
|
104
|
+
return json(status, { error: out.status, message: out.message })
|
|
105
105
|
}
|
|
106
|
-
await reconcileAlarm(out.record, store, deps)
|
|
107
|
-
return json(200, out.record)
|
|
106
|
+
await reconcileAlarm(out.record, store, deps)
|
|
107
|
+
return json(200, out.record)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (req.method === "GET" && url.pathname === "/get") {
|
|
111
|
-
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
112
|
-
if (!existing) return json(404, { error: "not_found" })
|
|
113
|
-
return json(200, existing)
|
|
111
|
+
const existing = await store.get((await getStoredRunId(store)) ?? "")
|
|
112
|
+
if (!existing) return json(404, { error: "not_found" })
|
|
113
|
+
return json(200, existing)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
return json(404, { error: "route_not_found", path: url.pathname })
|
|
116
|
+
return json(404, { error: "route_not_found", path: url.pathname })
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -122,48 +122,46 @@ export async function handleDurableObjectRequest(
|
|
|
122
122
|
* re-drives the run, and reschedules the next alarm if the run
|
|
123
123
|
* parked on another DATETIME.
|
|
124
124
|
*/
|
|
125
|
-
export async function handleDurableObjectAlarm(
|
|
126
|
-
deps
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
131
|
-
const record = await store.get(existingId);
|
|
132
|
-
if (!record) return;
|
|
125
|
+
export async function handleDurableObjectAlarm(deps: DurableObjectDeps): Promise<void> {
|
|
126
|
+
const store = createDurableObjectRunStore(deps.storage)
|
|
127
|
+
const existingId = await getStoredRunId(store)
|
|
128
|
+
if (!existingId) return
|
|
129
|
+
const record = await store.get(existingId)
|
|
130
|
+
if (!record) return
|
|
133
131
|
if (record.status !== "waiting") {
|
|
134
|
-
await deps.storage.deleteAlarm?.()
|
|
135
|
-
return
|
|
132
|
+
await deps.storage.deleteAlarm?.()
|
|
133
|
+
return
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
const now = (deps.now ?? (() => Date.now()))()
|
|
139
|
-
const stillPending: PendingWaitpoint[] = []
|
|
140
|
-
let resolvedAny = false
|
|
136
|
+
const now = (deps.now ?? (() => Date.now()))()
|
|
137
|
+
const stillPending: PendingWaitpoint[] = []
|
|
138
|
+
let resolvedAny = false
|
|
141
139
|
for (const wp of record.pendingWaitpoints) {
|
|
142
|
-
const wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined
|
|
140
|
+
const wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined
|
|
143
141
|
if (wp.kind === "DATETIME" && wakeAt !== undefined && wakeAt <= now) {
|
|
144
142
|
record.journal.waitpointsResolved[wp.clientWaitpointId] = {
|
|
145
143
|
kind: "DATETIME",
|
|
146
144
|
resolvedAt: now,
|
|
147
145
|
source: "replay",
|
|
148
|
-
}
|
|
149
|
-
resolvedAny = true
|
|
146
|
+
}
|
|
147
|
+
resolvedAny = true
|
|
150
148
|
} else {
|
|
151
|
-
stillPending.push(wp)
|
|
149
|
+
stillPending.push(wp)
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
|
-
record.pendingWaitpoints = stillPending
|
|
152
|
+
record.pendingWaitpoints = stillPending
|
|
155
153
|
if (!resolvedAny) {
|
|
156
154
|
// Spurious alarm — nothing due. Persist and reconcile.
|
|
157
|
-
await store.save(record)
|
|
158
|
-
await reconcileAlarm(record, store, deps)
|
|
159
|
-
return
|
|
155
|
+
await store.save(record)
|
|
156
|
+
await reconcileAlarm(record, store, deps)
|
|
157
|
+
return
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
record.status = "running"
|
|
163
|
-
const handler = deps.resolveStepHandler(record.tenantMeta.tenantScript ?? "")
|
|
164
|
-
await driveUntilPaused(record, { handler, now: deps.now })
|
|
165
|
-
await store.save(record)
|
|
166
|
-
await reconcileAlarm(record, store, deps)
|
|
160
|
+
record.status = "running"
|
|
161
|
+
const handler = deps.resolveStepHandler(record.tenantMeta.tenantScript ?? "")
|
|
162
|
+
await driveUntilPaused(record, { handler, now: deps.now })
|
|
163
|
+
await store.save(record)
|
|
164
|
+
await reconcileAlarm(record, store, deps)
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
/**
|
|
@@ -177,29 +175,29 @@ async function reconcileAlarm(
|
|
|
177
175
|
store: ReturnType<typeof createDurableObjectRunStore>,
|
|
178
176
|
deps: DurableObjectDeps,
|
|
179
177
|
): Promise<void> {
|
|
180
|
-
const now = (deps.now ?? (() => Date.now()))()
|
|
178
|
+
const now = (deps.now ?? (() => Date.now()))()
|
|
181
179
|
if (record.status !== "waiting") {
|
|
182
|
-
await deps.storage.deleteAlarm?.()
|
|
183
|
-
return
|
|
180
|
+
await deps.storage.deleteAlarm?.()
|
|
181
|
+
return
|
|
184
182
|
}
|
|
185
|
-
let earliest: number | undefined
|
|
186
|
-
let dirty = false
|
|
183
|
+
let earliest: number | undefined
|
|
184
|
+
let dirty = false
|
|
187
185
|
for (const wp of record.pendingWaitpoints) {
|
|
188
|
-
if (wp.kind !== "DATETIME") continue
|
|
189
|
-
let wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined
|
|
186
|
+
if (wp.kind !== "DATETIME") continue
|
|
187
|
+
let wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined
|
|
190
188
|
if (wakeAt === undefined) {
|
|
191
|
-
const ms = wp.timeoutMs ?? (typeof wp.meta.durationMs === "number" ? wp.meta.durationMs : 0)
|
|
192
|
-
wakeAt = now + ms
|
|
193
|
-
wp.meta.wakeAt = wakeAt
|
|
194
|
-
dirty = true
|
|
189
|
+
const ms = wp.timeoutMs ?? (typeof wp.meta.durationMs === "number" ? wp.meta.durationMs : 0)
|
|
190
|
+
wakeAt = now + ms
|
|
191
|
+
wp.meta.wakeAt = wakeAt
|
|
192
|
+
dirty = true
|
|
195
193
|
}
|
|
196
|
-
earliest = earliest === undefined ? wakeAt : Math.min(earliest, wakeAt)
|
|
194
|
+
earliest = earliest === undefined ? wakeAt : Math.min(earliest, wakeAt)
|
|
197
195
|
}
|
|
198
|
-
if (dirty) await store.save(record)
|
|
196
|
+
if (dirty) await store.save(record)
|
|
199
197
|
if (earliest !== undefined) {
|
|
200
|
-
await deps.storage.setAlarm?.(earliest)
|
|
198
|
+
await deps.storage.setAlarm?.(earliest)
|
|
201
199
|
} else {
|
|
202
|
-
await deps.storage.deleteAlarm?.()
|
|
200
|
+
await deps.storage.deleteAlarm?.()
|
|
203
201
|
}
|
|
204
202
|
}
|
|
205
203
|
|
|
@@ -207,18 +205,20 @@ async function reconcileAlarm(
|
|
|
207
205
|
* One DO holds one run, keyed by `record`. Returns the id of that
|
|
208
206
|
* stored run, if any.
|
|
209
207
|
*/
|
|
210
|
-
async function getStoredRunId(
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
async function getStoredRunId(
|
|
209
|
+
store: ReturnType<typeof createDurableObjectRunStore>,
|
|
210
|
+
): Promise<string | undefined> {
|
|
211
|
+
const all = await store.list()
|
|
212
|
+
return all[0]?.id
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
function json(status: number, body: unknown): Response {
|
|
216
216
|
return new Response(JSON.stringify(body), {
|
|
217
217
|
status,
|
|
218
218
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
219
|
-
})
|
|
219
|
+
})
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
export type { RunRecord }
|
|
222
223
|
// Re-exported for callers building their own DO class via composition.
|
|
223
|
-
export { applyWaitpointInjection, driveUntilPaused }
|
|
224
|
-
export type { RunRecord };
|
|
224
|
+
export { applyWaitpointInjection, driveUntilPaused }
|