@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.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { parse, evaluate } from "groq-js";
|
|
2
|
-
import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
|
|
2
|
+
import { actorFulfillsRole, isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, andConditions, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
|
|
3
|
+
import { isStartableDefinition } from "./_chunks-es/schema.js";
|
|
3
4
|
import * as v from "valibot";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
for (const part of path.split(".")) {
|
|
7
|
-
if (current == null || typeof current != "object") return;
|
|
8
|
-
current = current[part];
|
|
9
|
-
}
|
|
10
|
-
return current;
|
|
5
|
+
function findOpenStageEntry(host) {
|
|
6
|
+
return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
|
|
11
7
|
}
|
|
12
8
|
const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
13
9
|
function parseGdr(uri) {
|
|
@@ -111,12 +107,100 @@ function gdrRef(res, documentId, type) {
|
|
|
111
107
|
function isGdr(value) {
|
|
112
108
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
113
109
|
}
|
|
110
|
+
function buildParams(args) {
|
|
111
|
+
const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
|
|
112
|
+
return {
|
|
113
|
+
self: selfGdr(instance),
|
|
114
|
+
state: renderedState(instance.state ?? [], snapshot),
|
|
115
|
+
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
116
|
+
ancestors: instance.ancestors.map((a) => a.id),
|
|
117
|
+
stage: instance.currentStage,
|
|
118
|
+
now,
|
|
119
|
+
/** `$effects` — parent context handoff by entry name, completed-effect
|
|
120
|
+
* outputs namespaced under the effect's name (`$effects['x.y'].out`). */
|
|
121
|
+
effects: effectsContextMap(instance),
|
|
122
|
+
/** `$tasks` — the current stage's task rows, statuses included. */
|
|
123
|
+
tasks: currentTasks2,
|
|
124
|
+
allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
|
|
125
|
+
anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
|
|
126
|
+
...extra
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function renderedState(entries, snapshot) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function renderedValue(entry, snapshot) {
|
|
135
|
+
return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
|
|
136
|
+
}
|
|
137
|
+
function scopedStateOverlay(instance, snapshot, taskName) {
|
|
138
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
139
|
+
if (stageEntry === void 0) return {};
|
|
140
|
+
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
141
|
+
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
142
|
+
}
|
|
143
|
+
function assignedFor(instance, taskName, actor, roleAliases) {
|
|
144
|
+
if (actor === void 0) return !1;
|
|
145
|
+
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
146
|
+
return entry === void 0 ? !1 : entry.value.some(
|
|
147
|
+
(a) => a.type === "user" ? a.id === actor.id : actorFulfillsRole(actor.roles, a.role, roleAliases)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
function effectsContextMap(instance) {
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const entry of instance.effectsContext)
|
|
153
|
+
out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function parseJsonContextEntry(entry) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(entry.value);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
162
|
+
{ cause: err }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function paramsForLake(params) {
|
|
167
|
+
return {
|
|
168
|
+
...params,
|
|
169
|
+
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
170
|
+
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
171
|
+
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
172
|
+
state: stripStateForLake(params.state)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function stripStateForLake(value) {
|
|
176
|
+
if (value == null) return value;
|
|
177
|
+
if (typeof value == "string") return bareId(value);
|
|
178
|
+
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
179
|
+
if (typeof value == "object") {
|
|
180
|
+
const out = {};
|
|
181
|
+
for (const [k, v2] of Object.entries(value))
|
|
182
|
+
out[k] = stripStateForLake(v2);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function bareId(id) {
|
|
188
|
+
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
189
|
+
}
|
|
190
|
+
function getPath(value, path) {
|
|
191
|
+
let current = value;
|
|
192
|
+
for (const part of path.split(".")) {
|
|
193
|
+
if (current == null || typeof current != "object") return;
|
|
194
|
+
current = current[part];
|
|
195
|
+
}
|
|
196
|
+
return current;
|
|
197
|
+
}
|
|
114
198
|
function resourceOf(p) {
|
|
115
199
|
return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
|
|
116
200
|
}
|
|
117
|
-
const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
|
|
201
|
+
const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
|
|
118
202
|
function isGuardReadExpr(expr) {
|
|
119
|
-
return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
|
|
203
|
+
return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
|
|
120
204
|
}
|
|
121
205
|
function resolveGuardRead(expr, ctx) {
|
|
122
206
|
if (expr === "$self") return selfGdr(ctx.instance);
|
|
@@ -126,8 +210,13 @@ function resolveGuardRead(expr, ctx) {
|
|
|
126
210
|
const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
|
|
127
211
|
return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
|
|
128
212
|
}
|
|
213
|
+
const effectsRead = EFFECTS_READ.exec(expr);
|
|
214
|
+
if (effectsRead !== null) {
|
|
215
|
+
const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
|
|
216
|
+
return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
|
|
217
|
+
}
|
|
129
218
|
throw new Error(
|
|
130
|
-
`Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now",
|
|
219
|
+
`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)`
|
|
131
220
|
);
|
|
132
221
|
}
|
|
133
222
|
function resolveIdRefTarget(expr, ctx) {
|
|
@@ -209,7 +298,7 @@ function createDefinitionValidator() {
|
|
|
209
298
|
entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
|
|
210
299
|
}, checkGuardRead: (expr, where) => {
|
|
211
300
|
isGuardReadExpr(expr) || errors.push(
|
|
212
|
-
` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now",
|
|
301
|
+
` \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)`
|
|
213
302
|
);
|
|
214
303
|
} };
|
|
215
304
|
}
|
|
@@ -388,89 +477,6 @@ async function assertInstanceWriteAllowed(args) {
|
|
|
388
477
|
guards: denied
|
|
389
478
|
});
|
|
390
479
|
}
|
|
391
|
-
function findOpenStageEntry(host) {
|
|
392
|
-
return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
|
|
393
|
-
}
|
|
394
|
-
function buildParams(args) {
|
|
395
|
-
const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
|
|
396
|
-
return {
|
|
397
|
-
self: selfGdr(instance),
|
|
398
|
-
state: renderedState(instance.state ?? [], snapshot),
|
|
399
|
-
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
400
|
-
ancestors: instance.ancestors.map((a) => a.id),
|
|
401
|
-
stage: instance.currentStage,
|
|
402
|
-
now,
|
|
403
|
-
/** `$effects` — parent context handoff by entry name, completed-effect
|
|
404
|
-
* outputs namespaced under the effect's name (`$effects['x.y'].out`). */
|
|
405
|
-
effects: effectsContextMap(instance),
|
|
406
|
-
/** `$tasks` — the current stage's task rows, statuses included. */
|
|
407
|
-
tasks: currentTasks2,
|
|
408
|
-
allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
|
|
409
|
-
anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
|
|
410
|
-
...extra
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
function renderedState(entries, snapshot) {
|
|
414
|
-
const out = {};
|
|
415
|
-
for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
|
|
416
|
-
return out;
|
|
417
|
-
}
|
|
418
|
-
function renderedValue(entry, snapshot) {
|
|
419
|
-
return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
|
|
420
|
-
}
|
|
421
|
-
function scopedStateOverlay(instance, snapshot, taskName) {
|
|
422
|
-
const stageEntry = findOpenStageEntry(instance);
|
|
423
|
-
if (stageEntry === void 0) return {};
|
|
424
|
-
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
425
|
-
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
426
|
-
}
|
|
427
|
-
function assignedFor(instance, taskName, actor) {
|
|
428
|
-
if (actor === void 0) return !1;
|
|
429
|
-
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
430
|
-
return entry === void 0 ? !1 : entry.value.some(
|
|
431
|
-
(a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
function effectsContextMap(instance) {
|
|
435
|
-
const out = {};
|
|
436
|
-
for (const entry of instance.effectsContext)
|
|
437
|
-
out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
|
|
438
|
-
return out;
|
|
439
|
-
}
|
|
440
|
-
function parseJsonContextEntry(entry) {
|
|
441
|
-
try {
|
|
442
|
-
return JSON.parse(entry.value);
|
|
443
|
-
} catch (err) {
|
|
444
|
-
throw new Error(
|
|
445
|
-
`effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
446
|
-
{ cause: err }
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function paramsForLake(params) {
|
|
451
|
-
return {
|
|
452
|
-
...params,
|
|
453
|
-
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
454
|
-
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
455
|
-
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
456
|
-
state: stripStateForLake(params.state)
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function stripStateForLake(value) {
|
|
460
|
-
if (value == null) return value;
|
|
461
|
-
if (typeof value == "string") return bareId(value);
|
|
462
|
-
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
463
|
-
if (typeof value == "object") {
|
|
464
|
-
const out = {};
|
|
465
|
-
for (const [k, v2] of Object.entries(value))
|
|
466
|
-
out[k] = stripStateForLake(v2);
|
|
467
|
-
return out;
|
|
468
|
-
}
|
|
469
|
-
return value;
|
|
470
|
-
}
|
|
471
|
-
function bareId(id) {
|
|
472
|
-
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
473
|
-
}
|
|
474
480
|
function abortReason(instance) {
|
|
475
481
|
return instance.history.find(
|
|
476
482
|
(h) => h._type === "aborted"
|
|
@@ -593,6 +599,90 @@ function remediationsFor(diagnosis) {
|
|
|
593
599
|
available: RUNNABLE_VERBS.has(seed.verb)
|
|
594
600
|
}));
|
|
595
601
|
}
|
|
602
|
+
class ActionParamsInvalidError extends Error {
|
|
603
|
+
action;
|
|
604
|
+
task;
|
|
605
|
+
issues;
|
|
606
|
+
constructor(args) {
|
|
607
|
+
const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
|
|
608
|
+
`);
|
|
609
|
+
super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
|
|
610
|
+
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
class RequiredStateNotProvidedError extends Error {
|
|
614
|
+
definition;
|
|
615
|
+
missing;
|
|
616
|
+
constructor(args) {
|
|
617
|
+
const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
|
|
618
|
+
`), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
|
|
619
|
+
super(
|
|
620
|
+
`Required init state${where} was not provided:
|
|
621
|
+
${lines}
|
|
622
|
+
Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
|
|
623
|
+
), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
class WorkflowStateDivergedError extends Error {
|
|
627
|
+
instanceId;
|
|
628
|
+
/** The guard-deploy failure that triggered the (attempted) rollback. */
|
|
629
|
+
guardError;
|
|
630
|
+
/** The rollback failure, when rollback was attempted and itself failed. */
|
|
631
|
+
rollbackError;
|
|
632
|
+
constructor(args) {
|
|
633
|
+
super(
|
|
634
|
+
`Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
|
|
635
|
+
{ cause: args.guardError }
|
|
636
|
+
), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
class PartialGuardDeployError extends Error {
|
|
640
|
+
stageName;
|
|
641
|
+
deployed;
|
|
642
|
+
constructor(args) {
|
|
643
|
+
super(
|
|
644
|
+
`Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
|
|
645
|
+
{ cause: args.cause }
|
|
646
|
+
), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
650
|
+
class ConcurrentFireActionError extends Error {
|
|
651
|
+
instanceId;
|
|
652
|
+
task;
|
|
653
|
+
action;
|
|
654
|
+
attempts;
|
|
655
|
+
constructor(args) {
|
|
656
|
+
super(
|
|
657
|
+
`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.`
|
|
658
|
+
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function isRevisionConflict(error) {
|
|
662
|
+
if (typeof error != "object" || error === null) return !1;
|
|
663
|
+
const { statusCode, message } = error;
|
|
664
|
+
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
665
|
+
}
|
|
666
|
+
class ConcurrentEditStateError extends Error {
|
|
667
|
+
instanceId;
|
|
668
|
+
target;
|
|
669
|
+
attempts;
|
|
670
|
+
constructor(args) {
|
|
671
|
+
const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
|
|
672
|
+
super(
|
|
673
|
+
`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.`
|
|
674
|
+
), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
class CascadeLimitError extends Error {
|
|
678
|
+
instanceId;
|
|
679
|
+
limit;
|
|
680
|
+
constructor(args) {
|
|
681
|
+
super(
|
|
682
|
+
`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.`
|
|
683
|
+
), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
596
686
|
function buildSnapshot(args) {
|
|
597
687
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
598
688
|
for (const { doc, resource } of args.docs) {
|
|
@@ -858,9 +948,16 @@ async function committedInstance(args) {
|
|
|
858
948
|
}
|
|
859
949
|
async function deployStageGuards(args) {
|
|
860
950
|
const live = await committedInstance(args);
|
|
861
|
-
if (
|
|
862
|
-
|
|
951
|
+
if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
|
|
952
|
+
let deployed = 0;
|
|
953
|
+
for (const { client, doc } of resolvedStageGuards(args)) {
|
|
954
|
+
try {
|
|
863
955
|
await upsertGuard(client, doc);
|
|
956
|
+
} catch (cause) {
|
|
957
|
+
throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
|
|
958
|
+
}
|
|
959
|
+
deployed += 1;
|
|
960
|
+
}
|
|
864
961
|
}
|
|
865
962
|
async function retractStageGuards(args) {
|
|
866
963
|
const live = await committedInstance(args);
|
|
@@ -1049,6 +1146,7 @@ function derivePerspectiveFromState(entries) {
|
|
|
1049
1146
|
async function resolveDeclaredState(args) {
|
|
1050
1147
|
const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
|
|
1051
1148
|
if (entryDefs === void 0 || entryDefs.length === 0) return [];
|
|
1149
|
+
assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
|
|
1052
1150
|
const out = [];
|
|
1053
1151
|
for (const entry of entryDefs) {
|
|
1054
1152
|
const scopedCtx = { ...ctx, resolvedState: out };
|
|
@@ -1056,6 +1154,17 @@ async function resolveDeclaredState(args) {
|
|
|
1056
1154
|
}
|
|
1057
1155
|
return out;
|
|
1058
1156
|
}
|
|
1157
|
+
function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
|
|
1158
|
+
const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
|
|
1159
|
+
const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
|
|
1160
|
+
return match === void 0 || match.value === void 0 || match.value === null;
|
|
1161
|
+
}).map((entry) => ({ name: entry.name, type: entry.type }));
|
|
1162
|
+
if (missing.length !== 0)
|
|
1163
|
+
throw new RequiredStateNotProvidedError({
|
|
1164
|
+
...definitionName !== void 0 ? { definition: definitionName } : {},
|
|
1165
|
+
missing
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1059
1168
|
const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
1060
1169
|
"doc.refs",
|
|
1061
1170
|
"checklist",
|
|
@@ -1184,6 +1293,12 @@ function coerceToGdr(raw, workflowResource) {
|
|
|
1184
1293
|
function gdrToBareId(uri) {
|
|
1185
1294
|
return uri.includes(":") ? extractDocumentId(uri) : uri;
|
|
1186
1295
|
}
|
|
1296
|
+
function loadCallContext(client, instanceId, options) {
|
|
1297
|
+
return loadContext(client, instanceId, {
|
|
1298
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1299
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1187
1302
|
async function loadContext(client, instanceId, options) {
|
|
1188
1303
|
const instance = await client.getDocument(instanceId);
|
|
1189
1304
|
if (!instance)
|
|
@@ -1204,7 +1319,7 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1204
1319
|
...base,
|
|
1205
1320
|
state,
|
|
1206
1321
|
actor: opts?.actor,
|
|
1207
|
-
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
|
|
1322
|
+
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
|
|
1208
1323
|
...opts?.vars
|
|
1209
1324
|
};
|
|
1210
1325
|
return { ...await evaluatePredicates({
|
|
@@ -1249,86 +1364,36 @@ async function resolveStageStateEntries(args) {
|
|
|
1249
1364
|
ctx: {
|
|
1250
1365
|
client,
|
|
1251
1366
|
now,
|
|
1252
|
-
selfId: instance._id,
|
|
1253
|
-
tag: instance.tag,
|
|
1254
|
-
stageName: stage.name,
|
|
1255
|
-
workflowResource: instance.workflowResource,
|
|
1256
|
-
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
1257
|
-
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
1258
|
-
workflowState: instance.state ?? [],
|
|
1259
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1260
|
-
},
|
|
1261
|
-
randomKey
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
async function resolveTaskStateEntries(args) {
|
|
1265
|
-
const { client, instance, stage, task, now } = args;
|
|
1266
|
-
return resolveDeclaredState({
|
|
1267
|
-
entryDefs: task.state,
|
|
1268
|
-
initialState: [],
|
|
1269
|
-
ctx: {
|
|
1270
|
-
client,
|
|
1271
|
-
now,
|
|
1272
|
-
selfId: instance._id,
|
|
1273
|
-
tag: instance.tag,
|
|
1274
|
-
stageName: stage.name,
|
|
1275
|
-
workflowResource: instance.workflowResource,
|
|
1276
|
-
taskName: task.name,
|
|
1277
|
-
workflowState: instance.state ?? [],
|
|
1278
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1279
|
-
},
|
|
1280
|
-
randomKey
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
class ActionParamsInvalidError extends Error {
|
|
1284
|
-
action;
|
|
1285
|
-
task;
|
|
1286
|
-
issues;
|
|
1287
|
-
constructor(args) {
|
|
1288
|
-
const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
|
|
1289
|
-
`);
|
|
1290
|
-
super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
|
|
1291
|
-
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
class WorkflowStateDivergedError extends Error {
|
|
1295
|
-
instanceId;
|
|
1296
|
-
/** The guard-deploy failure that triggered the (attempted) rollback. */
|
|
1297
|
-
guardError;
|
|
1298
|
-
/** The rollback failure, when rollback was attempted and itself failed. */
|
|
1299
|
-
rollbackError;
|
|
1300
|
-
constructor(args) {
|
|
1301
|
-
super(
|
|
1302
|
-
`Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
|
|
1303
|
-
{ cause: args.guardError }
|
|
1304
|
-
), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
1308
|
-
class ConcurrentFireActionError extends Error {
|
|
1309
|
-
instanceId;
|
|
1310
|
-
task;
|
|
1311
|
-
action;
|
|
1312
|
-
attempts;
|
|
1313
|
-
constructor(args) {
|
|
1314
|
-
super(
|
|
1315
|
-
`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.`
|
|
1316
|
-
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
function isRevisionConflict(error) {
|
|
1320
|
-
if (typeof error != "object" || error === null) return !1;
|
|
1321
|
-
const { statusCode, message } = error;
|
|
1322
|
-
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
1367
|
+
selfId: instance._id,
|
|
1368
|
+
tag: instance.tag,
|
|
1369
|
+
stageName: stage.name,
|
|
1370
|
+
workflowResource: instance.workflowResource,
|
|
1371
|
+
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
1372
|
+
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
1373
|
+
workflowState: instance.state ?? [],
|
|
1374
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1375
|
+
},
|
|
1376
|
+
randomKey
|
|
1377
|
+
});
|
|
1323
1378
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1379
|
+
async function resolveTaskStateEntries(args) {
|
|
1380
|
+
const { client, instance, stage, task, now } = args;
|
|
1381
|
+
return resolveDeclaredState({
|
|
1382
|
+
entryDefs: task.state,
|
|
1383
|
+
initialState: [],
|
|
1384
|
+
ctx: {
|
|
1385
|
+
client,
|
|
1386
|
+
now,
|
|
1387
|
+
selfId: instance._id,
|
|
1388
|
+
tag: instance.tag,
|
|
1389
|
+
stageName: stage.name,
|
|
1390
|
+
workflowResource: instance.workflowResource,
|
|
1391
|
+
taskName: task.name,
|
|
1392
|
+
workflowState: instance.state ?? [],
|
|
1393
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1394
|
+
},
|
|
1395
|
+
randomKey
|
|
1396
|
+
});
|
|
1332
1397
|
}
|
|
1333
1398
|
async function deployOrRollback(args) {
|
|
1334
1399
|
try {
|
|
@@ -1351,7 +1416,11 @@ async function deployOrRollback(args) {
|
|
|
1351
1416
|
reason: "rollback of the committed move failed"
|
|
1352
1417
|
});
|
|
1353
1418
|
}
|
|
1354
|
-
throw guardError
|
|
1419
|
+
throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
|
|
1420
|
+
instanceId: args.instanceId,
|
|
1421
|
+
guardError,
|
|
1422
|
+
reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
|
|
1423
|
+
}) : guardError;
|
|
1355
1424
|
}
|
|
1356
1425
|
}
|
|
1357
1426
|
function applyTaskStatusChange(args) {
|
|
@@ -1440,7 +1509,8 @@ function materializeInstance(base, mutation) {
|
|
|
1440
1509
|
...base,
|
|
1441
1510
|
currentStage: mutation.currentStage,
|
|
1442
1511
|
state: mutation.state,
|
|
1443
|
-
stages: mutation.stages
|
|
1512
|
+
stages: mutation.stages,
|
|
1513
|
+
effectsContext: mutation.effectsContext
|
|
1444
1514
|
};
|
|
1445
1515
|
}
|
|
1446
1516
|
async function persist(ctx, mutation) {
|
|
@@ -1455,7 +1525,21 @@ async function persist(ctx, mutation) {
|
|
|
1455
1525
|
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1456
1526
|
const actorForPriming = void 0;
|
|
1457
1527
|
for (const body of pendingCreates)
|
|
1458
|
-
|
|
1528
|
+
try {
|
|
1529
|
+
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
|
|
1530
|
+
ctx.client,
|
|
1531
|
+
body._id,
|
|
1532
|
+
actorForPriming,
|
|
1533
|
+
ctx.clientForGdr,
|
|
1534
|
+
ctx.clock
|
|
1535
|
+
), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
1536
|
+
} catch (cause) {
|
|
1537
|
+
throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
|
|
1538
|
+
instanceId: ctx.instance._id,
|
|
1539
|
+
guardError: cause,
|
|
1540
|
+
reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1459
1543
|
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1460
1544
|
if (!reloaded)
|
|
1461
1545
|
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
@@ -1720,6 +1804,7 @@ async function prepareChildInstance(args) {
|
|
|
1720
1804
|
selfId: childDocId,
|
|
1721
1805
|
tag: childTag,
|
|
1722
1806
|
workflowResource,
|
|
1807
|
+
definitionName: definition.name,
|
|
1723
1808
|
...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
|
|
1724
1809
|
},
|
|
1725
1810
|
randomKey
|
|
@@ -1766,6 +1851,28 @@ async function resolveBindings(args) {
|
|
|
1766
1851
|
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
1767
1852
|
return { ...resolved, ...args.staticInput };
|
|
1768
1853
|
}
|
|
1854
|
+
async function persistThenDeploy(ctx, mutation, deploy) {
|
|
1855
|
+
const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
|
|
1856
|
+
await deployOrRollback({
|
|
1857
|
+
client: ctx.client,
|
|
1858
|
+
instanceId: ctx.instance._id,
|
|
1859
|
+
committedRev: committed._rev,
|
|
1860
|
+
restore: instanceStateFields(ctx.instance),
|
|
1861
|
+
unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
|
|
1862
|
+
reversible: !spawned,
|
|
1863
|
+
deploy
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
async function refreshStageGuards(ctx, mutation, stageName) {
|
|
1867
|
+
await deployStageGuards({
|
|
1868
|
+
client: ctx.client,
|
|
1869
|
+
clientForGdr: ctx.clientForGdr,
|
|
1870
|
+
instance: materializeInstance(ctx.instance, mutation),
|
|
1871
|
+
definition: ctx.definition,
|
|
1872
|
+
stageName,
|
|
1873
|
+
now: ctx.now
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1769
1876
|
async function completeEffect(args) {
|
|
1770
1877
|
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1771
1878
|
...options?.clock ? { clock: options.clock } : {},
|
|
@@ -1811,9 +1918,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
|
|
|
1811
1918
|
const mutation = startMutation(ctx.instance);
|
|
1812
1919
|
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1813
1920
|
const ranAt = ctx.now;
|
|
1814
|
-
|
|
1921
|
+
mutation.effectHistory.push(
|
|
1815
1922
|
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1816
|
-
)
|
|
1923
|
+
);
|
|
1924
|
+
const wroteEffectsContext = status === "done" && outputs !== void 0;
|
|
1925
|
+
return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
1817
1926
|
_key: randomKey(),
|
|
1818
1927
|
_type: "effectCompleted",
|
|
1819
1928
|
at: ranAt,
|
|
@@ -1823,7 +1932,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
|
|
|
1823
1932
|
...outputs !== void 0 ? { outputs } : {},
|
|
1824
1933
|
...detail !== void 0 ? { detail } : {},
|
|
1825
1934
|
...actor !== void 0 ? { actor } : {}
|
|
1826
|
-
}), await
|
|
1935
|
+
}), await persistThenDeploy(
|
|
1936
|
+
ctx,
|
|
1937
|
+
mutation,
|
|
1938
|
+
() => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
|
|
1939
|
+
), { effectKey, status };
|
|
1827
1940
|
}
|
|
1828
1941
|
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1829
1942
|
const key = randomKey(), pending = {
|
|
@@ -1937,22 +2050,27 @@ function runOps(args) {
|
|
|
1937
2050
|
const summaries = [];
|
|
1938
2051
|
for (const op of ops) {
|
|
1939
2052
|
const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
|
|
1940
|
-
summaries.push(summary), mutation.history.push({
|
|
1941
|
-
_key: randomKey(),
|
|
1942
|
-
_type: "opApplied",
|
|
1943
|
-
at: now,
|
|
1944
|
-
stage,
|
|
1945
|
-
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
1946
|
-
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
1947
|
-
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
1948
|
-
opType: summary.opType,
|
|
1949
|
-
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
1950
|
-
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
1951
|
-
...actor !== void 0 ? { actor } : {}
|
|
1952
|
-
});
|
|
2053
|
+
summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
|
|
1953
2054
|
}
|
|
1954
2055
|
return summaries;
|
|
1955
2056
|
}
|
|
2057
|
+
function opAppliedEntry(args) {
|
|
2058
|
+
const { origin, summary, stage, actor, now } = args;
|
|
2059
|
+
return {
|
|
2060
|
+
_key: randomKey(),
|
|
2061
|
+
_type: "opApplied",
|
|
2062
|
+
at: now,
|
|
2063
|
+
stage,
|
|
2064
|
+
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2065
|
+
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2066
|
+
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2067
|
+
...origin.edit === !0 ? { edit: !0 } : {},
|
|
2068
|
+
opType: summary.opType,
|
|
2069
|
+
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2070
|
+
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2071
|
+
...actor !== void 0 ? { actor } : {}
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
1956
2074
|
function applyOp(op, ctx) {
|
|
1957
2075
|
switch (op.type) {
|
|
1958
2076
|
case "state.set":
|
|
@@ -2283,28 +2401,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
|
|
|
2283
2401
|
now: ctx.now
|
|
2284
2402
|
});
|
|
2285
2403
|
}
|
|
2286
|
-
async function persistThenDeploy(ctx, mutation, deploy) {
|
|
2287
|
-
const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
|
|
2288
|
-
await deployOrRollback({
|
|
2289
|
-
client: ctx.client,
|
|
2290
|
-
instanceId: ctx.instance._id,
|
|
2291
|
-
committedRev: committed._rev,
|
|
2292
|
-
restore: instanceStateFields(ctx.instance),
|
|
2293
|
-
unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
|
|
2294
|
-
reversible: !spawned,
|
|
2295
|
-
deploy
|
|
2296
|
-
});
|
|
2297
|
-
}
|
|
2298
|
-
async function refreshStageGuards(ctx, mutation, stageName) {
|
|
2299
|
-
await deployStageGuards({
|
|
2300
|
-
client: ctx.client,
|
|
2301
|
-
clientForGdr: ctx.clientForGdr,
|
|
2302
|
-
instance: materializeInstance(ctx.instance, mutation),
|
|
2303
|
-
definition: ctx.definition,
|
|
2304
|
-
stageName,
|
|
2305
|
-
now: ctx.now
|
|
2306
|
-
});
|
|
2307
|
-
}
|
|
2308
2404
|
async function commitTransition(ctx, actor) {
|
|
2309
2405
|
if (isTerminal(ctx))
|
|
2310
2406
|
return { fired: !1 };
|
|
@@ -2608,6 +2704,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2608
2704
|
return;
|
|
2609
2705
|
}
|
|
2610
2706
|
}
|
|
2707
|
+
function effectiveEditable(baseline, override) {
|
|
2708
|
+
if (baseline === void 0) return;
|
|
2709
|
+
if (override === void 0) return baseline;
|
|
2710
|
+
if (baseline === !0 && override === !0) return !0;
|
|
2711
|
+
const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
|
|
2712
|
+
return andConditions(parts) ?? !0;
|
|
2713
|
+
}
|
|
2714
|
+
function slotSites(definition, stage) {
|
|
2715
|
+
const sites = [];
|
|
2716
|
+
for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
|
|
2717
|
+
for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
|
|
2718
|
+
for (const task of stage.tasks ?? [])
|
|
2719
|
+
for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
|
|
2720
|
+
return sites;
|
|
2721
|
+
}
|
|
2722
|
+
function toResolvedSlot(site, stage) {
|
|
2723
|
+
return {
|
|
2724
|
+
scope: site.scope,
|
|
2725
|
+
...site.task !== void 0 ? { task: site.task } : {},
|
|
2726
|
+
name: site.entry.name,
|
|
2727
|
+
type: site.entry.type,
|
|
2728
|
+
...site.entry.title !== void 0 ? { title: site.entry.title } : {},
|
|
2729
|
+
ref: { scope: site.scope, state: site.entry.name },
|
|
2730
|
+
effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
function editableSlotsInStage(definition, stage) {
|
|
2734
|
+
return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
|
|
2735
|
+
}
|
|
2736
|
+
function editTargetMatches(slot, target) {
|
|
2737
|
+
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;
|
|
2738
|
+
}
|
|
2739
|
+
const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
|
|
2740
|
+
function resolveEditTarget(args) {
|
|
2741
|
+
const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
|
|
2742
|
+
(site) => editTargetMatches(
|
|
2743
|
+
{
|
|
2744
|
+
scope: site.scope,
|
|
2745
|
+
name: site.entry.name,
|
|
2746
|
+
...site.task !== void 0 ? { task: site.task } : {}
|
|
2747
|
+
},
|
|
2748
|
+
target
|
|
2749
|
+
)
|
|
2750
|
+
).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
2751
|
+
return found ? toResolvedSlot(found, stage) : void 0;
|
|
2752
|
+
}
|
|
2753
|
+
function slotWindowOpen(instance, slot) {
|
|
2754
|
+
if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
|
|
2755
|
+
if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
|
|
2756
|
+
if (slot.scope !== "task") return { open: !0 };
|
|
2757
|
+
const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
|
|
2758
|
+
return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
|
|
2759
|
+
}
|
|
2760
|
+
function readSlotValue(instance, slot) {
|
|
2761
|
+
return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
|
|
2762
|
+
}
|
|
2763
|
+
function slotStateHost(instance, slot) {
|
|
2764
|
+
if (slot.scope === "workflow") return instance.state;
|
|
2765
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
2766
|
+
return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
|
|
2767
|
+
}
|
|
2768
|
+
function slotProvenance(instance, ref) {
|
|
2769
|
+
let latest;
|
|
2770
|
+
for (const entry of instance.history)
|
|
2771
|
+
entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
|
|
2772
|
+
return latest === void 0 ? {} : {
|
|
2773
|
+
...latest.actor !== void 0 ? { setBy: latest.actor } : {},
|
|
2774
|
+
setAt: latest.at
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function editDisabledReason(args) {
|
|
2778
|
+
const { effective, instance, window, guardDenial, predicateSatisfied } = args;
|
|
2779
|
+
if (effective === void 0) return { kind: "not-editable" };
|
|
2780
|
+
if (!window.open)
|
|
2781
|
+
return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
|
|
2782
|
+
if (guardDenial !== void 0) return guardDenial;
|
|
2783
|
+
if (!predicateSatisfied)
|
|
2784
|
+
return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
|
|
2785
|
+
}
|
|
2611
2786
|
async function evaluateInstance(args) {
|
|
2612
2787
|
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2613
2788
|
validateTag(tag);
|
|
@@ -2641,6 +2816,7 @@ async function evaluateFromSnapshot(args) {
|
|
|
2641
2816
|
actor,
|
|
2642
2817
|
snapshot,
|
|
2643
2818
|
scope,
|
|
2819
|
+
roleAliases: definition.roleAliases,
|
|
2644
2820
|
stageHasExits: !isTerminalStage(stage),
|
|
2645
2821
|
guardDenial
|
|
2646
2822
|
})
|
|
@@ -2662,16 +2838,65 @@ async function evaluateFromSnapshot(args) {
|
|
|
2662
2838
|
stage,
|
|
2663
2839
|
tasks: taskEvaluations,
|
|
2664
2840
|
transitions: transitionEvaluations
|
|
2665
|
-
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed))
|
|
2841
|
+
}, 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({
|
|
2842
|
+
instance,
|
|
2843
|
+
definition,
|
|
2844
|
+
stage,
|
|
2845
|
+
actor,
|
|
2846
|
+
snapshot,
|
|
2847
|
+
scope,
|
|
2848
|
+
guardDenial: editGuardDenial
|
|
2849
|
+
});
|
|
2666
2850
|
return {
|
|
2667
2851
|
instance,
|
|
2668
2852
|
definition,
|
|
2669
2853
|
actor,
|
|
2670
2854
|
currentStage,
|
|
2671
2855
|
pendingOnYou,
|
|
2672
|
-
canInteract
|
|
2856
|
+
canInteract,
|
|
2857
|
+
editableSlots
|
|
2673
2858
|
};
|
|
2674
2859
|
}
|
|
2860
|
+
async function evaluateEditableSlots(args) {
|
|
2861
|
+
const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
|
|
2862
|
+
for (const slot of editableSlotsInStage(definition, stage)) {
|
|
2863
|
+
const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
|
|
2864
|
+
slot,
|
|
2865
|
+
window,
|
|
2866
|
+
guardDenial,
|
|
2867
|
+
instance,
|
|
2868
|
+
snapshot,
|
|
2869
|
+
scope,
|
|
2870
|
+
actor,
|
|
2871
|
+
roleAliases: definition.roleAliases
|
|
2872
|
+
}), reason = editDisabledReason({
|
|
2873
|
+
effective: slot.effective,
|
|
2874
|
+
instance,
|
|
2875
|
+
window,
|
|
2876
|
+
guardDenial,
|
|
2877
|
+
predicateSatisfied
|
|
2878
|
+
}), value = readSlotValue(instance, slot);
|
|
2879
|
+
slots.push({
|
|
2880
|
+
scope: slot.scope,
|
|
2881
|
+
...slot.task !== void 0 ? { task: slot.task } : {},
|
|
2882
|
+
name: slot.name,
|
|
2883
|
+
type: slot.type,
|
|
2884
|
+
...slot.title !== void 0 ? { title: slot.title } : {},
|
|
2885
|
+
value,
|
|
2886
|
+
editable: reason === void 0,
|
|
2887
|
+
...reason !== void 0 ? { disabledReason: reason } : {},
|
|
2888
|
+
...slotProvenance(instance, slot.ref)
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
return slots;
|
|
2892
|
+
}
|
|
2893
|
+
async function editPredicateSatisfied(args) {
|
|
2894
|
+
const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
|
|
2895
|
+
if (!window.open || guardDenial !== void 0) return !1;
|
|
2896
|
+
if (slot.effective === !0) return !0;
|
|
2897
|
+
const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
|
|
2898
|
+
return evaluateCondition({ condition: slot.effective, snapshot, params });
|
|
2899
|
+
}
|
|
2675
2900
|
async function renderScope(args) {
|
|
2676
2901
|
const { instance, definition, actor, grants, snapshot, now } = args, params = {
|
|
2677
2902
|
...buildParams({ instance, now, snapshot }),
|
|
@@ -2697,15 +2922,36 @@ async function advisoryCan(instance, actor, grants) {
|
|
|
2697
2922
|
});
|
|
2698
2923
|
return can;
|
|
2699
2924
|
}
|
|
2700
|
-
|
|
2701
|
-
const {
|
|
2925
|
+
function taskScopeFor(args) {
|
|
2926
|
+
const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
|
|
2927
|
+
return {
|
|
2702
2928
|
...scope,
|
|
2703
2929
|
state: {
|
|
2704
2930
|
...scope.state,
|
|
2705
|
-
...scopedStateOverlay(instance, snapshot,
|
|
2931
|
+
...scopedStateOverlay(instance, snapshot, taskName)
|
|
2706
2932
|
},
|
|
2707
|
-
assigned
|
|
2708
|
-
}
|
|
2933
|
+
assigned: assignedFor(instance, taskName, actor, roleAliases)
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
async function evaluateTask(args) {
|
|
2937
|
+
const {
|
|
2938
|
+
task,
|
|
2939
|
+
statusEntry,
|
|
2940
|
+
instance,
|
|
2941
|
+
actor,
|
|
2942
|
+
snapshot,
|
|
2943
|
+
scope,
|
|
2944
|
+
roleAliases,
|
|
2945
|
+
stageHasExits,
|
|
2946
|
+
guardDenial
|
|
2947
|
+
} = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
|
|
2948
|
+
scope,
|
|
2949
|
+
instance,
|
|
2950
|
+
snapshot,
|
|
2951
|
+
actor,
|
|
2952
|
+
taskName: task.name,
|
|
2953
|
+
roleAliases
|
|
2954
|
+
}), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
|
|
2709
2955
|
requirements: task.requirements,
|
|
2710
2956
|
snapshot,
|
|
2711
2957
|
params: taskScope
|
|
@@ -2932,10 +3178,7 @@ async function commitAbort(ctx, reason, actor) {
|
|
|
2932
3178
|
async function fireAction(args) {
|
|
2933
3179
|
const { client, instanceId, task, action, params, options } = args;
|
|
2934
3180
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2935
|
-
const ctx = await
|
|
2936
|
-
...options?.clock ? { clock: options.clock } : {},
|
|
2937
|
-
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2938
|
-
});
|
|
3181
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
2939
3182
|
try {
|
|
2940
3183
|
return await commitAction(ctx, task, action, params, options);
|
|
2941
3184
|
} catch (error) {
|
|
@@ -3030,6 +3273,114 @@ function formatDisabledReason(task, action, reason) {
|
|
|
3030
3273
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
3031
3274
|
return `Action "${task}:${action}" is not allowed: ${detail}`;
|
|
3032
3275
|
}
|
|
3276
|
+
class EditStateDeniedError extends Error {
|
|
3277
|
+
reason;
|
|
3278
|
+
target;
|
|
3279
|
+
constructor(args) {
|
|
3280
|
+
super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const editDisabledReasonDetail = {
|
|
3284
|
+
"not-editable": () => "slot is not declared editable",
|
|
3285
|
+
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
3286
|
+
"edit-window-closed": (r) => `edit window closed (${r.detail})`,
|
|
3287
|
+
"editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
|
|
3288
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
|
|
3289
|
+
};
|
|
3290
|
+
function formatEditDisabledReason(target, reason) {
|
|
3291
|
+
const detail = editDisabledReasonDetail[reason.kind](
|
|
3292
|
+
reason
|
|
3293
|
+
), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3294
|
+
return `State "${target.scope}:${where}" is not editable: ${detail}`;
|
|
3295
|
+
}
|
|
3296
|
+
async function editState(args) {
|
|
3297
|
+
const { client, instanceId, target, mode = "set", value, options } = args;
|
|
3298
|
+
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3299
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3300
|
+
try {
|
|
3301
|
+
return await commitEdit(ctx, target, mode, value, options);
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
if (!isRevisionConflict(error)) throw error;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
throw new ConcurrentEditStateError({
|
|
3307
|
+
instanceId,
|
|
3308
|
+
target: labelTarget(target),
|
|
3309
|
+
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
async function commitEdit(ctx, target, mode, value, options) {
|
|
3313
|
+
const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
|
|
3314
|
+
if (slot === void 0)
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
`No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
3317
|
+
);
|
|
3318
|
+
await assertSlotEditable(ctx, slot, options);
|
|
3319
|
+
const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
|
|
3320
|
+
ops: [op],
|
|
3321
|
+
mutation,
|
|
3322
|
+
stage: stage.name,
|
|
3323
|
+
origin: {
|
|
3324
|
+
edit: !0,
|
|
3325
|
+
...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
|
|
3326
|
+
},
|
|
3327
|
+
params: {},
|
|
3328
|
+
actor,
|
|
3329
|
+
self: selfGdr(ctx.instance),
|
|
3330
|
+
now: ctx.now
|
|
3331
|
+
});
|
|
3332
|
+
return await persistThenDeploy(
|
|
3333
|
+
ctx,
|
|
3334
|
+
mutation,
|
|
3335
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
|
|
3336
|
+
), {
|
|
3337
|
+
edited: !0,
|
|
3338
|
+
target: slotTarget(slot),
|
|
3339
|
+
...ranOps.length > 0 ? { ranOps } : {}
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
async function assertSlotEditable(ctx, slot, options) {
|
|
3343
|
+
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, {
|
|
3344
|
+
...slot.task !== void 0 ? { taskName: slot.task } : {},
|
|
3345
|
+
...actor !== void 0 ? { actor } : {},
|
|
3346
|
+
...can !== void 0 ? { vars: { can } } : {}
|
|
3347
|
+
}) : slot.effective === !0, reason = editDisabledReason({
|
|
3348
|
+
effective: slot.effective,
|
|
3349
|
+
instance: ctx.instance,
|
|
3350
|
+
window,
|
|
3351
|
+
guardDenial: void 0,
|
|
3352
|
+
predicateSatisfied
|
|
3353
|
+
});
|
|
3354
|
+
if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
|
|
3355
|
+
}
|
|
3356
|
+
function buildEditOp(slot, mode, value) {
|
|
3357
|
+
if (mode === "unset") return { type: "state.unset", target: slot.ref };
|
|
3358
|
+
if (value === void 0)
|
|
3359
|
+
throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
|
|
3360
|
+
return {
|
|
3361
|
+
type: mode === "append" ? "state.append" : "state.set",
|
|
3362
|
+
target: slot.ref,
|
|
3363
|
+
value: { type: "literal", value }
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
function slotTarget(slot) {
|
|
3367
|
+
return {
|
|
3368
|
+
scope: slot.scope,
|
|
3369
|
+
state: slot.name,
|
|
3370
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
function labelTarget(target) {
|
|
3374
|
+
return {
|
|
3375
|
+
scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
|
|
3376
|
+
state: target.state,
|
|
3377
|
+
...target.task !== void 0 ? { task: target.task } : {}
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
function describeTarget(target) {
|
|
3381
|
+
const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3382
|
+
return target.scope !== void 0 ? `${target.scope}:${where}` : where;
|
|
3383
|
+
}
|
|
3033
3384
|
function bareIdFromSpawnRef(uri) {
|
|
3034
3385
|
if (!uri.includes(":")) return [uri];
|
|
3035
3386
|
try {
|
|
@@ -3076,18 +3427,60 @@ function buildClientForGdr(defaultClient, resolver) {
|
|
|
3076
3427
|
function toEffectsContextEntries(ctx) {
|
|
3077
3428
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
3078
3429
|
}
|
|
3430
|
+
async function dispatchGatedWrite(args) {
|
|
3431
|
+
const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
|
|
3432
|
+
client,
|
|
3433
|
+
tag,
|
|
3434
|
+
instanceId,
|
|
3435
|
+
access,
|
|
3436
|
+
clock: args.clock,
|
|
3437
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3438
|
+
}), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
|
|
3439
|
+
return {
|
|
3440
|
+
instance: await reload(client, instanceId, tag),
|
|
3441
|
+
cascaded,
|
|
3442
|
+
fired: !0,
|
|
3443
|
+
...ranOps !== void 0 ? { ranOps } : {}
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3079
3446
|
function assertActionAllowed(evaluation, task, action) {
|
|
3080
3447
|
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
|
|
3081
3448
|
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
3082
3449
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
3083
3450
|
}
|
|
3084
|
-
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3451
|
+
function assertEditAllowed(evaluation, target) {
|
|
3452
|
+
const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
3453
|
+
if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
|
|
3454
|
+
throw new EditStateDeniedError({
|
|
3455
|
+
target: {
|
|
3456
|
+
scope: slot.scope,
|
|
3457
|
+
state: slot.name,
|
|
3458
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3459
|
+
},
|
|
3460
|
+
reason: slot.disabledReason
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
async function applyEdit(args) {
|
|
3464
|
+
const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
|
|
3465
|
+
return (await editState({
|
|
3466
|
+
client,
|
|
3467
|
+
instanceId: instance._id,
|
|
3468
|
+
target,
|
|
3469
|
+
...mode !== void 0 ? { mode } : {},
|
|
3470
|
+
...value !== void 0 ? { value } : {},
|
|
3471
|
+
options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
|
|
3472
|
+
})).ranOps;
|
|
3473
|
+
}
|
|
3474
|
+
function buildEngineCallOptions(args) {
|
|
3475
|
+
return {
|
|
3476
|
+
actor: args.actor,
|
|
3477
|
+
...args.grants !== void 0 ? { grants: args.grants } : {},
|
|
3478
|
+
...args.clock !== void 0 ? { clock: args.clock } : {},
|
|
3479
|
+
...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
|
|
3090
3480
|
};
|
|
3481
|
+
}
|
|
3482
|
+
async function applyAction(args) {
|
|
3483
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
|
|
3091
3484
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
3092
3485
|
client,
|
|
3093
3486
|
instanceId: instance._id,
|
|
@@ -3261,6 +3654,7 @@ const workflow = {
|
|
|
3261
3654
|
selfId: id,
|
|
3262
3655
|
tag,
|
|
3263
3656
|
workflowResource,
|
|
3657
|
+
definitionName: definition.name,
|
|
3264
3658
|
...perspective !== void 0 ? { perspective } : {}
|
|
3265
3659
|
},
|
|
3266
3660
|
randomKey
|
|
@@ -3302,34 +3696,68 @@ const workflow = {
|
|
|
3302
3696
|
idempotent,
|
|
3303
3697
|
resourceClients
|
|
3304
3698
|
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3305
|
-
|
|
3306
|
-
return { instance: before, cascaded: 0, fired: !1 };
|
|
3307
|
-
const evaluation = await evaluateInstance({
|
|
3699
|
+
return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
|
|
3308
3700
|
client,
|
|
3309
3701
|
tag,
|
|
3310
3702
|
instanceId,
|
|
3311
3703
|
access,
|
|
3704
|
+
actor,
|
|
3705
|
+
clientForGdr,
|
|
3312
3706
|
clock,
|
|
3313
|
-
|
|
3707
|
+
before,
|
|
3708
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3709
|
+
apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
|
|
3710
|
+
client,
|
|
3711
|
+
instance,
|
|
3712
|
+
task,
|
|
3713
|
+
action,
|
|
3714
|
+
actor,
|
|
3715
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3716
|
+
clock,
|
|
3717
|
+
clientForGdr,
|
|
3718
|
+
...params !== void 0 ? { params } : {}
|
|
3719
|
+
}))
|
|
3314
3720
|
});
|
|
3315
|
-
|
|
3316
|
-
|
|
3721
|
+
},
|
|
3722
|
+
/**
|
|
3723
|
+
* Edit a declared-editable state slot directly — reassign, reschedule,
|
|
3724
|
+
* claim-by-hand, append to a running log — through the generic edit seam,
|
|
3725
|
+
* instead of a bespoke action per field. Soft-gates on the slot's declared
|
|
3726
|
+
* editability (the same projection a UI renders), applies the edit as a
|
|
3727
|
+
* `state.*` op (so provenance + history are stamped by the op path),
|
|
3728
|
+
* refreshes the stage's guards, then cascades — an edit to a value a
|
|
3729
|
+
* transition reads can and should move the instance. Advisory like every
|
|
3730
|
+
* engine gate; the lake ACL + guards are the only hard locks.
|
|
3731
|
+
*
|
|
3732
|
+
* Each call is a discrete COMMIT (a history entry, a guard refresh, a
|
|
3733
|
+
* cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
|
|
3734
|
+
* UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
|
|
3735
|
+
* never an `onChange` per keystroke.
|
|
3736
|
+
*/
|
|
3737
|
+
editState: async (args) => {
|
|
3738
|
+
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);
|
|
3739
|
+
return dispatchGatedWrite({
|
|
3317
3740
|
client,
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3741
|
+
tag,
|
|
3742
|
+
instanceId,
|
|
3743
|
+
access,
|
|
3321
3744
|
actor,
|
|
3322
|
-
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3323
|
-
clock,
|
|
3324
3745
|
clientForGdr,
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3746
|
+
clock,
|
|
3747
|
+
before,
|
|
3748
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3749
|
+
apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
|
|
3750
|
+
client,
|
|
3751
|
+
instance,
|
|
3752
|
+
target,
|
|
3753
|
+
...mode !== void 0 ? { mode } : {},
|
|
3754
|
+
...value !== void 0 ? { value } : {},
|
|
3755
|
+
actor,
|
|
3756
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3757
|
+
clock,
|
|
3758
|
+
clientForGdr
|
|
3759
|
+
}))
|
|
3760
|
+
});
|
|
3333
3761
|
},
|
|
3334
3762
|
/**
|
|
3335
3763
|
* Report a queued effect's outcome. Drains it from `pendingEffects`,
|
|
@@ -3747,6 +4175,9 @@ function createInstanceSession(args) {
|
|
|
3747
4175
|
...clock !== void 0 ? { now: clock() } : {},
|
|
3748
4176
|
...grants !== void 0 ? { grants } : {}
|
|
3749
4177
|
});
|
|
4178
|
+
}, settleAfterApply = async (actor, held, ranOps) => {
|
|
4179
|
+
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
4180
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3750
4181
|
}, commit = async (run) => {
|
|
3751
4182
|
committing = !0;
|
|
3752
4183
|
const held = new Map(overlay);
|
|
@@ -3797,8 +4228,25 @@ function createInstanceSession(args) {
|
|
|
3797
4228
|
...grants !== void 0 ? { grants } : {},
|
|
3798
4229
|
...clock !== void 0 ? { clock } : {},
|
|
3799
4230
|
...params !== void 0 ? { params } : {}
|
|
3800
|
-
})
|
|
3801
|
-
return
|
|
4231
|
+
});
|
|
4232
|
+
return settleAfterApply(actor, held, ranOps);
|
|
4233
|
+
});
|
|
4234
|
+
},
|
|
4235
|
+
editState({ target, mode, value }) {
|
|
4236
|
+
return commit(async (actor, held) => {
|
|
4237
|
+
assertEditAllowed(await evaluateWith(held, heldGuards), target);
|
|
4238
|
+
const { grants } = await access(), ranOps = await applyEdit({
|
|
4239
|
+
client,
|
|
4240
|
+
instance,
|
|
4241
|
+
target,
|
|
4242
|
+
...mode !== void 0 ? { mode } : {},
|
|
4243
|
+
...value !== void 0 ? { value } : {},
|
|
4244
|
+
actor,
|
|
4245
|
+
clientForGdr,
|
|
4246
|
+
...grants !== void 0 ? { grants } : {},
|
|
4247
|
+
...clock !== void 0 ? { clock } : {}
|
|
4248
|
+
});
|
|
4249
|
+
return settleAfterApply(actor, held, ranOps);
|
|
3802
4250
|
});
|
|
3803
4251
|
}
|
|
3804
4252
|
};
|
|
@@ -3846,6 +4294,7 @@ function createEngine(args) {
|
|
|
3846
4294
|
deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
|
|
3847
4295
|
startInstance: (rest) => workflow.startInstance(bind(rest)),
|
|
3848
4296
|
fireAction: (rest) => workflow.fireAction(bind(rest)),
|
|
4297
|
+
editState: (rest) => workflow.editState(bind(rest)),
|
|
3849
4298
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3850
4299
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3851
4300
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
@@ -4141,16 +4590,20 @@ export {
|
|
|
4141
4590
|
ActionDisabledError,
|
|
4142
4591
|
ActionParamsInvalidError,
|
|
4143
4592
|
CascadeLimitError,
|
|
4593
|
+
ConcurrentEditStateError,
|
|
4144
4594
|
ConcurrentFireActionError,
|
|
4145
4595
|
DEFAULT_CONTENT_PERSPECTIVE,
|
|
4146
4596
|
DISPLAY,
|
|
4147
4597
|
EFFECTS_CONTEXT_DISPLAY,
|
|
4598
|
+
EditStateDeniedError,
|
|
4148
4599
|
GUARD_DOC_TYPE,
|
|
4149
4600
|
GUARD_LIFTED_PREDICATE,
|
|
4150
4601
|
GUARD_OWNER,
|
|
4151
4602
|
HISTORY_DISPLAY,
|
|
4152
4603
|
MutationGuardDeniedError,
|
|
4153
4604
|
OP_DISPLAY,
|
|
4605
|
+
PartialGuardDeployError,
|
|
4606
|
+
RequiredStateNotProvidedError,
|
|
4154
4607
|
STATE_SLOT_DISPLAY,
|
|
4155
4608
|
WORKFLOW_DEFINITION_TYPE,
|
|
4156
4609
|
WORKFLOW_INSTANCE_TYPE,
|
|
@@ -4186,6 +4639,7 @@ export {
|
|
|
4186
4639
|
instanceGuardQuery,
|
|
4187
4640
|
isDefinitionUnchanged,
|
|
4188
4641
|
isGdr,
|
|
4642
|
+
isStartableDefinition,
|
|
4189
4643
|
isTerminalStage,
|
|
4190
4644
|
lakeGuardId,
|
|
4191
4645
|
parseGdr,
|