@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.
@@ -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
- attempt: number,
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
  }
@@ -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
  }
@@ -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: DurableObjectDeps,
127
- ): Promise<void> {
128
- const store = createDurableObjectRunStore(deps.storage);
129
- const existingId = await getStoredRunId(store);
130
- if (!existingId) return;
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(store: ReturnType<typeof createDurableObjectRunStore>): Promise<string | undefined> {
211
- const all = await store.list();
212
- return all[0]?.id;
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 }