@dudousxd/nestjs-durable-core 0.80.0 → 0.81.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 +400 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -8
- package/dist/index.d.ts +167 -8
- package/dist/index.js +392 -100
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
|
|
37
37
|
DurableWorkflow: () => DurableWorkflow,
|
|
38
38
|
ENGINE_MINTED_TAG_PREFIXES: () => ENGINE_MINTED_TAG_PREFIXES,
|
|
39
|
+
ENTITY_WORKFLOW: () => ENTITY_WORKFLOW,
|
|
39
40
|
EVENT_BUFFER_SCAN_LIMIT: () => EVENT_BUFFER_SCAN_LIMIT,
|
|
40
41
|
EVENT_TOKEN_PREFIX: () => EVENT_TOKEN_PREFIX,
|
|
41
42
|
Entities: () => Entities,
|
|
@@ -61,6 +62,8 @@ __export(index_exports, {
|
|
|
61
62
|
RunGateway: () => RunGateway,
|
|
62
63
|
SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
|
|
63
64
|
SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
|
|
65
|
+
SINGLETON_RESERVED_TAGS: () => SINGLETON_RESERVED_TAGS,
|
|
66
|
+
SINGLETON_TRACKED_TAG: () => SINGLETON_TRACKED_TAG,
|
|
64
67
|
STATE_STORE: () => STATE_STORE,
|
|
65
68
|
STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
|
|
66
69
|
SUMMARY_CHILD_LIMIT: () => SUMMARY_CHILD_LIMIT,
|
|
@@ -136,6 +139,7 @@ __export(index_exports, {
|
|
|
136
139
|
isLegacyDescriptor: () => isLegacyDescriptor,
|
|
137
140
|
isNonEmptyRunScope: () => isNonEmptyRunScope,
|
|
138
141
|
isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
|
|
142
|
+
legacySingletonHolders: () => legacySingletonHolders,
|
|
139
143
|
listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
|
|
140
144
|
liveHandoff: () => liveHandoff,
|
|
141
145
|
log: () => log,
|
|
@@ -153,8 +157,10 @@ __export(index_exports, {
|
|
|
153
157
|
pageFanFromStore: () => pageFanFromStore,
|
|
154
158
|
parentRunIdOf: () => parentRunIdOf,
|
|
155
159
|
parseDuration: () => parseDuration,
|
|
160
|
+
parseWebhookToken: () => parseWebhookToken,
|
|
156
161
|
partitionOfToken: () => partitionOfToken,
|
|
157
162
|
planDispatch: () => planDispatch,
|
|
163
|
+
poolNamespace: () => poolNamespace,
|
|
158
164
|
prevCronFireMs: () => prevCronFireMs,
|
|
159
165
|
queryCheckpointsOf: () => queryCheckpointsOf,
|
|
160
166
|
readSearchAttributes: () => readSearchAttributes,
|
|
@@ -190,6 +196,8 @@ __export(index_exports, {
|
|
|
190
196
|
tenantGroup: () => tenantGroup,
|
|
191
197
|
useDurableExecution: () => useDurableExecution,
|
|
192
198
|
warn: () => warn,
|
|
199
|
+
withSingletonTracked: () => withSingletonTracked,
|
|
200
|
+
withoutSingletonMarkers: () => withoutSingletonMarkers,
|
|
193
201
|
workflowName: () => workflowName
|
|
194
202
|
});
|
|
195
203
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1121,6 +1129,9 @@ async function assertConcurrency(store, workflow, quota) {
|
|
|
1121
1129
|
__name(assertConcurrency, "assertConcurrency");
|
|
1122
1130
|
|
|
1123
1131
|
// src/entities.ts
|
|
1132
|
+
var NUDGE = {
|
|
1133
|
+
nudge: true
|
|
1134
|
+
};
|
|
1124
1135
|
var ENTITY_WORKFLOW = "__entity";
|
|
1125
1136
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
1126
1137
|
var Entities = class {
|
|
@@ -1128,9 +1139,11 @@ var Entities = class {
|
|
|
1128
1139
|
__name(this, "Entities");
|
|
1129
1140
|
}
|
|
1130
1141
|
host;
|
|
1142
|
+
workflowHost;
|
|
1131
1143
|
configs = /* @__PURE__ */ new Map();
|
|
1132
|
-
constructor(host) {
|
|
1144
|
+
constructor(host, workflowHost) {
|
|
1133
1145
|
this.host = host;
|
|
1146
|
+
this.workflowHost = workflowHost;
|
|
1134
1147
|
host.register(ENTITY_WORKFLOW, "1", (ctx, input) => this.run(ctx, input));
|
|
1135
1148
|
}
|
|
1136
1149
|
register(name, config) {
|
|
@@ -1152,6 +1165,52 @@ var Entities = class {
|
|
|
1152
1165
|
}
|
|
1153
1166
|
});
|
|
1154
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* The durable half of a send from INSIDE a workflow (`ctx.callEntity` / `ctx.signalEntity`): write
|
|
1170
|
+
* the op to the entity's signal buffer and return once it is stored, so the caller can checkpoint
|
|
1171
|
+
* after it. The op carries `id`, the caller's stable `<runId>:<seq>`, so if the caller dies between
|
|
1172
|
+
* this write and its checkpoint and sends again on replay, the entity skips the second copy. The
|
|
1173
|
+
* entity's run is also ensured here, awaited (`start` only enqueues, it never runs the body
|
|
1174
|
+
* inline), so an op sent on first contact can't be stranded in a buffer no run will ever read if
|
|
1175
|
+
* the process dies right after. The wake-up ({@link kick}) is separate and may be lost: a busy
|
|
1176
|
+
* entity reads the buffer at its next wait, and a parked one is paired with it by the engine's
|
|
1177
|
+
* `reconcileMs` sweep. Returns `false` on a host without a workflow host.
|
|
1178
|
+
*/
|
|
1179
|
+
async enqueue(name, key, op, arg, id, reply) {
|
|
1180
|
+
if (!this.workflowHost) return false;
|
|
1181
|
+
const message = {
|
|
1182
|
+
op,
|
|
1183
|
+
arg,
|
|
1184
|
+
id,
|
|
1185
|
+
...reply ? {
|
|
1186
|
+
reply
|
|
1187
|
+
} : {}
|
|
1188
|
+
};
|
|
1189
|
+
const runId = entityRunId(name, key);
|
|
1190
|
+
await this.workflowHost.bufferSignal(runId, message);
|
|
1191
|
+
await this.workflowHost.ensureStarted(ENTITY_WORKFLOW, {
|
|
1192
|
+
name,
|
|
1193
|
+
key
|
|
1194
|
+
}, runId);
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Make the entity act on what {@link enqueue} buffered: wake its run if it is parked on its wait
|
|
1199
|
+
* (the run itself was ensured by `enqueue`; ensured again here, idempotently, for a caller replay
|
|
1200
|
+
* that only re-kicks). A run that is busy needs nothing: its next wait reads the buffer before
|
|
1201
|
+
* suspending. The wake carries a no-op `nudge`, never the op itself, so the op stays in
|
|
1202
|
+
* the buffer in arrival order and a lost or repeated kick costs at most one empty iteration.
|
|
1203
|
+
*/
|
|
1204
|
+
async kick(name, key) {
|
|
1205
|
+
const host = this.workflowHost;
|
|
1206
|
+
if (!host) return;
|
|
1207
|
+
const runId = entityRunId(name, key);
|
|
1208
|
+
await host.ensureStarted(ENTITY_WORKFLOW, {
|
|
1209
|
+
name,
|
|
1210
|
+
key
|
|
1211
|
+
}, runId);
|
|
1212
|
+
await host.wakeWaiter(runId, NUDGE);
|
|
1213
|
+
}
|
|
1155
1214
|
async signal(name, key, op, arg) {
|
|
1156
1215
|
await this.dispatch(name, key, op, arg);
|
|
1157
1216
|
}
|
|
@@ -1165,11 +1224,18 @@ var Entities = class {
|
|
|
1165
1224
|
if (!config) throw new FatalError(`entity "${name}" is not registered`);
|
|
1166
1225
|
const token = entityRunId(name, key);
|
|
1167
1226
|
let state = config.initialState();
|
|
1227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1168
1228
|
for (let i = 0; ; i += 1) {
|
|
1169
1229
|
const msg = await ctx.waitForSignal(token);
|
|
1230
|
+
if (msg.nudge) continue;
|
|
1231
|
+
if (msg.id !== void 0) {
|
|
1232
|
+
if (seen.has(msg.id)) continue;
|
|
1233
|
+
seen.add(msg.id);
|
|
1234
|
+
}
|
|
1235
|
+
const op = msg.op;
|
|
1170
1236
|
const out = await ctx.localStep(`op:${i}`, async () => {
|
|
1171
|
-
const handler = config.handlers[
|
|
1172
|
-
if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${
|
|
1237
|
+
const handler = config.handlers[op];
|
|
1238
|
+
if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${op}"`);
|
|
1173
1239
|
const result = await handler(state, msg.arg);
|
|
1174
1240
|
return {
|
|
1175
1241
|
result,
|
|
@@ -1179,8 +1245,9 @@ var Entities = class {
|
|
|
1179
1245
|
state = out.state;
|
|
1180
1246
|
await ctx.setEvent("state", state);
|
|
1181
1247
|
if (msg.reply) {
|
|
1248
|
+
const reply = msg.reply;
|
|
1182
1249
|
await ctx.localStep(`reply:${i}`, async () => {
|
|
1183
|
-
await this.host.signal(
|
|
1250
|
+
await this.host.signal(reply, out.result);
|
|
1184
1251
|
});
|
|
1185
1252
|
}
|
|
1186
1253
|
}
|
|
@@ -2104,6 +2171,10 @@ function checkpointNameKind(name) {
|
|
|
2104
2171
|
return "step";
|
|
2105
2172
|
}
|
|
2106
2173
|
__name(checkpointNameKind, "checkpointNameKind");
|
|
2174
|
+
function poolNamespace(namespace) {
|
|
2175
|
+
return namespace === "" ? "default" : namespace;
|
|
2176
|
+
}
|
|
2177
|
+
__name(poolNamespace, "poolNamespace");
|
|
2107
2178
|
var TERMINAL_RUN_STATUSES = [
|
|
2108
2179
|
"completed",
|
|
2109
2180
|
"failed",
|
|
@@ -2235,6 +2306,19 @@ var WEBHOOK_PREFIX = "wh:";
|
|
|
2235
2306
|
var CHILD_PREFIX = "child:";
|
|
2236
2307
|
var BREAKPOINT_PREFIX = "bp:";
|
|
2237
2308
|
var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
|
|
2309
|
+
function parseWebhookToken(token) {
|
|
2310
|
+
if (!token.startsWith(WEBHOOK_PREFIX)) return null;
|
|
2311
|
+
const rest = token.slice(WEBHOOK_PREFIX.length);
|
|
2312
|
+
const at = rest.lastIndexOf(":");
|
|
2313
|
+
if (at <= 0) return null;
|
|
2314
|
+
const seqText = rest.slice(at + 1);
|
|
2315
|
+
if (!/^\d+$/.test(seqText)) return null;
|
|
2316
|
+
return {
|
|
2317
|
+
runId: rest.slice(0, at),
|
|
2318
|
+
seq: Number(seqText)
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
__name(parseWebhookToken, "parseWebhookToken");
|
|
2238
2322
|
function classifyWaiterToken(token) {
|
|
2239
2323
|
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
2240
2324
|
on: "webhook",
|
|
@@ -2772,6 +2856,11 @@ __name(errorMessage, "errorMessage");
|
|
|
2772
2856
|
|
|
2773
2857
|
// src/singleton-admission.ts
|
|
2774
2858
|
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2859
|
+
var SINGLETON_TRACKED_TAG = "durable:singleton:tracked";
|
|
2860
|
+
var SINGLETON_RESERVED_TAGS = [
|
|
2861
|
+
SINGLETON_ADMITTED_TAG,
|
|
2862
|
+
SINGLETON_TRACKED_TAG
|
|
2863
|
+
];
|
|
2775
2864
|
var SINGLETON_ACTIVE_STATUSES = [
|
|
2776
2865
|
"pending",
|
|
2777
2866
|
"running",
|
|
@@ -2779,6 +2868,33 @@ var SINGLETON_ACTIVE_STATUSES = [
|
|
|
2779
2868
|
"blocked",
|
|
2780
2869
|
"cancelling"
|
|
2781
2870
|
];
|
|
2871
|
+
var LEGACY_SCAN_STATUSES = [
|
|
2872
|
+
"running",
|
|
2873
|
+
"suspended",
|
|
2874
|
+
"cancelling"
|
|
2875
|
+
];
|
|
2876
|
+
var byAdmissionOrder = /* @__PURE__ */ __name((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id), "byAdmissionOrder");
|
|
2877
|
+
function withoutSingletonMarkers(tags) {
|
|
2878
|
+
return tags?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
|
|
2879
|
+
}
|
|
2880
|
+
__name(withoutSingletonMarkers, "withoutSingletonMarkers");
|
|
2881
|
+
function withSingletonTracked(tags) {
|
|
2882
|
+
const current = tags ?? [];
|
|
2883
|
+
if (current.includes(SINGLETON_ADMITTED_TAG) || current.includes(SINGLETON_TRACKED_TAG)) {
|
|
2884
|
+
return current;
|
|
2885
|
+
}
|
|
2886
|
+
return [
|
|
2887
|
+
...current,
|
|
2888
|
+
SINGLETON_TRACKED_TAG
|
|
2889
|
+
];
|
|
2890
|
+
}
|
|
2891
|
+
__name(withSingletonTracked, "withSingletonTracked");
|
|
2892
|
+
function legacySingletonHolders(active, runId, limit) {
|
|
2893
|
+
const scanned = active.filter((run) => LEGACY_SCAN_STATUSES.includes(run.status)).sort(byAdmissionOrder);
|
|
2894
|
+
const window = new Set(scanned.slice(0, limit).map((run) => run.id));
|
|
2895
|
+
return scanned.filter((run) => run.id !== runId && !run.tags?.includes(SINGLETON_ADMITTED_TAG) && (!run.tags?.includes(SINGLETON_TRACKED_TAG) || window.has(run.id)));
|
|
2896
|
+
}
|
|
2897
|
+
__name(legacySingletonHolders, "legacySingletonHolders");
|
|
2782
2898
|
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2783
2899
|
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2784
2900
|
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
@@ -2787,12 +2903,13 @@ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
|
2787
2903
|
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2788
2904
|
if (candidate.status === "cancelling") return null;
|
|
2789
2905
|
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2790
|
-
const
|
|
2906
|
+
const legacy = new Set(legacySingletonHolders(active, runId, limit).map((run) => run.id));
|
|
2907
|
+
const remaining = limit - holders.length - legacy.size;
|
|
2791
2908
|
if (remaining <= 0) return null;
|
|
2792
|
-
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)
|
|
2909
|
+
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG) && !legacy.has(run.id)).sort(byAdmissionOrder);
|
|
2793
2910
|
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2794
2911
|
return [
|
|
2795
|
-
...candidate.tags ?? [],
|
|
2912
|
+
...(candidate.tags ?? []).filter((t) => t !== SINGLETON_TRACKED_TAG),
|
|
2796
2913
|
SINGLETON_ADMITTED_TAG
|
|
2797
2914
|
];
|
|
2798
2915
|
}
|
|
@@ -2802,7 +2919,7 @@ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
|
2802
2919
|
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2803
2920
|
return {
|
|
2804
2921
|
...patch,
|
|
2805
|
-
tags: tags
|
|
2922
|
+
tags: withoutSingletonMarkers(tags)
|
|
2806
2923
|
};
|
|
2807
2924
|
}
|
|
2808
2925
|
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
@@ -2813,6 +2930,7 @@ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt)
|
|
|
2813
2930
|
return {
|
|
2814
2931
|
status: "suspended",
|
|
2815
2932
|
wakeAt: retryWakeAt,
|
|
2933
|
+
tags: withSingletonTracked(candidate.tags),
|
|
2816
2934
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2817
2935
|
};
|
|
2818
2936
|
}
|
|
@@ -2865,7 +2983,7 @@ var SingletonGate = class {
|
|
|
2865
2983
|
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2866
2984
|
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2867
2985
|
run.tags = [
|
|
2868
|
-
...run.tags ?? [],
|
|
2986
|
+
...(run.tags ?? []).filter((tag) => tag !== SINGLETON_TRACKED_TAG),
|
|
2869
2987
|
SINGLETON_ADMITTED_TAG
|
|
2870
2988
|
];
|
|
2871
2989
|
}
|
|
@@ -3760,30 +3878,82 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3760
3878
|
}));
|
|
3761
3879
|
return id;
|
|
3762
3880
|
}, "startChild");
|
|
3881
|
+
const sendEntityOp = /* @__PURE__ */ __name(async (name, key, op, arg, seq, reply) => await host.enqueueEntityOp?.(name, key, op, arg, `${runId}:${seq}`, reply) ?? false, "sendEntityOp");
|
|
3763
3882
|
const callEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
|
|
3764
3883
|
const current = pos.next();
|
|
3765
|
-
|
|
3884
|
+
let existing = await readCheckpoint(current);
|
|
3766
3885
|
if (existing && existing.status === "completed") return existing.output;
|
|
3767
3886
|
const reply = `entityreply:${runId}:${current}`;
|
|
3887
|
+
const intentName = `signal:${reply}`;
|
|
3888
|
+
if (existing && existing.name !== intentName) {
|
|
3889
|
+
throw new NonDeterminismError(runId, current, intentName, existing.name);
|
|
3890
|
+
}
|
|
3891
|
+
if (existing && replay?.has(current)) {
|
|
3892
|
+
existing = await store.getCheckpoint(runId, current) ?? existing;
|
|
3893
|
+
if (existing.status === "completed") return existing.output;
|
|
3894
|
+
}
|
|
3895
|
+
const markSent = /* @__PURE__ */ __name(() => store.saveCheckpointIfPending?.(instantCheckpoint({
|
|
3896
|
+
runId,
|
|
3897
|
+
seq: current,
|
|
3898
|
+
name: intentName,
|
|
3899
|
+
kind: "signal",
|
|
3900
|
+
status: "pending",
|
|
3901
|
+
output: {
|
|
3902
|
+
sent: true
|
|
3903
|
+
}
|
|
3904
|
+
})) ?? Promise.resolve(false), "markSent");
|
|
3905
|
+
if (existing) {
|
|
3906
|
+
const buffered = await consumeBuffered(reply, current);
|
|
3907
|
+
if (buffered) {
|
|
3908
|
+
await store.removeSignalWaiter({
|
|
3909
|
+
token: reply,
|
|
3910
|
+
runId,
|
|
3911
|
+
seq: current
|
|
3912
|
+
});
|
|
3913
|
+
return buffered.value;
|
|
3914
|
+
}
|
|
3915
|
+
if (existing.output?.sent !== true) {
|
|
3916
|
+
if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
|
|
3917
|
+
}
|
|
3918
|
+
host.kickEntity?.(name, key);
|
|
3919
|
+
throw new WorkflowSuspended();
|
|
3920
|
+
}
|
|
3768
3921
|
await store.putSignalWaiter({
|
|
3769
3922
|
token: reply,
|
|
3770
3923
|
runId,
|
|
3771
3924
|
seq: current
|
|
3772
3925
|
});
|
|
3773
|
-
host.
|
|
3926
|
+
if (!host.enqueueEntityOp) {
|
|
3927
|
+
host.signalEntity?.(name, key, op, arg, reply);
|
|
3928
|
+
throw new WorkflowSuspended();
|
|
3929
|
+
}
|
|
3930
|
+
await writeCheckpoint(instantCheckpoint({
|
|
3931
|
+
runId,
|
|
3932
|
+
seq: current,
|
|
3933
|
+
name: intentName,
|
|
3934
|
+
kind: "signal",
|
|
3935
|
+
status: "pending",
|
|
3936
|
+
output: {
|
|
3937
|
+
sent: false
|
|
3938
|
+
}
|
|
3939
|
+
}));
|
|
3940
|
+
if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
|
|
3941
|
+
host.kickEntity?.(name, key);
|
|
3774
3942
|
throw new WorkflowSuspended();
|
|
3775
3943
|
}, "callEntity");
|
|
3776
3944
|
const signalEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
|
|
3777
3945
|
const current = pos.next();
|
|
3778
3946
|
const existing = await readCheckpoint(current);
|
|
3779
3947
|
if (existing && existing.status === "completed") return;
|
|
3780
|
-
|
|
3948
|
+
const durable = await sendEntityOp(name, key, op, arg, current);
|
|
3949
|
+
if (!durable) host.signalEntity?.(name, key, op, arg);
|
|
3781
3950
|
await writeCheckpoint(instantCheckpoint({
|
|
3782
3951
|
runId,
|
|
3783
3952
|
seq: current,
|
|
3784
3953
|
name: `entitysig:${name}:${key}`,
|
|
3785
3954
|
kind: "local"
|
|
3786
3955
|
}));
|
|
3956
|
+
if (durable) host.kickEntity?.(name, key);
|
|
3787
3957
|
}, "signalEntity");
|
|
3788
3958
|
const breakpoint = /* @__PURE__ */ __name(async (label) => {
|
|
3789
3959
|
const current = pos.next();
|
|
@@ -3851,17 +4021,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3851
4021
|
const deadlineSeq = timeoutMs == null ? -1 : pos.next();
|
|
3852
4022
|
const existing = await readCheckpoint(current);
|
|
3853
4023
|
if (existing && existing.status === "completed") return existing.output;
|
|
3854
|
-
|
|
3855
|
-
|
|
4024
|
+
const buffered = await consumeBuffered(token, current);
|
|
4025
|
+
if (buffered) return buffered.value;
|
|
4026
|
+
const deadline = timeoutMs == null ? void 0 : await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
4027
|
+
if (deadline !== void 0 && host.clock() >= deadline) {
|
|
4028
|
+
await store.removeSignalWaiter({
|
|
3856
4029
|
token,
|
|
3857
4030
|
runId,
|
|
3858
4031
|
seq: current
|
|
3859
|
-
});
|
|
3860
|
-
throw new WorkflowSuspended();
|
|
3861
|
-
}
|
|
3862
|
-
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
3863
|
-
if (host.clock() >= deadline) {
|
|
3864
|
-
await store.takeSignalWaiter(token).catch(() => void 0);
|
|
4032
|
+
}).catch(() => void 0);
|
|
3865
4033
|
throw new SignalTimeoutError(token, timeoutMs);
|
|
3866
4034
|
}
|
|
3867
4035
|
await store.putSignalWaiter({
|
|
@@ -3869,6 +4037,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3869
4037
|
runId,
|
|
3870
4038
|
seq: current
|
|
3871
4039
|
});
|
|
4040
|
+
const lateBuffered = await consumeBuffered(token, current);
|
|
4041
|
+
if (lateBuffered) {
|
|
4042
|
+
await store.removeSignalWaiter({
|
|
4043
|
+
token,
|
|
4044
|
+
runId,
|
|
4045
|
+
seq: current
|
|
4046
|
+
});
|
|
4047
|
+
return lateBuffered.value;
|
|
4048
|
+
}
|
|
3872
4049
|
throw new WorkflowSuspended(deadline);
|
|
3873
4050
|
}, "wait");
|
|
3874
4051
|
return {
|
|
@@ -4060,6 +4237,12 @@ var SETTLE_ELIGIBLE_STATUSES = [
|
|
|
4060
4237
|
"failed",
|
|
4061
4238
|
"cancelling"
|
|
4062
4239
|
];
|
|
4240
|
+
var TIMEOUT_SWEPT_STATUSES = [
|
|
4241
|
+
"pending",
|
|
4242
|
+
"running",
|
|
4243
|
+
"suspended",
|
|
4244
|
+
"blocked"
|
|
4245
|
+
];
|
|
4063
4246
|
var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
|
|
4064
4247
|
var RECOVERY_BATCH = 500;
|
|
4065
4248
|
var NamespaceMismatch = class NamespaceMismatch2 extends Error {
|
|
@@ -4082,6 +4265,12 @@ var WorkflowEngine = class {
|
|
|
4082
4265
|
instanceId;
|
|
4083
4266
|
namespace;
|
|
4084
4267
|
leaseMs;
|
|
4268
|
+
/**
|
|
4269
|
+
* Leases held by this `instanceId` that expire strictly before this epoch ms were taken by a
|
|
4270
|
+
* PREVIOUS process with the same id (every lease this one takes or renews expires at
|
|
4271
|
+
* `clock() + leaseMs`, so at or after it). See {@link orphanedRuns}.
|
|
4272
|
+
*/
|
|
4273
|
+
ownLeaseCutoffMs;
|
|
4085
4274
|
maxRecoveryAttempts;
|
|
4086
4275
|
remoteAdvanceSilenceMs;
|
|
4087
4276
|
reconcileMs;
|
|
@@ -4203,8 +4392,9 @@ var WorkflowEngine = class {
|
|
|
4203
4392
|
this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
|
|
4204
4393
|
this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
|
|
4205
4394
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
4206
|
-
this.namespace = deps.namespace;
|
|
4395
|
+
this.namespace = poolNamespace(deps.namespace);
|
|
4207
4396
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
4397
|
+
this.ownLeaseCutoffMs = this.clock() + this.leaseMs;
|
|
4208
4398
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
4209
4399
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
4210
4400
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
@@ -4324,7 +4514,11 @@ var WorkflowEngine = class {
|
|
|
4324
4514
|
}
|
|
4325
4515
|
});
|
|
4326
4516
|
this.accumulators = new EventAccumulators(this);
|
|
4327
|
-
this.entities = new Entities(this
|
|
4517
|
+
this.entities = new Entities(this, {
|
|
4518
|
+
bufferSignal: /* @__PURE__ */ __name((token, payload) => this.store.bufferSignal(token, payload), "bufferSignal"),
|
|
4519
|
+
wakeWaiter: /* @__PURE__ */ __name((token, payload) => this.wakeSignalWaiter(token, payload), "wakeWaiter"),
|
|
4520
|
+
ensureStarted: /* @__PURE__ */ __name((workflow, input, runId) => this.start(workflow, input, runId), "ensureStarted")
|
|
4521
|
+
});
|
|
4328
4522
|
}
|
|
4329
4523
|
/**
|
|
4330
4524
|
* Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
|
|
@@ -4600,7 +4794,7 @@ var WorkflowEngine = class {
|
|
|
4600
4794
|
// tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
|
|
4601
4795
|
// `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
|
|
4602
4796
|
// not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
|
|
4603
|
-
namespace: opts?.namespace ?? this.namespace,
|
|
4797
|
+
namespace: poolNamespace(opts?.namespace) ?? this.namespace,
|
|
4604
4798
|
input,
|
|
4605
4799
|
createdAt: /* @__PURE__ */ new Date(),
|
|
4606
4800
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -4637,7 +4831,7 @@ var WorkflowEngine = class {
|
|
|
4637
4831
|
VERSION_UNDECLARED_TAG
|
|
4638
4832
|
] : []
|
|
4639
4833
|
];
|
|
4640
|
-
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag
|
|
4834
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
|
|
4641
4835
|
if (registered.singleton) {
|
|
4642
4836
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4643
4837
|
}
|
|
@@ -4651,7 +4845,7 @@ var WorkflowEngine = class {
|
|
|
4651
4845
|
...delayed ? {
|
|
4652
4846
|
wakeAt: startAtMs
|
|
4653
4847
|
} : {},
|
|
4654
|
-
namespace: opts?.namespace ?? this.namespace,
|
|
4848
|
+
namespace: poolNamespace(opts?.namespace) ?? this.namespace,
|
|
4655
4849
|
// DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
|
|
4656
4850
|
// deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
|
|
4657
4851
|
// `undefined` when the registration carried none (remote/convention/synthesized child, or a
|
|
@@ -4833,12 +5027,12 @@ var WorkflowEngine = class {
|
|
|
4833
5027
|
error: run.error
|
|
4834
5028
|
};
|
|
4835
5029
|
}
|
|
4836
|
-
if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
|
|
5030
|
+
if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
|
|
4837
5031
|
await this.store.releaseRunLock(runId);
|
|
4838
5032
|
throw new NamespaceMismatch();
|
|
4839
5033
|
}
|
|
4840
|
-
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
4841
|
-
run.tags = run.tags
|
|
5034
|
+
if (run.status === "failed" && (run.tags?.includes(SINGLETON_ADMITTED_TAG) || run.tags?.includes(SINGLETON_TRACKED_TAG))) {
|
|
5035
|
+
run.tags = withoutSingletonMarkers(run.tags);
|
|
4842
5036
|
await this.store.updateRun(run.id, {
|
|
4843
5037
|
tags: run.tags
|
|
4844
5038
|
});
|
|
@@ -5057,6 +5251,15 @@ var WorkflowEngine = class {
|
|
|
5057
5251
|
* `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
|
|
5058
5252
|
* are cascaded exactly as an explicit {@link cancel} cascades them.
|
|
5059
5253
|
*
|
|
5254
|
+
* WHICH RUNS. Every run not yet finished, counted from `createdAt`: `pending` and `blocked` as well
|
|
5255
|
+
* as `running` and `suspended`. A run nobody picks up (no worker on its queue) or nobody can run (no
|
|
5256
|
+
* worker with its capabilities) is as stuck as one that hangs mid-body, and the cap is the backstop
|
|
5257
|
+
* for exactly that — leaving them out meant a run that never started lived forever while one that
|
|
5258
|
+
* started and hung was cut off. Cancelling one a worker is picking up at that moment is safe: the
|
|
5259
|
+
* worker's settle only writes over a live status ({@link SETTLE_ELIGIBLE_STATUSES}), so it can't
|
|
5260
|
+
* bring the run back. `cancelling` is left alone on purpose — a compensating cancel's saga undo is
|
|
5261
|
+
* running, it ends `cancelled` by itself, and cutting it short would skip the undo.
|
|
5262
|
+
*
|
|
5060
5263
|
* WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
|
|
5061
5264
|
* the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
|
|
5062
5265
|
* a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
|
|
@@ -5069,8 +5272,8 @@ var WorkflowEngine = class {
|
|
|
5069
5272
|
* children kept running with the parent gone and nothing pointing at them — a silent orphan you
|
|
5070
5273
|
* could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
|
|
5071
5274
|
* walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
|
|
5072
|
-
* `cancel` returns early on a run that is already
|
|
5073
|
-
*
|
|
5275
|
+
* `cancel` returns early on a run that is already terminal (a child that beat the sweep to the
|
|
5276
|
+
* finish is never clobbered, and a re-cancel is a no-op), and it writes the
|
|
5074
5277
|
* parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
|
|
5075
5278
|
* second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
|
|
5076
5279
|
* the same terminal state twice and cascade into already-cancelled children — idempotent, not
|
|
@@ -5086,18 +5289,11 @@ var WorkflowEngine = class {
|
|
|
5086
5289
|
if (this.draining) return;
|
|
5087
5290
|
if (reg.executionTimeoutMs == null) continue;
|
|
5088
5291
|
const deadline = now - reg.executionTimeoutMs;
|
|
5089
|
-
const inflight =
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
}),
|
|
5095
|
-
...await this.store.listRuns({
|
|
5096
|
-
workflow: reg.name,
|
|
5097
|
-
status: "suspended",
|
|
5098
|
-
namespace: this.namespace
|
|
5099
|
-
})
|
|
5100
|
-
];
|
|
5292
|
+
const inflight = await this.store.listRuns({
|
|
5293
|
+
workflow: reg.name,
|
|
5294
|
+
statuses: TIMEOUT_SWEPT_STATUSES,
|
|
5295
|
+
namespace: this.namespace
|
|
5296
|
+
});
|
|
5101
5297
|
for (const run of inflight) {
|
|
5102
5298
|
if (this.draining) return;
|
|
5103
5299
|
if (run.createdAt.getTime() > deadline) continue;
|
|
@@ -5130,48 +5326,103 @@ var WorkflowEngine = class {
|
|
|
5130
5326
|
const results = [];
|
|
5131
5327
|
for (const listed of await this.orphanedRuns(nowMs)) {
|
|
5132
5328
|
if (this.draining) break;
|
|
5133
|
-
const
|
|
5134
|
-
if (!
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5329
|
+
const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
|
|
5330
|
+
if (!outcome) continue;
|
|
5331
|
+
if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
|
|
5332
|
+
results.push(outcome.result);
|
|
5333
|
+
}
|
|
5334
|
+
return results;
|
|
5335
|
+
}
|
|
5336
|
+
/** A run whose still-live lease is held by this very `instanceId` (see {@link reclaimOwnLease}). */
|
|
5337
|
+
isOwnLiveLease(run, nowMs) {
|
|
5338
|
+
return run.lockedBy === this.instanceId && (run.lockedUntil ?? 0) > nowMs;
|
|
5339
|
+
}
|
|
5340
|
+
/**
|
|
5341
|
+
* Recover a run whose lease is still live but held by THIS `instanceId` and predates this process
|
|
5342
|
+
* (listed by {@link orphanedRuns}): the previous process with this stable id died without a
|
|
5343
|
+
* graceful shutdown (OOM, SIGKILL), so nothing is renewing that lease and waiting for it to expire
|
|
5344
|
+
* only delays the run. The lease is taken back with `renewRunLock` (a compare-and-set on the
|
|
5345
|
+
* owner; `tryLockRun` refuses a live lease), so the lease alone cannot fence a live execution in
|
|
5346
|
+
* THIS process from its own recovery: the in-process run gate does. A run with a
|
|
5347
|
+
* gate is executing (or about to) here and is skipped; otherwise a gate is held while the run is
|
|
5348
|
+
* recovered, so a resume landing meanwhile queues behind the recovery instead of running beside it.
|
|
5349
|
+
* The gate is released before the re-dispatch, which may resume the run on this instance.
|
|
5350
|
+
*/
|
|
5351
|
+
async reclaimOwnLease(listed, nowMs) {
|
|
5352
|
+
if (this.runGates.has(listed.id)) return void 0;
|
|
5353
|
+
let release;
|
|
5354
|
+
const tail = new Promise((resolve) => {
|
|
5355
|
+
release = resolve;
|
|
5356
|
+
});
|
|
5357
|
+
const gate = {
|
|
5358
|
+
tail
|
|
5359
|
+
};
|
|
5360
|
+
this.runGates.set(listed.id, gate);
|
|
5361
|
+
this.closeGateAfter(listed.id, gate, tail);
|
|
5362
|
+
let outcome;
|
|
5363
|
+
try {
|
|
5364
|
+
const fresh = await this.store.getRun(listed.id);
|
|
5365
|
+
const liveHere = fresh?.lockedBy === this.instanceId && (fresh.lockedUntil ?? 0) > nowMs && (fresh.lockedUntil ?? 0) >= this.ownLeaseCutoffMs;
|
|
5366
|
+
if (fresh && !liveHere) outcome = await this.recoverOrphan(listed, nowMs, true);
|
|
5367
|
+
} finally {
|
|
5368
|
+
release(outcome?.result ?? {
|
|
5369
|
+
runId: listed.id,
|
|
5370
|
+
status: listed.status
|
|
5371
|
+
});
|
|
5372
|
+
}
|
|
5373
|
+
return outcome;
|
|
5374
|
+
}
|
|
5375
|
+
/**
|
|
5376
|
+
* One run of a {@link recoverIncomplete} pass: take its lease and decide on it. Resolves the
|
|
5377
|
+
* pass's result for it and whether it must be re-dispatched, or `undefined` when it was not ours.
|
|
5378
|
+
* `ownLease`: the run's live lease is this instance id's own, left by a previous process (see
|
|
5379
|
+
* {@link reclaimOwnLease}), so it is taken back by owner rather than acquired as a free one.
|
|
5380
|
+
*/
|
|
5381
|
+
async recoverOrphan(listed, nowMs, ownLease = false) {
|
|
5382
|
+
const acquired = ownLease ? await this.store.renewRunLock(listed.id, this.instanceId, nowMs + this.leaseMs) : await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
5383
|
+
if (!acquired) return void 0;
|
|
5384
|
+
const run = await this.store.getRun(listed.id);
|
|
5385
|
+
if (!run || run.status !== "running" && run.status !== "cancelling") {
|
|
5386
|
+
await this.store.releaseRunLock(listed.id);
|
|
5387
|
+
return void 0;
|
|
5388
|
+
}
|
|
5389
|
+
if (run.status === "running") {
|
|
5390
|
+
const dispatched = await this.inFlightRemoteStep(run.id);
|
|
5391
|
+
if (dispatched) {
|
|
5163
5392
|
await this.store.updateRun(run.id, {
|
|
5164
|
-
status: "
|
|
5393
|
+
status: "suspended",
|
|
5394
|
+
wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
|
|
5165
5395
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5166
5396
|
});
|
|
5397
|
+
await this.store.releaseRunLock(run.id);
|
|
5398
|
+
return {
|
|
5399
|
+
result: {
|
|
5400
|
+
runId: run.id,
|
|
5401
|
+
status: "suspended"
|
|
5402
|
+
},
|
|
5403
|
+
dispatch: false
|
|
5404
|
+
};
|
|
5167
5405
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5406
|
+
}
|
|
5407
|
+
const settled = await this.countRecovery(run);
|
|
5408
|
+
if (settled) return {
|
|
5409
|
+
result: settled,
|
|
5410
|
+
dispatch: false
|
|
5411
|
+
};
|
|
5412
|
+
await this.store.releaseRunLock(run.id);
|
|
5413
|
+
if (run.status !== "cancelling") {
|
|
5414
|
+
await this.store.updateRun(run.id, {
|
|
5415
|
+
status: "pending",
|
|
5416
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5172
5417
|
});
|
|
5173
5418
|
}
|
|
5174
|
-
return
|
|
5419
|
+
return {
|
|
5420
|
+
result: {
|
|
5421
|
+
runId: run.id,
|
|
5422
|
+
status: run.status === "cancelling" ? "cancelling" : "pending"
|
|
5423
|
+
},
|
|
5424
|
+
dispatch: true
|
|
5425
|
+
};
|
|
5175
5426
|
}
|
|
5176
5427
|
/**
|
|
5177
5428
|
* The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
|
|
@@ -5181,12 +5432,20 @@ var WorkflowEngine = class {
|
|
|
5181
5432
|
* ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
|
|
5182
5433
|
* filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
|
|
5183
5434
|
* recovering at once spread over the batch instead of contending for the same leases.
|
|
5435
|
+
*
|
|
5436
|
+
* Also listed: runs still leased by THIS `instanceId` with a lease taken before this process
|
|
5437
|
+
* booted ({@link ownLeaseCutoffMs}). With a stable `instanceId` (pinned to the pod identity), that
|
|
5438
|
+
* is the previous process, killed without a graceful shutdown; see {@link reclaimOwnLease}.
|
|
5184
5439
|
*/
|
|
5185
5440
|
async orphanedRuns(nowMs) {
|
|
5441
|
+
const ownedBy = {
|
|
5442
|
+
owner: this.instanceId,
|
|
5443
|
+
lockedBefore: this.ownLeaseCutoffMs
|
|
5444
|
+
};
|
|
5186
5445
|
if (this.store.listOrphanedRuns) {
|
|
5187
|
-
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
|
|
5446
|
+
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace, ownedBy));
|
|
5188
5447
|
}
|
|
5189
|
-
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs));
|
|
5448
|
+
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs || run.lockedBy === ownedBy.owner && run.lockedUntil < ownedBy.lockedBefore).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
|
|
5190
5449
|
}
|
|
5191
5450
|
/**
|
|
5192
5451
|
* The remote step this run dispatched and is still awaiting a result for, if any — the signature
|
|
@@ -5229,6 +5488,7 @@ var WorkflowEngine = class {
|
|
|
5229
5488
|
error: error2,
|
|
5230
5489
|
recoveryAttempts: attempts
|
|
5231
5490
|
});
|
|
5491
|
+
this.notifyParent(run.id, failedCompletion(error2));
|
|
5232
5492
|
return {
|
|
5233
5493
|
runId: run.id,
|
|
5234
5494
|
status: "dead",
|
|
@@ -5435,7 +5695,7 @@ var WorkflowEngine = class {
|
|
|
5435
5695
|
error: void 0,
|
|
5436
5696
|
...resurrecting ? {
|
|
5437
5697
|
recoveryAttempts: 0,
|
|
5438
|
-
tags: run.tags
|
|
5698
|
+
tags: withoutSingletonMarkers(run.tags)
|
|
5439
5699
|
} : {},
|
|
5440
5700
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5441
5701
|
});
|
|
@@ -5847,6 +6107,16 @@ var WorkflowEngine = class {
|
|
|
5847
6107
|
return this.deliverSignal(lateWaiter, reclaimed.payload);
|
|
5848
6108
|
}
|
|
5849
6109
|
/**
|
|
6110
|
+
* Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
|
|
6111
|
+
* {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
|
|
6112
|
+
* token (an entity op, see `Entities.kick`): a run that isn't parked reads that buffer at its next
|
|
6113
|
+
* wait, so there is nothing to keep.
|
|
6114
|
+
*/
|
|
6115
|
+
async wakeSignalWaiter(token, payload) {
|
|
6116
|
+
const waiter = await this.store.takeSignalWaiter(token);
|
|
6117
|
+
return waiter ? this.deliverSignal(waiter, payload) : null;
|
|
6118
|
+
}
|
|
6119
|
+
/**
|
|
5850
6120
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
5851
6121
|
* signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
|
|
5852
6122
|
* consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
|
|
@@ -6442,6 +6712,25 @@ var WorkflowEngine = class {
|
|
|
6442
6712
|
});
|
|
6443
6713
|
}
|
|
6444
6714
|
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
6715
|
+
/**
|
|
6716
|
+
* A run's first execution: `pending` -> `running`. A singleton run not yet admitted is stamped
|
|
6717
|
+
* {@link SINGLETON_TRACKED_TAG} in the same write, BEFORE it competes for a slot, so a run this
|
|
6718
|
+
* replica is about to admit or deny is never mistaken for a holder admitted by a replica older
|
|
6719
|
+
* than 0.73 (see `legacySingletonHolders`).
|
|
6720
|
+
*/
|
|
6721
|
+
async markStarted(run, singleton) {
|
|
6722
|
+
const tags = singleton ? withSingletonTracked(run.tags) : run.tags;
|
|
6723
|
+
const retag = tags !== run.tags;
|
|
6724
|
+
await this.store.updateRun(run.id, {
|
|
6725
|
+
status: "running",
|
|
6726
|
+
...retag ? {
|
|
6727
|
+
tags
|
|
6728
|
+
} : {},
|
|
6729
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6730
|
+
});
|
|
6731
|
+
run.status = "running";
|
|
6732
|
+
if (retag) run.tags = tags;
|
|
6733
|
+
}
|
|
6445
6734
|
async singletonWaitResult(run) {
|
|
6446
6735
|
const current = await this.store.getRun(run.id);
|
|
6447
6736
|
await this.store.releaseRunLock(run.id);
|
|
@@ -6815,11 +7104,7 @@ ${run.workflowVersion}`;
|
|
|
6815
7104
|
}
|
|
6816
7105
|
const remote = registered.remote;
|
|
6817
7106
|
if (run.status === "pending") {
|
|
6818
|
-
await this.
|
|
6819
|
-
status: "running",
|
|
6820
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6821
|
-
});
|
|
6822
|
-
run.status = "running";
|
|
7107
|
+
await this.markStarted(run, registered?.singleton !== void 0);
|
|
6823
7108
|
this.emit({
|
|
6824
7109
|
type: "run.started",
|
|
6825
7110
|
runId: run.id,
|
|
@@ -7435,11 +7720,7 @@ ${run.workflowVersion}`;
|
|
|
7435
7720
|
async runExecutionTurn(run, fn, registered) {
|
|
7436
7721
|
if (run.status === "cancelling") this.cancelRequested.add(run.id);
|
|
7437
7722
|
if (run.status === "pending") {
|
|
7438
|
-
await this.
|
|
7439
|
-
status: "running",
|
|
7440
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
7441
|
-
});
|
|
7442
|
-
run.status = "running";
|
|
7723
|
+
await this.markStarted(run, registered?.singleton !== void 0);
|
|
7443
7724
|
this.emit({
|
|
7444
7725
|
type: "run.started",
|
|
7445
7726
|
runId: run.id,
|
|
@@ -7815,6 +8096,17 @@ ${run.workflowVersion}`;
|
|
|
7815
8096
|
signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
|
|
7816
8097
|
queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
|
|
7817
8098
|
}, "signalEntity"),
|
|
8099
|
+
// The durable half of a workflow-side entity send: awaited inside the turn (a store write
|
|
8100
|
+
// only, never an inline entity turn), so the ctx can checkpoint after it. See Entities.enqueue.
|
|
8101
|
+
enqueueEntityOp: /* @__PURE__ */ __name((name, key, op, arg, id, reply) => this.entities.enqueue(name, key, op, arg, id, reply), "enqueueEntityOp"),
|
|
8102
|
+
// Start/wake the entity for what enqueueEntityOp buffered — deferred like signalEntity above,
|
|
8103
|
+
// so it never re-enters a still-executing turn. A failed wake loses nothing: the op stays
|
|
8104
|
+
// buffered for the entity's next wait (and recovery's buffered-entity wake).
|
|
8105
|
+
kickEntity: /* @__PURE__ */ __name((name, key) => {
|
|
8106
|
+
queueMicrotask(() => void this.entities.kick(name, key).catch((err) => {
|
|
8107
|
+
console.warn(`[nestjs-durable] waking entity "${name}" key "${key}" failed (its op stays buffered for the next wake):`, err);
|
|
8108
|
+
}));
|
|
8109
|
+
}, "kickEntity"),
|
|
7818
8110
|
interceptStep: /* @__PURE__ */ __name((invocation, body) => this.interceptStep(invocation, body), "interceptStep")
|
|
7819
8111
|
};
|
|
7820
8112
|
}
|
|
@@ -7928,7 +8220,7 @@ ${run.workflowVersion}`;
|
|
|
7928
8220
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7929
8221
|
});
|
|
7930
8222
|
if (!applied) {
|
|
7931
|
-
await this.
|
|
8223
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
7932
8224
|
return this.echoCurrentStatus(run.id);
|
|
7933
8225
|
}
|
|
7934
8226
|
this.emit({
|
|
@@ -7940,7 +8232,7 @@ ${run.workflowVersion}`;
|
|
|
7940
8232
|
requires: blocked.requires,
|
|
7941
8233
|
diagnostics: blocked.diagnostics
|
|
7942
8234
|
});
|
|
7943
|
-
await this.
|
|
8235
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
7944
8236
|
return {
|
|
7945
8237
|
runId: run.id,
|
|
7946
8238
|
status: "blocked",
|
|
@@ -7956,7 +8248,7 @@ ${run.workflowVersion}`;
|
|
|
7956
8248
|
"blocked"
|
|
7957
8249
|
]
|
|
7958
8250
|
});
|
|
7959
|
-
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
8251
|
+
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || poolNamespace(r.namespace) === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
7960
8252
|
}
|
|
7961
8253
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
|
|
7962
8254
|
const group = stepGroup(step, routing?.namespace);
|
|
@@ -8495,7 +8787,7 @@ ${run.workflowVersion}`;
|
|
|
8495
8787
|
const run = await this.store.getRun(decision.runId);
|
|
8496
8788
|
if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
|
|
8497
8789
|
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
|
|
8498
|
-
if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
|
|
8790
|
+
if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
|
|
8499
8791
|
return;
|
|
8500
8792
|
}
|
|
8501
8793
|
const nowMs = this.clock();
|
|
@@ -9335,7 +9627,7 @@ var CodecStateStore = class {
|
|
|
9335
9627
|
const deleteRuns = inner.deleteRuns;
|
|
9336
9628
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
9337
9629
|
const orphans = inner.listOrphanedRuns;
|
|
9338
|
-
if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
|
|
9630
|
+
if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace, ownedBy) => (await orphans.call(inner, nowMs, limit, namespace, ownedBy)).map((r) => this.decRun(r));
|
|
9339
9631
|
const updateIf = inner.updateRunIf;
|
|
9340
9632
|
if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
|
|
9341
9633
|
const saveIfPending = inner.saveCheckpointIfPending;
|
|
@@ -9856,7 +10148,7 @@ var InMemoryStateStore = class {
|
|
|
9856
10148
|
async createRun(run) {
|
|
9857
10149
|
this.runs.set(run.id, {
|
|
9858
10150
|
...run,
|
|
9859
|
-
namespace: run.namespace
|
|
10151
|
+
namespace: run.namespace || "default"
|
|
9860
10152
|
});
|
|
9861
10153
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
9862
10154
|
}
|
|
@@ -9871,7 +10163,7 @@ var InMemoryStateStore = class {
|
|
|
9871
10163
|
applyRunPatch(runId, inputPatch) {
|
|
9872
10164
|
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
9873
10165
|
...inputPatch,
|
|
9874
|
-
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
|
|
10166
|
+
tags: withoutSingletonMarkers("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
|
|
9875
10167
|
} : inputPatch;
|
|
9876
10168
|
const existing = this.runs.get(runId);
|
|
9877
10169
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
@@ -9959,10 +10251,10 @@ var InMemoryStateStore = class {
|
|
|
9959
10251
|
return true;
|
|
9960
10252
|
});
|
|
9961
10253
|
}
|
|
9962
|
-
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
10254
|
+
async listOrphanedRuns(nowMs, limit, namespace, ownedBy) {
|
|
9963
10255
|
return [
|
|
9964
10256
|
...this.runs.values()
|
|
9965
|
-
].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
|
|
10257
|
+
].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs || ownedBy !== void 0 && r.lockedBy === ownedBy.owner && r.lockedUntil < ownedBy.lockedBefore) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
|
|
9966
10258
|
...r
|
|
9967
10259
|
}));
|
|
9968
10260
|
}
|
|
@@ -10355,6 +10647,7 @@ var InMemoryTransport = class {
|
|
|
10355
10647
|
DURABLE_STEP_NAME,
|
|
10356
10648
|
DurableWorkflow,
|
|
10357
10649
|
ENGINE_MINTED_TAG_PREFIXES,
|
|
10650
|
+
ENTITY_WORKFLOW,
|
|
10358
10651
|
EVENT_BUFFER_SCAN_LIMIT,
|
|
10359
10652
|
EVENT_TOKEN_PREFIX,
|
|
10360
10653
|
Entities,
|
|
@@ -10380,6 +10673,8 @@ var InMemoryTransport = class {
|
|
|
10380
10673
|
RunGateway,
|
|
10381
10674
|
SINGLETON_ACTIVE_STATUSES,
|
|
10382
10675
|
SINGLETON_ADMITTED_TAG,
|
|
10676
|
+
SINGLETON_RESERVED_TAGS,
|
|
10677
|
+
SINGLETON_TRACKED_TAG,
|
|
10383
10678
|
STATE_STORE,
|
|
10384
10679
|
STATE_STORE_CANONICAL,
|
|
10385
10680
|
SUMMARY_CHILD_LIMIT,
|
|
@@ -10455,6 +10750,7 @@ var InMemoryTransport = class {
|
|
|
10455
10750
|
isLegacyDescriptor,
|
|
10456
10751
|
isNonEmptyRunScope,
|
|
10457
10752
|
isWorkflowControlFlowSignal,
|
|
10753
|
+
legacySingletonHolders,
|
|
10458
10754
|
listSignalWaitersOfRuns,
|
|
10459
10755
|
liveHandoff,
|
|
10460
10756
|
log,
|
|
@@ -10472,8 +10768,10 @@ var InMemoryTransport = class {
|
|
|
10472
10768
|
pageFanFromStore,
|
|
10473
10769
|
parentRunIdOf,
|
|
10474
10770
|
parseDuration,
|
|
10771
|
+
parseWebhookToken,
|
|
10475
10772
|
partitionOfToken,
|
|
10476
10773
|
planDispatch,
|
|
10774
|
+
poolNamespace,
|
|
10477
10775
|
prevCronFireMs,
|
|
10478
10776
|
queryCheckpointsOf,
|
|
10479
10777
|
readSearchAttributes,
|
|
@@ -10509,6 +10807,8 @@ var InMemoryTransport = class {
|
|
|
10509
10807
|
tenantGroup,
|
|
10510
10808
|
useDurableExecution,
|
|
10511
10809
|
warn,
|
|
10810
|
+
withSingletonTracked,
|
|
10811
|
+
withoutSingletonMarkers,
|
|
10512
10812
|
workflowName
|
|
10513
10813
|
});
|
|
10514
10814
|
//# sourceMappingURL=index.cjs.map
|