@dudousxd/nestjs-durable-core 0.46.1 → 0.48.0
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/index.cjs +511 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -28
- package/dist/index.d.ts +225 -28
- package/dist/index.js +507 -178
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -156,6 +156,218 @@ var InMemoryAdmissionBackend = class {
|
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
+
// src/ambient-ctx.ts
|
|
160
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
161
|
+
var STORAGE_KEY = Symbol.for("nestjs-durable:ambient-workflow-ctx");
|
|
162
|
+
var globalRef = globalThis;
|
|
163
|
+
if (!globalRef[STORAGE_KEY]) {
|
|
164
|
+
globalRef[STORAGE_KEY] = new AsyncLocalStorage();
|
|
165
|
+
}
|
|
166
|
+
var storage = globalRef[STORAGE_KEY];
|
|
167
|
+
function runInWorkflowCtx(ctx, fn) {
|
|
168
|
+
return storage.run(ctx, fn);
|
|
169
|
+
}
|
|
170
|
+
__name(runInWorkflowCtx, "runInWorkflowCtx");
|
|
171
|
+
function currentWorkflowCtx() {
|
|
172
|
+
return storage.getStore();
|
|
173
|
+
}
|
|
174
|
+
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
175
|
+
|
|
176
|
+
// src/errors.ts
|
|
177
|
+
var FatalError = class extends Error {
|
|
178
|
+
static {
|
|
179
|
+
__name(this, "FatalError");
|
|
180
|
+
}
|
|
181
|
+
code;
|
|
182
|
+
constructor(message, code) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.name = "FatalError";
|
|
185
|
+
this.code = code;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var SingletonQueueFullError = class extends Error {
|
|
189
|
+
static {
|
|
190
|
+
__name(this, "SingletonQueueFullError");
|
|
191
|
+
}
|
|
192
|
+
workflow;
|
|
193
|
+
key;
|
|
194
|
+
maxQueueDepth;
|
|
195
|
+
constructor(workflow, key, maxQueueDepth) {
|
|
196
|
+
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
197
|
+
this.name = "SingletonQueueFullError";
|
|
198
|
+
this.workflow = workflow;
|
|
199
|
+
this.key = key;
|
|
200
|
+
this.maxQueueDepth = maxQueueDepth;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var SignalTimeoutError = class extends Error {
|
|
204
|
+
static {
|
|
205
|
+
__name(this, "SignalTimeoutError");
|
|
206
|
+
}
|
|
207
|
+
token;
|
|
208
|
+
timeoutMs;
|
|
209
|
+
constructor(token, timeoutMs) {
|
|
210
|
+
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
211
|
+
this.name = "SignalTimeoutError";
|
|
212
|
+
this.token = token;
|
|
213
|
+
this.timeoutMs = timeoutMs;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var RemoteStepTimeout = class extends Error {
|
|
217
|
+
static {
|
|
218
|
+
__name(this, "RemoteStepTimeout");
|
|
219
|
+
}
|
|
220
|
+
stepId;
|
|
221
|
+
timeoutMs;
|
|
222
|
+
constructor(stepId2, timeoutMs) {
|
|
223
|
+
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
224
|
+
this.name = "RemoteStepTimeout";
|
|
225
|
+
this.stepId = stepId2;
|
|
226
|
+
this.timeoutMs = timeoutMs;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var RemoteWorkflowTimeout = class extends Error {
|
|
230
|
+
static {
|
|
231
|
+
__name(this, "RemoteWorkflowTimeout");
|
|
232
|
+
}
|
|
233
|
+
taskId;
|
|
234
|
+
timeoutMs;
|
|
235
|
+
constructor(taskId, timeoutMs) {
|
|
236
|
+
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
237
|
+
this.name = "RemoteWorkflowTimeout";
|
|
238
|
+
this.taskId = taskId;
|
|
239
|
+
this.timeoutMs = timeoutMs;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var NonDeterminismError = class extends Error {
|
|
243
|
+
static {
|
|
244
|
+
__name(this, "NonDeterminismError");
|
|
245
|
+
}
|
|
246
|
+
runId;
|
|
247
|
+
seq;
|
|
248
|
+
constructor(runId, seq, expected, recorded) {
|
|
249
|
+
super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
|
|
250
|
+
this.name = "NonDeterminismError";
|
|
251
|
+
this.runId = runId;
|
|
252
|
+
this.seq = seq;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var GatherError = class extends Error {
|
|
256
|
+
static {
|
|
257
|
+
__name(this, "GatherError");
|
|
258
|
+
}
|
|
259
|
+
failures;
|
|
260
|
+
constructor(failures, total) {
|
|
261
|
+
const ids = failures.map((f) => f.id).join(", ");
|
|
262
|
+
const denom = total ?? failures.length;
|
|
263
|
+
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
264
|
+
this.name = "GatherError";
|
|
265
|
+
this.failures = failures;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
var WorkflowSuspended = class extends Error {
|
|
269
|
+
static {
|
|
270
|
+
__name(this, "WorkflowSuspended");
|
|
271
|
+
}
|
|
272
|
+
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
273
|
+
wakeAt;
|
|
274
|
+
constructor(wakeAt) {
|
|
275
|
+
super("workflow suspended");
|
|
276
|
+
this.name = "WorkflowSuspended";
|
|
277
|
+
this.wakeAt = wakeAt;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var ContinueAsNew = class extends Error {
|
|
281
|
+
static {
|
|
282
|
+
__name(this, "ContinueAsNew");
|
|
283
|
+
}
|
|
284
|
+
input;
|
|
285
|
+
constructor(input) {
|
|
286
|
+
super("workflow continued as new"), this.input = input;
|
|
287
|
+
this.name = "ContinueAsNew";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/workflow-ref.ts
|
|
292
|
+
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
293
|
+
function workflowName(ref) {
|
|
294
|
+
if (typeof ref === "string") return ref;
|
|
295
|
+
const name = ref[WORKFLOW_NAME_KEY];
|
|
296
|
+
if (!name) {
|
|
297
|
+
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
298
|
+
}
|
|
299
|
+
return name;
|
|
300
|
+
}
|
|
301
|
+
__name(workflowName, "workflowName");
|
|
302
|
+
|
|
303
|
+
// src/durable-workflow.ts
|
|
304
|
+
var BINDINGS_KEY = Symbol.for("nestjs-durable:workflow-class-bindings");
|
|
305
|
+
var globalRef2 = globalThis;
|
|
306
|
+
if (!globalRef2[BINDINGS_KEY]) {
|
|
307
|
+
globalRef2[BINDINGS_KEY] = /* @__PURE__ */ new WeakMap();
|
|
308
|
+
}
|
|
309
|
+
var bindings = globalRef2[BINDINGS_KEY];
|
|
310
|
+
function bindWorkflowClass(ctor, client) {
|
|
311
|
+
bindings.set(ctor, client);
|
|
312
|
+
}
|
|
313
|
+
__name(bindWorkflowClass, "bindWorkflowClass");
|
|
314
|
+
function clientOf(ctor) {
|
|
315
|
+
const client = bindings.get(ctor);
|
|
316
|
+
if (!client) {
|
|
317
|
+
throw new Error(`${workflowName(ctor)} is not bound to a durable engine \u2014 the class-first statics only work after the module that registers this workflow has booted (or, in a store-less tenant worker, use the injected WorkflowService/engine facade instead).`);
|
|
318
|
+
}
|
|
319
|
+
return client;
|
|
320
|
+
}
|
|
321
|
+
__name(clientOf, "clientOf");
|
|
322
|
+
var DurableWorkflow = class {
|
|
323
|
+
static {
|
|
324
|
+
__name(this, "DurableWorkflow");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Fire-and-forget start — `engine.start` outside a workflow, `ctx.startChild` (parent-linked)
|
|
328
|
+
* inside one. `opts.id` pins the run id (outside) / child id (inside) for idempotent starts.
|
|
329
|
+
*/
|
|
330
|
+
static async start(input, opts) {
|
|
331
|
+
const cls = this;
|
|
332
|
+
const ctx = currentWorkflowCtx();
|
|
333
|
+
if (ctx) {
|
|
334
|
+
const runId2 = await ctx.startChild(cls, input, opts?.id);
|
|
335
|
+
return {
|
|
336
|
+
runId: runId2,
|
|
337
|
+
status: "pending"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
341
|
+
return clientOf(cls).start(workflowName(cls), input, runId, opts);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Run-and-await the typed output — `ctx.child` inside a workflow (suspends the parent, zero
|
|
345
|
+
* compute); outside, starts the run and waits until it settles terminally. Throws `FatalError`
|
|
346
|
+
* when the run does not complete. `opts.timeoutMs` bounds the OUTSIDE wait only (inside, a
|
|
347
|
+
* suspended parent costs nothing — bound the child itself with `executionTimeout` instead).
|
|
348
|
+
*/
|
|
349
|
+
static async execute(input, opts) {
|
|
350
|
+
const cls = this;
|
|
351
|
+
const ctx = currentWorkflowCtx();
|
|
352
|
+
if (ctx) return ctx.child(cls, input, opts?.id);
|
|
353
|
+
const client = clientOf(cls);
|
|
354
|
+
if (!client.waitForRun) {
|
|
355
|
+
throw new FatalError(`${workflowName(cls)}.execute() outside a workflow needs a store-backed engine to await the result \u2014 this class is bound to a store-less start facade (tenant worker). Use ${workflowName(cls)}.start() and observe the run instead.`, "execute_unsupported");
|
|
356
|
+
}
|
|
357
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
358
|
+
await client.start(workflowName(cls), input, runId, opts);
|
|
359
|
+
const settled = await client.waitForRun(runId, {
|
|
360
|
+
...opts?.timeoutMs != null ? {
|
|
361
|
+
timeoutMs: opts.timeoutMs
|
|
362
|
+
} : {},
|
|
363
|
+
until: "terminal"
|
|
364
|
+
});
|
|
365
|
+
if (settled.status === "completed") return settled.output;
|
|
366
|
+
const detail = settled.error ? `: ${settled.error.message}` : "";
|
|
367
|
+
throw new FatalError(`${workflowName(cls)} run ${runId} settled ${settled.status}${detail}`, settled.error?.code ?? "execute_failed");
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
159
371
|
// src/duration.ts
|
|
160
372
|
var UNIT_MS = {
|
|
161
373
|
ms: 1,
|
|
@@ -412,121 +624,6 @@ function instantCheckpoint(p) {
|
|
|
412
624
|
}
|
|
413
625
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
414
626
|
|
|
415
|
-
// src/errors.ts
|
|
416
|
-
var FatalError = class extends Error {
|
|
417
|
-
static {
|
|
418
|
-
__name(this, "FatalError");
|
|
419
|
-
}
|
|
420
|
-
code;
|
|
421
|
-
constructor(message, code) {
|
|
422
|
-
super(message);
|
|
423
|
-
this.name = "FatalError";
|
|
424
|
-
this.code = code;
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
var SingletonQueueFullError = class extends Error {
|
|
428
|
-
static {
|
|
429
|
-
__name(this, "SingletonQueueFullError");
|
|
430
|
-
}
|
|
431
|
-
workflow;
|
|
432
|
-
key;
|
|
433
|
-
maxQueueDepth;
|
|
434
|
-
constructor(workflow, key, maxQueueDepth) {
|
|
435
|
-
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
436
|
-
this.name = "SingletonQueueFullError";
|
|
437
|
-
this.workflow = workflow;
|
|
438
|
-
this.key = key;
|
|
439
|
-
this.maxQueueDepth = maxQueueDepth;
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
var SignalTimeoutError = class extends Error {
|
|
443
|
-
static {
|
|
444
|
-
__name(this, "SignalTimeoutError");
|
|
445
|
-
}
|
|
446
|
-
token;
|
|
447
|
-
timeoutMs;
|
|
448
|
-
constructor(token, timeoutMs) {
|
|
449
|
-
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
450
|
-
this.name = "SignalTimeoutError";
|
|
451
|
-
this.token = token;
|
|
452
|
-
this.timeoutMs = timeoutMs;
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
var RemoteStepTimeout = class extends Error {
|
|
456
|
-
static {
|
|
457
|
-
__name(this, "RemoteStepTimeout");
|
|
458
|
-
}
|
|
459
|
-
stepId;
|
|
460
|
-
timeoutMs;
|
|
461
|
-
constructor(stepId2, timeoutMs) {
|
|
462
|
-
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
463
|
-
this.name = "RemoteStepTimeout";
|
|
464
|
-
this.stepId = stepId2;
|
|
465
|
-
this.timeoutMs = timeoutMs;
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
var RemoteWorkflowTimeout = class extends Error {
|
|
469
|
-
static {
|
|
470
|
-
__name(this, "RemoteWorkflowTimeout");
|
|
471
|
-
}
|
|
472
|
-
taskId;
|
|
473
|
-
timeoutMs;
|
|
474
|
-
constructor(taskId, timeoutMs) {
|
|
475
|
-
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
476
|
-
this.name = "RemoteWorkflowTimeout";
|
|
477
|
-
this.taskId = taskId;
|
|
478
|
-
this.timeoutMs = timeoutMs;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
var NonDeterminismError = class extends Error {
|
|
482
|
-
static {
|
|
483
|
-
__name(this, "NonDeterminismError");
|
|
484
|
-
}
|
|
485
|
-
runId;
|
|
486
|
-
seq;
|
|
487
|
-
constructor(runId, seq, expected, recorded) {
|
|
488
|
-
super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
|
|
489
|
-
this.name = "NonDeterminismError";
|
|
490
|
-
this.runId = runId;
|
|
491
|
-
this.seq = seq;
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
var GatherError = class extends Error {
|
|
495
|
-
static {
|
|
496
|
-
__name(this, "GatherError");
|
|
497
|
-
}
|
|
498
|
-
failures;
|
|
499
|
-
constructor(failures, total) {
|
|
500
|
-
const ids = failures.map((f) => f.id).join(", ");
|
|
501
|
-
const denom = total ?? failures.length;
|
|
502
|
-
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
503
|
-
this.name = "GatherError";
|
|
504
|
-
this.failures = failures;
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
var WorkflowSuspended = class extends Error {
|
|
508
|
-
static {
|
|
509
|
-
__name(this, "WorkflowSuspended");
|
|
510
|
-
}
|
|
511
|
-
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
512
|
-
wakeAt;
|
|
513
|
-
constructor(wakeAt) {
|
|
514
|
-
super("workflow suspended");
|
|
515
|
-
this.name = "WorkflowSuspended";
|
|
516
|
-
this.wakeAt = wakeAt;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
var ContinueAsNew = class extends Error {
|
|
520
|
-
static {
|
|
521
|
-
__name(this, "ContinueAsNew");
|
|
522
|
-
}
|
|
523
|
-
input;
|
|
524
|
-
constructor(input) {
|
|
525
|
-
super("workflow continued as new"), this.input = input;
|
|
526
|
-
this.name = "ContinueAsNew";
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
627
|
// src/entities.ts
|
|
531
628
|
var ENTITY_WORKFLOW = "__entity";
|
|
532
629
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
@@ -988,18 +1085,6 @@ function stepConfigOf(ref) {
|
|
|
988
1085
|
}
|
|
989
1086
|
__name(stepConfigOf, "stepConfigOf");
|
|
990
1087
|
|
|
991
|
-
// src/workflow-ref.ts
|
|
992
|
-
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
993
|
-
function workflowName(ref) {
|
|
994
|
-
if (typeof ref === "string") return ref;
|
|
995
|
-
const name = ref[WORKFLOW_NAME_KEY];
|
|
996
|
-
if (!name) {
|
|
997
|
-
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
998
|
-
}
|
|
999
|
-
return name;
|
|
1000
|
-
}
|
|
1001
|
-
__name(workflowName, "workflowName");
|
|
1002
|
-
|
|
1003
1088
|
// src/workflow-ctx.ts
|
|
1004
1089
|
function normalizeChildOptions(options) {
|
|
1005
1090
|
return typeof options === "string" ? {
|
|
@@ -1185,6 +1270,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1185
1270
|
value: buffered.payload
|
|
1186
1271
|
};
|
|
1187
1272
|
}, "consumeBuffered");
|
|
1273
|
+
const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
|
|
1274
|
+
const recorded = await readCheckpoint(deadlineSeq);
|
|
1275
|
+
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1276
|
+
if (!recorded) {
|
|
1277
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1278
|
+
runId,
|
|
1279
|
+
seq: deadlineSeq,
|
|
1280
|
+
name: `timeout:${label}`,
|
|
1281
|
+
kind: "sleep",
|
|
1282
|
+
wakeAt: deadline
|
|
1283
|
+
}));
|
|
1284
|
+
}
|
|
1285
|
+
return deadline;
|
|
1286
|
+
}, "stampDeadline");
|
|
1188
1287
|
const waitForSignal = /* @__PURE__ */ __name(async (token, opts) => {
|
|
1189
1288
|
if (opts?.timeoutMs == null) {
|
|
1190
1289
|
const current = pos.next();
|
|
@@ -1202,17 +1301,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1202
1301
|
const timeoutMs = opts.timeoutMs;
|
|
1203
1302
|
const deadlineSeq = pos.next();
|
|
1204
1303
|
const waitSeq = pos.next();
|
|
1205
|
-
const
|
|
1206
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1207
|
-
if (!recorded) {
|
|
1208
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1209
|
-
runId,
|
|
1210
|
-
seq: deadlineSeq,
|
|
1211
|
-
name: `timeout:${token}`,
|
|
1212
|
-
kind: "sleep",
|
|
1213
|
-
wakeAt: deadline
|
|
1214
|
-
}));
|
|
1215
|
-
}
|
|
1304
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1216
1305
|
const waited = await readCheckpoint(waitSeq);
|
|
1217
1306
|
if (waited && waited.status === "completed") return waited.output;
|
|
1218
1307
|
const buffered = await consumeBuffered(token, waitSeq);
|
|
@@ -1245,17 +1334,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1245
1334
|
const deadlineSeq = pos.next();
|
|
1246
1335
|
const waitSeq = pos.next();
|
|
1247
1336
|
const token = eventToken(name, opts.match, runId, waitSeq);
|
|
1248
|
-
const
|
|
1249
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1250
|
-
if (!recorded) {
|
|
1251
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1252
|
-
runId,
|
|
1253
|
-
seq: deadlineSeq,
|
|
1254
|
-
name: `timeout:event:${name}`,
|
|
1255
|
-
kind: "sleep",
|
|
1256
|
-
wakeAt: deadline
|
|
1257
|
-
}));
|
|
1258
|
-
}
|
|
1337
|
+
const deadline = await stampDeadline(deadlineSeq, `event:${name}`, timeoutMs);
|
|
1259
1338
|
const waited = await readCheckpoint(waitSeq);
|
|
1260
1339
|
if (waited && waited.status === "completed") return waited.output;
|
|
1261
1340
|
if (host.clock() >= deadline) {
|
|
@@ -1311,6 +1390,9 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1311
1390
|
const cp = existing[i];
|
|
1312
1391
|
const c = cp?.status === "completed" ? cp.output : null;
|
|
1313
1392
|
if (c && c.ok === false) {
|
|
1393
|
+
for (let j = 0; j < inputs.length; j += 1) {
|
|
1394
|
+
if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
|
|
1395
|
+
}
|
|
1314
1396
|
throw new GatherError([
|
|
1315
1397
|
{
|
|
1316
1398
|
index: i,
|
|
@@ -1467,15 +1549,30 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1467
1549
|
const webhook = /* @__PURE__ */ __name(() => {
|
|
1468
1550
|
const current = pos.next();
|
|
1469
1551
|
const token = `wh:${runId}:${current}`;
|
|
1470
|
-
const wait = /* @__PURE__ */ __name(async () => {
|
|
1552
|
+
const wait = /* @__PURE__ */ __name(async (opts) => {
|
|
1553
|
+
const timeoutMs = opts?.timeoutMs;
|
|
1554
|
+
const deadlineSeq = timeoutMs == null ? -1 : pos.next();
|
|
1471
1555
|
const existing = await readCheckpoint(current);
|
|
1472
1556
|
if (existing && existing.status === "completed") return existing.output;
|
|
1557
|
+
if (timeoutMs == null) {
|
|
1558
|
+
await store.putSignalWaiter({
|
|
1559
|
+
token,
|
|
1560
|
+
runId,
|
|
1561
|
+
seq: current
|
|
1562
|
+
});
|
|
1563
|
+
throw new WorkflowSuspended();
|
|
1564
|
+
}
|
|
1565
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1566
|
+
if (host.clock() >= deadline) {
|
|
1567
|
+
await store.takeSignalWaiter(token).catch(() => void 0);
|
|
1568
|
+
throw new SignalTimeoutError(token, timeoutMs);
|
|
1569
|
+
}
|
|
1473
1570
|
await store.putSignalWaiter({
|
|
1474
1571
|
token,
|
|
1475
1572
|
runId,
|
|
1476
1573
|
seq: current
|
|
1477
1574
|
});
|
|
1478
|
-
throw new WorkflowSuspended();
|
|
1575
|
+
throw new WorkflowSuspended(deadline);
|
|
1479
1576
|
}, "wait");
|
|
1480
1577
|
return {
|
|
1481
1578
|
token,
|
|
@@ -1503,6 +1600,18 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1503
1600
|
output: attrs
|
|
1504
1601
|
}));
|
|
1505
1602
|
}, "upsertSearchAttributes");
|
|
1603
|
+
function resolveCompensateDef(compensate) {
|
|
1604
|
+
const name = typeof compensate === "string" ? compensate : stepNameOf(compensate);
|
|
1605
|
+
if (name === void 0) {
|
|
1606
|
+
throw new Error("ctx.step: compensate handler is not a @Step reference (no stamped step name) \u2014 pass a @Step-decorated method or a step name string");
|
|
1607
|
+
}
|
|
1608
|
+
const config = typeof compensate === "string" ? void 0 : stepConfigOf(compensate);
|
|
1609
|
+
return {
|
|
1610
|
+
name,
|
|
1611
|
+
...resolveStepPolicy(config, void 0)
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
__name(resolveCompensateDef, "resolveCompensateDef");
|
|
1506
1615
|
function step(handlerOrName, input, opts) {
|
|
1507
1616
|
const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
|
|
1508
1617
|
if (name === void 0) {
|
|
@@ -1516,6 +1625,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1516
1625
|
return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
|
|
1517
1626
|
priority: opts?.priority,
|
|
1518
1627
|
fairnessKey: opts?.fairnessKey
|
|
1628
|
+
}).then((output) => {
|
|
1629
|
+
if (opts?.compensate) {
|
|
1630
|
+
compensations.push({
|
|
1631
|
+
name,
|
|
1632
|
+
dispatch: {
|
|
1633
|
+
def: resolveCompensateDef(opts.compensate),
|
|
1634
|
+
args: {
|
|
1635
|
+
input,
|
|
1636
|
+
output
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return output;
|
|
1519
1642
|
});
|
|
1520
1643
|
}
|
|
1521
1644
|
__name(step, "step");
|
|
@@ -2409,7 +2532,13 @@ var WorkflowEngine = class {
|
|
|
2409
2532
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
2410
2533
|
*/
|
|
2411
2534
|
waitForRun(runId, opts) {
|
|
2412
|
-
const
|
|
2535
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
2536
|
+
"completed",
|
|
2537
|
+
"failed",
|
|
2538
|
+
"cancelled",
|
|
2539
|
+
"dead"
|
|
2540
|
+
]);
|
|
2541
|
+
const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
|
|
2413
2542
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
2414
2543
|
runId,
|
|
2415
2544
|
status: run.status,
|
|
@@ -2423,6 +2552,7 @@ var WorkflowEngine = class {
|
|
|
2423
2552
|
const timer = opts?.timeoutMs != null ? setTimeout(() => {
|
|
2424
2553
|
if (done) return;
|
|
2425
2554
|
done = true;
|
|
2555
|
+
if (poller) clearInterval(poller);
|
|
2426
2556
|
off();
|
|
2427
2557
|
reject(new Error(`waitForRun(${runId}) timed out after ${opts.timeoutMs}ms`));
|
|
2428
2558
|
}, opts.timeoutMs) : void 0;
|
|
@@ -2430,6 +2560,7 @@ var WorkflowEngine = class {
|
|
|
2430
2560
|
if (done) return;
|
|
2431
2561
|
done = true;
|
|
2432
2562
|
if (timer) clearTimeout(timer);
|
|
2563
|
+
if (poller) clearInterval(poller);
|
|
2433
2564
|
off();
|
|
2434
2565
|
resolve(toResult(run));
|
|
2435
2566
|
}, "finish");
|
|
@@ -2438,6 +2569,11 @@ var WorkflowEngine = class {
|
|
|
2438
2569
|
if (run && isSettled(run)) finish(run);
|
|
2439
2570
|
});
|
|
2440
2571
|
}, "check");
|
|
2572
|
+
const poller = opts?.until === "terminal" ? (() => {
|
|
2573
|
+
const interval = setInterval(check, 250);
|
|
2574
|
+
interval.unref?.();
|
|
2575
|
+
return interval;
|
|
2576
|
+
})() : void 0;
|
|
2441
2577
|
off = this.subscribe((ev) => {
|
|
2442
2578
|
if (ev.runId === runId && (ev.type === "run.completed" || ev.type === "run.failed" || ev.type === "run.suspended")) {
|
|
2443
2579
|
check();
|
|
@@ -3361,14 +3497,24 @@ var WorkflowEngine = class {
|
|
|
3361
3497
|
* Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
|
|
3362
3498
|
* exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
|
|
3363
3499
|
* suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
|
|
3364
|
-
* stop at a suspension point.
|
|
3500
|
+
* stop at a suspension point.
|
|
3501
|
+
*
|
|
3502
|
+
* A dispatched compensation still in flight (or backing off a retry) needs the run to keep waiting:
|
|
3503
|
+
* unlike the failure path (which is fine going `suspended` — a resumable state no different from an
|
|
3504
|
+
* ordinary in-flight `ctx.step`), THIS run must stay `cancelling` — `engine.cancel`'s idempotent
|
|
3505
|
+
* re-check, `waitForRun`'s non-terminal treatment, and crash recovery (`recoverIncomplete` only
|
|
3506
|
+
* re-drives `running`/`cancelling` runs — a `cancelling` one with a free lease, whatever it's
|
|
3507
|
+
* internally waiting on) all key off that status. So on a pending unwind this returns WITHOUT
|
|
3508
|
+
* touching `status` (already `cancelling`, untouched since `engine.cancel` set it) — the in-memory
|
|
3509
|
+
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
3365
3510
|
*/
|
|
3366
|
-
async compensateAndCancel(run, compensations) {
|
|
3511
|
+
async compensateAndCancel(run, compensations, replay) {
|
|
3512
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3513
|
+
if (pending) return {
|
|
3514
|
+
runId: run.id,
|
|
3515
|
+
status: "cancelling"
|
|
3516
|
+
};
|
|
3367
3517
|
this.cancelRequested.delete(run.id);
|
|
3368
|
-
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3369
|
-
const comp = compensations[i];
|
|
3370
|
-
if (comp) await this.runCompensation(run, comp);
|
|
3371
|
-
}
|
|
3372
3518
|
const error = {
|
|
3373
3519
|
message: "cancelled"
|
|
3374
3520
|
};
|
|
@@ -3431,15 +3577,19 @@ var WorkflowEngine = class {
|
|
|
3431
3577
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
3432
3578
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
|
|
3433
3579
|
try {
|
|
3434
|
-
const output = await fn(ctx, run.input);
|
|
3435
|
-
if (this.cancelRequested.has(run.id))
|
|
3580
|
+
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
3581
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3582
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3583
|
+
}
|
|
3436
3584
|
return this.settleRun(run, {
|
|
3437
3585
|
kind: "completed",
|
|
3438
3586
|
output
|
|
3439
3587
|
});
|
|
3440
3588
|
} catch (err) {
|
|
3441
3589
|
if (err instanceof ContinueAsNew) {
|
|
3442
|
-
if (this.cancelRequested.has(run.id))
|
|
3590
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3591
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3592
|
+
}
|
|
3443
3593
|
await this.store.updateRun(run.id, {
|
|
3444
3594
|
status: "completed",
|
|
3445
3595
|
output: void 0,
|
|
@@ -3464,22 +3614,26 @@ var WorkflowEngine = class {
|
|
|
3464
3614
|
};
|
|
3465
3615
|
}
|
|
3466
3616
|
if (err instanceof WorkflowSuspended) {
|
|
3467
|
-
if (this.cancelRequested.has(run.id))
|
|
3617
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3618
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3619
|
+
}
|
|
3468
3620
|
return this.settleRun(run, {
|
|
3469
3621
|
kind: "suspended",
|
|
3470
3622
|
wakeAt: err.wakeAt
|
|
3471
3623
|
});
|
|
3472
3624
|
}
|
|
3473
|
-
if (this.cancelRequested.has(run.id))
|
|
3625
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3626
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3627
|
+
}
|
|
3474
3628
|
const error = {
|
|
3475
3629
|
message: err instanceof Error ? err.message : String(err),
|
|
3476
3630
|
stack: err instanceof Error ? err.stack : void 0
|
|
3477
3631
|
};
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
}
|
|
3632
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3633
|
+
if (pending) return this.settleRun(run, {
|
|
3634
|
+
kind: "suspended",
|
|
3635
|
+
wakeAt: pending.wakeAt
|
|
3636
|
+
});
|
|
3483
3637
|
return this.settleRun(run, {
|
|
3484
3638
|
kind: "failed",
|
|
3485
3639
|
error
|
|
@@ -3489,14 +3643,62 @@ var WorkflowEngine = class {
|
|
|
3489
3643
|
}
|
|
3490
3644
|
}
|
|
3491
3645
|
/**
|
|
3492
|
-
*
|
|
3493
|
-
*
|
|
3494
|
-
*
|
|
3646
|
+
* Undo every registered saga compensation in strict reverse order — the shared unwind both the
|
|
3647
|
+
* run-failure path (`runExecution`'s catch) and the compensating-cancel path
|
|
3648
|
+
* (`compensateAndCancel`) drive. Every entry, LOCAL or DISPATCHED, gets a NEGATIVE checkpoint seq
|
|
3649
|
+
* `-(k + 1)` for its position `k` (0, 1, 2… walking the stack in reverse) — reserved space that can
|
|
3650
|
+
* never collide with a body position (those start at 0), so a saga's undo trail composes onto the
|
|
3651
|
+
* run's ordinary checkpoint history with no store-schema change, and shows up in run detail exactly
|
|
3652
|
+
* like any other step. Deterministic because `compensations` is rebuilt identically on every replay
|
|
3653
|
+
* (see `workflow-ctx.ts`), so the same comp always lands on the same negative seq across a crash +
|
|
3654
|
+
* re-drive — the crash-safety this whole scheme rests on.
|
|
3655
|
+
*
|
|
3656
|
+
* A LOCAL entry runs in-process (`runCompensation`), which checkpoints its own outcome — completed,
|
|
3657
|
+
* or failed once `compensationRetries` are exhausted — so a crash mid-unwind SKIPS an
|
|
3658
|
+
* already-resolved one on the next pass instead of re-running it (and re-spending its retry budget)
|
|
3659
|
+
* from attempt 1. A DISPATCHED entry is an ordinary remote-step dispatch under the hood
|
|
3660
|
+
* (`dispatchCompensation`): when it needs the run to WAIT — freshly dispatched, still in flight, or
|
|
3661
|
+
* backing off a durable retry — this function returns that wait signal immediately (skipping every
|
|
3662
|
+
* comp after it in this pass); the CALLER decides how to represent "still unwinding" (see
|
|
3663
|
+
* `compensateAndCancel`'s doc comment for why that differs between the two callers). Either way, the
|
|
3664
|
+
* run resumes later exactly like any other dispatched step: `completeRemoteResult` completes the
|
|
3665
|
+
* negative-seq checkpoint and re-drives, the body replays and deterministically re-throws the same
|
|
3666
|
+
* exit it took before (the original failure, or the suspension `compensateAndCancel` resumed from),
|
|
3667
|
+
* this function runs again, the now-resolved comp is skipped, and the unwind continues from there.
|
|
3668
|
+
*
|
|
3669
|
+
* Returns `undefined` once every compensation has resolved — the caller then finalizes the run
|
|
3670
|
+
* (`failed` with the ORIGINAL body error, never masked by a comp outcome; or `cancelled`).
|
|
3671
|
+
*/
|
|
3672
|
+
async unwindCompensations(run, compensations, replay) {
|
|
3673
|
+
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3674
|
+
const comp = compensations[i];
|
|
3675
|
+
if (!comp) continue;
|
|
3676
|
+
const seq = -(compensations.length - i);
|
|
3677
|
+
if ("fn" in comp) {
|
|
3678
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3679
|
+
if (existing) continue;
|
|
3680
|
+
await this.runCompensation(run, comp, seq, replay);
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
const pending = await this.dispatchCompensation(run, comp, seq, replay);
|
|
3684
|
+
if (pending) return pending;
|
|
3685
|
+
}
|
|
3686
|
+
return void 0;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Run one LOCAL saga compensation, retried up to `compensationRetries`, emitting a
|
|
3690
|
+
* `compensate:<step>` step event for its outcome so a stranded undo is visible. Never throws — a
|
|
3691
|
+
* permanently-failing compensation is skipped so it can't mask the original failure. Checkpoints
|
|
3692
|
+
* its outcome at `seq` (a negative unwind position reserved by {@link unwindCompensations}) so a
|
|
3693
|
+
* crash mid-unwind skips an already-resolved undo on a later pass instead of re-running it (and
|
|
3694
|
+
* re-spending its retry budget) from attempt 1 — the checkpoint is durability/visibility only; the
|
|
3695
|
+
* runtime retry-in-process-back-to-back semantics of `comp.fn()` are unchanged.
|
|
3495
3696
|
*/
|
|
3496
|
-
async runCompensation(run, comp) {
|
|
3697
|
+
async runCompensation(run, comp, seq, replay) {
|
|
3497
3698
|
const name = `compensate:${comp.name}`;
|
|
3699
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3498
3700
|
for (let attempt = 1; attempt <= this.compensationRetries; attempt += 1) {
|
|
3499
|
-
const startedAt = Date
|
|
3701
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
3500
3702
|
try {
|
|
3501
3703
|
await comp.fn();
|
|
3502
3704
|
this.emit({
|
|
@@ -3505,26 +3707,143 @@ var WorkflowEngine = class {
|
|
|
3505
3707
|
workflow: run.workflow,
|
|
3506
3708
|
name,
|
|
3507
3709
|
kind: "local",
|
|
3508
|
-
durationMs: Date.now() - startedAt
|
|
3710
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3509
3711
|
});
|
|
3712
|
+
const cp = stepCheckpoint({
|
|
3713
|
+
runId: run.id,
|
|
3714
|
+
seq,
|
|
3715
|
+
name,
|
|
3716
|
+
kind: "local",
|
|
3717
|
+
status: "completed",
|
|
3718
|
+
attempts: attempt,
|
|
3719
|
+
enqueuedAt,
|
|
3720
|
+
startedAt,
|
|
3721
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3722
|
+
});
|
|
3723
|
+
await this.store.saveCheckpoint(cp);
|
|
3724
|
+
replay?.set(seq, cp);
|
|
3510
3725
|
return;
|
|
3511
3726
|
} catch (err) {
|
|
3512
3727
|
if (attempt >= this.compensationRetries) {
|
|
3728
|
+
const error = {
|
|
3729
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3730
|
+
};
|
|
3513
3731
|
this.emit({
|
|
3514
3732
|
type: "step.failed",
|
|
3515
3733
|
runId: run.id,
|
|
3516
3734
|
workflow: run.workflow,
|
|
3517
3735
|
name,
|
|
3518
3736
|
kind: "local",
|
|
3519
|
-
error
|
|
3520
|
-
|
|
3521
|
-
},
|
|
3522
|
-
durationMs: Date.now() - startedAt
|
|
3737
|
+
error,
|
|
3738
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3523
3739
|
});
|
|
3740
|
+
const cp = stepCheckpoint({
|
|
3741
|
+
runId: run.id,
|
|
3742
|
+
seq,
|
|
3743
|
+
name,
|
|
3744
|
+
kind: "local",
|
|
3745
|
+
status: "failed",
|
|
3746
|
+
attempts: attempt,
|
|
3747
|
+
error,
|
|
3748
|
+
enqueuedAt,
|
|
3749
|
+
startedAt,
|
|
3750
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3751
|
+
});
|
|
3752
|
+
await this.store.saveCheckpoint(cp);
|
|
3753
|
+
replay?.set(seq, cp);
|
|
3524
3754
|
}
|
|
3525
3755
|
}
|
|
3526
3756
|
}
|
|
3527
3757
|
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Resolve (or advance) ONE dispatched saga compensation at its reserved negative `seq` — the undo
|
|
3760
|
+
* handler is an ordinary remote `@Step`, so this mirrors `callRemote`'s pending/failed/absent
|
|
3761
|
+
* branches (engine.ts's `callRemote`), minus the queue/admission and in-memory-`timeoutMs` paths
|
|
3762
|
+
* (V1: dispatch direct by name, always durable). Returns the wait signal when the run must pause for
|
|
3763
|
+
* this compensation (freshly dispatched, still in flight, or backing off before a durable retry);
|
|
3764
|
+
* `undefined` once it's resolved — completed (crash-safe skip), or retries exhausted/non-retryable
|
|
3765
|
+
* (already visible: `completeRemoteResult` emitted `step.failed` for `compensate:<name>` when that
|
|
3766
|
+
* checkpoint landed — never re-emitted here, and never allowed to block the rest of the unwind or
|
|
3767
|
+
* mask the run's original failure).
|
|
3768
|
+
*/
|
|
3769
|
+
async dispatchCompensation(run, comp, seq, replay) {
|
|
3770
|
+
const { def, args } = comp.dispatch;
|
|
3771
|
+
const name = `compensate:${comp.name}`;
|
|
3772
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3773
|
+
if (existing?.status === "completed") return void 0;
|
|
3774
|
+
if (existing?.status === "pending") return {};
|
|
3775
|
+
if (existing?.status === "failed") {
|
|
3776
|
+
const maxAttempts = Math.max(1, def.retries ?? 1);
|
|
3777
|
+
const retryable = existing.error?.retryable !== false;
|
|
3778
|
+
if (!retryable || existing.attempts >= maxAttempts) return void 0;
|
|
3779
|
+
if (existing.wakeAt == null) {
|
|
3780
|
+
const wakeAt = this.clock() + backoffDelay(existing.attempts, def);
|
|
3781
|
+
const withWakeAt = {
|
|
3782
|
+
...existing,
|
|
3783
|
+
wakeAt
|
|
3784
|
+
};
|
|
3785
|
+
await this.store.saveCheckpoint(withWakeAt);
|
|
3786
|
+
replay?.set(seq, withWakeAt);
|
|
3787
|
+
return {
|
|
3788
|
+
wakeAt
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
if (this.clock() < existing.wakeAt) return {
|
|
3792
|
+
wakeAt: existing.wakeAt
|
|
3793
|
+
};
|
|
3794
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, existing.attempts + 1, replay);
|
|
3795
|
+
return {};
|
|
3796
|
+
}
|
|
3797
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, 1, replay);
|
|
3798
|
+
return {};
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Dispatch (or re-dispatch, on a durable retry) one compensation's undo handler — mirrors
|
|
3802
|
+
* `callRemote`'s dispatch block (persist the `pending` checkpoint BEFORE handing off to the
|
|
3803
|
+
* transport, so a fast result always finds it to complete). Two DIFFERENT names are in play: the
|
|
3804
|
+
* checkpoint's `name` is `compensate:<originalStepName>` (what makes this undo show up on the run's
|
|
3805
|
+
* timeline, and what `completeRemoteResult` emits `step.completed`/`step.failed` under); the WIRE
|
|
3806
|
+
* task's `name` is the undo handler's OWN routing name (`def.name`) — a worker dispatches to the
|
|
3807
|
+
* `@Step` it registered, not to this saga-visibility label.
|
|
3808
|
+
*/
|
|
3809
|
+
async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
|
|
3810
|
+
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
3811
|
+
const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
|
|
3812
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3813
|
+
const cp = stepCheckpoint({
|
|
3814
|
+
runId: run.id,
|
|
3815
|
+
seq,
|
|
3816
|
+
name,
|
|
3817
|
+
kind: "remote",
|
|
3818
|
+
status: "pending",
|
|
3819
|
+
input: args,
|
|
3820
|
+
attempts: attempt,
|
|
3821
|
+
workerGroup: group,
|
|
3822
|
+
enqueuedAt,
|
|
3823
|
+
startedAt: enqueuedAt,
|
|
3824
|
+
finishedAt: enqueuedAt
|
|
3825
|
+
});
|
|
3826
|
+
await this.store.saveCheckpoint(cp);
|
|
3827
|
+
replay?.set(seq, cp);
|
|
3828
|
+
await this.pool.dispatch({
|
|
3829
|
+
runId: run.id,
|
|
3830
|
+
seq,
|
|
3831
|
+
name: def.name,
|
|
3832
|
+
stepId: cp.stepId,
|
|
3833
|
+
group,
|
|
3834
|
+
input: args,
|
|
3835
|
+
traceparent: this.traceparent?.(),
|
|
3836
|
+
context: this.context?.(),
|
|
3837
|
+
attempt
|
|
3838
|
+
});
|
|
3839
|
+
this.emit({
|
|
3840
|
+
type: "step.started",
|
|
3841
|
+
runId: run.id,
|
|
3842
|
+
seq,
|
|
3843
|
+
name,
|
|
3844
|
+
kind: "remote"
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3528
3847
|
/** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
|
|
3529
3848
|
* (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
|
|
3530
3849
|
* module and the engine stays the orchestrator. */
|
|
@@ -3550,6 +3869,12 @@ var WorkflowEngine = class {
|
|
|
3550
3869
|
namespace: parentNamespace
|
|
3551
3870
|
}).catch(() => void 0));
|
|
3552
3871
|
}, "startChild"),
|
|
3872
|
+
// Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
|
|
3873
|
+
// idempotent on a terminal/cancelled run (returns its existing status without side effects), so
|
|
3874
|
+
// no extra guard is needed here for the failFast replay case (re-issuing the same cancel calls).
|
|
3875
|
+
cancelChild: /* @__PURE__ */ __name((childId) => {
|
|
3876
|
+
queueMicrotask(() => void this.cancel(childId).catch(() => void 0));
|
|
3877
|
+
}, "cancelChild"),
|
|
3553
3878
|
// Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
|
|
3554
3879
|
upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
|
|
3555
3880
|
const run = await this.store.getRun(runId);
|
|
@@ -4672,6 +4997,7 @@ export {
|
|
|
4672
4997
|
DURABLE_OPTIONS_CANONICAL,
|
|
4673
4998
|
DURABLE_STEP_CONFIG,
|
|
4674
4999
|
DURABLE_STEP_NAME,
|
|
5000
|
+
DurableWorkflow,
|
|
4675
5001
|
Entities,
|
|
4676
5002
|
EventAccumulators,
|
|
4677
5003
|
FatalError,
|
|
@@ -4698,9 +5024,11 @@ export {
|
|
|
4698
5024
|
applyAttributeQuery,
|
|
4699
5025
|
attributeColumnFor,
|
|
4700
5026
|
attributeOperand,
|
|
5027
|
+
bindWorkflowClass,
|
|
4701
5028
|
breakpointToken,
|
|
4702
5029
|
collectMetrics,
|
|
4703
5030
|
createStepLogger,
|
|
5031
|
+
currentWorkflowCtx,
|
|
4704
5032
|
eventMatchOf,
|
|
4705
5033
|
eventMatches,
|
|
4706
5034
|
eventPrefix,
|
|
@@ -4709,6 +5037,7 @@ export {
|
|
|
4709
5037
|
normalizeAttributeRows,
|
|
4710
5038
|
parseDuration,
|
|
4711
5039
|
prevCronFireMs,
|
|
5040
|
+
runInWorkflowCtx,
|
|
4712
5041
|
runSchedules,
|
|
4713
5042
|
runStepHandler,
|
|
4714
5043
|
sanitizeQueueToken,
|