@sanity/workflow-engine 0.7.0 → 0.9.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/_chunks-cjs/schema.cjs +83 -14
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +83 -14
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +99 -20
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +659 -58
- package/dist/define.d.ts +659 -58
- package/dist/define.js +100 -21
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +708 -255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1180 -97
- package/dist/index.d.ts +1180 -97
- package/dist/index.js +710 -256
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -17,13 +17,8 @@ function _interopNamespaceCompat(e) {
|
|
|
17
17
|
}), n.default = e, Object.freeze(n);
|
|
18
18
|
}
|
|
19
19
|
var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
for (const part of path.split(".")) {
|
|
23
|
-
if (current == null || typeof current != "object") return;
|
|
24
|
-
current = current[part];
|
|
25
|
-
}
|
|
26
|
-
return current;
|
|
20
|
+
function findOpenStageEntry(host) {
|
|
21
|
+
return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
|
|
27
22
|
}
|
|
28
23
|
const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
29
24
|
function parseGdr(uri) {
|
|
@@ -127,12 +122,100 @@ function gdrRef(res, documentId, type) {
|
|
|
127
122
|
function isGdr(value) {
|
|
128
123
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
129
124
|
}
|
|
125
|
+
function buildParams(args) {
|
|
126
|
+
const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
|
|
127
|
+
return {
|
|
128
|
+
self: selfGdr(instance),
|
|
129
|
+
state: renderedState(instance.state ?? [], snapshot),
|
|
130
|
+
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
131
|
+
ancestors: instance.ancestors.map((a) => a.id),
|
|
132
|
+
stage: instance.currentStage,
|
|
133
|
+
now,
|
|
134
|
+
/** `$effects` — parent context handoff by entry name, completed-effect
|
|
135
|
+
* outputs namespaced under the effect's name (`$effects['x.y'].out`). */
|
|
136
|
+
effects: effectsContextMap(instance),
|
|
137
|
+
/** `$tasks` — the current stage's task rows, statuses included. */
|
|
138
|
+
tasks: currentTasks2,
|
|
139
|
+
allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
|
|
140
|
+
anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
|
|
141
|
+
...extra
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function renderedState(entries, snapshot) {
|
|
145
|
+
const out = {};
|
|
146
|
+
for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function renderedValue(entry, snapshot) {
|
|
150
|
+
return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
|
|
151
|
+
}
|
|
152
|
+
function scopedStateOverlay(instance, snapshot, taskName) {
|
|
153
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
154
|
+
if (stageEntry === void 0) return {};
|
|
155
|
+
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
156
|
+
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
157
|
+
}
|
|
158
|
+
function assignedFor(instance, taskName, actor, roleAliases) {
|
|
159
|
+
if (actor === void 0) return !1;
|
|
160
|
+
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
161
|
+
return entry === void 0 ? !1 : entry.value.some(
|
|
162
|
+
(a) => a.type === "user" ? a.id === actor.id : schema.actorFulfillsRole(actor.roles, a.role, roleAliases)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
function effectsContextMap(instance) {
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const entry of instance.effectsContext)
|
|
168
|
+
out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
function parseJsonContextEntry(entry) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(entry.value);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
|
+
{ cause: err }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function paramsForLake(params) {
|
|
182
|
+
return {
|
|
183
|
+
...params,
|
|
184
|
+
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
185
|
+
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
186
|
+
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
187
|
+
state: stripStateForLake(params.state)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function stripStateForLake(value) {
|
|
191
|
+
if (value == null) return value;
|
|
192
|
+
if (typeof value == "string") return bareId(value);
|
|
193
|
+
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
194
|
+
if (typeof value == "object") {
|
|
195
|
+
const out = {};
|
|
196
|
+
for (const [k, v2] of Object.entries(value))
|
|
197
|
+
out[k] = stripStateForLake(v2);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function bareId(id) {
|
|
203
|
+
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
204
|
+
}
|
|
205
|
+
function getPath(value, path) {
|
|
206
|
+
let current = value;
|
|
207
|
+
for (const part of path.split(".")) {
|
|
208
|
+
if (current == null || typeof current != "object") return;
|
|
209
|
+
current = current[part];
|
|
210
|
+
}
|
|
211
|
+
return current;
|
|
212
|
+
}
|
|
130
213
|
function resourceOf(p) {
|
|
131
214
|
return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
|
|
132
215
|
}
|
|
133
|
-
const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
|
|
216
|
+
const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
|
|
134
217
|
function isGuardReadExpr(expr) {
|
|
135
|
-
return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
|
|
218
|
+
return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
|
|
136
219
|
}
|
|
137
220
|
function resolveGuardRead(expr, ctx) {
|
|
138
221
|
if (expr === "$self") return selfGdr(ctx.instance);
|
|
@@ -142,8 +225,13 @@ function resolveGuardRead(expr, ctx) {
|
|
|
142
225
|
const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
|
|
143
226
|
return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
|
|
144
227
|
}
|
|
228
|
+
const effectsRead = EFFECTS_READ.exec(expr);
|
|
229
|
+
if (effectsRead !== null) {
|
|
230
|
+
const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
|
|
231
|
+
return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
|
|
232
|
+
}
|
|
145
233
|
throw new Error(
|
|
146
|
-
`Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now",
|
|
234
|
+
`Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
|
|
147
235
|
);
|
|
148
236
|
}
|
|
149
237
|
function resolveIdRefTarget(expr, ctx) {
|
|
@@ -225,7 +313,7 @@ function createDefinitionValidator() {
|
|
|
225
313
|
entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
|
|
226
314
|
}, checkGuardRead: (expr, where) => {
|
|
227
315
|
isGuardReadExpr(expr) || errors.push(
|
|
228
|
-
` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now",
|
|
316
|
+
` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
|
|
229
317
|
);
|
|
230
318
|
} };
|
|
231
319
|
}
|
|
@@ -404,89 +492,6 @@ async function assertInstanceWriteAllowed(args) {
|
|
|
404
492
|
guards: denied
|
|
405
493
|
});
|
|
406
494
|
}
|
|
407
|
-
function findOpenStageEntry(host) {
|
|
408
|
-
return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
|
|
409
|
-
}
|
|
410
|
-
function buildParams(args) {
|
|
411
|
-
const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
|
|
412
|
-
return {
|
|
413
|
-
self: selfGdr(instance),
|
|
414
|
-
state: renderedState(instance.state ?? [], snapshot),
|
|
415
|
-
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
416
|
-
ancestors: instance.ancestors.map((a) => a.id),
|
|
417
|
-
stage: instance.currentStage,
|
|
418
|
-
now,
|
|
419
|
-
/** `$effects` — parent context handoff by entry name, completed-effect
|
|
420
|
-
* outputs namespaced under the effect's name (`$effects['x.y'].out`). */
|
|
421
|
-
effects: effectsContextMap(instance),
|
|
422
|
-
/** `$tasks` — the current stage's task rows, statuses included. */
|
|
423
|
-
tasks: currentTasks2,
|
|
424
|
-
allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
|
|
425
|
-
anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
|
|
426
|
-
...extra
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
function renderedState(entries, snapshot) {
|
|
430
|
-
const out = {};
|
|
431
|
-
for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
|
|
432
|
-
return out;
|
|
433
|
-
}
|
|
434
|
-
function renderedValue(entry, snapshot) {
|
|
435
|
-
return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
|
|
436
|
-
}
|
|
437
|
-
function scopedStateOverlay(instance, snapshot, taskName) {
|
|
438
|
-
const stageEntry = findOpenStageEntry(instance);
|
|
439
|
-
if (stageEntry === void 0) return {};
|
|
440
|
-
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
441
|
-
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
442
|
-
}
|
|
443
|
-
function assignedFor(instance, taskName, actor) {
|
|
444
|
-
if (actor === void 0) return !1;
|
|
445
|
-
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
446
|
-
return entry === void 0 ? !1 : entry.value.some(
|
|
447
|
-
(a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
function effectsContextMap(instance) {
|
|
451
|
-
const out = {};
|
|
452
|
-
for (const entry of instance.effectsContext)
|
|
453
|
-
out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
|
|
454
|
-
return out;
|
|
455
|
-
}
|
|
456
|
-
function parseJsonContextEntry(entry) {
|
|
457
|
-
try {
|
|
458
|
-
return JSON.parse(entry.value);
|
|
459
|
-
} catch (err) {
|
|
460
|
-
throw new Error(
|
|
461
|
-
`effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
462
|
-
{ cause: err }
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function paramsForLake(params) {
|
|
467
|
-
return {
|
|
468
|
-
...params,
|
|
469
|
-
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
470
|
-
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
471
|
-
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
472
|
-
state: stripStateForLake(params.state)
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
function stripStateForLake(value) {
|
|
476
|
-
if (value == null) return value;
|
|
477
|
-
if (typeof value == "string") return bareId(value);
|
|
478
|
-
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
479
|
-
if (typeof value == "object") {
|
|
480
|
-
const out = {};
|
|
481
|
-
for (const [k, v2] of Object.entries(value))
|
|
482
|
-
out[k] = stripStateForLake(v2);
|
|
483
|
-
return out;
|
|
484
|
-
}
|
|
485
|
-
return value;
|
|
486
|
-
}
|
|
487
|
-
function bareId(id) {
|
|
488
|
-
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
489
|
-
}
|
|
490
495
|
function abortReason(instance) {
|
|
491
496
|
return instance.history.find(
|
|
492
497
|
(h) => h._type === "aborted"
|
|
@@ -609,6 +614,90 @@ function remediationsFor(diagnosis) {
|
|
|
609
614
|
available: RUNNABLE_VERBS.has(seed.verb)
|
|
610
615
|
}));
|
|
611
616
|
}
|
|
617
|
+
class ActionParamsInvalidError extends Error {
|
|
618
|
+
action;
|
|
619
|
+
task;
|
|
620
|
+
issues;
|
|
621
|
+
constructor(args) {
|
|
622
|
+
const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
|
|
623
|
+
`);
|
|
624
|
+
super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
|
|
625
|
+
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
class RequiredStateNotProvidedError extends Error {
|
|
629
|
+
definition;
|
|
630
|
+
missing;
|
|
631
|
+
constructor(args) {
|
|
632
|
+
const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
|
|
633
|
+
`), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
|
|
634
|
+
super(
|
|
635
|
+
`Required init state${where} was not provided:
|
|
636
|
+
${lines}
|
|
637
|
+
Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
|
|
638
|
+
), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
class WorkflowStateDivergedError extends Error {
|
|
642
|
+
instanceId;
|
|
643
|
+
/** The guard-deploy failure that triggered the (attempted) rollback. */
|
|
644
|
+
guardError;
|
|
645
|
+
/** The rollback failure, when rollback was attempted and itself failed. */
|
|
646
|
+
rollbackError;
|
|
647
|
+
constructor(args) {
|
|
648
|
+
super(
|
|
649
|
+
`Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
|
|
650
|
+
{ cause: args.guardError }
|
|
651
|
+
), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
class PartialGuardDeployError extends Error {
|
|
655
|
+
stageName;
|
|
656
|
+
deployed;
|
|
657
|
+
constructor(args) {
|
|
658
|
+
super(
|
|
659
|
+
`Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
|
|
660
|
+
{ cause: args.cause }
|
|
661
|
+
), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
665
|
+
class ConcurrentFireActionError extends Error {
|
|
666
|
+
instanceId;
|
|
667
|
+
task;
|
|
668
|
+
action;
|
|
669
|
+
attempts;
|
|
670
|
+
constructor(args) {
|
|
671
|
+
super(
|
|
672
|
+
`Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
|
|
673
|
+
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function isRevisionConflict(error) {
|
|
677
|
+
if (typeof error != "object" || error === null) return !1;
|
|
678
|
+
const { statusCode, message } = error;
|
|
679
|
+
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
680
|
+
}
|
|
681
|
+
class ConcurrentEditStateError extends Error {
|
|
682
|
+
instanceId;
|
|
683
|
+
target;
|
|
684
|
+
attempts;
|
|
685
|
+
constructor(args) {
|
|
686
|
+
const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
|
|
687
|
+
super(
|
|
688
|
+
`Edit of "${args.target.scope}:${where}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
|
|
689
|
+
), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
class CascadeLimitError extends Error {
|
|
693
|
+
instanceId;
|
|
694
|
+
limit;
|
|
695
|
+
constructor(args) {
|
|
696
|
+
super(
|
|
697
|
+
`Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
|
|
698
|
+
), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
612
701
|
function buildSnapshot(args) {
|
|
613
702
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
614
703
|
for (const { doc, resource } of args.docs) {
|
|
@@ -874,9 +963,16 @@ async function committedInstance(args) {
|
|
|
874
963
|
}
|
|
875
964
|
async function deployStageGuards(args) {
|
|
876
965
|
const live = await committedInstance(args);
|
|
877
|
-
if (
|
|
878
|
-
|
|
966
|
+
if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
|
|
967
|
+
let deployed = 0;
|
|
968
|
+
for (const { client, doc } of resolvedStageGuards(args)) {
|
|
969
|
+
try {
|
|
879
970
|
await upsertGuard(client, doc);
|
|
971
|
+
} catch (cause) {
|
|
972
|
+
throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
|
|
973
|
+
}
|
|
974
|
+
deployed += 1;
|
|
975
|
+
}
|
|
880
976
|
}
|
|
881
977
|
async function retractStageGuards(args) {
|
|
882
978
|
const live = await committedInstance(args);
|
|
@@ -1065,6 +1161,7 @@ function derivePerspectiveFromState(entries) {
|
|
|
1065
1161
|
async function resolveDeclaredState(args) {
|
|
1066
1162
|
const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
|
|
1067
1163
|
if (entryDefs === void 0 || entryDefs.length === 0) return [];
|
|
1164
|
+
assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
|
|
1068
1165
|
const out = [];
|
|
1069
1166
|
for (const entry of entryDefs) {
|
|
1070
1167
|
const scopedCtx = { ...ctx, resolvedState: out };
|
|
@@ -1072,6 +1169,17 @@ async function resolveDeclaredState(args) {
|
|
|
1072
1169
|
}
|
|
1073
1170
|
return out;
|
|
1074
1171
|
}
|
|
1172
|
+
function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
|
|
1173
|
+
const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
|
|
1174
|
+
const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
|
|
1175
|
+
return match === void 0 || match.value === void 0 || match.value === null;
|
|
1176
|
+
}).map((entry) => ({ name: entry.name, type: entry.type }));
|
|
1177
|
+
if (missing.length !== 0)
|
|
1178
|
+
throw new RequiredStateNotProvidedError({
|
|
1179
|
+
...definitionName !== void 0 ? { definition: definitionName } : {},
|
|
1180
|
+
missing
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1075
1183
|
const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
1076
1184
|
"doc.refs",
|
|
1077
1185
|
"checklist",
|
|
@@ -1200,6 +1308,12 @@ function coerceToGdr(raw, workflowResource) {
|
|
|
1200
1308
|
function gdrToBareId(uri) {
|
|
1201
1309
|
return uri.includes(":") ? extractDocumentId(uri) : uri;
|
|
1202
1310
|
}
|
|
1311
|
+
function loadCallContext(client, instanceId, options) {
|
|
1312
|
+
return loadContext(client, instanceId, {
|
|
1313
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1314
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1203
1317
|
async function loadContext(client, instanceId, options) {
|
|
1204
1318
|
const instance = await client.getDocument(instanceId);
|
|
1205
1319
|
if (!instance)
|
|
@@ -1220,7 +1334,7 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1220
1334
|
...base,
|
|
1221
1335
|
state,
|
|
1222
1336
|
actor: opts?.actor,
|
|
1223
|
-
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
|
|
1337
|
+
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
|
|
1224
1338
|
...opts?.vars
|
|
1225
1339
|
};
|
|
1226
1340
|
return { ...await evaluatePredicates({
|
|
@@ -1265,86 +1379,36 @@ async function resolveStageStateEntries(args) {
|
|
|
1265
1379
|
ctx: {
|
|
1266
1380
|
client,
|
|
1267
1381
|
now,
|
|
1268
|
-
selfId: instance._id,
|
|
1269
|
-
tag: instance.tag,
|
|
1270
|
-
stageName: stage.name,
|
|
1271
|
-
workflowResource: instance.workflowResource,
|
|
1272
|
-
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
1273
|
-
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
1274
|
-
workflowState: instance.state ?? [],
|
|
1275
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1276
|
-
},
|
|
1277
|
-
randomKey
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
async function resolveTaskStateEntries(args) {
|
|
1281
|
-
const { client, instance, stage, task, now } = args;
|
|
1282
|
-
return resolveDeclaredState({
|
|
1283
|
-
entryDefs: task.state,
|
|
1284
|
-
initialState: [],
|
|
1285
|
-
ctx: {
|
|
1286
|
-
client,
|
|
1287
|
-
now,
|
|
1288
|
-
selfId: instance._id,
|
|
1289
|
-
tag: instance.tag,
|
|
1290
|
-
stageName: stage.name,
|
|
1291
|
-
workflowResource: instance.workflowResource,
|
|
1292
|
-
taskName: task.name,
|
|
1293
|
-
workflowState: instance.state ?? [],
|
|
1294
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1295
|
-
},
|
|
1296
|
-
randomKey
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
class ActionParamsInvalidError extends Error {
|
|
1300
|
-
action;
|
|
1301
|
-
task;
|
|
1302
|
-
issues;
|
|
1303
|
-
constructor(args) {
|
|
1304
|
-
const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
|
|
1305
|
-
`);
|
|
1306
|
-
super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
|
|
1307
|
-
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
class WorkflowStateDivergedError extends Error {
|
|
1311
|
-
instanceId;
|
|
1312
|
-
/** The guard-deploy failure that triggered the (attempted) rollback. */
|
|
1313
|
-
guardError;
|
|
1314
|
-
/** The rollback failure, when rollback was attempted and itself failed. */
|
|
1315
|
-
rollbackError;
|
|
1316
|
-
constructor(args) {
|
|
1317
|
-
super(
|
|
1318
|
-
`Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
|
|
1319
|
-
{ cause: args.guardError }
|
|
1320
|
-
), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
1324
|
-
class ConcurrentFireActionError extends Error {
|
|
1325
|
-
instanceId;
|
|
1326
|
-
task;
|
|
1327
|
-
action;
|
|
1328
|
-
attempts;
|
|
1329
|
-
constructor(args) {
|
|
1330
|
-
super(
|
|
1331
|
-
`Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
|
|
1332
|
-
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function isRevisionConflict(error) {
|
|
1336
|
-
if (typeof error != "object" || error === null) return !1;
|
|
1337
|
-
const { statusCode, message } = error;
|
|
1338
|
-
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
1382
|
+
selfId: instance._id,
|
|
1383
|
+
tag: instance.tag,
|
|
1384
|
+
stageName: stage.name,
|
|
1385
|
+
workflowResource: instance.workflowResource,
|
|
1386
|
+
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
1387
|
+
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
1388
|
+
workflowState: instance.state ?? [],
|
|
1389
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1390
|
+
},
|
|
1391
|
+
randomKey
|
|
1392
|
+
});
|
|
1339
1393
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1394
|
+
async function resolveTaskStateEntries(args) {
|
|
1395
|
+
const { client, instance, stage, task, now } = args;
|
|
1396
|
+
return resolveDeclaredState({
|
|
1397
|
+
entryDefs: task.state,
|
|
1398
|
+
initialState: [],
|
|
1399
|
+
ctx: {
|
|
1400
|
+
client,
|
|
1401
|
+
now,
|
|
1402
|
+
selfId: instance._id,
|
|
1403
|
+
tag: instance.tag,
|
|
1404
|
+
stageName: stage.name,
|
|
1405
|
+
workflowResource: instance.workflowResource,
|
|
1406
|
+
taskName: task.name,
|
|
1407
|
+
workflowState: instance.state ?? [],
|
|
1408
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1409
|
+
},
|
|
1410
|
+
randomKey
|
|
1411
|
+
});
|
|
1348
1412
|
}
|
|
1349
1413
|
async function deployOrRollback(args) {
|
|
1350
1414
|
try {
|
|
@@ -1367,7 +1431,11 @@ async function deployOrRollback(args) {
|
|
|
1367
1431
|
reason: "rollback of the committed move failed"
|
|
1368
1432
|
});
|
|
1369
1433
|
}
|
|
1370
|
-
throw guardError
|
|
1434
|
+
throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
|
|
1435
|
+
instanceId: args.instanceId,
|
|
1436
|
+
guardError,
|
|
1437
|
+
reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
|
|
1438
|
+
}) : guardError;
|
|
1371
1439
|
}
|
|
1372
1440
|
}
|
|
1373
1441
|
function applyTaskStatusChange(args) {
|
|
@@ -1456,7 +1524,8 @@ function materializeInstance(base, mutation) {
|
|
|
1456
1524
|
...base,
|
|
1457
1525
|
currentStage: mutation.currentStage,
|
|
1458
1526
|
state: mutation.state,
|
|
1459
|
-
stages: mutation.stages
|
|
1527
|
+
stages: mutation.stages,
|
|
1528
|
+
effectsContext: mutation.effectsContext
|
|
1460
1529
|
};
|
|
1461
1530
|
}
|
|
1462
1531
|
async function persist(ctx, mutation) {
|
|
@@ -1471,7 +1540,21 @@ async function persist(ctx, mutation) {
|
|
|
1471
1540
|
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1472
1541
|
const actorForPriming = void 0;
|
|
1473
1542
|
for (const body of pendingCreates)
|
|
1474
|
-
|
|
1543
|
+
try {
|
|
1544
|
+
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
|
|
1545
|
+
ctx.client,
|
|
1546
|
+
body._id,
|
|
1547
|
+
actorForPriming,
|
|
1548
|
+
ctx.clientForGdr,
|
|
1549
|
+
ctx.clock
|
|
1550
|
+
), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
1551
|
+
} catch (cause) {
|
|
1552
|
+
throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
|
|
1553
|
+
instanceId: ctx.instance._id,
|
|
1554
|
+
guardError: cause,
|
|
1555
|
+
reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1475
1558
|
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1476
1559
|
if (!reloaded)
|
|
1477
1560
|
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
@@ -1736,6 +1819,7 @@ async function prepareChildInstance(args) {
|
|
|
1736
1819
|
selfId: childDocId,
|
|
1737
1820
|
tag: childTag,
|
|
1738
1821
|
workflowResource,
|
|
1822
|
+
definitionName: definition.name,
|
|
1739
1823
|
...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
|
|
1740
1824
|
},
|
|
1741
1825
|
randomKey
|
|
@@ -1782,6 +1866,28 @@ async function resolveBindings(args) {
|
|
|
1782
1866
|
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
1783
1867
|
return { ...resolved, ...args.staticInput };
|
|
1784
1868
|
}
|
|
1869
|
+
async function persistThenDeploy(ctx, mutation, deploy) {
|
|
1870
|
+
const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
|
|
1871
|
+
await deployOrRollback({
|
|
1872
|
+
client: ctx.client,
|
|
1873
|
+
instanceId: ctx.instance._id,
|
|
1874
|
+
committedRev: committed._rev,
|
|
1875
|
+
restore: instanceStateFields(ctx.instance),
|
|
1876
|
+
unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
|
|
1877
|
+
reversible: !spawned,
|
|
1878
|
+
deploy
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
async function refreshStageGuards(ctx, mutation, stageName) {
|
|
1882
|
+
await deployStageGuards({
|
|
1883
|
+
client: ctx.client,
|
|
1884
|
+
clientForGdr: ctx.clientForGdr,
|
|
1885
|
+
instance: materializeInstance(ctx.instance, mutation),
|
|
1886
|
+
definition: ctx.definition,
|
|
1887
|
+
stageName,
|
|
1888
|
+
now: ctx.now
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1785
1891
|
async function completeEffect(args) {
|
|
1786
1892
|
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1787
1893
|
...options?.clock ? { clock: options.clock } : {},
|
|
@@ -1827,9 +1933,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
|
|
|
1827
1933
|
const mutation = startMutation(ctx.instance);
|
|
1828
1934
|
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1829
1935
|
const ranAt = ctx.now;
|
|
1830
|
-
|
|
1936
|
+
mutation.effectHistory.push(
|
|
1831
1937
|
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1832
|
-
)
|
|
1938
|
+
);
|
|
1939
|
+
const wroteEffectsContext = status === "done" && outputs !== void 0;
|
|
1940
|
+
return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
1833
1941
|
_key: randomKey(),
|
|
1834
1942
|
_type: "effectCompleted",
|
|
1835
1943
|
at: ranAt,
|
|
@@ -1839,7 +1947,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
|
|
|
1839
1947
|
...outputs !== void 0 ? { outputs } : {},
|
|
1840
1948
|
...detail !== void 0 ? { detail } : {},
|
|
1841
1949
|
...actor !== void 0 ? { actor } : {}
|
|
1842
|
-
}), await
|
|
1950
|
+
}), await persistThenDeploy(
|
|
1951
|
+
ctx,
|
|
1952
|
+
mutation,
|
|
1953
|
+
() => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
|
|
1954
|
+
), { effectKey, status };
|
|
1843
1955
|
}
|
|
1844
1956
|
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1845
1957
|
const key = randomKey(), pending = {
|
|
@@ -1953,22 +2065,27 @@ function runOps(args) {
|
|
|
1953
2065
|
const summaries = [];
|
|
1954
2066
|
for (const op of ops) {
|
|
1955
2067
|
const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
|
|
1956
|
-
summaries.push(summary), mutation.history.push({
|
|
1957
|
-
_key: randomKey(),
|
|
1958
|
-
_type: "opApplied",
|
|
1959
|
-
at: now,
|
|
1960
|
-
stage,
|
|
1961
|
-
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
1962
|
-
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
1963
|
-
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
1964
|
-
opType: summary.opType,
|
|
1965
|
-
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
1966
|
-
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
1967
|
-
...actor !== void 0 ? { actor } : {}
|
|
1968
|
-
});
|
|
2068
|
+
summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
|
|
1969
2069
|
}
|
|
1970
2070
|
return summaries;
|
|
1971
2071
|
}
|
|
2072
|
+
function opAppliedEntry(args) {
|
|
2073
|
+
const { origin, summary, stage, actor, now } = args;
|
|
2074
|
+
return {
|
|
2075
|
+
_key: randomKey(),
|
|
2076
|
+
_type: "opApplied",
|
|
2077
|
+
at: now,
|
|
2078
|
+
stage,
|
|
2079
|
+
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2080
|
+
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2081
|
+
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2082
|
+
...origin.edit === !0 ? { edit: !0 } : {},
|
|
2083
|
+
opType: summary.opType,
|
|
2084
|
+
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2085
|
+
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2086
|
+
...actor !== void 0 ? { actor } : {}
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
1972
2089
|
function applyOp(op, ctx) {
|
|
1973
2090
|
switch (op.type) {
|
|
1974
2091
|
case "state.set":
|
|
@@ -2299,28 +2416,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
|
|
|
2299
2416
|
now: ctx.now
|
|
2300
2417
|
});
|
|
2301
2418
|
}
|
|
2302
|
-
async function persistThenDeploy(ctx, mutation, deploy) {
|
|
2303
|
-
const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
|
|
2304
|
-
await deployOrRollback({
|
|
2305
|
-
client: ctx.client,
|
|
2306
|
-
instanceId: ctx.instance._id,
|
|
2307
|
-
committedRev: committed._rev,
|
|
2308
|
-
restore: instanceStateFields(ctx.instance),
|
|
2309
|
-
unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
|
|
2310
|
-
reversible: !spawned,
|
|
2311
|
-
deploy
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
async function refreshStageGuards(ctx, mutation, stageName) {
|
|
2315
|
-
await deployStageGuards({
|
|
2316
|
-
client: ctx.client,
|
|
2317
|
-
clientForGdr: ctx.clientForGdr,
|
|
2318
|
-
instance: materializeInstance(ctx.instance, mutation),
|
|
2319
|
-
definition: ctx.definition,
|
|
2320
|
-
stageName,
|
|
2321
|
-
now: ctx.now
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
2419
|
async function commitTransition(ctx, actor) {
|
|
2325
2420
|
if (isTerminal(ctx))
|
|
2326
2421
|
return { fired: !1 };
|
|
@@ -2624,6 +2719,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2624
2719
|
return;
|
|
2625
2720
|
}
|
|
2626
2721
|
}
|
|
2722
|
+
function effectiveEditable(baseline, override) {
|
|
2723
|
+
if (baseline === void 0) return;
|
|
2724
|
+
if (override === void 0) return baseline;
|
|
2725
|
+
if (baseline === !0 && override === !0) return !0;
|
|
2726
|
+
const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
|
|
2727
|
+
return schema.andConditions(parts) ?? !0;
|
|
2728
|
+
}
|
|
2729
|
+
function slotSites(definition, stage) {
|
|
2730
|
+
const sites = [];
|
|
2731
|
+
for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
|
|
2732
|
+
for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
|
|
2733
|
+
for (const task of stage.tasks ?? [])
|
|
2734
|
+
for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
|
|
2735
|
+
return sites;
|
|
2736
|
+
}
|
|
2737
|
+
function toResolvedSlot(site, stage) {
|
|
2738
|
+
return {
|
|
2739
|
+
scope: site.scope,
|
|
2740
|
+
...site.task !== void 0 ? { task: site.task } : {},
|
|
2741
|
+
name: site.entry.name,
|
|
2742
|
+
type: site.entry.type,
|
|
2743
|
+
...site.entry.title !== void 0 ? { title: site.entry.title } : {},
|
|
2744
|
+
ref: { scope: site.scope, state: site.entry.name },
|
|
2745
|
+
effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
function editableSlotsInStage(definition, stage) {
|
|
2749
|
+
return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
|
|
2750
|
+
}
|
|
2751
|
+
function editTargetMatches(slot, target) {
|
|
2752
|
+
return slot.name !== target.state ? !1 : target.task !== void 0 ? slot.scope === "task" && slot.task === target.task : target.scope !== void 0 ? slot.scope === target.scope : !0;
|
|
2753
|
+
}
|
|
2754
|
+
const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
|
|
2755
|
+
function resolveEditTarget(args) {
|
|
2756
|
+
const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
|
|
2757
|
+
(site) => editTargetMatches(
|
|
2758
|
+
{
|
|
2759
|
+
scope: site.scope,
|
|
2760
|
+
name: site.entry.name,
|
|
2761
|
+
...site.task !== void 0 ? { task: site.task } : {}
|
|
2762
|
+
},
|
|
2763
|
+
target
|
|
2764
|
+
)
|
|
2765
|
+
).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
2766
|
+
return found ? toResolvedSlot(found, stage) : void 0;
|
|
2767
|
+
}
|
|
2768
|
+
function slotWindowOpen(instance, slot) {
|
|
2769
|
+
if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
|
|
2770
|
+
if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
|
|
2771
|
+
if (slot.scope !== "task") return { open: !0 };
|
|
2772
|
+
const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
|
|
2773
|
+
return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
|
|
2774
|
+
}
|
|
2775
|
+
function readSlotValue(instance, slot) {
|
|
2776
|
+
return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
|
|
2777
|
+
}
|
|
2778
|
+
function slotStateHost(instance, slot) {
|
|
2779
|
+
if (slot.scope === "workflow") return instance.state;
|
|
2780
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
2781
|
+
return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
|
|
2782
|
+
}
|
|
2783
|
+
function slotProvenance(instance, ref) {
|
|
2784
|
+
let latest;
|
|
2785
|
+
for (const entry of instance.history)
|
|
2786
|
+
entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
|
|
2787
|
+
return latest === void 0 ? {} : {
|
|
2788
|
+
...latest.actor !== void 0 ? { setBy: latest.actor } : {},
|
|
2789
|
+
setAt: latest.at
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function editDisabledReason(args) {
|
|
2793
|
+
const { effective, instance, window, guardDenial, predicateSatisfied } = args;
|
|
2794
|
+
if (effective === void 0) return { kind: "not-editable" };
|
|
2795
|
+
if (!window.open)
|
|
2796
|
+
return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
|
|
2797
|
+
if (guardDenial !== void 0) return guardDenial;
|
|
2798
|
+
if (!predicateSatisfied)
|
|
2799
|
+
return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
|
|
2800
|
+
}
|
|
2627
2801
|
async function evaluateInstance(args) {
|
|
2628
2802
|
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2629
2803
|
validateTag(tag);
|
|
@@ -2657,6 +2831,7 @@ async function evaluateFromSnapshot(args) {
|
|
|
2657
2831
|
actor,
|
|
2658
2832
|
snapshot,
|
|
2659
2833
|
scope,
|
|
2834
|
+
roleAliases: definition.roleAliases,
|
|
2660
2835
|
stageHasExits: !isTerminalStage(stage),
|
|
2661
2836
|
guardDenial
|
|
2662
2837
|
})
|
|
@@ -2678,16 +2853,65 @@ async function evaluateFromSnapshot(args) {
|
|
|
2678
2853
|
stage,
|
|
2679
2854
|
tasks: taskEvaluations,
|
|
2680
2855
|
transitions: transitionEvaluations
|
|
2681
|
-
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed))
|
|
2856
|
+
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableSlots = await evaluateEditableSlots({
|
|
2857
|
+
instance,
|
|
2858
|
+
definition,
|
|
2859
|
+
stage,
|
|
2860
|
+
actor,
|
|
2861
|
+
snapshot,
|
|
2862
|
+
scope,
|
|
2863
|
+
guardDenial: editGuardDenial
|
|
2864
|
+
});
|
|
2682
2865
|
return {
|
|
2683
2866
|
instance,
|
|
2684
2867
|
definition,
|
|
2685
2868
|
actor,
|
|
2686
2869
|
currentStage,
|
|
2687
2870
|
pendingOnYou,
|
|
2688
|
-
canInteract
|
|
2871
|
+
canInteract,
|
|
2872
|
+
editableSlots
|
|
2689
2873
|
};
|
|
2690
2874
|
}
|
|
2875
|
+
async function evaluateEditableSlots(args) {
|
|
2876
|
+
const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
|
|
2877
|
+
for (const slot of editableSlotsInStage(definition, stage)) {
|
|
2878
|
+
const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
|
|
2879
|
+
slot,
|
|
2880
|
+
window,
|
|
2881
|
+
guardDenial,
|
|
2882
|
+
instance,
|
|
2883
|
+
snapshot,
|
|
2884
|
+
scope,
|
|
2885
|
+
actor,
|
|
2886
|
+
roleAliases: definition.roleAliases
|
|
2887
|
+
}), reason = editDisabledReason({
|
|
2888
|
+
effective: slot.effective,
|
|
2889
|
+
instance,
|
|
2890
|
+
window,
|
|
2891
|
+
guardDenial,
|
|
2892
|
+
predicateSatisfied
|
|
2893
|
+
}), value = readSlotValue(instance, slot);
|
|
2894
|
+
slots.push({
|
|
2895
|
+
scope: slot.scope,
|
|
2896
|
+
...slot.task !== void 0 ? { task: slot.task } : {},
|
|
2897
|
+
name: slot.name,
|
|
2898
|
+
type: slot.type,
|
|
2899
|
+
...slot.title !== void 0 ? { title: slot.title } : {},
|
|
2900
|
+
value,
|
|
2901
|
+
editable: reason === void 0,
|
|
2902
|
+
...reason !== void 0 ? { disabledReason: reason } : {},
|
|
2903
|
+
...slotProvenance(instance, slot.ref)
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
return slots;
|
|
2907
|
+
}
|
|
2908
|
+
async function editPredicateSatisfied(args) {
|
|
2909
|
+
const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
|
|
2910
|
+
if (!window.open || guardDenial !== void 0) return !1;
|
|
2911
|
+
if (slot.effective === !0) return !0;
|
|
2912
|
+
const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
|
|
2913
|
+
return evaluateCondition({ condition: slot.effective, snapshot, params });
|
|
2914
|
+
}
|
|
2691
2915
|
async function renderScope(args) {
|
|
2692
2916
|
const { instance, definition, actor, grants, snapshot, now } = args, params = {
|
|
2693
2917
|
...buildParams({ instance, now, snapshot }),
|
|
@@ -2713,15 +2937,36 @@ async function advisoryCan(instance, actor, grants) {
|
|
|
2713
2937
|
});
|
|
2714
2938
|
return can;
|
|
2715
2939
|
}
|
|
2716
|
-
|
|
2717
|
-
const {
|
|
2940
|
+
function taskScopeFor(args) {
|
|
2941
|
+
const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
|
|
2942
|
+
return {
|
|
2718
2943
|
...scope,
|
|
2719
2944
|
state: {
|
|
2720
2945
|
...scope.state,
|
|
2721
|
-
...scopedStateOverlay(instance, snapshot,
|
|
2946
|
+
...scopedStateOverlay(instance, snapshot, taskName)
|
|
2722
2947
|
},
|
|
2723
|
-
assigned
|
|
2724
|
-
}
|
|
2948
|
+
assigned: assignedFor(instance, taskName, actor, roleAliases)
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
async function evaluateTask(args) {
|
|
2952
|
+
const {
|
|
2953
|
+
task,
|
|
2954
|
+
statusEntry,
|
|
2955
|
+
instance,
|
|
2956
|
+
actor,
|
|
2957
|
+
snapshot,
|
|
2958
|
+
scope,
|
|
2959
|
+
roleAliases,
|
|
2960
|
+
stageHasExits,
|
|
2961
|
+
guardDenial
|
|
2962
|
+
} = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
|
|
2963
|
+
scope,
|
|
2964
|
+
instance,
|
|
2965
|
+
snapshot,
|
|
2966
|
+
actor,
|
|
2967
|
+
taskName: task.name,
|
|
2968
|
+
roleAliases
|
|
2969
|
+
}), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
|
|
2725
2970
|
requirements: task.requirements,
|
|
2726
2971
|
snapshot,
|
|
2727
2972
|
params: taskScope
|
|
@@ -2948,10 +3193,7 @@ async function commitAbort(ctx, reason, actor) {
|
|
|
2948
3193
|
async function fireAction(args) {
|
|
2949
3194
|
const { client, instanceId, task, action, params, options } = args;
|
|
2950
3195
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2951
|
-
const ctx = await
|
|
2952
|
-
...options?.clock ? { clock: options.clock } : {},
|
|
2953
|
-
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2954
|
-
});
|
|
3196
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
2955
3197
|
try {
|
|
2956
3198
|
return await commitAction(ctx, task, action, params, options);
|
|
2957
3199
|
} catch (error) {
|
|
@@ -3046,6 +3288,114 @@ function formatDisabledReason(task, action, reason) {
|
|
|
3046
3288
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
3047
3289
|
return `Action "${task}:${action}" is not allowed: ${detail}`;
|
|
3048
3290
|
}
|
|
3291
|
+
class EditStateDeniedError extends Error {
|
|
3292
|
+
reason;
|
|
3293
|
+
target;
|
|
3294
|
+
constructor(args) {
|
|
3295
|
+
super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
const editDisabledReasonDetail = {
|
|
3299
|
+
"not-editable": () => "slot is not declared editable",
|
|
3300
|
+
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
3301
|
+
"edit-window-closed": (r) => `edit window closed (${r.detail})`,
|
|
3302
|
+
"editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
|
|
3303
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
|
|
3304
|
+
};
|
|
3305
|
+
function formatEditDisabledReason(target, reason) {
|
|
3306
|
+
const detail = editDisabledReasonDetail[reason.kind](
|
|
3307
|
+
reason
|
|
3308
|
+
), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3309
|
+
return `State "${target.scope}:${where}" is not editable: ${detail}`;
|
|
3310
|
+
}
|
|
3311
|
+
async function editState(args) {
|
|
3312
|
+
const { client, instanceId, target, mode = "set", value, options } = args;
|
|
3313
|
+
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3314
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3315
|
+
try {
|
|
3316
|
+
return await commitEdit(ctx, target, mode, value, options);
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
if (!isRevisionConflict(error)) throw error;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
throw new ConcurrentEditStateError({
|
|
3322
|
+
instanceId,
|
|
3323
|
+
target: labelTarget(target),
|
|
3324
|
+
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
async function commitEdit(ctx, target, mode, value, options) {
|
|
3328
|
+
const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
|
|
3329
|
+
if (slot === void 0)
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
3332
|
+
);
|
|
3333
|
+
await assertSlotEditable(ctx, slot, options);
|
|
3334
|
+
const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
|
|
3335
|
+
ops: [op],
|
|
3336
|
+
mutation,
|
|
3337
|
+
stage: stage.name,
|
|
3338
|
+
origin: {
|
|
3339
|
+
edit: !0,
|
|
3340
|
+
...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
|
|
3341
|
+
},
|
|
3342
|
+
params: {},
|
|
3343
|
+
actor,
|
|
3344
|
+
self: selfGdr(ctx.instance),
|
|
3345
|
+
now: ctx.now
|
|
3346
|
+
});
|
|
3347
|
+
return await persistThenDeploy(
|
|
3348
|
+
ctx,
|
|
3349
|
+
mutation,
|
|
3350
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
|
|
3351
|
+
), {
|
|
3352
|
+
edited: !0,
|
|
3353
|
+
target: slotTarget(slot),
|
|
3354
|
+
...ranOps.length > 0 ? { ranOps } : {}
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
async function assertSlotEditable(ctx, slot, options) {
|
|
3358
|
+
const actor = options?.actor, window = slotWindowOpen(ctx.instance, slot), can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0, predicateSatisfied = window.open && slot.effective !== !0 && slot.effective !== void 0 ? await ctxEvaluateCondition(ctx, slot.effective, {
|
|
3359
|
+
...slot.task !== void 0 ? { taskName: slot.task } : {},
|
|
3360
|
+
...actor !== void 0 ? { actor } : {},
|
|
3361
|
+
...can !== void 0 ? { vars: { can } } : {}
|
|
3362
|
+
}) : slot.effective === !0, reason = editDisabledReason({
|
|
3363
|
+
effective: slot.effective,
|
|
3364
|
+
instance: ctx.instance,
|
|
3365
|
+
window,
|
|
3366
|
+
guardDenial: void 0,
|
|
3367
|
+
predicateSatisfied
|
|
3368
|
+
});
|
|
3369
|
+
if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
|
|
3370
|
+
}
|
|
3371
|
+
function buildEditOp(slot, mode, value) {
|
|
3372
|
+
if (mode === "unset") return { type: "state.unset", target: slot.ref };
|
|
3373
|
+
if (value === void 0)
|
|
3374
|
+
throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
|
|
3375
|
+
return {
|
|
3376
|
+
type: mode === "append" ? "state.append" : "state.set",
|
|
3377
|
+
target: slot.ref,
|
|
3378
|
+
value: { type: "literal", value }
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function slotTarget(slot) {
|
|
3382
|
+
return {
|
|
3383
|
+
scope: slot.scope,
|
|
3384
|
+
state: slot.name,
|
|
3385
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
function labelTarget(target) {
|
|
3389
|
+
return {
|
|
3390
|
+
scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
|
|
3391
|
+
state: target.state,
|
|
3392
|
+
...target.task !== void 0 ? { task: target.task } : {}
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function describeTarget(target) {
|
|
3396
|
+
const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3397
|
+
return target.scope !== void 0 ? `${target.scope}:${where}` : where;
|
|
3398
|
+
}
|
|
3049
3399
|
function bareIdFromSpawnRef(uri) {
|
|
3050
3400
|
if (!uri.includes(":")) return [uri];
|
|
3051
3401
|
try {
|
|
@@ -3092,18 +3442,60 @@ function buildClientForGdr(defaultClient, resolver) {
|
|
|
3092
3442
|
function toEffectsContextEntries(ctx) {
|
|
3093
3443
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
3094
3444
|
}
|
|
3445
|
+
async function dispatchGatedWrite(args) {
|
|
3446
|
+
const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
|
|
3447
|
+
client,
|
|
3448
|
+
tag,
|
|
3449
|
+
instanceId,
|
|
3450
|
+
access,
|
|
3451
|
+
clock: args.clock,
|
|
3452
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3453
|
+
}), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
|
|
3454
|
+
return {
|
|
3455
|
+
instance: await reload(client, instanceId, tag),
|
|
3456
|
+
cascaded,
|
|
3457
|
+
fired: !0,
|
|
3458
|
+
...ranOps !== void 0 ? { ranOps } : {}
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3095
3461
|
function assertActionAllowed(evaluation, task, action) {
|
|
3096
3462
|
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
|
|
3097
3463
|
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
3098
3464
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
3099
3465
|
}
|
|
3100
|
-
|
|
3101
|
-
const
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3466
|
+
function assertEditAllowed(evaluation, target) {
|
|
3467
|
+
const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
3468
|
+
if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
|
|
3469
|
+
throw new EditStateDeniedError({
|
|
3470
|
+
target: {
|
|
3471
|
+
scope: slot.scope,
|
|
3472
|
+
state: slot.name,
|
|
3473
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3474
|
+
},
|
|
3475
|
+
reason: slot.disabledReason
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
async function applyEdit(args) {
|
|
3479
|
+
const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
|
|
3480
|
+
return (await editState({
|
|
3481
|
+
client,
|
|
3482
|
+
instanceId: instance._id,
|
|
3483
|
+
target,
|
|
3484
|
+
...mode !== void 0 ? { mode } : {},
|
|
3485
|
+
...value !== void 0 ? { value } : {},
|
|
3486
|
+
options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
|
|
3487
|
+
})).ranOps;
|
|
3488
|
+
}
|
|
3489
|
+
function buildEngineCallOptions(args) {
|
|
3490
|
+
return {
|
|
3491
|
+
actor: args.actor,
|
|
3492
|
+
...args.grants !== void 0 ? { grants: args.grants } : {},
|
|
3493
|
+
...args.clock !== void 0 ? { clock: args.clock } : {},
|
|
3494
|
+
...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
|
|
3106
3495
|
};
|
|
3496
|
+
}
|
|
3497
|
+
async function applyAction(args) {
|
|
3498
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
|
|
3107
3499
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
3108
3500
|
client,
|
|
3109
3501
|
instanceId: instance._id,
|
|
@@ -3277,6 +3669,7 @@ const workflow = {
|
|
|
3277
3669
|
selfId: id,
|
|
3278
3670
|
tag,
|
|
3279
3671
|
workflowResource,
|
|
3672
|
+
definitionName: definition.name,
|
|
3280
3673
|
...perspective !== void 0 ? { perspective } : {}
|
|
3281
3674
|
},
|
|
3282
3675
|
randomKey
|
|
@@ -3318,34 +3711,68 @@ const workflow = {
|
|
|
3318
3711
|
idempotent,
|
|
3319
3712
|
resourceClients
|
|
3320
3713
|
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3321
|
-
|
|
3322
|
-
return { instance: before, cascaded: 0, fired: !1 };
|
|
3323
|
-
const evaluation = await evaluateInstance({
|
|
3714
|
+
return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
|
|
3324
3715
|
client,
|
|
3325
3716
|
tag,
|
|
3326
3717
|
instanceId,
|
|
3327
3718
|
access,
|
|
3719
|
+
actor,
|
|
3720
|
+
clientForGdr,
|
|
3328
3721
|
clock,
|
|
3329
|
-
|
|
3722
|
+
before,
|
|
3723
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3724
|
+
apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
|
|
3725
|
+
client,
|
|
3726
|
+
instance,
|
|
3727
|
+
task,
|
|
3728
|
+
action,
|
|
3729
|
+
actor,
|
|
3730
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3731
|
+
clock,
|
|
3732
|
+
clientForGdr,
|
|
3733
|
+
...params !== void 0 ? { params } : {}
|
|
3734
|
+
}))
|
|
3330
3735
|
});
|
|
3331
|
-
|
|
3332
|
-
|
|
3736
|
+
},
|
|
3737
|
+
/**
|
|
3738
|
+
* Edit a declared-editable state slot directly — reassign, reschedule,
|
|
3739
|
+
* claim-by-hand, append to a running log — through the generic edit seam,
|
|
3740
|
+
* instead of a bespoke action per field. Soft-gates on the slot's declared
|
|
3741
|
+
* editability (the same projection a UI renders), applies the edit as a
|
|
3742
|
+
* `state.*` op (so provenance + history are stamped by the op path),
|
|
3743
|
+
* refreshes the stage's guards, then cascades — an edit to a value a
|
|
3744
|
+
* transition reads can and should move the instance. Advisory like every
|
|
3745
|
+
* engine gate; the lake ACL + guards are the only hard locks.
|
|
3746
|
+
*
|
|
3747
|
+
* Each call is a discrete COMMIT (a history entry, a guard refresh, a
|
|
3748
|
+
* cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
|
|
3749
|
+
* UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
|
|
3750
|
+
* never an `onChange` per keystroke.
|
|
3751
|
+
*/
|
|
3752
|
+
editState: async (args) => {
|
|
3753
|
+
const { client, tag, instanceId, target, mode, value, resourceClients } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3754
|
+
return dispatchGatedWrite({
|
|
3333
3755
|
client,
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3756
|
+
tag,
|
|
3757
|
+
instanceId,
|
|
3758
|
+
access,
|
|
3337
3759
|
actor,
|
|
3338
|
-
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3339
|
-
clock,
|
|
3340
3760
|
clientForGdr,
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3761
|
+
clock,
|
|
3762
|
+
before,
|
|
3763
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3764
|
+
apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
|
|
3765
|
+
client,
|
|
3766
|
+
instance,
|
|
3767
|
+
target,
|
|
3768
|
+
...mode !== void 0 ? { mode } : {},
|
|
3769
|
+
...value !== void 0 ? { value } : {},
|
|
3770
|
+
actor,
|
|
3771
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3772
|
+
clock,
|
|
3773
|
+
clientForGdr
|
|
3774
|
+
}))
|
|
3775
|
+
});
|
|
3349
3776
|
},
|
|
3350
3777
|
/**
|
|
3351
3778
|
* Report a queued effect's outcome. Drains it from `pendingEffects`,
|
|
@@ -3763,6 +4190,9 @@ function createInstanceSession(args) {
|
|
|
3763
4190
|
...clock !== void 0 ? { now: clock() } : {},
|
|
3764
4191
|
...grants !== void 0 ? { grants } : {}
|
|
3765
4192
|
});
|
|
4193
|
+
}, settleAfterApply = async (actor, held, ranOps) => {
|
|
4194
|
+
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
4195
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3766
4196
|
}, commit = async (run) => {
|
|
3767
4197
|
committing = !0;
|
|
3768
4198
|
const held = new Map(overlay);
|
|
@@ -3813,8 +4243,25 @@ function createInstanceSession(args) {
|
|
|
3813
4243
|
...grants !== void 0 ? { grants } : {},
|
|
3814
4244
|
...clock !== void 0 ? { clock } : {},
|
|
3815
4245
|
...params !== void 0 ? { params } : {}
|
|
3816
|
-
})
|
|
3817
|
-
return
|
|
4246
|
+
});
|
|
4247
|
+
return settleAfterApply(actor, held, ranOps);
|
|
4248
|
+
});
|
|
4249
|
+
},
|
|
4250
|
+
editState({ target, mode, value }) {
|
|
4251
|
+
return commit(async (actor, held) => {
|
|
4252
|
+
assertEditAllowed(await evaluateWith(held, heldGuards), target);
|
|
4253
|
+
const { grants } = await access(), ranOps = await applyEdit({
|
|
4254
|
+
client,
|
|
4255
|
+
instance,
|
|
4256
|
+
target,
|
|
4257
|
+
...mode !== void 0 ? { mode } : {},
|
|
4258
|
+
...value !== void 0 ? { value } : {},
|
|
4259
|
+
actor,
|
|
4260
|
+
clientForGdr,
|
|
4261
|
+
...grants !== void 0 ? { grants } : {},
|
|
4262
|
+
...clock !== void 0 ? { clock } : {}
|
|
4263
|
+
});
|
|
4264
|
+
return settleAfterApply(actor, held, ranOps);
|
|
3818
4265
|
});
|
|
3819
4266
|
}
|
|
3820
4267
|
};
|
|
@@ -3862,6 +4309,7 @@ function createEngine(args) {
|
|
|
3862
4309
|
deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
|
|
3863
4310
|
startInstance: (rest) => workflow.startInstance(bind(rest)),
|
|
3864
4311
|
fireAction: (rest) => workflow.fireAction(bind(rest)),
|
|
4312
|
+
editState: (rest) => workflow.editState(bind(rest)),
|
|
3865
4313
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3866
4314
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3867
4315
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
@@ -4153,20 +4601,25 @@ function displayDescription(typeKey) {
|
|
|
4153
4601
|
return DISPLAY[typeKey]?.description;
|
|
4154
4602
|
}
|
|
4155
4603
|
exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
|
|
4604
|
+
exports.isStartableDefinition = schema.isStartableDefinition;
|
|
4156
4605
|
exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
|
|
4157
4606
|
exports.ActionDisabledError = ActionDisabledError;
|
|
4158
4607
|
exports.ActionParamsInvalidError = ActionParamsInvalidError;
|
|
4159
4608
|
exports.CascadeLimitError = CascadeLimitError;
|
|
4609
|
+
exports.ConcurrentEditStateError = ConcurrentEditStateError;
|
|
4160
4610
|
exports.ConcurrentFireActionError = ConcurrentFireActionError;
|
|
4161
4611
|
exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
|
|
4162
4612
|
exports.DISPLAY = DISPLAY;
|
|
4163
4613
|
exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
|
|
4614
|
+
exports.EditStateDeniedError = EditStateDeniedError;
|
|
4164
4615
|
exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
|
|
4165
4616
|
exports.GUARD_LIFTED_PREDICATE = GUARD_LIFTED_PREDICATE;
|
|
4166
4617
|
exports.GUARD_OWNER = GUARD_OWNER;
|
|
4167
4618
|
exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
|
|
4168
4619
|
exports.MutationGuardDeniedError = MutationGuardDeniedError;
|
|
4169
4620
|
exports.OP_DISPLAY = OP_DISPLAY;
|
|
4621
|
+
exports.PartialGuardDeployError = PartialGuardDeployError;
|
|
4622
|
+
exports.RequiredStateNotProvidedError = RequiredStateNotProvidedError;
|
|
4170
4623
|
exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
|
|
4171
4624
|
exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
|
|
4172
4625
|
exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
|