@fluidgrids/workflows 2026.1028.2 → 2026.1030.1
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/README.md +181 -11
- package/dist/{activation-BBh1p82z.d.cts → activation-DqUfpHzk.d.cts} +1 -1
- package/dist/{activation-DnhG7SmF.d.ts → activation-xpx1oWuo.d.ts} +1 -1
- package/dist/{chunk-VRHHBPT6.js → chunk-2D3MYIIF.js} +1212 -128
- package/dist/chunk-3U2NU7YC.cjs +124 -0
- package/dist/{chunk-NEIUQASE.cjs → chunk-4QKB4ARC.cjs} +3 -3
- package/dist/{chunk-GBQ33SIZ.cjs → chunk-54FLQNUM.cjs} +12 -1
- package/dist/chunk-5HIVIV53.js +330 -0
- package/dist/chunk-5MFVJM76.js +1222 -0
- package/dist/{chunk-JD2VQIDZ.cjs → chunk-672KMMBK.cjs} +2 -2
- package/dist/{chunk-C5OLWZ42.js → chunk-6A2FGSUS.js} +22 -14
- package/dist/{chunk-XKJA7KX3.js → chunk-6OLN6BMB.js} +52 -3
- package/dist/{chunk-6FSHT5IL.js → chunk-7EZIANC7.js} +1 -1
- package/dist/{chunk-2J6TF7YI.cjs → chunk-A5SXCA62.cjs} +10 -3
- package/dist/{chunk-BO6OVILK.js → chunk-AC6ZEXVR.js} +10 -4
- package/dist/{chunk-ABFZWYHT.js → chunk-AE5Q6ZAJ.js} +315 -50
- package/dist/chunk-C4F5KJTI.js +293 -0
- package/dist/{chunk-IISBRUWJ.js → chunk-G5H32GIG.js} +1 -1
- package/dist/chunk-GEQDGUGB.cjs +2719 -0
- package/dist/{chunk-XHXIDVEC.js → chunk-HCM7ZC7Q.js} +1 -1
- package/dist/chunk-HMZUGSMU.cjs +1554 -0
- package/dist/{chunk-SAOBPYYJ.cjs → chunk-IQPXHQQQ.cjs} +8 -4
- package/dist/{chunk-P6TA5EZT.cjs → chunk-ISOIA7NV.cjs} +335 -58
- package/dist/chunk-JSC75O5C.cjs +167 -0
- package/dist/chunk-M272AHEX.js +25 -0
- package/dist/{chunk-BD7ZPG53.js → chunk-NWQ5WRLE.js} +10 -2
- package/dist/chunk-ODZ2IAR2.cjs +1247 -0
- package/dist/chunk-Q4MWQNJK.cjs +28 -0
- package/dist/{chunk-HWJZVOG7.js → chunk-SR7TR5KX.js} +6 -2
- package/dist/{chunk-FEW3JBYA.cjs → chunk-TCTE7DX5.cjs} +3 -3
- package/dist/{chunk-623FVQSN.cjs → chunk-TYF56RGB.cjs} +123 -0
- package/dist/chunk-VA4OFR72.cjs +324 -0
- package/dist/chunk-VU5S5A7V.js +150 -0
- package/dist/chunk-YEHR4NNE.js +1522 -0
- package/dist/{chunk-FMSW2HDF.cjs → chunk-ZCGKGDCL.cjs} +23 -15
- package/dist/codegen/index.d.cts +2 -2
- package/dist/codegen/index.d.ts +2 -2
- package/dist/collect-6NIEufHd.d.ts +103 -0
- package/dist/collect-CcTCKqx6.d.cts +103 -0
- package/dist/{file-BOoHZgoQ.d.cts → file-BxYpwjRQ.d.cts} +1007 -21
- package/dist/{file-yuPCFU5b.d.ts → file-CFdPQJI9.d.ts} +1007 -21
- package/dist/history/index.cjs +375 -86
- package/dist/history/index.d.cts +590 -12
- package/dist/history/index.d.ts +590 -12
- package/dist/history/index.js +50 -4
- package/dist/index.cjs +156 -50
- package/dist/index.d.cts +38 -58
- package/dist/index.d.ts +38 -58
- package/dist/index.js +6 -4
- package/dist/interceptors-CNcr2dlk.d.ts +159 -0
- package/dist/interceptors-CqkNZVfI.d.cts +159 -0
- package/dist/ir/index.cjs +362 -38
- package/dist/ir/index.d.cts +282 -7
- package/dist/ir/index.d.ts +282 -7
- package/dist/ir/index.js +329 -23
- package/dist/{lower-BzmUz0sq.d.cts → lower-BtRfIW2A.d.cts} +56 -8
- package/dist/{lower-_-iT4HYs.d.ts → lower-CpsMFOFX.d.ts} +56 -8
- package/dist/{manifest-C8N6HDYl.d.ts → manifest-D-I_twys.d.ts} +1 -1
- package/dist/{manifest-0OLw0i7k.d.cts → manifest-DahBQVM8.d.cts} +1 -1
- package/dist/node-evaluator-DGQDPSPI.cjs +24 -0
- package/dist/node-evaluator-LEEVD7H7.js +14 -0
- package/dist/payload-fields-BVRQn2yH.d.cts +122 -0
- package/dist/payload-fields-oOBWxDiG.d.ts +122 -0
- package/dist/quickjs/index.cjs +28 -27
- package/dist/quickjs/index.d.cts +6 -6
- package/dist/quickjs/index.d.ts +6 -6
- package/dist/quickjs/index.js +10 -9
- package/dist/roundtrip/index.cjs +259 -51
- package/dist/roundtrip/index.d.cts +100 -9
- package/dist/roundtrip/index.d.ts +100 -9
- package/dist/roundtrip/index.js +177 -11
- package/dist/runtime/index.cjs +21 -20
- package/dist/runtime/index.d.cts +184 -15
- package/dist/runtime/index.d.ts +184 -15
- package/dist/runtime/index.js +8 -7
- package/dist/testing/index.cjs +1122 -115
- package/dist/testing/index.d.cts +294 -13
- package/dist/testing/index.d.ts +294 -13
- package/dist/testing/index.js +1086 -84
- package/dist/{types-DV-HX_Gt.d.ts → types-Bv2i4cBX.d.cts} +244 -33
- package/dist/{types-CpBBjaGX.d.cts → types-DGx-b9eq.d.ts} +244 -33
- package/dist/{types-BGoPzgcl.d.ts → types-V7jEEUaL.d.cts} +20 -1
- package/dist/{types-BGoPzgcl.d.cts → types-V7jEEUaL.d.ts} +20 -1
- package/dist/{workflow-Bpr3vnA_.d.ts → workflow-B18OeF7g.d.ts} +2 -2
- package/dist/workflow-BGnv8SS6.d.ts +803 -0
- package/dist/{workflow-FXUkpIBc.d.cts → workflow-BJPFv3cp.d.cts} +2 -2
- package/dist/workflow-DS32Gtws.d.cts +803 -0
- package/fixtures/histories/activity-options-updated.history.json +159 -0
- package/fixtures/histories/activity-paused-unpaused.history.json +160 -0
- package/fixtures/histories/auto-upgrade-patched.history.json +276 -0
- package/fixtures/histories/benign-failure.history.json +146 -0
- package/fixtures/histories/cancel-activity-try-cancel.history.json +150 -0
- package/fixtures/histories/child-cancel-wait-requested.history.json +217 -0
- package/fixtures/histories/child-options-roundtrip.history.json +192 -0
- package/fixtures/histories/codec-opaque.history.json +229 -0
- package/fixtures/histories/continue-as-new-last-result.history.json +86 -0
- package/fixtures/histories/continue-as-new-options.history.json +99 -0
- package/fixtures/histories/continue-as-new-r1-long-task-queue.history.json +78 -0
- package/fixtures/histories/continue-as-new-r1-retry-options.history.json +83 -0
- package/fixtures/histories/dynamic-workflow.history.json +150 -0
- package/fixtures/histories/execution-timeout-chain.history.json +98 -0
- package/fixtures/histories/external-cancel-resolved.history.json +123 -0
- package/fixtures/histories/external-cancel-unknown-target.history.json +127 -0
- package/fixtures/histories/failure-error-types-fails-run.history.json +144 -0
- package/fixtures/histories/handler-drain-warn.history.json +222 -0
- package/fixtures/histories/heartbeat-details-on-retry.history.json +145 -0
- package/fixtures/histories/interceptor-headers.history.json +339 -0
- package/fixtures/histories/local-activity-abandoned-at-completion.history.json +73 -0
- package/fixtures/histories/local-activity-cancelled-in-scope.history.json +118 -0
- package/fixtures/histories/local-activity-completed.history.json +157 -0
- package/fixtures/histories/local-activity-failed.history.json +93 -0
- package/fixtures/histories/local-activity-retry-in-task.history.json +88 -0
- package/fixtures/histories/query-custom.history.json +213 -0
- package/fixtures/histories/query-default-handler.history.json +120 -0
- package/fixtures/histories/reset-forked-reapplied-signals.history.json +239 -0
- package/fixtures/histories/reset-reapplied-update-admitted.history.json +228 -0
- package/fixtures/histories/run-retry-last-failure.history.json +90 -0
- package/fixtures/histories/run-timeout.history.json +159 -0
- package/fixtures/histories/schedule-to-start-timeout.history.json +132 -0
- package/fixtures/histories/search-attributes-upserted.history.json +177 -0
- package/fixtures/histories/start-delay-signal-ends-delay.history.json +155 -0
- package/fixtures/histories/typeerror-task-retry-100.history.json +137 -0
- package/fixtures/histories/update-accepted-completed.history.json +45 -30
- package/fixtures/histories/update-default-handler.history.json +246 -0
- package/fixtures/histories/update-handler-awaits-activity.history.json +196 -0
- package/fixtures/histories/update-not-handled.history.json +138 -0
- package/fixtures/histories/update-rejected-no-event.history.json +190 -0
- package/fixtures/histories/update-with-start-admitted.history.json +150 -0
- package/fixtures/histories/user-activity-cancel.history.json +149 -0
- package/fixtures/histories/user-activity-retry.history.json +143 -0
- package/fixtures/histories/user-activity.history.json +149 -0
- package/package.json +1 -1
- package/schema/bridge-v2.schema.json +1062 -0
- package/schema/history-file.schema.json +538 -83
- package/schema/ir-v2.schema.json +290 -0
- package/schema/payload-fields.json +162 -0
- package/schema/run-event.schema.json +503 -82
- package/schema/workflow-command.schema.json +372 -69
- package/schema/workflow-task.schema.json +924 -171
- package/dist/assemble-BLqQ8lSu.d.cts +0 -39
- package/dist/assemble-DWgaXHd-.d.ts +0 -39
- package/dist/chunk-DHTLALBL.cjs +0 -80
- package/dist/chunk-DJYMDLV5.js +0 -227
- package/dist/chunk-I5LR3D2K.js +0 -869
- package/dist/chunk-M4BBAERY.cjs +0 -1635
- package/dist/chunk-N23SKIG4.js +0 -73
- package/dist/chunk-PZCVF76Q.cjs +0 -868
- package/dist/chunk-QQ6ULXHI.cjs +0 -885
- package/dist/chunk-UK3WM7YZ.cjs +0 -75
- package/dist/chunk-VOKMAAJP.js +0 -848
- package/dist/node-evaluator-4MSDKWOG.cjs +0 -22
- package/dist/node-evaluator-SX4LEYQC.js +0 -12
- package/dist/workflow-C2a05WUJ.d.ts +0 -444
- package/dist/workflow-J-yYixny.d.cts +0 -444
package/dist/testing/index.js
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import { failedTaskResult } from '../chunk-
|
|
2
|
-
import { isTier1Workflow, toIR } from '../chunk-
|
|
3
|
-
import { irHash, CODE_WORKFLOW_NODE_TYPE, normalizeDefinition } from '../chunk-
|
|
1
|
+
import { failedTaskResult } from '../chunk-G5H32GIG.js';
|
|
2
|
+
import { isTier1Workflow, toIR } from '../chunk-SR7TR5KX.js';
|
|
3
|
+
import { irHash, CODE_WORKFLOW_NODE_TYPE, normalizeDefinition } from '../chunk-NWQ5WRLE.js';
|
|
4
4
|
import '../chunk-5LUIDMF4.js';
|
|
5
5
|
import '../chunk-JZ4TOOLN.js';
|
|
6
6
|
import '../chunk-E2VK7J2C.js';
|
|
7
7
|
import '../chunk-7AAZGVKD.js';
|
|
8
|
-
import { createHistoryFile, validateCommands, parseHistoryFile } from '../chunk-
|
|
8
|
+
import { createHistoryFile, validateCommands, parseHistoryFile } from '../chunk-YEHR4NNE.js';
|
|
9
|
+
import '../chunk-VU5S5A7V.js';
|
|
9
10
|
import { isRecord } from '../chunk-PMCM2OH4.js';
|
|
10
|
-
import { buildWorkflowRegistry, createWorkflowRuntime } from '../chunk-
|
|
11
|
-
import { failureToError, signalName,
|
|
12
|
-
import '../chunk-
|
|
11
|
+
import { buildWorkflowRegistry, createWorkflowRuntime } from '../chunk-2D3MYIIF.js';
|
|
12
|
+
import { failureToError, signalName, ApplicationError, errorToFailure, CancelledFailure, DeterminismViolation } from '../chunk-6A2FGSUS.js';
|
|
13
|
+
import '../chunk-M272AHEX.js';
|
|
13
14
|
import { TASK_PRELUDE } from '../chunk-4LYSEANA.js';
|
|
14
|
-
import { isDurableWorkflow } from '../chunk-
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import '../chunk-
|
|
15
|
+
import { isActivityImplementation, isActivityDefinition, ACTIVITY_IMPL_MARKER, ACTIVITY_MARKER, runRetryPolicyOf, toMs, findActivityImplementation, executeActivity, isDurableWorkflow } from '../chunk-C4F5KJTI.js';
|
|
16
|
+
import '../chunk-LNUQE6D6.js';
|
|
17
|
+
import { COMMAND_FEATURES, codeFeaturesEnabledBy, applyEvent, projectRunState, isActivityOpen, hasUnprocessedInput, initialRunState, eventsForCommands, continueAsNewRetryShape, isChildOpen } from '../chunk-AE5Q6ZAJ.js';
|
|
18
|
+
import '../chunk-AC6ZEXVR.js';
|
|
19
|
+
import { RUNTIME_RELEASE, HISTORY_PROTOCOL, isCommandEvent, TERMINAL_EVENTS, isInputEvent, TERMINAL_COMMANDS, computeRetryDelay, computeRunRetryDelay } from '../chunk-5HIVIV53.js';
|
|
18
20
|
import { createContext, Script } from 'vm';
|
|
19
21
|
|
|
20
22
|
// src/testing/host.ts
|
|
21
23
|
function drainJobs() {
|
|
22
24
|
return new Promise((resolve) => setImmediate(resolve));
|
|
23
25
|
}
|
|
24
|
-
function registryOf(workflows) {
|
|
25
|
-
return buildWorkflowRegistry([
|
|
26
|
+
function registryOf(workflows, interceptors) {
|
|
27
|
+
return buildWorkflowRegistry([
|
|
28
|
+
["in-process", { ...workflows, ...interceptors !== void 0 ? { interceptors } : {} }]
|
|
29
|
+
]);
|
|
26
30
|
}
|
|
27
31
|
async function hostLoop(abi, requestJson) {
|
|
28
32
|
abi.start(requestJson);
|
|
@@ -38,6 +42,58 @@ async function runWorkflowTaskInProcess(registry, request) {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
// src/testing/environment.ts
|
|
45
|
+
var QueryRejectedError = class extends Error {
|
|
46
|
+
constructor(runId, queryName, message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.runId = runId;
|
|
49
|
+
this.queryName = queryName;
|
|
50
|
+
this.name = "QueryRejectedError";
|
|
51
|
+
}
|
|
52
|
+
runId;
|
|
53
|
+
queryName;
|
|
54
|
+
};
|
|
55
|
+
var QueryFailedError = class extends Error {
|
|
56
|
+
constructor(runId, queryName, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.runId = runId;
|
|
59
|
+
this.queryName = queryName;
|
|
60
|
+
this.name = "QueryFailedError";
|
|
61
|
+
}
|
|
62
|
+
runId;
|
|
63
|
+
queryName;
|
|
64
|
+
};
|
|
65
|
+
var UpdateRefusedError = class extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "UpdateRefusedError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var WorkflowAlreadyStartedError = class extends Error {
|
|
72
|
+
constructor(workflowKey, workflowId, existingRunId) {
|
|
73
|
+
super(
|
|
74
|
+
`Workflow ${workflowKey} already has a run with workflow id "${workflowId}" (${existingRunId})`
|
|
75
|
+
);
|
|
76
|
+
this.workflowKey = workflowKey;
|
|
77
|
+
this.workflowId = workflowId;
|
|
78
|
+
this.existingRunId = existingRunId;
|
|
79
|
+
this.name = "WorkflowAlreadyStartedError";
|
|
80
|
+
}
|
|
81
|
+
workflowKey;
|
|
82
|
+
workflowId;
|
|
83
|
+
existingRunId;
|
|
84
|
+
};
|
|
85
|
+
var UnfinishedHandlersError = class extends Error {
|
|
86
|
+
constructor(runId, handlers) {
|
|
87
|
+
super(
|
|
88
|
+
`Run ${runId} finished while handlers were still running: ${handlers.map((handler) => `${handler.kind} ${handler.name} x${handler.count}`).join(", ")} (await ctx.allHandlersFinished(), or set unfinishedPolicy: 'ABANDON')`
|
|
89
|
+
);
|
|
90
|
+
this.runId = runId;
|
|
91
|
+
this.handlers = handlers;
|
|
92
|
+
this.name = "UnfinishedHandlersError";
|
|
93
|
+
}
|
|
94
|
+
runId;
|
|
95
|
+
handlers;
|
|
96
|
+
};
|
|
41
97
|
var WorkflowFailedError = class extends Error {
|
|
42
98
|
constructor(runId, failure) {
|
|
43
99
|
super(`Run ${runId} failed: ${failure.message}`);
|
|
@@ -69,7 +125,7 @@ var RunBlockedError = class extends Error {
|
|
|
69
125
|
}
|
|
70
126
|
runId;
|
|
71
127
|
};
|
|
72
|
-
var TestRunHandle = class {
|
|
128
|
+
var TestRunHandle = class _TestRunHandle {
|
|
73
129
|
constructor(env, runId) {
|
|
74
130
|
this.env = env;
|
|
75
131
|
this.runId = runId;
|
|
@@ -99,6 +155,22 @@ var TestRunHandle = class {
|
|
|
99
155
|
this.env.terminate(this.runId, reason);
|
|
100
156
|
await this.env.runUntilIdle(false);
|
|
101
157
|
}
|
|
158
|
+
/** Send an update and run until idle (Phase C). */
|
|
159
|
+
update(update, args, options = {}) {
|
|
160
|
+
return this.env.update(this.runId, update, args, options);
|
|
161
|
+
}
|
|
162
|
+
/** Answer a query from the run's history (Phase C custom queries and the built-ins). */
|
|
163
|
+
query(query, args) {
|
|
164
|
+
return this.env.query(this.runId, query, args);
|
|
165
|
+
}
|
|
166
|
+
/** Reset the run (11 §2.7): a new run forked at a completed task; this one is terminated. */
|
|
167
|
+
async reset(options = {}) {
|
|
168
|
+
return new _TestRunHandle(this.env, await this.env.reset(this.runId, options));
|
|
169
|
+
}
|
|
170
|
+
/** Sink calls the run's tasks made (Phase C). */
|
|
171
|
+
sinkCalls() {
|
|
172
|
+
return this.env.sinkCallsOf(this.runId);
|
|
173
|
+
}
|
|
102
174
|
/** Run until this run (or the last run of its continue-as-new chain) ends; its output. */
|
|
103
175
|
async result() {
|
|
104
176
|
return this.env.resultOf(this.runId);
|
|
@@ -113,6 +185,23 @@ function toDate(value) {
|
|
|
113
185
|
if (value instanceof Date) return value.getTime();
|
|
114
186
|
return typeof value === "number" ? value : Date.parse(value);
|
|
115
187
|
}
|
|
188
|
+
function optionalDuration(value, what) {
|
|
189
|
+
return value === void 0 ? void 0 : toMs(value, what);
|
|
190
|
+
}
|
|
191
|
+
var FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
192
|
+
"FAILED",
|
|
193
|
+
"CANCELLED",
|
|
194
|
+
"TERMINATED",
|
|
195
|
+
"TIMED_OUT"
|
|
196
|
+
]);
|
|
197
|
+
function admittedPayload(update) {
|
|
198
|
+
return {
|
|
199
|
+
updateId: update.updateId,
|
|
200
|
+
name: update.name,
|
|
201
|
+
args: update.args,
|
|
202
|
+
...update.headers !== void 0 ? { headers: { ...update.headers } } : {}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
116
205
|
function toJsonValue(value) {
|
|
117
206
|
if (value === void 0) return null;
|
|
118
207
|
const text = JSON.stringify(value);
|
|
@@ -122,6 +211,12 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
122
211
|
clock;
|
|
123
212
|
timeSkipping;
|
|
124
213
|
enabled;
|
|
214
|
+
/** The bundle features the gate's flags enable, sent with every task (`request.features`). */
|
|
215
|
+
codeFeatures;
|
|
216
|
+
failOnUnfinishedHandlers;
|
|
217
|
+
historyRelease;
|
|
218
|
+
dues = [];
|
|
219
|
+
held = /* @__PURE__ */ new Set();
|
|
125
220
|
workspaceId;
|
|
126
221
|
runs = /* @__PURE__ */ new Map();
|
|
127
222
|
runOrder = [];
|
|
@@ -133,17 +228,65 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
133
228
|
timers = [];
|
|
134
229
|
mockedChildren = [];
|
|
135
230
|
runCounter = 0;
|
|
231
|
+
updateCounter = 0;
|
|
136
232
|
executor;
|
|
137
233
|
workflowKeys;
|
|
234
|
+
interceptors;
|
|
235
|
+
activityImpls;
|
|
236
|
+
activityInterceptors;
|
|
237
|
+
connections;
|
|
238
|
+
sinks;
|
|
239
|
+
/** Attempts that handed their completion to an outside system (`completeAsync`), by token. */
|
|
240
|
+
asyncAttempts = /* @__PURE__ */ new Map();
|
|
138
241
|
constructor(options) {
|
|
139
242
|
this.clock = toDate(options.startTime);
|
|
140
243
|
this.timeSkipping = options.timeSkipping !== false;
|
|
141
|
-
this.enabled = options.enabled ??
|
|
244
|
+
this.enabled = options.enabled ?? new Set(COMMAND_FEATURES);
|
|
245
|
+
this.codeFeatures = codeFeaturesEnabledBy(this.enabled);
|
|
246
|
+
this.failOnUnfinishedHandlers = options.failOnUnfinishedHandlers !== false;
|
|
247
|
+
this.historyRelease = options.historyRelease ?? RUNTIME_RELEASE;
|
|
142
248
|
this.workspaceId = options.workspaceId ?? "test-workspace";
|
|
143
249
|
this.workflows = { ...options.workflows ?? {} };
|
|
144
|
-
this.
|
|
250
|
+
this.interceptors = options.interceptors;
|
|
251
|
+
this.registry = registryOf(this.workflows, this.interceptors);
|
|
145
252
|
this.executor = options.taskExecutor;
|
|
146
253
|
this.workflowKeys = { ...options.workflowKeys ?? {} };
|
|
254
|
+
this.activityImpls = [...options.activities ?? []];
|
|
255
|
+
for (const impl of this.activityImpls)
|
|
256
|
+
if (!isActivityImplementation(impl))
|
|
257
|
+
throw new TypeError("activities must be implementActivity() or defineActivity() values");
|
|
258
|
+
this.activityInterceptors = [...options.activityInterceptors ?? []];
|
|
259
|
+
this.connections = { ...options.connections ?? {} };
|
|
260
|
+
this.sinks = { ...options.sinks ?? {} };
|
|
261
|
+
}
|
|
262
|
+
/** Register a user-activity implementation (Phase C). A later one replaces an earlier of the same name. */
|
|
263
|
+
registerActivity(implementation) {
|
|
264
|
+
if (!isActivityImplementation(implementation))
|
|
265
|
+
throw new TypeError(
|
|
266
|
+
"registerActivity needs an implementActivity() or defineActivity() value"
|
|
267
|
+
);
|
|
268
|
+
const index = this.activityImpls.findIndex(
|
|
269
|
+
(impl) => impl.name === implementation.name && impl.dynamic === implementation.dynamic
|
|
270
|
+
);
|
|
271
|
+
if (index >= 0) this.activityImpls.splice(index, 1);
|
|
272
|
+
this.activityImpls.push(implementation);
|
|
273
|
+
}
|
|
274
|
+
/** Mock a user activity by name or declaration (Phase C). */
|
|
275
|
+
mockActivity(activity, fn) {
|
|
276
|
+
const name = typeof activity === "string" ? activity : activity.name;
|
|
277
|
+
if (typeof activity !== "string" && !isActivityDefinition(activity))
|
|
278
|
+
throw new TypeError("mockActivity needs an activity name or a declareActivity() value");
|
|
279
|
+
this.registerActivity(
|
|
280
|
+
Object.freeze({
|
|
281
|
+
__fluidgrids: ACTIVITY_MARKER,
|
|
282
|
+
name,
|
|
283
|
+
options: typeof activity === "string" ? {} : activity.options,
|
|
284
|
+
implementation: ACTIVITY_IMPL_MARKER,
|
|
285
|
+
// The mock receives the activity's JSON args, typed by the declaration.
|
|
286
|
+
fn: (ctx, args) => fn(args, ctx),
|
|
287
|
+
dynamic: false
|
|
288
|
+
})
|
|
289
|
+
);
|
|
147
290
|
}
|
|
148
291
|
/** Create an environment. */
|
|
149
292
|
static async create(options = {}) {
|
|
@@ -162,6 +305,26 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
162
305
|
impl
|
|
163
306
|
);
|
|
164
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Hold the activity queue of a node type (every node type when omitted):
|
|
310
|
+
* attempts wait instead of starting, and a `scheduleToStartTimeout` expires
|
|
311
|
+
* (`ACTIVITY_TIMED_OUT { timeoutType: SCHEDULE_TO_START }`, not retried).
|
|
312
|
+
*/
|
|
313
|
+
holdActivities(nodeType = "*") {
|
|
314
|
+
this.held.add(nodeType);
|
|
315
|
+
}
|
|
316
|
+
/** Release held activity queues (all when no node type is given). */
|
|
317
|
+
releaseActivities(nodeType) {
|
|
318
|
+
if (nodeType === void 0) this.held.clear();
|
|
319
|
+
else this.held.delete(nodeType);
|
|
320
|
+
}
|
|
321
|
+
/** The hold key of an attempt: its node type, or the task queue of a user activity. */
|
|
322
|
+
holdKeyOf(entry) {
|
|
323
|
+
return entry.kind === "user" ? entry.taskQueue ?? "" : entry.nodeType;
|
|
324
|
+
}
|
|
325
|
+
isHeld(nodeType) {
|
|
326
|
+
return this.held.has("*") || this.held.has(nodeType);
|
|
327
|
+
}
|
|
165
328
|
/** Mock a child workflow started by key (graph or durable). */
|
|
166
329
|
mockChild(workflowKey, impl) {
|
|
167
330
|
this.childMocks.set(workflowKey, impl);
|
|
@@ -172,17 +335,327 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
172
335
|
// A declared workflow is stored by its declaration; its input type is the caller's.
|
|
173
336
|
this.ensureRegistered(definition)
|
|
174
337
|
);
|
|
338
|
+
const declared = this.workflows[workflowFn]?.options ?? {};
|
|
339
|
+
const workflowKey = options.workflowKey ?? (typeof definition === "string" ? this.workflows[workflowFn]?.name ?? workflowFn : definition.name);
|
|
340
|
+
if (options.update !== void 0 && options.startDelay !== void 0)
|
|
341
|
+
throw new UpdateRefusedError(
|
|
342
|
+
"Update-with-start cannot be combined with a start delay (11 \xA72.6)"
|
|
343
|
+
);
|
|
344
|
+
if (options.workflowId !== void 0) {
|
|
345
|
+
const existing = this.useExisting(workflowKey, options);
|
|
346
|
+
if (existing !== void 0) {
|
|
347
|
+
for (const signal of options.signals ?? [])
|
|
348
|
+
this.deliverSignal(existing, signal.name, signal.payload);
|
|
349
|
+
if (options.update !== void 0)
|
|
350
|
+
this.addUpdate(this.run(existing), options.update, "PENDING");
|
|
351
|
+
await this.runUntilIdle(false);
|
|
352
|
+
return new TestRunHandle(this, existing);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const executionTimeoutMs = optionalDuration(
|
|
356
|
+
options.executionTimeout ?? declared.executionTimeout,
|
|
357
|
+
"executionTimeout"
|
|
358
|
+
);
|
|
359
|
+
const retry = options.retry ?? declared.retry;
|
|
360
|
+
const config = {
|
|
361
|
+
workflowKey,
|
|
362
|
+
runAttempt: 1,
|
|
363
|
+
...options.workflowId !== void 0 ? { businessId: options.workflowId } : {},
|
|
364
|
+
...executionTimeoutMs !== void 0 ? { executionTimeoutMs, executionDeadline: this.clock + executionTimeoutMs } : {},
|
|
365
|
+
...this.durationConfig(options.runTimeout ?? declared.runTimeout, "runTimeoutMs"),
|
|
366
|
+
...this.durationConfig(
|
|
367
|
+
options.workflowTaskTimeout ?? declared.workflowTaskTimeout,
|
|
368
|
+
"workflowTaskTimeoutMs"
|
|
369
|
+
),
|
|
370
|
+
...this.durationConfig(options.startDelay, "startDelayMs"),
|
|
371
|
+
...retry !== void 0 ? { retry: runRetryPolicyOf(retry) } : {},
|
|
372
|
+
...options.summary !== void 0 ? { summary: options.summary } : {},
|
|
373
|
+
...options.details !== void 0 ? { details: options.details } : {},
|
|
374
|
+
...options.memo !== void 0 ? { memo: toJsonValue(options.memo) } : {},
|
|
375
|
+
...(options.versioningBehavior ?? declared.versioningBehavior) !== void 0 ? { versioningBehavior: options.versioningBehavior ?? declared.versioningBehavior } : {},
|
|
376
|
+
...options.headers !== void 0 ? { headers: { ...options.headers } } : {},
|
|
377
|
+
...options.searchAttributes !== void 0 ? { searchAttributes: toJsonValue(options.searchAttributes) } : {},
|
|
378
|
+
...options.fairnessKey !== void 0 ? { fairnessKey: options.fairnessKey } : {},
|
|
379
|
+
...options.fairnessWeight !== void 0 ? { fairnessWeight: options.fairnessWeight } : {}
|
|
380
|
+
};
|
|
175
381
|
const runId = this.createRun(
|
|
176
382
|
workflowFn,
|
|
177
383
|
toJsonValue(options.input),
|
|
178
384
|
{ ...options.info, ...options.runId !== void 0 ? { runId: options.runId } : {} },
|
|
179
|
-
void 0
|
|
385
|
+
void 0,
|
|
386
|
+
{},
|
|
387
|
+
config,
|
|
388
|
+
options.update !== void 0 ? [this.newUpdate(options.update, "ADMITTED")] : []
|
|
180
389
|
);
|
|
181
390
|
for (const signal of options.signals ?? [])
|
|
182
391
|
this.deliverSignal(runId, signal.name, signal.payload);
|
|
183
392
|
await this.runUntilIdle(false);
|
|
184
393
|
return new TestRunHandle(this, runId);
|
|
185
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Update-with-start (11 §2.6): start the run (or, with
|
|
397
|
+
* `idConflictPolicy: USE_EXISTING`, reach the open one) and deliver the
|
|
398
|
+
* update before any other task runs.
|
|
399
|
+
*/
|
|
400
|
+
async startWithUpdate(definition, options) {
|
|
401
|
+
const updateId = options.update.updateId ?? this.nextUpdateId();
|
|
402
|
+
const handle = await this.start(definition, {
|
|
403
|
+
...options,
|
|
404
|
+
update: { ...options.update, updateId }
|
|
405
|
+
});
|
|
406
|
+
return { handle, update: this.updateResultOf(handle.runId, updateId) };
|
|
407
|
+
}
|
|
408
|
+
nextUpdateId() {
|
|
409
|
+
this.updateCounter += 1;
|
|
410
|
+
return `u${this.updateCounter}`;
|
|
411
|
+
}
|
|
412
|
+
/** A new update row (not attached to a run yet). */
|
|
413
|
+
newUpdate(update, status) {
|
|
414
|
+
return {
|
|
415
|
+
updateId: update.updateId ?? this.nextUpdateId(),
|
|
416
|
+
name: update.name,
|
|
417
|
+
args: toJsonValue(update.args),
|
|
418
|
+
...update.headers !== void 0 ? { headers: { ...update.headers } } : {},
|
|
419
|
+
status
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** Record an update on a run: pending (delivered by the next task) or admitted into history. */
|
|
423
|
+
addUpdate(run, update, status) {
|
|
424
|
+
const existing = update.updateId !== void 0 ? run.updates.get(update.updateId) : void 0;
|
|
425
|
+
if (existing) return existing;
|
|
426
|
+
const entry = this.newUpdate(update, status);
|
|
427
|
+
run.updates.set(entry.updateId, entry);
|
|
428
|
+
if (status === "ADMITTED")
|
|
429
|
+
this.append(run, { type: "UPDATE_ADMITTED", payload: admittedPayload(entry) });
|
|
430
|
+
else {
|
|
431
|
+
if (run.delayedUntil !== void 0) this.endDelay(run);
|
|
432
|
+
else this.scheduleTask(run);
|
|
433
|
+
}
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
/** Schedule a workflow task for a run that has none (an update needs no input event). */
|
|
437
|
+
scheduleTask(run) {
|
|
438
|
+
if (run.state.status !== "RUNNING" || run.state.task.state !== "NONE") return;
|
|
439
|
+
if (run.delayedUntil !== void 0) return;
|
|
440
|
+
run.taskCounter += 1;
|
|
441
|
+
const taskId = `${run.runId}-task-${run.taskCounter}`;
|
|
442
|
+
const scheduled = {
|
|
443
|
+
seq: run.history.length + 1,
|
|
444
|
+
type: "WORKFLOW_TASK_SCHEDULED",
|
|
445
|
+
payload: { taskId },
|
|
446
|
+
createdAt: new Date(this.clock).toISOString()
|
|
447
|
+
};
|
|
448
|
+
applyEvent(run.state, scheduled);
|
|
449
|
+
run.history.push(scheduled);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Send an update to a run and run until idle (Phase C, 05 §6). The result
|
|
453
|
+
* is `REJECTED` (the validator refused it or no handler took it; nothing is
|
|
454
|
+
* written to history), `COMPLETED`, or `ACCEPTED` while its handler still
|
|
455
|
+
* waits (for a timer without time skipping, or with `waitFor: ACCEPTED`).
|
|
456
|
+
*/
|
|
457
|
+
async update(runId, update, args, options = {}) {
|
|
458
|
+
const run = this.run(runId);
|
|
459
|
+
const name = typeof update === "string" ? update : update.name;
|
|
460
|
+
if (options.updateId !== void 0 && run.updates.has(options.updateId))
|
|
461
|
+
return this.updateResultOf(runId, options.updateId);
|
|
462
|
+
if (run.state.status !== "RUNNING")
|
|
463
|
+
throw new UpdateRefusedError(`Run ${runId} is ${run.state.status}; it takes no updates`);
|
|
464
|
+
const entry = this.addUpdate(
|
|
465
|
+
run,
|
|
466
|
+
{
|
|
467
|
+
name,
|
|
468
|
+
args,
|
|
469
|
+
...options.updateId !== void 0 ? { updateId: options.updateId } : {},
|
|
470
|
+
...options.headers !== void 0 ? { headers: options.headers } : {}
|
|
471
|
+
},
|
|
472
|
+
"PENDING"
|
|
473
|
+
);
|
|
474
|
+
await this.runUntilIdle(false);
|
|
475
|
+
return this.updateResultOf(runId, entry.updateId);
|
|
476
|
+
}
|
|
477
|
+
/** The current result of an update. */
|
|
478
|
+
updateResultOf(runId, updateId) {
|
|
479
|
+
const entry = this.run(runId).updates.get(updateId);
|
|
480
|
+
if (!entry) throw new Error(`Run ${runId} has no update ${updateId}`);
|
|
481
|
+
return {
|
|
482
|
+
updateId,
|
|
483
|
+
status: entry.status === "ADMITTED" ? "PENDING" : entry.status,
|
|
484
|
+
...entry.result !== void 0 ? { result: entry.result } : {},
|
|
485
|
+
...entry.failure !== void 0 ? { failure: entry.failure } : {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/** Answer a query from the run's history (a replay-only `query` task). */
|
|
489
|
+
async query(runId, query, args) {
|
|
490
|
+
const run = this.run(runId);
|
|
491
|
+
const name = typeof query === "string" ? query : query.name;
|
|
492
|
+
const request = {
|
|
493
|
+
protocol: HISTORY_PROTOCOL,
|
|
494
|
+
kind: "query",
|
|
495
|
+
workflowFn: run.workflowFn,
|
|
496
|
+
info: run.info,
|
|
497
|
+
history: run.history,
|
|
498
|
+
nowMs: this.clock,
|
|
499
|
+
queries: [{ queryId: "q1", name, args: toJsonValue(args) }]
|
|
500
|
+
};
|
|
501
|
+
const result = await this.execute(request);
|
|
502
|
+
const answer = result.queryResults?.[0];
|
|
503
|
+
if (answer?.rejected === true)
|
|
504
|
+
throw new QueryRejectedError(runId, name, answer.error ?? `No handler for query ${name}`);
|
|
505
|
+
if (answer === void 0 || answer.error !== void 0)
|
|
506
|
+
throw new QueryFailedError(
|
|
507
|
+
runId,
|
|
508
|
+
name,
|
|
509
|
+
answer?.error ?? result.failure?.message ?? "The query was not answered"
|
|
510
|
+
);
|
|
511
|
+
return answer.value ?? null;
|
|
512
|
+
}
|
|
513
|
+
/** Sink calls the run's tasks made. */
|
|
514
|
+
sinkCallsOf(runId) {
|
|
515
|
+
return this.run(runId).sinkCalls.map((call) => ({ ...call, args: [...call.args] }));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Reset a run (11 §2.7): a new run whose history is the original's up to
|
|
519
|
+
* and including the chosen completed workflow task (with its commands),
|
|
520
|
+
* then `WORKFLOW_RESET`, then the signals (and, as `UPDATE_ADMITTED`, the
|
|
521
|
+
* accepted updates) received after that point. The original is terminated
|
|
522
|
+
* with reason `RESET`; children it started before the point follow the new
|
|
523
|
+
* run, the others get their parent-close policy. Returns the new run id.
|
|
524
|
+
*/
|
|
525
|
+
async reset(runId, options = {}) {
|
|
526
|
+
const original = this.run(runId);
|
|
527
|
+
const history = original.history;
|
|
528
|
+
const points = [];
|
|
529
|
+
for (const event of history) {
|
|
530
|
+
if (event.type !== "WORKFLOW_TASK_COMPLETED") continue;
|
|
531
|
+
const taskId = typeof event.payload["taskId"] === "string" ? event.payload["taskId"] : "";
|
|
532
|
+
let end = event.seq;
|
|
533
|
+
let closes = false;
|
|
534
|
+
while (end < history.length) {
|
|
535
|
+
const next = history[end];
|
|
536
|
+
if (!next || !isCommandEvent(next) || next.taskId !== taskId) break;
|
|
537
|
+
if (TERMINAL_EVENTS.has(next.type)) closes = true;
|
|
538
|
+
end += 1;
|
|
539
|
+
}
|
|
540
|
+
if (!closes) points.push({ seq: event.seq, cut: end });
|
|
541
|
+
}
|
|
542
|
+
const point = options.toSeq !== void 0 ? [...points].reverse().find((entry) => entry.seq <= (options.toSeq ?? 0)) : options.to === "FIRST_TASK" ? points[0] : points[points.length - 1];
|
|
543
|
+
if (!point) throw new Error(`Run ${runId} has no completed workflow task to reset to`);
|
|
544
|
+
const cut = point.cut;
|
|
545
|
+
const copied = history.slice(0, cut).map((event) => ({ ...event, payload: { ...event.payload } }));
|
|
546
|
+
const later = history.slice(cut);
|
|
547
|
+
const reapplySignals = options.reapplySignals !== false;
|
|
548
|
+
const reapplyUpdates = options.reapplyUpdates !== false;
|
|
549
|
+
const keptChildren = new Set(
|
|
550
|
+
copied.filter((event) => event.type === "CHILD_RUN_INITIATED").map((event) => String(event.payload["childId"]))
|
|
551
|
+
);
|
|
552
|
+
this.runCounter += 1;
|
|
553
|
+
const newRunId = `run-${this.runCounter}`;
|
|
554
|
+
if (original.state.status === "RUNNING") {
|
|
555
|
+
original.resetTo = newRunId;
|
|
556
|
+
this.append(original, { type: "WORKFLOW_TERMINATED", payload: { reason: "RESET" } });
|
|
557
|
+
this.onClosed(original, keptChildren);
|
|
558
|
+
} else original.resetTo = newRunId;
|
|
559
|
+
const fork = {
|
|
560
|
+
runId: newRunId,
|
|
561
|
+
workflowFn: original.workflowFn,
|
|
562
|
+
history: copied,
|
|
563
|
+
state: projectRunState(copied),
|
|
564
|
+
taskCounter: copied.filter((event) => event.type === "WORKFLOW_TASK_SCHEDULED").length,
|
|
565
|
+
info: { ...original.info, runId: newRunId },
|
|
566
|
+
children: new Map([...original.children].filter(([childId]) => keptChildren.has(childId))),
|
|
567
|
+
config: { ...original.config },
|
|
568
|
+
...original.parent ? { parent: original.parent } : {},
|
|
569
|
+
updates: /* @__PURE__ */ new Map(),
|
|
570
|
+
sinkCalls: []
|
|
571
|
+
};
|
|
572
|
+
this.runs.set(newRunId, fork);
|
|
573
|
+
this.runOrder.push(newRunId);
|
|
574
|
+
for (const [childId, childRunId] of fork.children) {
|
|
575
|
+
const child = childRunId !== void 0 ? this.runs.get(childRunId) : void 0;
|
|
576
|
+
if (child) child.parent = { runId: newRunId, childId };
|
|
577
|
+
}
|
|
578
|
+
this.append(fork, {
|
|
579
|
+
type: "WORKFLOW_RESET",
|
|
580
|
+
payload: {
|
|
581
|
+
fromRunId: runId,
|
|
582
|
+
toSeq: cut,
|
|
583
|
+
reapplySignals,
|
|
584
|
+
reapplyUpdates,
|
|
585
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
for (const [activityId, activity] of Object.entries(fork.state.activities)) {
|
|
589
|
+
if (!isActivityOpen(activity)) continue;
|
|
590
|
+
const scheduled = copied[activity.scheduledSeq - 1];
|
|
591
|
+
if (scheduled?.type !== "ACTIVITY_SCHEDULED") continue;
|
|
592
|
+
this.afterCommand(fork, {
|
|
593
|
+
// The run-event schema checked the payload; it is the command that scheduled it.
|
|
594
|
+
...scheduled.payload,
|
|
595
|
+
type: "SCHEDULE_ACTIVITY",
|
|
596
|
+
activityId
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
for (const [timerId, timer] of Object.entries(fork.state.timers))
|
|
600
|
+
if (timer.status === "PENDING")
|
|
601
|
+
this.timers.push({ runId: newRunId, timerId, fireAt: Date.parse(timer.fireAt) });
|
|
602
|
+
for (const event of later) {
|
|
603
|
+
if (event.type === "SIGNAL_RECEIVED" && reapplySignals)
|
|
604
|
+
this.append(fork, { type: "SIGNAL_RECEIVED", payload: { ...event.payload } });
|
|
605
|
+
if (reapplyUpdates && (event.type === "UPDATE_ACCEPTED" || event.type === "UPDATE_ADMITTED") && !fork.updates.has(String(event.payload["updateId"])) && fork.state.updates[String(event.payload["updateId"])] === void 0) {
|
|
606
|
+
const headers = event.payload["headers"];
|
|
607
|
+
this.addUpdate(
|
|
608
|
+
fork,
|
|
609
|
+
{
|
|
610
|
+
updateId: String(event.payload["updateId"]),
|
|
611
|
+
name: String(event.payload["name"]),
|
|
612
|
+
args: event.payload["args"] ?? null,
|
|
613
|
+
...typeof headers === "object" && headers !== null && !Array.isArray(headers) ? (
|
|
614
|
+
// The run-event schema checked the headers map.
|
|
615
|
+
{ headers }
|
|
616
|
+
) : {}
|
|
617
|
+
},
|
|
618
|
+
"ADMITTED"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (hasUnprocessedInput(fork.history)) this.scheduleTask(fork);
|
|
623
|
+
await this.runUntilIdle(false);
|
|
624
|
+
return newRunId;
|
|
625
|
+
}
|
|
626
|
+
durationConfig(value, key) {
|
|
627
|
+
const ms = optionalDuration(value, key);
|
|
628
|
+
if (ms === void 0) return {};
|
|
629
|
+
const out = {};
|
|
630
|
+
out[key] = ms;
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The workflow-id policies of a start (11 §2.1): the open run to use, or
|
|
635
|
+
* undefined to start a new run (after terminating the open one for
|
|
636
|
+
* `TERMINATE_EXISTING`). Throws {@link WorkflowAlreadyStartedError}.
|
|
637
|
+
*/
|
|
638
|
+
useExisting(workflowKey, options) {
|
|
639
|
+
const workflowId = options.workflowId ?? "";
|
|
640
|
+
const runs = this.runOrder.map((id) => this.run(id)).filter(
|
|
641
|
+
(run) => run.config.workflowKey === workflowKey && run.config.businessId === workflowId
|
|
642
|
+
);
|
|
643
|
+
const open = runs.find((run) => run.state.status === "RUNNING");
|
|
644
|
+
if (open) {
|
|
645
|
+
const conflict = options.idConflictPolicy ?? "FAIL";
|
|
646
|
+
if (conflict === "USE_EXISTING") return open.runId;
|
|
647
|
+
if (conflict === "FAIL")
|
|
648
|
+
throw new WorkflowAlreadyStartedError(workflowKey, workflowId, open.runId);
|
|
649
|
+
this.terminate(open.runId, "ID_CONFLICT");
|
|
650
|
+
return void 0;
|
|
651
|
+
}
|
|
652
|
+
const last = runs[runs.length - 1];
|
|
653
|
+
if (!last) return void 0;
|
|
654
|
+
const reuse = options.idReusePolicy ?? "ALLOW_DUPLICATE";
|
|
655
|
+
if (reuse === "REJECT_DUPLICATE" || reuse === "ALLOW_DUPLICATE_FAILED_ONLY" && !FAILED_STATUSES.has(last.state.status))
|
|
656
|
+
throw new WorkflowAlreadyStartedError(workflowKey, workflowId, last.runId);
|
|
657
|
+
return void 0;
|
|
658
|
+
}
|
|
186
659
|
/** Advance the clock by `duration`, firing timers and running work as it comes due. */
|
|
187
660
|
async sleep(duration) {
|
|
188
661
|
const until = this.clock + toMs(duration, "sleep duration");
|
|
@@ -225,7 +698,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
225
698
|
if (value === definition) return name2;
|
|
226
699
|
const name = Object.hasOwn(this.workflows, definition.name) ? `${definition.name}#${this.runCounter}` : definition.name;
|
|
227
700
|
this.workflows[name] = definition;
|
|
228
|
-
this.registry = registryOf(this.workflows);
|
|
701
|
+
this.registry = registryOf(this.workflows, this.interceptors);
|
|
229
702
|
return name;
|
|
230
703
|
}
|
|
231
704
|
run(runId) {
|
|
@@ -233,7 +706,7 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
233
706
|
if (!run) throw new Error(`Unknown run ${runId}`);
|
|
234
707
|
return run;
|
|
235
708
|
}
|
|
236
|
-
createRun(workflowFn, input, info, parent, extraStarted = {}) {
|
|
709
|
+
createRun(workflowFn, input, info, parent, extraStarted = {}, config = { workflowKey: workflowFn, runAttempt: 1 }, admitted = []) {
|
|
237
710
|
this.runCounter += 1;
|
|
238
711
|
const runId = info.runId ?? `run-${this.runCounter}`;
|
|
239
712
|
const snapshot = {
|
|
@@ -254,19 +727,80 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
254
727
|
taskCounter: 0,
|
|
255
728
|
info: snapshot,
|
|
256
729
|
children: /* @__PURE__ */ new Map(),
|
|
257
|
-
|
|
730
|
+
config,
|
|
731
|
+
...parent ? { parent } : {},
|
|
732
|
+
updates: /* @__PURE__ */ new Map(),
|
|
733
|
+
sinkCalls: []
|
|
258
734
|
};
|
|
735
|
+
const firstTaskDelay = config.startDelayMs ?? config.firstTaskDelayMs;
|
|
736
|
+
if (firstTaskDelay !== void 0 && firstTaskDelay > 0) {
|
|
737
|
+
run.delayedUntil = this.clock + firstTaskDelay;
|
|
738
|
+
this.dues.push({ runId, kind: "start", at: run.delayedUntil });
|
|
739
|
+
}
|
|
740
|
+
if (config.runTimeoutMs !== void 0)
|
|
741
|
+
this.dues.push({ runId, kind: "runTimeout", at: this.clock + config.runTimeoutMs });
|
|
742
|
+
if (config.executionDeadline !== void 0)
|
|
743
|
+
this.dues.push({ runId, kind: "executionTimeout", at: config.executionDeadline });
|
|
259
744
|
this.runs.set(runId, run);
|
|
260
745
|
this.runOrder.push(runId);
|
|
261
|
-
this.append(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
746
|
+
this.append(
|
|
747
|
+
run,
|
|
748
|
+
{
|
|
749
|
+
type: "WORKFLOW_STARTED",
|
|
750
|
+
// The service writes the seed of ctx.random()/ctx.uuid() into the start
|
|
751
|
+
// event so the history replays on its own; it uses the run id.
|
|
752
|
+
payload: {
|
|
753
|
+
workflowFn,
|
|
754
|
+
input,
|
|
755
|
+
taskQueue: config.taskQueue ?? "managed",
|
|
756
|
+
randomnessSeed: runId,
|
|
757
|
+
...extraStarted,
|
|
758
|
+
...this.startedFields(config)
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
admitted.length === 0
|
|
762
|
+
);
|
|
763
|
+
if (admitted.length > 0) {
|
|
764
|
+
for (const update of admitted) {
|
|
765
|
+
run.updates.set(update.updateId, update);
|
|
766
|
+
this.append(run, { type: "UPDATE_ADMITTED", payload: admittedPayload(update) }, false);
|
|
767
|
+
}
|
|
768
|
+
this.scheduleTask(run);
|
|
769
|
+
}
|
|
267
770
|
return runId;
|
|
268
771
|
}
|
|
269
|
-
|
|
772
|
+
/** The R2 fields of `WORKFLOW_STARTED`, each written only when it is set. */
|
|
773
|
+
startedFields(config) {
|
|
774
|
+
const out = {};
|
|
775
|
+
if (config.businessId !== void 0) {
|
|
776
|
+
out["businessId"] = config.businessId;
|
|
777
|
+
out["workflowId"] = config.businessId;
|
|
778
|
+
}
|
|
779
|
+
if (config.startDelayMs !== void 0) out["startDelayMs"] = config.startDelayMs;
|
|
780
|
+
if (config.retry !== void 0 || config.runAttempt > 1) out["runAttempt"] = config.runAttempt;
|
|
781
|
+
if (config.summary !== void 0) out["summary"] = config.summary;
|
|
782
|
+
if (config.details !== void 0) out["details"] = config.details;
|
|
783
|
+
if (config.memo !== void 0) out["context"] = config.memo;
|
|
784
|
+
const timeouts = {};
|
|
785
|
+
if (config.executionTimeoutMs !== void 0)
|
|
786
|
+
timeouts["executionTimeout"] = config.executionTimeoutMs;
|
|
787
|
+
if (config.runTimeoutMs !== void 0) timeouts["runTimeout"] = config.runTimeoutMs;
|
|
788
|
+
if (config.workflowTaskTimeoutMs !== void 0)
|
|
789
|
+
timeouts["workflowTaskTimeout"] = config.workflowTaskTimeoutMs;
|
|
790
|
+
if (Object.keys(timeouts).length > 0) out["timeouts"] = timeouts;
|
|
791
|
+
if (config.retry !== void 0) out["retry"] = toJsonValue(config.retry);
|
|
792
|
+
if (config.versioningBehavior !== void 0)
|
|
793
|
+
out["versioningBehavior"] = config.versioningBehavior;
|
|
794
|
+
if (config.lastCompletionResult !== void 0 && this.historyRelease >= 2)
|
|
795
|
+
out["lastCompletionResult"] = config.lastCompletionResult;
|
|
796
|
+
if (config.lastFailure !== void 0) out["lastFailure"] = toJsonValue(config.lastFailure);
|
|
797
|
+
if (config.headers !== void 0) out["headers"] = { ...config.headers };
|
|
798
|
+
if (config.searchAttributes !== void 0) out["searchAttributes"] = config.searchAttributes;
|
|
799
|
+
if (config.fairnessKey !== void 0) out["fairnessKey"] = config.fairnessKey;
|
|
800
|
+
if (config.fairnessWeight !== void 0) out["fairnessWeight"] = config.fairnessWeight;
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
append(run, draft, schedule = true) {
|
|
270
804
|
const event = {
|
|
271
805
|
seq: run.history.length + 1,
|
|
272
806
|
type: draft.type,
|
|
@@ -276,7 +810,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
276
810
|
};
|
|
277
811
|
applyEvent(run.state, event);
|
|
278
812
|
run.history.push(event);
|
|
279
|
-
if (
|
|
813
|
+
if (run.delayedUntil !== void 0 && event.type === "SIGNAL_RECEIVED") this.endDelay(run);
|
|
814
|
+
if (schedule && run.delayedUntil === void 0 && isInputEvent(event) && run.state.status === "RUNNING" && run.state.task.state === "NONE") {
|
|
280
815
|
run.taskCounter += 1;
|
|
281
816
|
const taskId = `${run.runId}-task-${run.taskCounter}`;
|
|
282
817
|
const scheduled = {
|
|
@@ -290,7 +825,13 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
290
825
|
}
|
|
291
826
|
return event;
|
|
292
827
|
}
|
|
293
|
-
|
|
828
|
+
/** End a start delay: the first workflow task is scheduled now. */
|
|
829
|
+
endDelay(run) {
|
|
830
|
+
run.delayedUntil = void 0;
|
|
831
|
+
this.dues = this.dues.filter((due) => !(due.runId === run.runId && due.kind === "start"));
|
|
832
|
+
this.scheduleTask(run);
|
|
833
|
+
}
|
|
834
|
+
deliverSignal(runId, name, payload, headers) {
|
|
294
835
|
const run = this.runs.get(runId);
|
|
295
836
|
if (!run || run.state.status !== "RUNNING") return false;
|
|
296
837
|
this.append(run, {
|
|
@@ -298,7 +839,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
298
839
|
payload: {
|
|
299
840
|
name,
|
|
300
841
|
payload: toJsonValue(payload),
|
|
301
|
-
receivedAt: new Date(this.clock).toISOString()
|
|
842
|
+
receivedAt: new Date(this.clock).toISOString(),
|
|
843
|
+
...headers !== void 0 ? { headers: { ...headers } } : {}
|
|
302
844
|
}
|
|
303
845
|
});
|
|
304
846
|
return true;
|
|
@@ -311,6 +853,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
311
853
|
type: "WORKFLOW_CANCEL_REQUESTED",
|
|
312
854
|
payload: reason !== void 0 ? { reason } : {}
|
|
313
855
|
});
|
|
856
|
+
if (run.delayedUntil !== void 0) {
|
|
857
|
+
run.delayedUntil = void 0;
|
|
858
|
+
this.append(run, { type: "WORKFLOW_CANCELLED", payload: {} });
|
|
859
|
+
this.onClosed(run);
|
|
860
|
+
}
|
|
314
861
|
return true;
|
|
315
862
|
}
|
|
316
863
|
terminate(runId, reason) {
|
|
@@ -330,7 +877,9 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
330
877
|
await this.runTask(task);
|
|
331
878
|
continue;
|
|
332
879
|
}
|
|
333
|
-
const attempt = this.attempts.find(
|
|
880
|
+
const attempt = this.attempts.find(
|
|
881
|
+
(entry) => entry.dueAt <= this.clock && !this.isHeld(this.holdKeyOf(entry))
|
|
882
|
+
);
|
|
334
883
|
if (attempt) {
|
|
335
884
|
this.attempts = this.attempts.filter((entry) => entry !== attempt);
|
|
336
885
|
await this.runAttempt(attempt);
|
|
@@ -349,16 +898,78 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
349
898
|
}
|
|
350
899
|
throw new Error("The test environment did not settle after 100000 steps");
|
|
351
900
|
}
|
|
901
|
+
/** When a held attempt's schedule-to-start timeout expires, if it has one. */
|
|
902
|
+
heldDeadline(entry) {
|
|
903
|
+
return this.isHeld(this.holdKeyOf(entry)) && entry.scheduleToStart !== void 0 ? entry.dueAt + entry.scheduleToStart : void 0;
|
|
904
|
+
}
|
|
352
905
|
nextDue() {
|
|
353
|
-
const due = [
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
906
|
+
const due = [...this.timers.map((timer) => timer.fireAt)];
|
|
907
|
+
for (const entry of this.attempts) {
|
|
908
|
+
if (!this.isHeld(this.holdKeyOf(entry))) due.push(entry.dueAt);
|
|
909
|
+
const deadline = this.heldDeadline(entry);
|
|
910
|
+
if (deadline !== void 0) due.push(deadline);
|
|
911
|
+
}
|
|
912
|
+
for (const entry of this.dues) due.push(entry.at);
|
|
357
913
|
return due.length === 0 ? void 0 : Math.min(...due);
|
|
358
914
|
}
|
|
915
|
+
/** Expire held attempts whose schedule-to-start timeout passed (not retried, 11 §2.3). */
|
|
916
|
+
expireHeld() {
|
|
917
|
+
const expired = this.attempts.filter((entry) => {
|
|
918
|
+
const deadline = this.heldDeadline(entry);
|
|
919
|
+
return deadline !== void 0 && deadline <= this.clock;
|
|
920
|
+
});
|
|
921
|
+
if (expired.length === 0) return false;
|
|
922
|
+
this.attempts = this.attempts.filter((entry) => !expired.includes(entry));
|
|
923
|
+
for (const entry of expired) {
|
|
924
|
+
const run = this.runs.get(entry.runId);
|
|
925
|
+
const activity = run?.state.activities[entry.activityId];
|
|
926
|
+
if (!run || run.state.status !== "RUNNING" || !activity || !isActivityOpen(activity))
|
|
927
|
+
continue;
|
|
928
|
+
this.append(run, {
|
|
929
|
+
type: "ACTIVITY_TIMED_OUT",
|
|
930
|
+
payload: {
|
|
931
|
+
activityId: entry.activityId,
|
|
932
|
+
attempt: entry.attempt,
|
|
933
|
+
timeoutType: "SCHEDULE_TO_START"
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
/** Fire run-level deadlines: the end of a start delay, run and execution timeouts. */
|
|
940
|
+
fireRunDues() {
|
|
941
|
+
const due = this.dues.filter((entry) => entry.at <= this.clock).sort((a, b) => a.at - b.at);
|
|
942
|
+
if (due.length === 0) return false;
|
|
943
|
+
this.dues = this.dues.filter((entry) => !due.includes(entry));
|
|
944
|
+
for (const entry of due) {
|
|
945
|
+
const run = this.runs.get(entry.runId);
|
|
946
|
+
if (!run || run.state.status !== "RUNNING") continue;
|
|
947
|
+
if (entry.kind === "start") {
|
|
948
|
+
if (run.delayedUntil !== void 0) this.endDelay(run);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const timeoutType = entry.kind === "runTimeout" ? "WORKFLOW_RUN" : "WORKFLOW_EXECUTION";
|
|
952
|
+
const failure = {
|
|
953
|
+
errorType: "TIMEOUT",
|
|
954
|
+
message: timeoutType === "WORKFLOW_RUN" ? "The run timeout expired" : "The execution timeout of the chain expired",
|
|
955
|
+
type: "TimeoutFailure",
|
|
956
|
+
timeoutType,
|
|
957
|
+
source: "engine"
|
|
958
|
+
};
|
|
959
|
+
run.delayedUntil = void 0;
|
|
960
|
+
this.append(run, {
|
|
961
|
+
type: "WORKFLOW_TIMED_OUT",
|
|
962
|
+
payload: { timeoutType, failure: toJsonValue(failure) }
|
|
963
|
+
});
|
|
964
|
+
this.onClosed(run);
|
|
965
|
+
}
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
359
968
|
fireDue() {
|
|
969
|
+
const expired = this.expireHeld();
|
|
970
|
+
const fired = this.fireRunDues();
|
|
360
971
|
const due = this.timers.filter((timer) => timer.fireAt <= this.clock).sort((a, b) => a.fireAt - b.fireAt);
|
|
361
|
-
if (due.length === 0) return
|
|
972
|
+
if (due.length === 0) return expired || fired;
|
|
362
973
|
this.timers = this.timers.filter((timer) => !due.includes(timer));
|
|
363
974
|
for (const timer of due) {
|
|
364
975
|
const run = this.runs.get(timer.runId);
|
|
@@ -371,15 +982,27 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
371
982
|
async runTask(run) {
|
|
372
983
|
const taskId = run.state.task.taskId ?? `${run.runId}-task-${run.taskCounter}`;
|
|
373
984
|
this.append(run, { type: "WORKFLOW_TASK_STARTED", payload: { taskId, attempt: 1 } });
|
|
985
|
+
const delivered = [...run.updates.values()].filter(
|
|
986
|
+
(update) => update.status === "PENDING" || update.status === "ADMITTED"
|
|
987
|
+
);
|
|
988
|
+
const updates = delivered.map((update) => ({
|
|
989
|
+
updateId: update.updateId,
|
|
990
|
+
name: update.name,
|
|
991
|
+
args: update.args,
|
|
992
|
+
...update.headers !== void 0 ? { headers: { ...update.headers } } : {},
|
|
993
|
+
...update.status === "ADMITTED" ? { admitted: true } : {}
|
|
994
|
+
}));
|
|
374
995
|
const request = {
|
|
375
996
|
protocol: HISTORY_PROTOCOL,
|
|
376
997
|
kind: "task",
|
|
377
998
|
workflowFn: run.workflowFn,
|
|
378
999
|
info: run.info,
|
|
379
1000
|
history: run.history,
|
|
380
|
-
nowMs: this.clock
|
|
1001
|
+
nowMs: this.clock,
|
|
1002
|
+
...updates.length > 0 ? { updates } : {},
|
|
1003
|
+
...this.codeFeatures.length > 0 ? { features: [...this.codeFeatures] } : {}
|
|
381
1004
|
};
|
|
382
|
-
const result =
|
|
1005
|
+
const result = await this.executeWithLocalActivities(run, request);
|
|
383
1006
|
const fail = (cause, message) => {
|
|
384
1007
|
this.append(run, {
|
|
385
1008
|
type: "WORKFLOW_TASK_FAILED",
|
|
@@ -392,7 +1015,11 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
392
1015
|
run.taskFailure = result;
|
|
393
1016
|
return;
|
|
394
1017
|
}
|
|
395
|
-
const
|
|
1018
|
+
const relief = {
|
|
1019
|
+
grantedLocalActivityIds: [],
|
|
1020
|
+
deliveredUpdateIds: delivered.map((update) => update.updateId)
|
|
1021
|
+
};
|
|
1022
|
+
const checked = validateCommands(result.commands, { enabled: this.enabled, relief });
|
|
396
1023
|
if (!checked.ok) {
|
|
397
1024
|
fail(checked.cause, checked.issues.map((issue) => issue.message).join("; "));
|
|
398
1025
|
return;
|
|
@@ -400,21 +1027,133 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
400
1027
|
const drafts = eventsForCommands(run.state, checked.commands, {
|
|
401
1028
|
taskId,
|
|
402
1029
|
nowMs: this.clock,
|
|
403
|
-
enabled: this.enabled
|
|
1030
|
+
enabled: this.enabled,
|
|
1031
|
+
relief,
|
|
1032
|
+
updates: Object.fromEntries(
|
|
1033
|
+
delivered.map((update) => [
|
|
1034
|
+
update.updateId,
|
|
1035
|
+
{
|
|
1036
|
+
name: update.name,
|
|
1037
|
+
args: update.args,
|
|
1038
|
+
...update.headers !== void 0 ? { headers: update.headers } : {}
|
|
1039
|
+
}
|
|
1040
|
+
])
|
|
1041
|
+
)
|
|
404
1042
|
});
|
|
405
1043
|
if (!drafts.ok) {
|
|
406
1044
|
fail(drafts.cause, drafts.issues.map((issue) => issue.message).join("; "));
|
|
407
1045
|
return;
|
|
408
1046
|
}
|
|
1047
|
+
if (result.unfinishedHandlers !== void 0 && result.unfinishedHandlers.length > 0)
|
|
1048
|
+
run.unfinishedHandlers = result.unfinishedHandlers;
|
|
1049
|
+
this.recordUpdateReplies(run, checked.commands, delivered);
|
|
1050
|
+
this.runSinks(run, result.sinkCalls ?? []);
|
|
409
1051
|
this.append(run, { type: "WORKFLOW_TASK_COMPLETED", taskId, payload: { taskId } });
|
|
410
1052
|
for (const draft of drafts.events) this.append(run, draft);
|
|
411
1053
|
for (const command of checked.commands)
|
|
412
1054
|
if (run.state.status === "RUNNING" || TERMINAL_COMMANDS.has(command.type))
|
|
413
1055
|
this.afterCommand(run, command);
|
|
414
1056
|
}
|
|
1057
|
+
/** What a task's update replies mean for the run's update rows. */
|
|
1058
|
+
recordUpdateReplies(run, commands, delivered) {
|
|
1059
|
+
const answered = /* @__PURE__ */ new Set();
|
|
1060
|
+
for (const command of commands) {
|
|
1061
|
+
if (command.type !== "ACCEPT_UPDATE" && command.type !== "REJECT_UPDATE" && command.type !== "COMPLETE_UPDATE")
|
|
1062
|
+
continue;
|
|
1063
|
+
const entry = run.updates.get(command.updateId);
|
|
1064
|
+
if (!entry) continue;
|
|
1065
|
+
answered.add(command.updateId);
|
|
1066
|
+
if (command.type === "ACCEPT_UPDATE") entry.status = "ACCEPTED";
|
|
1067
|
+
else if (command.type === "REJECT_UPDATE") {
|
|
1068
|
+
entry.status = "REJECTED";
|
|
1069
|
+
entry.failure = command.failure;
|
|
1070
|
+
} else {
|
|
1071
|
+
entry.status = "COMPLETED";
|
|
1072
|
+
if (command.failure !== void 0) entry.failure = command.failure;
|
|
1073
|
+
else entry.result = command.result ?? null;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const update of delivered)
|
|
1077
|
+
if (!answered.has(update.updateId) && update.status === "PENDING") {
|
|
1078
|
+
update.status = "REJECTED";
|
|
1079
|
+
update.failure = {
|
|
1080
|
+
errorType: "VALIDATION",
|
|
1081
|
+
type: "UpdateNotHandled",
|
|
1082
|
+
message: `The workflow task did not answer update "${update.name}"`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/** Keep a task's sink calls and call the configured sinks with them. */
|
|
1087
|
+
runSinks(run, calls) {
|
|
1088
|
+
for (const call of calls) {
|
|
1089
|
+
run.sinkCalls.push(call);
|
|
1090
|
+
const fn = this.sinks[call.sink]?.[call.fn];
|
|
1091
|
+
if (fn === void 0) continue;
|
|
1092
|
+
try {
|
|
1093
|
+
fn(...call.args);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
execute(request) {
|
|
1099
|
+
return this.executor ? this.executor(request) : runWorkflowTaskInProcess(this.registry, request);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Run a workflow task as the worker does (11 §2.12): while it asks for
|
|
1103
|
+
* local activities, run each through its mock and run the same task again
|
|
1104
|
+
* with the outcomes, up to 1,000 rounds.
|
|
1105
|
+
*/
|
|
1106
|
+
async executeWithLocalActivities(run, request) {
|
|
1107
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
1108
|
+
let result = await this.execute(request);
|
|
1109
|
+
for (let round = 0; result.outcome === "localActivityRequested"; round++) {
|
|
1110
|
+
if (round >= 1e3)
|
|
1111
|
+
return {
|
|
1112
|
+
...result,
|
|
1113
|
+
outcome: "failed",
|
|
1114
|
+
commands: [],
|
|
1115
|
+
failure: { cause: "INTERNAL", message: "The task asked for local activities 1000 times" }
|
|
1116
|
+
};
|
|
1117
|
+
for (const wanted of result.localActivityRequests ?? [])
|
|
1118
|
+
outcomes.set(wanted.localActivityId, await this.runLocalActivity(run, wanted));
|
|
1119
|
+
result = await this.execute({ ...request, localActivityResults: [...outcomes.values()] });
|
|
1120
|
+
}
|
|
1121
|
+
return result;
|
|
1122
|
+
}
|
|
1123
|
+
async runLocalActivity(run, wanted) {
|
|
1124
|
+
const mock = this.mocks.get(`${wanted.nodeType}#${wanted.operation}`) ?? this.mocks.get(wanted.nodeType);
|
|
1125
|
+
try {
|
|
1126
|
+
if (!mock)
|
|
1127
|
+
throw new ApplicationError(
|
|
1128
|
+
`No mock for ${wanted.nodeType} ${wanted.operation} (use env.mock)`,
|
|
1129
|
+
{ errorType: "VALIDATION", nonRetryable: true, type: "UnmockedActivity" }
|
|
1130
|
+
);
|
|
1131
|
+
const result = toJsonValue(
|
|
1132
|
+
await mock(wanted.args, {
|
|
1133
|
+
runId: run.runId,
|
|
1134
|
+
activityId: wanted.localActivityId,
|
|
1135
|
+
attempt: wanted.attempt,
|
|
1136
|
+
nodeType: wanted.nodeType,
|
|
1137
|
+
operation: wanted.operation,
|
|
1138
|
+
local: true,
|
|
1139
|
+
...wanted.connection !== void 0 ? { connection: wanted.connection } : {}
|
|
1140
|
+
})
|
|
1141
|
+
);
|
|
1142
|
+
return { localActivityId: wanted.localActivityId, result, attempts: wanted.attempt };
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
const failure = errorToFailure(error, `node:${wanted.nodeType}`);
|
|
1145
|
+
delete failure.stackTrace;
|
|
1146
|
+
return {
|
|
1147
|
+
localActivityId: wanted.localActivityId,
|
|
1148
|
+
failure: toJsonValue(failure),
|
|
1149
|
+
attempts: wanted.attempt
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
415
1153
|
afterCommand(run, command) {
|
|
416
1154
|
switch (command.type) {
|
|
417
1155
|
case "SCHEDULE_ACTIVITY": {
|
|
1156
|
+
const user = command.kind === "user";
|
|
418
1157
|
const attempt = {
|
|
419
1158
|
runId: run.runId,
|
|
420
1159
|
activityId: command.activityId,
|
|
@@ -422,12 +1161,16 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
422
1161
|
dueAt: this.clock,
|
|
423
1162
|
scheduledAt: this.clock,
|
|
424
1163
|
args: command.args,
|
|
425
|
-
|
|
426
|
-
|
|
1164
|
+
kind: command.kind,
|
|
1165
|
+
nodeType: user ? "" : command.nodeType,
|
|
1166
|
+
operation: user ? command.activityName : command.operation,
|
|
427
1167
|
retry: command.retry,
|
|
428
1168
|
cancelRequested: false,
|
|
429
|
-
|
|
430
|
-
...
|
|
1169
|
+
...!user && command.connection !== void 0 ? { connection: command.connection } : {},
|
|
1170
|
+
...user ? { taskQueue: command.taskQueue } : {},
|
|
1171
|
+
...command.headers !== void 0 ? { headers: { ...command.headers } } : {},
|
|
1172
|
+
...command.scheduleToCloseTimeout !== void 0 ? { scheduleToClose: command.scheduleToCloseTimeout } : {},
|
|
1173
|
+
...command.scheduleToStartTimeout !== void 0 ? { scheduleToStart: command.scheduleToStartTimeout } : {}
|
|
431
1174
|
};
|
|
432
1175
|
this.attempts.push(attempt);
|
|
433
1176
|
return;
|
|
@@ -502,18 +1245,25 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
502
1245
|
return;
|
|
503
1246
|
}
|
|
504
1247
|
case "CONTINUE_AS_NEW": {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
)
|
|
516
|
-
|
|
1248
|
+
const options = command.options;
|
|
1249
|
+
const config = {
|
|
1250
|
+
...this.chainConfig(run.config),
|
|
1251
|
+
// The run timeout restarts and the attempt count resets; the
|
|
1252
|
+
// execution timeout of the chain carries over (11 §2.20).
|
|
1253
|
+
lastCompletionResult: command.input
|
|
1254
|
+
};
|
|
1255
|
+
if (typeof options["taskQueue"] === "string") config.taskQueue = options["taskQueue"];
|
|
1256
|
+
if (typeof options["workflowTaskTimeout"] === "number")
|
|
1257
|
+
config.workflowTaskTimeoutMs = options["workflowTaskTimeout"];
|
|
1258
|
+
if (options["versioningBehavior"] === "PINNED" || options["versioningBehavior"] === "AUTO_UPGRADE")
|
|
1259
|
+
config.versioningBehavior = options["versioningBehavior"];
|
|
1260
|
+
const memo = options["context"];
|
|
1261
|
+
if (typeof memo === "object" && memo !== null && !Array.isArray(memo)) config.memo = memo;
|
|
1262
|
+
else if (Object.keys(run.state.context).length > 0) config.memo = { ...run.state.context };
|
|
1263
|
+
const retry = options["retry"];
|
|
1264
|
+
if (continueAsNewRetryShape(options) === "run" && typeof retry === "object" && retry !== null && !Array.isArray(retry))
|
|
1265
|
+
config.retry = retry;
|
|
1266
|
+
this.startSuccessor(run, command.input, config);
|
|
517
1267
|
this.onClosed(run);
|
|
518
1268
|
return;
|
|
519
1269
|
}
|
|
@@ -526,11 +1276,62 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
526
1276
|
return;
|
|
527
1277
|
}
|
|
528
1278
|
}
|
|
1279
|
+
/** What a successor of a run inherits (continue-as-new, run retry). */
|
|
1280
|
+
chainConfig(config) {
|
|
1281
|
+
const out = { workflowKey: config.workflowKey, runAttempt: 1 };
|
|
1282
|
+
for (const key of [
|
|
1283
|
+
"businessId",
|
|
1284
|
+
"executionDeadline",
|
|
1285
|
+
"executionTimeoutMs",
|
|
1286
|
+
"runTimeoutMs",
|
|
1287
|
+
"workflowTaskTimeoutMs",
|
|
1288
|
+
"retry",
|
|
1289
|
+
"versioningBehavior",
|
|
1290
|
+
"taskQueue",
|
|
1291
|
+
"memo"
|
|
1292
|
+
])
|
|
1293
|
+
if (config[key] !== void 0) Object.assign(out, { [key]: config[key] });
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
/** Start the next run of a chain; the closed run's parent and handles follow it. */
|
|
1297
|
+
startSuccessor(run, input, config) {
|
|
1298
|
+
const next = this.createRun(
|
|
1299
|
+
run.workflowFn,
|
|
1300
|
+
input,
|
|
1301
|
+
{
|
|
1302
|
+
...run.info,
|
|
1303
|
+
runId: void 0,
|
|
1304
|
+
continuedFromRunId: run.runId,
|
|
1305
|
+
firstRunId: run.info.firstRunId ?? run.runId
|
|
1306
|
+
},
|
|
1307
|
+
run.parent,
|
|
1308
|
+
{},
|
|
1309
|
+
config
|
|
1310
|
+
);
|
|
1311
|
+
run.continuedTo = next;
|
|
1312
|
+
return next;
|
|
1313
|
+
}
|
|
529
1314
|
resolveTarget(run, target) {
|
|
530
1315
|
if ("runId" in target) return target.runId;
|
|
531
1316
|
if ("parent" in target) return run.parent?.runId ?? run.info.parentRunId ?? void 0;
|
|
532
|
-
if ("childId" in target)
|
|
533
|
-
|
|
1317
|
+
if ("childId" in target) {
|
|
1318
|
+
let childRunId = run.children.get(target.childId);
|
|
1319
|
+
while (childRunId !== void 0) {
|
|
1320
|
+
const next = this.runs.get(childRunId)?.continuedTo;
|
|
1321
|
+
if (next === void 0) break;
|
|
1322
|
+
childRunId = next;
|
|
1323
|
+
}
|
|
1324
|
+
return childRunId;
|
|
1325
|
+
}
|
|
1326
|
+
return this.resolveWorkflowId(target.workflowKey, target.workflowId, target.firstRunId);
|
|
1327
|
+
}
|
|
1328
|
+
/** The open run of a workflow id, else its latest run (11 §2.1). */
|
|
1329
|
+
resolveWorkflowId(workflowKey, workflowId, firstRunId) {
|
|
1330
|
+
const matching = this.runOrder.map((id) => this.run(id)).filter(
|
|
1331
|
+
(candidate) => candidate.config.workflowKey === workflowKey && candidate.config.businessId === workflowId && (firstRunId === void 0 || (candidate.info.firstRunId ?? candidate.runId) === firstRunId)
|
|
1332
|
+
);
|
|
1333
|
+
const open = matching.find((candidate) => candidate.state.status === "RUNNING");
|
|
1334
|
+
return (open ?? matching[matching.length - 1])?.runId;
|
|
534
1335
|
}
|
|
535
1336
|
startChild(run, command) {
|
|
536
1337
|
const byFn = command.workflowFn !== void 0 && (this.executor !== void 0 || this.registry.byName.has(command.workflowFn));
|
|
@@ -563,6 +1364,28 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
563
1364
|
});
|
|
564
1365
|
return;
|
|
565
1366
|
}
|
|
1367
|
+
const options = command.options;
|
|
1368
|
+
const ms = (key) => typeof options[key] === "number" ? options[key] : void 0;
|
|
1369
|
+
const executionTimeoutMs = ms("executionTimeout");
|
|
1370
|
+
const childKey = command.workflowKey ?? this.registry.byName.get(workflowFn)?.name ?? workflowFn;
|
|
1371
|
+
const config = { workflowKey: childKey, runAttempt: 1 };
|
|
1372
|
+
if (typeof options.workflowId === "string") config.businessId = options.workflowId;
|
|
1373
|
+
if (executionTimeoutMs !== void 0) {
|
|
1374
|
+
config.executionTimeoutMs = executionTimeoutMs;
|
|
1375
|
+
config.executionDeadline = this.clock + executionTimeoutMs;
|
|
1376
|
+
}
|
|
1377
|
+
const runTimeoutMs = ms("runTimeout");
|
|
1378
|
+
if (runTimeoutMs !== void 0) config.runTimeoutMs = runTimeoutMs;
|
|
1379
|
+
const taskTimeoutMs = ms("workflowTaskTimeout");
|
|
1380
|
+
if (taskTimeoutMs !== void 0) config.workflowTaskTimeoutMs = taskTimeoutMs;
|
|
1381
|
+
const startDelayMs = ms("startDelay");
|
|
1382
|
+
if (startDelayMs !== void 0) config.startDelayMs = startDelayMs;
|
|
1383
|
+
if (typeof options.summary === "string") config.summary = options.summary;
|
|
1384
|
+
if (typeof options.details === "string") config.details = options.details;
|
|
1385
|
+
if (options.context !== void 0) config.memo = options.context;
|
|
1386
|
+
if (options.versioningBehavior !== void 0)
|
|
1387
|
+
config.versioningBehavior = options.versioningBehavior;
|
|
1388
|
+
if (options.retry !== void 0) config.retry = options.retry;
|
|
566
1389
|
const childRunId = this.createRun(
|
|
567
1390
|
workflowFn,
|
|
568
1391
|
command.input,
|
|
@@ -571,7 +1394,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
571
1394
|
{
|
|
572
1395
|
parentRunId: run.runId,
|
|
573
1396
|
parentClosePolicy: command.options.parentClosePolicy
|
|
574
|
-
}
|
|
1397
|
+
},
|
|
1398
|
+
config
|
|
575
1399
|
);
|
|
576
1400
|
run.children.set(command.childId, childRunId);
|
|
577
1401
|
this.append(run, {
|
|
@@ -625,35 +1449,151 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
625
1449
|
});
|
|
626
1450
|
return;
|
|
627
1451
|
}
|
|
628
|
-
const mock = this.mocks.get(`${attempt.nodeType}#${attempt.operation}`) ?? this.mocks.get(attempt.nodeType);
|
|
629
1452
|
let failure;
|
|
630
1453
|
let result = null;
|
|
1454
|
+
let heartbeatDetails = attempt.heartbeatDetails;
|
|
1455
|
+
let asyncToken;
|
|
631
1456
|
try {
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
nonRetryable: true,
|
|
638
|
-
|
|
1457
|
+
if (attempt.kind === "user") {
|
|
1458
|
+
const impl = findActivityImplementation(this.activityImpls, attempt.operation);
|
|
1459
|
+
if (!impl)
|
|
1460
|
+
throw new ApplicationError(
|
|
1461
|
+
`No implementation of activity ${attempt.operation} (use env.registerActivity or env.mockActivity)`,
|
|
1462
|
+
{ errorType: "VALIDATION", nonRetryable: true, type: "ActivityNotRegistered" }
|
|
1463
|
+
);
|
|
1464
|
+
const ctx = this.activityContext(
|
|
1465
|
+
attempt,
|
|
1466
|
+
(details) => {
|
|
1467
|
+
heartbeatDetails = details;
|
|
1468
|
+
},
|
|
1469
|
+
(token) => {
|
|
1470
|
+
asyncToken = token;
|
|
639
1471
|
}
|
|
640
1472
|
);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1473
|
+
result = toJsonValue(
|
|
1474
|
+
await executeActivity(impl, ctx, attempt.args, this.activityInterceptors)
|
|
1475
|
+
);
|
|
1476
|
+
} else {
|
|
1477
|
+
const mock = this.mocks.get(`${attempt.nodeType}#${attempt.operation}`) ?? this.mocks.get(attempt.nodeType);
|
|
1478
|
+
if (!mock)
|
|
1479
|
+
throw new ApplicationError(
|
|
1480
|
+
`No mock for ${attempt.nodeType} ${attempt.operation} (use env.mock)`,
|
|
1481
|
+
{
|
|
1482
|
+
errorType: "VALIDATION",
|
|
1483
|
+
nonRetryable: true,
|
|
1484
|
+
type: "UnmockedActivity"
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
result = toJsonValue(
|
|
1488
|
+
await mock(attempt.args, {
|
|
1489
|
+
runId: attempt.runId,
|
|
1490
|
+
activityId: attempt.activityId,
|
|
1491
|
+
attempt: attempt.attempt,
|
|
1492
|
+
nodeType: attempt.nodeType,
|
|
1493
|
+
operation: attempt.operation,
|
|
1494
|
+
...attempt.connection !== void 0 ? { connection: attempt.connection } : {},
|
|
1495
|
+
...attempt.heartbeatDetails !== void 0 ? { heartbeatDetails: attempt.heartbeatDetails } : {},
|
|
1496
|
+
heartbeat: (details) => {
|
|
1497
|
+
heartbeatDetails = toJsonValue(details);
|
|
1498
|
+
}
|
|
1499
|
+
})
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
651
1502
|
} catch (error) {
|
|
652
|
-
failure = errorToFailure(
|
|
1503
|
+
failure = errorToFailure(
|
|
1504
|
+
error,
|
|
1505
|
+
attempt.kind === "user" ? "sdk-ts" : `node:${attempt.nodeType}`
|
|
1506
|
+
);
|
|
653
1507
|
delete failure.stackTrace;
|
|
654
1508
|
}
|
|
655
1509
|
if (run.state.status !== "RUNNING" || !isActivityOpen(run.state.activities[attempt.activityId] ?? activity))
|
|
656
1510
|
return;
|
|
1511
|
+
if (asyncToken !== void 0 && failure === void 0) {
|
|
1512
|
+
this.asyncAttempts.set(asyncToken, {
|
|
1513
|
+
...attempt,
|
|
1514
|
+
...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
|
|
1515
|
+
});
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
this.finishAttempt(run, attempt, failure, result, heartbeatDetails);
|
|
1519
|
+
}
|
|
1520
|
+
/** The `ActivityContext` of one user-activity attempt. */
|
|
1521
|
+
activityContext(attempt, onHeartbeat, onAsync) {
|
|
1522
|
+
const run = this.runs.get(attempt.runId);
|
|
1523
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1524
|
+
const signal = {
|
|
1525
|
+
get aborted() {
|
|
1526
|
+
return attempt.cancelRequested;
|
|
1527
|
+
},
|
|
1528
|
+
addEventListener: (_type, listener) => {
|
|
1529
|
+
listeners.add(listener);
|
|
1530
|
+
},
|
|
1531
|
+
removeEventListener: (_type, listener) => {
|
|
1532
|
+
listeners.delete(listener);
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
return {
|
|
1536
|
+
info: {
|
|
1537
|
+
runId: attempt.runId,
|
|
1538
|
+
workspaceId: this.workspaceId,
|
|
1539
|
+
...run?.config.workflowKey !== void 0 ? { workflowKey: run.config.workflowKey } : {},
|
|
1540
|
+
activityId: attempt.activityId,
|
|
1541
|
+
activityName: attempt.operation,
|
|
1542
|
+
taskQueue: attempt.taskQueue ?? "managed",
|
|
1543
|
+
attempt: attempt.attempt,
|
|
1544
|
+
...attempt.heartbeatDetails !== void 0 ? { heartbeatDetails: attempt.heartbeatDetails } : {},
|
|
1545
|
+
headers: { ...attempt.headers ?? {} }
|
|
1546
|
+
},
|
|
1547
|
+
heartbeat: (details) => {
|
|
1548
|
+
onHeartbeat(toJsonValue(details));
|
|
1549
|
+
return Promise.resolve({ cancelRequested: attempt.cancelRequested, paused: false });
|
|
1550
|
+
},
|
|
1551
|
+
cancellationSignal: signal,
|
|
1552
|
+
completeAsync: () => {
|
|
1553
|
+
const token = `test-task-token:${attempt.runId}:${attempt.activityId}:${attempt.attempt}`;
|
|
1554
|
+
onAsync(token);
|
|
1555
|
+
return Promise.resolve(token);
|
|
1556
|
+
},
|
|
1557
|
+
connection: (name) => {
|
|
1558
|
+
const found = this.connections[name];
|
|
1559
|
+
if (found === void 0)
|
|
1560
|
+
return Promise.reject(
|
|
1561
|
+
new ApplicationError(`No connection "${name}" in the test environment`, {
|
|
1562
|
+
errorType: "AUTHORIZATION",
|
|
1563
|
+
nonRetryable: true,
|
|
1564
|
+
type: "ConnectionNotReleased"
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
1567
|
+
return Promise.resolve({ ...found });
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/** Complete an activity that called `completeAsync()` (`completeActivityAsync`). */
|
|
1572
|
+
async completeActivityAsync(token, result) {
|
|
1573
|
+
const attempt = this.takeAsync(token);
|
|
1574
|
+
const run = this.run(attempt.runId);
|
|
1575
|
+
this.finishAttempt(run, attempt, void 0, toJsonValue(result), attempt.heartbeatDetails);
|
|
1576
|
+
await this.runUntilIdle(false);
|
|
1577
|
+
}
|
|
1578
|
+
/** Fail an activity that called `completeAsync()` (`failActivityAsync`); the retry policy applies. */
|
|
1579
|
+
async failActivityAsync(token, error) {
|
|
1580
|
+
const attempt = this.takeAsync(token);
|
|
1581
|
+
const run = this.run(attempt.runId);
|
|
1582
|
+
const failure = errorToFailure(error, "sdk-ts");
|
|
1583
|
+
delete failure.stackTrace;
|
|
1584
|
+
this.finishAttempt(run, attempt, failure, null, attempt.heartbeatDetails);
|
|
1585
|
+
await this.runUntilIdle(false);
|
|
1586
|
+
}
|
|
1587
|
+
takeAsync(token) {
|
|
1588
|
+
const attempt = this.asyncAttempts.get(token);
|
|
1589
|
+
if (!attempt) throw new Error(`Unknown or used activity task token ${token}`);
|
|
1590
|
+
this.asyncAttempts.delete(token);
|
|
1591
|
+
return attempt;
|
|
1592
|
+
}
|
|
1593
|
+
/** Record an attempt's outcome: completion, a final failure or timeout, or the next attempt. */
|
|
1594
|
+
finishAttempt(run, attempt, failure, result, heartbeatDetails) {
|
|
1595
|
+
const open = run.state.activities[attempt.activityId];
|
|
1596
|
+
if (run.state.status !== "RUNNING" || !open || !isActivityOpen(open)) return;
|
|
657
1597
|
const started = () => {
|
|
658
1598
|
this.append(run, {
|
|
659
1599
|
type: "ACTIVITY_STARTED",
|
|
@@ -686,7 +1626,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
686
1626
|
this.attempts.push({
|
|
687
1627
|
...attempt,
|
|
688
1628
|
attempt: attempt.attempt + 1,
|
|
689
|
-
dueAt: this.clock + decision2.delayMs
|
|
1629
|
+
dueAt: this.clock + decision2.delayMs,
|
|
1630
|
+
...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
|
|
690
1631
|
});
|
|
691
1632
|
return;
|
|
692
1633
|
}
|
|
@@ -695,7 +1636,8 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
695
1636
|
this.attempts.push({
|
|
696
1637
|
...attempt,
|
|
697
1638
|
attempt: attempt.attempt + 1,
|
|
698
|
-
dueAt: this.clock + decision.delayMs
|
|
1639
|
+
dueAt: this.clock + decision.delayMs,
|
|
1640
|
+
...heartbeatDetails !== void 0 ? { heartbeatDetails } : {}
|
|
699
1641
|
});
|
|
700
1642
|
return;
|
|
701
1643
|
}
|
|
@@ -709,16 +1651,57 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
709
1651
|
}
|
|
710
1652
|
});
|
|
711
1653
|
}
|
|
712
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* Run-level retry (11 §2.17): a failed or run-timed-out run whose policy
|
|
1656
|
+
* allows another attempt is followed by a new run of the chain after the
|
|
1657
|
+
* backoff, with `runAttempt + 1` and the failure as `lastFailure`.
|
|
1658
|
+
*/
|
|
1659
|
+
retryRun(run) {
|
|
1660
|
+
if (run.continuedTo !== void 0) return;
|
|
1661
|
+
const last = run.history[run.history.length - 1];
|
|
1662
|
+
if (!last || last.type !== "WORKFLOW_FAILED" && last.type !== "WORKFLOW_TIMED_OUT") return;
|
|
1663
|
+
const failure = last.payload["failure"] ?? { errorType: "TIMEOUT", message: "timed out", timeoutType: "WORKFLOW_RUN" };
|
|
1664
|
+
const decision = computeRunRetryDelay(run.config.retry, run.config.runAttempt, failure);
|
|
1665
|
+
if (!decision.retry) return;
|
|
1666
|
+
const config = {
|
|
1667
|
+
...this.chainConfig(run.config),
|
|
1668
|
+
runAttempt: run.config.runAttempt + 1,
|
|
1669
|
+
lastFailure: failure
|
|
1670
|
+
};
|
|
1671
|
+
if (decision.delayMs > 0) config.firstTaskDelayMs = decision.delayMs;
|
|
1672
|
+
this.startSuccessor(run, run.state.input ?? null, config);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* A run closed: drop its pending work, retry it, apply the parent-close
|
|
1676
|
+
* policy to its children (except `keptChildren`, which follow a reset
|
|
1677
|
+
* fork), settle its open updates and notify its parent.
|
|
1678
|
+
*/
|
|
1679
|
+
onClosed(run, keptChildren = /* @__PURE__ */ new Set()) {
|
|
713
1680
|
this.timers = this.timers.filter((timer) => timer.runId !== run.runId);
|
|
714
1681
|
this.attempts = this.attempts.filter((entry) => entry.runId !== run.runId);
|
|
1682
|
+
this.dues = this.dues.filter((entry) => entry.runId !== run.runId);
|
|
1683
|
+
for (const [token, entry] of this.asyncAttempts)
|
|
1684
|
+
if (entry.runId === run.runId) this.asyncAttempts.delete(token);
|
|
1685
|
+
this.retryRun(run);
|
|
715
1686
|
for (const [childId2, childRunId] of run.children) {
|
|
1687
|
+
if (keptChildren.has(childId2)) continue;
|
|
716
1688
|
const child = run.state.children[childId2];
|
|
717
1689
|
if (!child || !isChildOpen(child) || childRunId === void 0) continue;
|
|
718
1690
|
if (child.parentClosePolicy === "TERMINATE") this.terminate(childRunId, "parent closed");
|
|
719
1691
|
else if (child.parentClosePolicy === "REQUEST_CANCEL")
|
|
720
1692
|
this.requestCancel(childRunId, "parent closed");
|
|
721
1693
|
}
|
|
1694
|
+
for (const update of run.updates.values()) {
|
|
1695
|
+
if (update.status !== "PENDING" && update.status !== "ACCEPTED" && update.status !== "ADMITTED")
|
|
1696
|
+
continue;
|
|
1697
|
+
const accepted = update.status === "ACCEPTED";
|
|
1698
|
+
update.status = accepted ? "COMPLETED" : "REJECTED";
|
|
1699
|
+
update.failure = {
|
|
1700
|
+
errorType: accepted ? "PERMANENT" : "VALIDATION",
|
|
1701
|
+
type: "RunClosed",
|
|
1702
|
+
message: `The run closed before update "${update.name}" ${accepted ? "completed" : "was accepted"}`
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
722
1705
|
if (!run.parent) return;
|
|
723
1706
|
const parent = this.runs.get(run.parent.runId);
|
|
724
1707
|
if (!parent || parent.state.status !== "RUNNING") return;
|
|
@@ -726,6 +1709,10 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
726
1709
|
if (!parent.state.children[childId] || !isChildOpen(parent.state.children[childId])) return;
|
|
727
1710
|
const last = run.history[run.history.length - 1];
|
|
728
1711
|
if (!last) return;
|
|
1712
|
+
if (run.resetTo !== void 0) {
|
|
1713
|
+
parent.children.set(childId, run.resetTo);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
729
1716
|
if (run.continuedTo !== void 0) {
|
|
730
1717
|
const next = this.runs.get(run.continuedTo);
|
|
731
1718
|
if (next) {
|
|
@@ -766,19 +1753,34 @@ var TestWorkflowEnvironment = class _TestWorkflowEnvironment {
|
|
|
766
1753
|
default:
|
|
767
1754
|
this.append(parent, {
|
|
768
1755
|
type: "CHILD_RUN_TIMED_OUT",
|
|
769
|
-
payload: {
|
|
1756
|
+
payload: {
|
|
1757
|
+
...base,
|
|
1758
|
+
timeoutType: last.payload["timeoutType"] === "WORKFLOW_EXECUTION" ? "WORKFLOW_EXECUTION" : "WORKFLOW_RUN"
|
|
1759
|
+
}
|
|
770
1760
|
});
|
|
771
1761
|
}
|
|
772
1762
|
}
|
|
773
1763
|
async resultOf(runId) {
|
|
774
1764
|
await this.runUntilIdle(this.timeSkipping);
|
|
775
1765
|
let run = this.run(runId);
|
|
776
|
-
while (run.continuedTo !== void 0
|
|
1766
|
+
while (run.continuedTo !== void 0 || run.resetTo !== void 0)
|
|
1767
|
+
run = this.run(run.continuedTo ?? run.resetTo ?? run.runId);
|
|
777
1768
|
if (run.taskFailure) throw new WorkflowTaskFailedError(run.runId, run.taskFailure);
|
|
1769
|
+
if (this.failOnUnfinishedHandlers && run.unfinishedHandlers !== void 0)
|
|
1770
|
+
throw new UnfinishedHandlersError(run.runId, run.unfinishedHandlers);
|
|
778
1771
|
const last = run.history[run.history.length - 1];
|
|
779
1772
|
switch (run.state.status) {
|
|
780
1773
|
case "COMPLETED":
|
|
781
1774
|
return run.state.output ?? null;
|
|
1775
|
+
case "TIMED_OUT":
|
|
1776
|
+
throw new WorkflowFailedError(
|
|
1777
|
+
run.runId,
|
|
1778
|
+
last?.payload["failure"] ?? {
|
|
1779
|
+
errorType: "TIMEOUT",
|
|
1780
|
+
message: "timed out",
|
|
1781
|
+
type: "TimeoutFailure"
|
|
1782
|
+
}
|
|
1783
|
+
);
|
|
782
1784
|
case "FAILED":
|
|
783
1785
|
throw new WorkflowFailedError(
|
|
784
1786
|
run.runId,
|
|
@@ -1034,4 +2036,4 @@ async function runWorkflowTaskInVm(script, request, options = {}) {
|
|
|
1034
2036
|
}
|
|
1035
2037
|
}
|
|
1036
2038
|
|
|
1037
|
-
export { HistoryAssertionError, ReplayError, RunBlockedError, TestRunHandle, TestWorkflowEnvironment, WorkflowFailedError, WorkflowTaskFailedError, assertHistory, bundleToScript, drainJobs, expectIR, jsonCopy, registryOf, replay, replayRequest, runWorkflowTaskInProcess, runWorkflowTaskInVm, simulate };
|
|
2039
|
+
export { HistoryAssertionError, QueryFailedError, QueryRejectedError, ReplayError, RunBlockedError, TestRunHandle, TestWorkflowEnvironment, UnfinishedHandlersError, UpdateRefusedError, WorkflowAlreadyStartedError, WorkflowFailedError, WorkflowTaskFailedError, assertHistory, bundleToScript, drainJobs, expectIR, jsonCopy, registryOf, replay, replayRequest, runWorkflowTaskInProcess, runWorkflowTaskInVm, simulate };
|