@sanity/workflow-engine 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +926 -673
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +279 -4
- package/dist/index.d.ts +279 -4
- package/dist/index.js +926 -673
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -262,6 +262,20 @@ function effectBindings(effects) {
|
|
|
262
262
|
(e) => Object.entries(e.bindings ?? {}).map(([k, groq]) => [`${e.name}.${k}`, groq])
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
|
+
function actionVerdict(task, action) {
|
|
266
|
+
return {
|
|
267
|
+
task: task.task.name,
|
|
268
|
+
taskStatus: task.status,
|
|
269
|
+
action: action.action.name,
|
|
270
|
+
title: action.action.title,
|
|
271
|
+
allowed: action.allowed,
|
|
272
|
+
disabledReason: action.disabledReason,
|
|
273
|
+
params: action.action.params ?? []
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function availableActions(tasks) {
|
|
277
|
+
return tasks.flatMap((t) => t.actions.map((a) => actionVerdict(t, a)));
|
|
278
|
+
}
|
|
265
279
|
const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
|
|
266
280
|
class MutationGuardDeniedError extends Error {
|
|
267
281
|
denied;
|
|
@@ -456,79 +470,63 @@ function stripStateForLake(value) {
|
|
|
456
470
|
function bareId(id) {
|
|
457
471
|
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
458
472
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
464
|
-
return { doc: compileGuard({
|
|
465
|
-
id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
|
|
466
|
-
resourceType: resource.type,
|
|
467
|
-
resourceId: resource.id,
|
|
468
|
-
owner: GUARD_OWNER,
|
|
469
|
-
sourceInstanceId: instance._id,
|
|
470
|
-
sourceDefinition: instance.definition,
|
|
471
|
-
sourceStage: stageName,
|
|
472
|
-
name: guard.name,
|
|
473
|
-
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
474
|
-
match: {
|
|
475
|
-
...types !== void 0 ? { types } : {},
|
|
476
|
-
idRefs: bareIdRefs(targets),
|
|
477
|
-
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
478
|
-
actions: guard.match.actions
|
|
479
|
-
},
|
|
480
|
-
predicate: guard.predicate ?? "",
|
|
481
|
-
metadata: resolveMetadata(guard.metadata, ctx)
|
|
482
|
-
}), routeGdr: targets[0].parsed };
|
|
473
|
+
function abortReason(instance) {
|
|
474
|
+
return instance.history.find(
|
|
475
|
+
(h) => h._type === "aborted"
|
|
476
|
+
)?.reason;
|
|
483
477
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
await client.patch(doc._id).set(body).commit();
|
|
478
|
+
function diagnoseInputFromEvaluation(evaluation) {
|
|
479
|
+
return {
|
|
480
|
+
instance: evaluation.instance,
|
|
481
|
+
tasks: evaluation.currentStage.tasks,
|
|
482
|
+
transitions: evaluation.currentStage.transitions
|
|
483
|
+
};
|
|
491
484
|
}
|
|
492
|
-
function
|
|
493
|
-
|
|
494
|
-
for (const guard of stage?.guards ?? []) {
|
|
495
|
-
const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
|
|
496
|
-
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
497
|
-
}
|
|
498
|
-
return out;
|
|
485
|
+
function openStage(instance) {
|
|
486
|
+
return findOpenStageEntry(instance);
|
|
499
487
|
}
|
|
500
|
-
|
|
501
|
-
return
|
|
488
|
+
function assigneesOf(stage, taskName) {
|
|
489
|
+
return (stage?.tasks.find((t) => t.name === taskName)?.state ?? []).filter((s) => s._type === "assignees").flatMap((s) => s.value);
|
|
502
490
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
|
|
506
|
-
for (const { client, doc } of resolvedStageGuards(args))
|
|
507
|
-
await upsertGuard(client, doc);
|
|
491
|
+
function isResolved(status) {
|
|
492
|
+
return status === "done" || status === "skipped";
|
|
508
493
|
}
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
for (const { client, doc } of resolvedStageGuards(args))
|
|
513
|
-
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
494
|
+
function failedEffectCause(input) {
|
|
495
|
+
const stalled = new Set(input.tasks.filter((t) => !isResolved(t.status)).map((t) => t.task.name)), effect = [...input.instance.effectHistory].reverse().find((e) => e.status === "failed" && e.origin.kind === "task" && stalled.has(e.origin.name));
|
|
496
|
+
return effect ? { kind: "failed-effect", effect } : void 0;
|
|
514
497
|
}
|
|
515
|
-
function
|
|
516
|
-
const
|
|
517
|
-
return
|
|
498
|
+
function hungEffectCause(input) {
|
|
499
|
+
const effect = input.instance.pendingEffects.find((e) => e.claim !== void 0);
|
|
500
|
+
return effect ? { kind: "hung-effect", effect } : void 0;
|
|
518
501
|
}
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
return
|
|
502
|
+
function failedTaskCause(input) {
|
|
503
|
+
const failed = input.tasks.find((t) => t.status === "failed");
|
|
504
|
+
return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
|
|
522
505
|
}
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
506
|
+
function waitingState(input) {
|
|
507
|
+
const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
|
|
508
|
+
if (active !== void 0)
|
|
509
|
+
return {
|
|
510
|
+
state: "waiting",
|
|
511
|
+
task: active.task.name,
|
|
512
|
+
assignees: assigneesOf(openStage(input.instance), active.task.name),
|
|
513
|
+
actions: (active.task.actions ?? []).map((a) => a.name)
|
|
514
|
+
};
|
|
528
515
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
516
|
+
function noTransitionFiresCause(input) {
|
|
517
|
+
if (input.transitions.length !== 0 && !input.transitions.some((t) => t.filterSatisfied) && input.tasks.every((t) => isResolved(t.status)))
|
|
518
|
+
return { kind: "no-transition-fires" };
|
|
519
|
+
}
|
|
520
|
+
function diagnoseInstance(input) {
|
|
521
|
+
const { instance } = input;
|
|
522
|
+
if (instance.abortedAt !== void 0) {
|
|
523
|
+
const reason = abortReason(instance);
|
|
524
|
+
return { state: "aborted", at: instance.abortedAt, ...reason !== void 0 ? { reason } : {} };
|
|
525
|
+
}
|
|
526
|
+
if (instance.completedAt !== void 0)
|
|
527
|
+
return { state: "completed", at: instance.completedAt };
|
|
528
|
+
const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
|
|
529
|
+
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
|
|
532
530
|
}
|
|
533
531
|
function buildSnapshot(args) {
|
|
534
532
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
@@ -647,156 +645,357 @@ function resourceFromParsed(parsed) {
|
|
|
647
645
|
function collectEntryDocUris(resolvedStateEntries) {
|
|
648
646
|
return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
|
|
649
647
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
issues;
|
|
654
|
-
constructor(args) {
|
|
655
|
-
const issueText = args.issues.join("; ");
|
|
656
|
-
super(
|
|
657
|
-
`State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
|
|
658
|
-
), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const GdrShape = v__namespace.looseObject({
|
|
662
|
-
id: v__namespace.pipe(
|
|
663
|
-
v__namespace.string(),
|
|
664
|
-
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
665
|
-
),
|
|
666
|
-
type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
667
|
-
}), ReleaseRefShape = v__namespace.looseObject({
|
|
668
|
-
id: v__namespace.pipe(
|
|
669
|
-
v__namespace.string(),
|
|
670
|
-
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
671
|
-
),
|
|
672
|
-
type: v__namespace.literal("system.release"),
|
|
673
|
-
releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
674
|
-
}), ActorShape = v__namespace.looseObject({
|
|
675
|
-
kind: v__namespace.picklist(["user", "ai", "system"]),
|
|
676
|
-
id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
677
|
-
roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
|
|
678
|
-
onBehalfOf: v__namespace.optional(v__namespace.string())
|
|
679
|
-
}), AssigneeShape = v__namespace.union([
|
|
680
|
-
v__namespace.looseObject({ type: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
|
|
681
|
-
v__namespace.looseObject({ type: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
|
|
682
|
-
]), ChecklistItemShape = v__namespace.looseObject({
|
|
683
|
-
label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
684
|
-
done: v__namespace.boolean(),
|
|
685
|
-
doneBy: v__namespace.optional(v__namespace.string()),
|
|
686
|
-
doneAt: v__namespace.optional(v__namespace.string()),
|
|
687
|
-
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
688
|
-
}), NoteItemShape = v__namespace.pipe(
|
|
689
|
-
v__namespace.looseObject({
|
|
690
|
-
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
691
|
-
}),
|
|
692
|
-
v__namespace.check(
|
|
693
|
-
(val) => typeof val == "object" && val !== null && !Array.isArray(val),
|
|
694
|
-
"must be an object"
|
|
695
|
-
)
|
|
696
|
-
), NullableString = v__namespace.union([v__namespace.null(), v__namespace.string()]), NullableNumber = v__namespace.union([v__namespace.null(), v__namespace.number()]), NullableBoolean = v__namespace.union([v__namespace.null(), v__namespace.boolean()]), NullableDateTime = v__namespace.union([
|
|
697
|
-
v__namespace.null(),
|
|
698
|
-
v__namespace.pipe(
|
|
699
|
-
v__namespace.string(),
|
|
700
|
-
v__namespace.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
701
|
-
)
|
|
702
|
-
]), NullableUrl = NullableString, valueSchemas = {
|
|
703
|
-
"doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
|
|
704
|
-
"doc.refs": v__namespace.array(GdrShape),
|
|
705
|
-
"release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
|
|
706
|
-
query: v__namespace.any(),
|
|
707
|
-
"value.string": NullableString,
|
|
708
|
-
"value.url": NullableUrl,
|
|
709
|
-
"value.number": NullableNumber,
|
|
710
|
-
"value.boolean": NullableBoolean,
|
|
711
|
-
"value.dateTime": NullableDateTime,
|
|
712
|
-
"value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
|
|
713
|
-
checklist: v__namespace.array(ChecklistItemShape),
|
|
714
|
-
notes: v__namespace.array(NoteItemShape),
|
|
715
|
-
assignees: v__namespace.array(AssigneeShape)
|
|
716
|
-
}, itemSchemas = {
|
|
717
|
-
"doc.refs": GdrShape,
|
|
718
|
-
checklist: ChecklistItemShape,
|
|
719
|
-
notes: NoteItemShape,
|
|
720
|
-
assignees: AssigneeShape
|
|
721
|
-
};
|
|
722
|
-
function isAppendable(entryType) {
|
|
723
|
-
return entryType in itemSchemas;
|
|
648
|
+
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
649
|
+
function guardsForResource(client) {
|
|
650
|
+
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
724
651
|
}
|
|
725
|
-
function
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
entryName: args.entryName,
|
|
731
|
-
issues: [`unknown state entry type ${args.entryType}`],
|
|
732
|
-
mode: "value"
|
|
733
|
-
});
|
|
734
|
-
const result = v__namespace.safeParse(schema2, args.value);
|
|
735
|
-
if (!result.success)
|
|
736
|
-
throw new StateValueShapeError({
|
|
737
|
-
entryType: args.entryType,
|
|
738
|
-
entryName: args.entryName,
|
|
739
|
-
issues: formatIssues(result.issues),
|
|
740
|
-
mode: "value"
|
|
741
|
-
});
|
|
652
|
+
function instanceGuardQuery(instanceId) {
|
|
653
|
+
return {
|
|
654
|
+
query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
|
|
655
|
+
params: { guardType: GUARD_DOC_TYPE, instanceId }
|
|
656
|
+
};
|
|
742
657
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
entryType: args.entryType,
|
|
747
|
-
entryName: args.entryName,
|
|
748
|
-
issues: [`state entry type ${args.entryType} does not support append`],
|
|
749
|
-
mode: "item"
|
|
750
|
-
});
|
|
751
|
-
const schema2 = itemSchemas[args.entryType], result = v__namespace.safeParse(schema2, args.item);
|
|
752
|
-
if (!result.success)
|
|
753
|
-
throw new StateValueShapeError({
|
|
754
|
-
entryType: args.entryType,
|
|
755
|
-
entryName: args.entryName,
|
|
756
|
-
issues: formatIssues(result.issues),
|
|
757
|
-
mode: "item"
|
|
758
|
-
});
|
|
658
|
+
function verdictGuardsForInstance(client, instanceId) {
|
|
659
|
+
const { query, params } = instanceGuardQuery(instanceId);
|
|
660
|
+
return client.fetch(query, params);
|
|
759
661
|
}
|
|
760
|
-
function
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
662
|
+
async function guardsForInstance(args) {
|
|
663
|
+
const { client, clientForGdr, instance } = args, stateUris = [
|
|
664
|
+
...collectEntryDocUris(instance.state),
|
|
665
|
+
...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
|
|
666
|
+
], clients = resourceClientMap(
|
|
667
|
+
instance.workflowResource,
|
|
668
|
+
client,
|
|
669
|
+
clientForGdr,
|
|
670
|
+
stateUris.map((uri) => tryParseGdr(uri))
|
|
671
|
+
), { query, params } = instanceGuardQuery(instance._id);
|
|
672
|
+
return queryGuardsAcross(clients.values(), query, params);
|
|
765
673
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (entry._type === "release.ref" && entry.value !== null) {
|
|
770
|
-
const releaseName = entry.value.releaseName;
|
|
771
|
-
if (typeof releaseName == "string" && releaseName.length > 0)
|
|
772
|
-
return [releaseName];
|
|
773
|
-
}
|
|
774
|
-
}
|
|
674
|
+
async function guardsForDefinition(args) {
|
|
675
|
+
const perClient = await guardsForDefinitionByClient(args);
|
|
676
|
+
return dedupById(perClient.flatMap((entry) => entry.guards));
|
|
775
677
|
}
|
|
776
|
-
async function
|
|
777
|
-
const {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
678
|
+
async function guardsForDefinitionByClient(args) {
|
|
679
|
+
const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
|
|
680
|
+
(def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
|
|
681
|
+
), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs), query = "*[_type == $t && sourceDefinition == $definition]", params = { t: GUARD_DOC_TYPE, definition };
|
|
682
|
+
return Promise.all(
|
|
683
|
+
[...clients.values()].map(async (resourceClient) => ({
|
|
684
|
+
client: resourceClient,
|
|
685
|
+
guards: await resourceClient.fetch(query, params)
|
|
686
|
+
}))
|
|
687
|
+
);
|
|
785
688
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
function defaultEntryValue(entryType) {
|
|
793
|
-
return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
|
|
689
|
+
function guardIdRefs(definition) {
|
|
690
|
+
return definition.stages.flatMap(
|
|
691
|
+
(stage) => (stage.guards ?? []).flatMap(
|
|
692
|
+
(guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
|
|
693
|
+
)
|
|
694
|
+
);
|
|
794
695
|
}
|
|
795
|
-
function
|
|
796
|
-
const
|
|
797
|
-
|
|
696
|
+
function staticIdRefGdr(idRef, stage, definition) {
|
|
697
|
+
const read = /^\$state\.([\w-]+)/.exec(idRef);
|
|
698
|
+
if (read === null) return;
|
|
699
|
+
const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
|
|
700
|
+
return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
|
|
798
701
|
}
|
|
799
|
-
function
|
|
702
|
+
function entriesInScope(stage, definition) {
|
|
703
|
+
return [
|
|
704
|
+
...definition.state ?? [],
|
|
705
|
+
...stage.state ?? [],
|
|
706
|
+
...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
|
|
707
|
+
];
|
|
708
|
+
}
|
|
709
|
+
function gdrFromValue(value) {
|
|
710
|
+
if (typeof value == "string") return tryParseGdr(value);
|
|
711
|
+
if (typeof value == "object" && value !== null && "id" in value) {
|
|
712
|
+
const id = value.id;
|
|
713
|
+
return typeof id == "string" ? tryParseGdr(id) : void 0;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function tryParseGdr(uri) {
|
|
717
|
+
try {
|
|
718
|
+
return parseGdr(uri);
|
|
719
|
+
} catch {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
|
|
724
|
+
const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
|
|
725
|
+
for (const parsed of gdrs) {
|
|
726
|
+
if (parsed === void 0) continue;
|
|
727
|
+
const key = resourceKey(resourceFromParsed(parsed));
|
|
728
|
+
map.has(key) || map.set(key, clientForGdr(parsed));
|
|
729
|
+
}
|
|
730
|
+
return map;
|
|
731
|
+
}
|
|
732
|
+
async function queryGuardsAcross(clients, query, params) {
|
|
733
|
+
const perResource = await Promise.all(
|
|
734
|
+
[...clients].map((c) => c.fetch(query, params))
|
|
735
|
+
);
|
|
736
|
+
return dedupById(perResource.flat());
|
|
737
|
+
}
|
|
738
|
+
function dedupById(guards) {
|
|
739
|
+
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
740
|
+
}
|
|
741
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
742
|
+
function validateTags(tags) {
|
|
743
|
+
if (tags.length === 0)
|
|
744
|
+
throw new Error("tags: must be a non-empty array");
|
|
745
|
+
for (const tag of tags)
|
|
746
|
+
if (!TAG_RE.test(tag))
|
|
747
|
+
throw new Error(
|
|
748
|
+
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
function canonicalTag(tags) {
|
|
752
|
+
return tags[0];
|
|
753
|
+
}
|
|
754
|
+
function tagScopeFilter(param = "engineTags") {
|
|
755
|
+
return `count(tags[@ in $${param}]) > 0`;
|
|
756
|
+
}
|
|
757
|
+
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
758
|
+
function resolveGuard(guard, instance, stageName, now) {
|
|
759
|
+
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
760
|
+
if (targets === null) return null;
|
|
761
|
+
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
762
|
+
return { doc: compileGuard({
|
|
763
|
+
id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
|
|
764
|
+
resourceType: resource.type,
|
|
765
|
+
resourceId: resource.id,
|
|
766
|
+
owner: GUARD_OWNER,
|
|
767
|
+
sourceInstanceId: instance._id,
|
|
768
|
+
sourceDefinition: instance.definition,
|
|
769
|
+
sourceStage: stageName,
|
|
770
|
+
name: guard.name,
|
|
771
|
+
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
772
|
+
match: {
|
|
773
|
+
...types !== void 0 ? { types } : {},
|
|
774
|
+
idRefs: bareIdRefs(targets),
|
|
775
|
+
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
776
|
+
actions: guard.match.actions
|
|
777
|
+
},
|
|
778
|
+
predicate: guard.predicate ?? "",
|
|
779
|
+
metadata: resolveMetadata(guard.metadata, ctx)
|
|
780
|
+
}), routeGdr: targets[0].parsed };
|
|
781
|
+
}
|
|
782
|
+
async function upsertGuard(client, doc) {
|
|
783
|
+
if (!await client.getDocument(doc._id)) {
|
|
784
|
+
await client.create(doc);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
788
|
+
await client.patch(doc._id).set(body).commit();
|
|
789
|
+
}
|
|
790
|
+
function resolvedStageGuards(args) {
|
|
791
|
+
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
792
|
+
for (const guard of stage?.guards ?? []) {
|
|
793
|
+
const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
|
|
794
|
+
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
async function committedInstance(args) {
|
|
799
|
+
return await args.client.getDocument(args.instance._id) ?? void 0;
|
|
800
|
+
}
|
|
801
|
+
async function deployStageGuards(args) {
|
|
802
|
+
const live = await committedInstance(args);
|
|
803
|
+
if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
|
|
804
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
805
|
+
await upsertGuard(client, doc);
|
|
806
|
+
}
|
|
807
|
+
async function retractStageGuards(args) {
|
|
808
|
+
const live = await committedInstance(args);
|
|
809
|
+
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
810
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
811
|
+
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
812
|
+
}
|
|
813
|
+
async function deleteOrphanedDefinitionGuards(args) {
|
|
814
|
+
const { client, clientForGdr, workflowResource, definition, definitions, tags } = args, perClient = await guardsForDefinitionByClient({
|
|
815
|
+
client,
|
|
816
|
+
clientForGdr,
|
|
817
|
+
workflowResource,
|
|
818
|
+
definition,
|
|
819
|
+
definitions
|
|
820
|
+
}), ownPartitionPrefix = `${canonicalTag(tags)}.`;
|
|
821
|
+
let count = 0;
|
|
822
|
+
for (const { client: resourceClient, guards } of perClient) {
|
|
823
|
+
const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
|
|
824
|
+
if (own.length === 0) continue;
|
|
825
|
+
const tx = resourceClient.transaction();
|
|
826
|
+
for (const guard of own) tx.delete(guard._id);
|
|
827
|
+
await tx.commit(), count += own.length;
|
|
828
|
+
}
|
|
829
|
+
return count;
|
|
830
|
+
}
|
|
831
|
+
function randomKey(length = 12) {
|
|
832
|
+
const bytes = new Uint8Array(length);
|
|
833
|
+
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
834
|
+
}
|
|
835
|
+
async function evaluateCondition(args) {
|
|
836
|
+
const { condition, snapshot, params } = args;
|
|
837
|
+
return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
|
|
838
|
+
}
|
|
839
|
+
async function evaluatePredicates(args) {
|
|
840
|
+
const out = {};
|
|
841
|
+
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
842
|
+
out[name] = !!await runGroq(groq, args.params, args.snapshot);
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
async function runGroq(groq, params, snapshot) {
|
|
846
|
+
const tree = groqJs.parse(groq, { params });
|
|
847
|
+
return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
848
|
+
}
|
|
849
|
+
class StateValueShapeError extends Error {
|
|
850
|
+
entryType;
|
|
851
|
+
entryName;
|
|
852
|
+
issues;
|
|
853
|
+
constructor(args) {
|
|
854
|
+
const issueText = args.issues.join("; ");
|
|
855
|
+
super(
|
|
856
|
+
`State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
|
|
857
|
+
), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const GdrShape = v__namespace.looseObject({
|
|
861
|
+
id: v__namespace.pipe(
|
|
862
|
+
v__namespace.string(),
|
|
863
|
+
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
864
|
+
),
|
|
865
|
+
type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
866
|
+
}), ReleaseRefShape = v__namespace.looseObject({
|
|
867
|
+
id: v__namespace.pipe(
|
|
868
|
+
v__namespace.string(),
|
|
869
|
+
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
870
|
+
),
|
|
871
|
+
type: v__namespace.literal("system.release"),
|
|
872
|
+
releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
873
|
+
}), ActorShape = v__namespace.looseObject({
|
|
874
|
+
kind: v__namespace.picklist(["user", "ai", "system"]),
|
|
875
|
+
id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
876
|
+
roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
|
|
877
|
+
onBehalfOf: v__namespace.optional(v__namespace.string())
|
|
878
|
+
}), AssigneeShape = v__namespace.union([
|
|
879
|
+
v__namespace.looseObject({ type: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
|
|
880
|
+
v__namespace.looseObject({ type: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
|
|
881
|
+
]), ChecklistItemShape = v__namespace.looseObject({
|
|
882
|
+
label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
883
|
+
done: v__namespace.boolean(),
|
|
884
|
+
doneBy: v__namespace.optional(v__namespace.string()),
|
|
885
|
+
doneAt: v__namespace.optional(v__namespace.string()),
|
|
886
|
+
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
887
|
+
}), NoteItemShape = v__namespace.pipe(
|
|
888
|
+
v__namespace.looseObject({
|
|
889
|
+
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
890
|
+
}),
|
|
891
|
+
v__namespace.check(
|
|
892
|
+
(val) => typeof val == "object" && val !== null && !Array.isArray(val),
|
|
893
|
+
"must be an object"
|
|
894
|
+
)
|
|
895
|
+
), NullableString = v__namespace.union([v__namespace.null(), v__namespace.string()]), NullableNumber = v__namespace.union([v__namespace.null(), v__namespace.number()]), NullableBoolean = v__namespace.union([v__namespace.null(), v__namespace.boolean()]), NullableDateTime = v__namespace.union([
|
|
896
|
+
v__namespace.null(),
|
|
897
|
+
v__namespace.pipe(
|
|
898
|
+
v__namespace.string(),
|
|
899
|
+
v__namespace.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
900
|
+
)
|
|
901
|
+
]), NullableUrl = NullableString, valueSchemas = {
|
|
902
|
+
"doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
|
|
903
|
+
"doc.refs": v__namespace.array(GdrShape),
|
|
904
|
+
"release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
|
|
905
|
+
query: v__namespace.any(),
|
|
906
|
+
"value.string": NullableString,
|
|
907
|
+
"value.url": NullableUrl,
|
|
908
|
+
"value.number": NullableNumber,
|
|
909
|
+
"value.boolean": NullableBoolean,
|
|
910
|
+
"value.dateTime": NullableDateTime,
|
|
911
|
+
"value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
|
|
912
|
+
checklist: v__namespace.array(ChecklistItemShape),
|
|
913
|
+
notes: v__namespace.array(NoteItemShape),
|
|
914
|
+
assignees: v__namespace.array(AssigneeShape)
|
|
915
|
+
}, itemSchemas = {
|
|
916
|
+
"doc.refs": GdrShape,
|
|
917
|
+
checklist: ChecklistItemShape,
|
|
918
|
+
notes: NoteItemShape,
|
|
919
|
+
assignees: AssigneeShape
|
|
920
|
+
};
|
|
921
|
+
function isAppendable(entryType) {
|
|
922
|
+
return entryType in itemSchemas;
|
|
923
|
+
}
|
|
924
|
+
function validateStateValue(args) {
|
|
925
|
+
const schema2 = valueSchemas[args.entryType];
|
|
926
|
+
if (schema2 === void 0)
|
|
927
|
+
throw new StateValueShapeError({
|
|
928
|
+
entryType: args.entryType,
|
|
929
|
+
entryName: args.entryName,
|
|
930
|
+
issues: [`unknown state entry type ${args.entryType}`],
|
|
931
|
+
mode: "value"
|
|
932
|
+
});
|
|
933
|
+
const result = v__namespace.safeParse(schema2, args.value);
|
|
934
|
+
if (!result.success)
|
|
935
|
+
throw new StateValueShapeError({
|
|
936
|
+
entryType: args.entryType,
|
|
937
|
+
entryName: args.entryName,
|
|
938
|
+
issues: formatIssues(result.issues),
|
|
939
|
+
mode: "value"
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function validateStateAppendItem(args) {
|
|
943
|
+
if (!isAppendable(args.entryType))
|
|
944
|
+
throw new StateValueShapeError({
|
|
945
|
+
entryType: args.entryType,
|
|
946
|
+
entryName: args.entryName,
|
|
947
|
+
issues: [`state entry type ${args.entryType} does not support append`],
|
|
948
|
+
mode: "item"
|
|
949
|
+
});
|
|
950
|
+
const schema2 = itemSchemas[args.entryType], result = v__namespace.safeParse(schema2, args.item);
|
|
951
|
+
if (!result.success)
|
|
952
|
+
throw new StateValueShapeError({
|
|
953
|
+
entryType: args.entryType,
|
|
954
|
+
entryName: args.entryName,
|
|
955
|
+
issues: formatIssues(result.issues),
|
|
956
|
+
mode: "item"
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
function formatIssues(issues) {
|
|
960
|
+
return issues.map((i) => {
|
|
961
|
+
const keys = i.path?.map((p) => p.key) ?? [];
|
|
962
|
+
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
function derivePerspectiveFromState(entries) {
|
|
966
|
+
if (entries !== void 0) {
|
|
967
|
+
for (const entry of entries)
|
|
968
|
+
if (entry._type === "release.ref" && entry.value !== null) {
|
|
969
|
+
const releaseName = entry.value.releaseName;
|
|
970
|
+
if (typeof releaseName == "string" && releaseName.length > 0)
|
|
971
|
+
return [releaseName];
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function resolveDeclaredState(args) {
|
|
976
|
+
const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
|
|
977
|
+
if (entryDefs === void 0 || entryDefs.length === 0) return [];
|
|
978
|
+
const out = [];
|
|
979
|
+
for (const entry of entryDefs) {
|
|
980
|
+
const scopedCtx = { ...ctx, resolvedState: out };
|
|
981
|
+
out.push(await resolveOneEntry(entry, initialState, scopedCtx, randomKey2));
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
985
|
+
const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
986
|
+
"doc.refs",
|
|
987
|
+
"checklist",
|
|
988
|
+
"notes",
|
|
989
|
+
"assignees"
|
|
990
|
+
]);
|
|
991
|
+
function defaultEntryValue(entryType) {
|
|
992
|
+
return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
|
|
993
|
+
}
|
|
994
|
+
function resolveInitValue(entry, initialState, defaultValue) {
|
|
995
|
+
const initMatch = initialState.find((s) => s.name === entry.name && s.type === entry.type);
|
|
996
|
+
return initMatch === void 0 ? defaultValue : (assertInitValueShape(entry, initMatch.value), initMatch.value);
|
|
997
|
+
}
|
|
998
|
+
function resolveStateReadValue(source, ctx, defaultValue) {
|
|
800
999
|
const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.name === source.state), targetValue = target === void 0 ? void 0 : target.value;
|
|
801
1000
|
return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
|
|
802
1001
|
}
|
|
@@ -979,77 +1178,30 @@ async function resolveStageStateEntries(args) {
|
|
|
979
1178
|
workflowResource: instance.workflowResource,
|
|
980
1179
|
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
981
1180
|
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
982
|
-
workflowState: instance.state ?? [],
|
|
983
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
984
|
-
},
|
|
985
|
-
randomKey
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
async function resolveTaskStateEntries(args) {
|
|
989
|
-
const { client, instance, stage, task, now } = args;
|
|
990
|
-
return resolveDeclaredState({
|
|
991
|
-
entryDefs: task.state,
|
|
992
|
-
initialState: [],
|
|
993
|
-
ctx: {
|
|
994
|
-
client,
|
|
995
|
-
now,
|
|
996
|
-
selfId: instance._id,
|
|
997
|
-
tags: instance.tags ?? [],
|
|
998
|
-
stageName: stage.name,
|
|
999
|
-
workflowResource: instance.workflowResource,
|
|
1000
|
-
taskName: task.name,
|
|
1001
|
-
workflowState: instance.state ?? [],
|
|
1002
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1003
|
-
},
|
|
1004
|
-
randomKey
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
async function resolveBindings(args) {
|
|
1008
|
-
const resolved = {};
|
|
1009
|
-
for (const [key, groq] of Object.entries(args.bindings ?? {}))
|
|
1010
|
-
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
1011
|
-
return { ...resolved, ...args.staticInput };
|
|
1012
|
-
}
|
|
1013
|
-
function effectsContextEntry(name, value) {
|
|
1014
|
-
const _key = randomKey();
|
|
1015
|
-
return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
|
|
1016
|
-
}
|
|
1017
|
-
function effectsContextJsonEntry(name, value) {
|
|
1018
|
-
return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
|
|
1019
|
-
}
|
|
1020
|
-
function buildInstanceBase(args) {
|
|
1021
|
-
const { id, now, actor, perspective } = args;
|
|
1022
|
-
return {
|
|
1023
|
-
_id: id,
|
|
1024
|
-
_type: WORKFLOW_INSTANCE_TYPE,
|
|
1025
|
-
_rev: "",
|
|
1026
|
-
_createdAt: now,
|
|
1027
|
-
_updatedAt: now,
|
|
1028
|
-
tags: args.tags,
|
|
1029
|
-
workflowResource: args.workflowResource,
|
|
1030
|
-
definition: args.definitionName,
|
|
1031
|
-
pinnedVersion: args.pinnedVersion,
|
|
1032
|
-
definitionSnapshot: JSON.stringify(args.definition),
|
|
1033
|
-
state: args.state,
|
|
1034
|
-
effectsContext: args.effectsContext,
|
|
1035
|
-
ancestors: args.ancestors,
|
|
1036
|
-
...perspective !== void 0 ? { perspective } : {},
|
|
1037
|
-
currentStage: args.initialStage,
|
|
1038
|
-
stages: [],
|
|
1039
|
-
pendingEffects: [],
|
|
1040
|
-
effectHistory: [],
|
|
1041
|
-
history: [
|
|
1042
|
-
{
|
|
1043
|
-
_key: randomKey(),
|
|
1044
|
-
_type: "stageEntered",
|
|
1045
|
-
at: now,
|
|
1046
|
-
stage: args.initialStage,
|
|
1047
|
-
...actor !== void 0 ? { actor } : {}
|
|
1048
|
-
}
|
|
1049
|
-
],
|
|
1050
|
-
startedAt: now,
|
|
1051
|
-
lastChangedAt: now
|
|
1052
|
-
};
|
|
1181
|
+
workflowState: instance.state ?? [],
|
|
1182
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1183
|
+
},
|
|
1184
|
+
randomKey
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
async function resolveTaskStateEntries(args) {
|
|
1188
|
+
const { client, instance, stage, task, now } = args;
|
|
1189
|
+
return resolveDeclaredState({
|
|
1190
|
+
entryDefs: task.state,
|
|
1191
|
+
initialState: [],
|
|
1192
|
+
ctx: {
|
|
1193
|
+
client,
|
|
1194
|
+
now,
|
|
1195
|
+
selfId: instance._id,
|
|
1196
|
+
tags: instance.tags ?? [],
|
|
1197
|
+
stageName: stage.name,
|
|
1198
|
+
workflowResource: instance.workflowResource,
|
|
1199
|
+
taskName: task.name,
|
|
1200
|
+
workflowState: instance.state ?? [],
|
|
1201
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1202
|
+
},
|
|
1203
|
+
randomKey
|
|
1204
|
+
});
|
|
1053
1205
|
}
|
|
1054
1206
|
class ActionParamsInvalidError extends Error {
|
|
1055
1207
|
action;
|
|
@@ -1170,21 +1322,98 @@ function stageTransitionHistory(args) {
|
|
|
1170
1322
|
}
|
|
1171
1323
|
];
|
|
1172
1324
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
)
|
|
1325
|
+
function startMutation(instance) {
|
|
1326
|
+
return {
|
|
1327
|
+
currentStage: instance.currentStage,
|
|
1328
|
+
// Deep-ish copy. Per-scope state entry arrays need their own copies
|
|
1329
|
+
// so ops can mutate without leaking into the source.
|
|
1330
|
+
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
1331
|
+
stages: instance.stages.map((s) => ({
|
|
1332
|
+
...s,
|
|
1333
|
+
state: (s.state ?? []).map((entry) => ({ ...entry })),
|
|
1334
|
+
tasks: s.tasks.map((t) => ({
|
|
1335
|
+
...t,
|
|
1336
|
+
...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
|
|
1337
|
+
}))
|
|
1338
|
+
})),
|
|
1339
|
+
pendingEffects: [...instance.pendingEffects],
|
|
1340
|
+
effectHistory: [...instance.effectHistory],
|
|
1341
|
+
effectsContext: [...instance.effectsContext],
|
|
1342
|
+
history: [...instance.history],
|
|
1343
|
+
lastChangedAt: instance.lastChangedAt,
|
|
1344
|
+
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
1345
|
+
...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
|
|
1346
|
+
pendingCreates: []
|
|
1347
|
+
};
|
|
1182
1348
|
}
|
|
1183
|
-
function
|
|
1184
|
-
|
|
1349
|
+
function instanceStateFields(src) {
|
|
1350
|
+
const fields = {
|
|
1351
|
+
currentStage: src.currentStage,
|
|
1352
|
+
state: src.state,
|
|
1353
|
+
stages: src.stages,
|
|
1354
|
+
pendingEffects: src.pendingEffects,
|
|
1355
|
+
effectHistory: src.effectHistory,
|
|
1356
|
+
effectsContext: src.effectsContext,
|
|
1357
|
+
history: src.history
|
|
1358
|
+
};
|
|
1359
|
+
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
|
|
1185
1360
|
}
|
|
1186
|
-
function
|
|
1187
|
-
return
|
|
1361
|
+
function materializeInstance(base, mutation) {
|
|
1362
|
+
return {
|
|
1363
|
+
...base,
|
|
1364
|
+
currentStage: mutation.currentStage,
|
|
1365
|
+
state: mutation.state,
|
|
1366
|
+
stages: mutation.stages
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async function persist(ctx, mutation) {
|
|
1370
|
+
const set = {
|
|
1371
|
+
...instanceStateFields(mutation),
|
|
1372
|
+
lastChangedAt: ctx.now
|
|
1373
|
+
}, pendingCreates = mutation.pendingCreates;
|
|
1374
|
+
if (pendingCreates.length === 0)
|
|
1375
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1376
|
+
const tx = ctx.client.transaction();
|
|
1377
|
+
for (const body of pendingCreates) tx.create(body);
|
|
1378
|
+
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1379
|
+
const actorForPriming = void 0;
|
|
1380
|
+
for (const body of pendingCreates)
|
|
1381
|
+
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
1382
|
+
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1383
|
+
if (!reloaded)
|
|
1384
|
+
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
1385
|
+
return reloaded;
|
|
1386
|
+
}
|
|
1387
|
+
function currentStageEntry(mutation) {
|
|
1388
|
+
const entry = findOpenStageEntry(mutation);
|
|
1389
|
+
if (entry === void 0)
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
|
|
1392
|
+
);
|
|
1393
|
+
return entry;
|
|
1394
|
+
}
|
|
1395
|
+
function currentTasks(mutation) {
|
|
1396
|
+
return currentStageEntry(mutation).tasks;
|
|
1397
|
+
}
|
|
1398
|
+
function findTaskInCurrentStage(ctx, taskName) {
|
|
1399
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
|
|
1400
|
+
if (task === void 0)
|
|
1401
|
+
throw new Error(
|
|
1402
|
+
`Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
1403
|
+
);
|
|
1404
|
+
return { stage, task };
|
|
1405
|
+
}
|
|
1406
|
+
function requireMutationTaskEntry(mutation, task) {
|
|
1407
|
+
const mutEntry = currentTasks(mutation).find((t) => t.name === task);
|
|
1408
|
+
if (mutEntry === void 0)
|
|
1409
|
+
throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
|
|
1410
|
+
return mutEntry;
|
|
1411
|
+
}
|
|
1412
|
+
function findCurrentStageEntry(instance) {
|
|
1413
|
+
return findOpenStageEntry(instance);
|
|
1414
|
+
}
|
|
1415
|
+
function findCurrentTasks(instance) {
|
|
1416
|
+
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
1188
1417
|
}
|
|
1189
1418
|
function definitionLookupGroq(explicit) {
|
|
1190
1419
|
const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
|
|
@@ -1202,6 +1431,47 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
1202
1431
|
return defaultClient;
|
|
1203
1432
|
}
|
|
1204
1433
|
}
|
|
1434
|
+
function effectsContextEntry(name, value) {
|
|
1435
|
+
const _key = randomKey();
|
|
1436
|
+
return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
|
|
1437
|
+
}
|
|
1438
|
+
function effectsContextJsonEntry(name, value) {
|
|
1439
|
+
return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
|
|
1440
|
+
}
|
|
1441
|
+
function buildInstanceBase(args) {
|
|
1442
|
+
const { id, now, actor, perspective } = args;
|
|
1443
|
+
return {
|
|
1444
|
+
_id: id,
|
|
1445
|
+
_type: WORKFLOW_INSTANCE_TYPE,
|
|
1446
|
+
_rev: "",
|
|
1447
|
+
_createdAt: now,
|
|
1448
|
+
_updatedAt: now,
|
|
1449
|
+
tags: args.tags,
|
|
1450
|
+
workflowResource: args.workflowResource,
|
|
1451
|
+
definition: args.definitionName,
|
|
1452
|
+
pinnedVersion: args.pinnedVersion,
|
|
1453
|
+
definitionSnapshot: JSON.stringify(args.definition),
|
|
1454
|
+
state: args.state,
|
|
1455
|
+
effectsContext: args.effectsContext,
|
|
1456
|
+
ancestors: args.ancestors,
|
|
1457
|
+
...perspective !== void 0 ? { perspective } : {},
|
|
1458
|
+
currentStage: args.initialStage,
|
|
1459
|
+
stages: [],
|
|
1460
|
+
pendingEffects: [],
|
|
1461
|
+
effectHistory: [],
|
|
1462
|
+
history: [
|
|
1463
|
+
{
|
|
1464
|
+
_key: randomKey(),
|
|
1465
|
+
_type: "stageEntered",
|
|
1466
|
+
at: now,
|
|
1467
|
+
stage: args.initialStage,
|
|
1468
|
+
...actor !== void 0 ? { actor } : {}
|
|
1469
|
+
}
|
|
1470
|
+
],
|
|
1471
|
+
startedAt: now,
|
|
1472
|
+
lastChangedAt: now
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1205
1475
|
const MAX_SPAWN_DEPTH = 6, SUBWORKFLOWS_ALL_DONE = "count($subworkflows[status != 'done']) == 0", FAIL_WHEN_SYSTEM_ID = "engine.failWhen", COMPLETE_WHEN_SYSTEM_ID = "engine.completeWhen";
|
|
1206
1476
|
function gateActor(outcome) {
|
|
1207
1477
|
return {
|
|
@@ -1380,6 +1650,111 @@ async function subworkflowRows(ctx, refs) {
|
|
|
1380
1650
|
status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
|
|
1381
1651
|
}));
|
|
1382
1652
|
}
|
|
1653
|
+
async function resolveBindings(args) {
|
|
1654
|
+
const resolved = {};
|
|
1655
|
+
for (const [key, groq] of Object.entries(args.bindings ?? {}))
|
|
1656
|
+
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
1657
|
+
return { ...resolved, ...args.staticInput };
|
|
1658
|
+
}
|
|
1659
|
+
async function completeEffect(args) {
|
|
1660
|
+
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1661
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1662
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1663
|
+
});
|
|
1664
|
+
return commitCompleteEffect(
|
|
1665
|
+
ctx,
|
|
1666
|
+
effectKey,
|
|
1667
|
+
status,
|
|
1668
|
+
outputs,
|
|
1669
|
+
detail,
|
|
1670
|
+
error,
|
|
1671
|
+
durationMs,
|
|
1672
|
+
options?.actor
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
function buildEffectHistoryEntry(pending, outcome) {
|
|
1676
|
+
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
1677
|
+
return {
|
|
1678
|
+
_key: pending._key,
|
|
1679
|
+
name: pending.name,
|
|
1680
|
+
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
1681
|
+
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
1682
|
+
params: pending.params,
|
|
1683
|
+
origin: pending.origin,
|
|
1684
|
+
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
1685
|
+
ranAt,
|
|
1686
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
1687
|
+
status,
|
|
1688
|
+
...detail !== void 0 ? { detail } : {},
|
|
1689
|
+
...error !== void 0 ? { error } : {},
|
|
1690
|
+
...outputs !== void 0 ? { outputs } : {}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function upsertEffectsContext(mutation, effectName, outputs) {
|
|
1694
|
+
const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
|
|
1695
|
+
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
1696
|
+
}
|
|
1697
|
+
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
1698
|
+
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
1699
|
+
if (pending === void 0)
|
|
1700
|
+
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
1701
|
+
const mutation = startMutation(ctx.instance);
|
|
1702
|
+
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1703
|
+
const ranAt = ctx.now;
|
|
1704
|
+
return mutation.effectHistory.push(
|
|
1705
|
+
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1706
|
+
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
1707
|
+
_key: randomKey(),
|
|
1708
|
+
_type: "effectCompleted",
|
|
1709
|
+
at: ranAt,
|
|
1710
|
+
effectKey,
|
|
1711
|
+
effect: pending.name,
|
|
1712
|
+
status,
|
|
1713
|
+
...outputs !== void 0 ? { outputs } : {},
|
|
1714
|
+
...detail !== void 0 ? { detail } : {},
|
|
1715
|
+
...actor !== void 0 ? { actor } : {}
|
|
1716
|
+
}), await persist(ctx, mutation), { effectKey, status };
|
|
1717
|
+
}
|
|
1718
|
+
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1719
|
+
const key = randomKey(), pending = {
|
|
1720
|
+
_key: key,
|
|
1721
|
+
_type: "pendingEffect",
|
|
1722
|
+
name: effect.name,
|
|
1723
|
+
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
1724
|
+
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
1725
|
+
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
1726
|
+
params,
|
|
1727
|
+
origin,
|
|
1728
|
+
...actor !== void 0 ? { actor } : {},
|
|
1729
|
+
queuedAt: now
|
|
1730
|
+
}, history = {
|
|
1731
|
+
_key: randomKey(),
|
|
1732
|
+
_type: "effectQueued",
|
|
1733
|
+
at: now,
|
|
1734
|
+
effectKey: key,
|
|
1735
|
+
effect: effect.name,
|
|
1736
|
+
origin
|
|
1737
|
+
};
|
|
1738
|
+
return { pending, history };
|
|
1739
|
+
}
|
|
1740
|
+
async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
|
|
1741
|
+
if (!effects || effects.length === 0) return;
|
|
1742
|
+
const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
|
|
1743
|
+
...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
|
|
1744
|
+
...actor !== void 0 ? { actor } : {},
|
|
1745
|
+
// Caller-supplied action params, readable in bindings as `$params.<name>`.
|
|
1746
|
+
vars: { params: opts?.callerParams ?? {} }
|
|
1747
|
+
});
|
|
1748
|
+
for (const effect of effects) {
|
|
1749
|
+
const resolved = await resolveBindings({
|
|
1750
|
+
bindings: effect.bindings,
|
|
1751
|
+
staticInput: effect.input,
|
|
1752
|
+
snapshot: ctx.snapshot,
|
|
1753
|
+
params
|
|
1754
|
+
}), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
|
|
1755
|
+
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1383
1758
|
function resolveStaticSource(src, ctx) {
|
|
1384
1759
|
switch (src.type) {
|
|
1385
1760
|
case "literal":
|
|
@@ -1609,7 +1984,8 @@ function evalOpPredicate(p, row, ctx) {
|
|
|
1609
1984
|
}
|
|
1610
1985
|
async function invokeTask(args) {
|
|
1611
1986
|
const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1612
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
1987
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1988
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1613
1989
|
});
|
|
1614
1990
|
return commitInvoke(ctx, taskName, options?.actor);
|
|
1615
1991
|
}
|
|
@@ -1717,7 +2093,8 @@ function isTerminalStage(stage) {
|
|
|
1717
2093
|
}
|
|
1718
2094
|
async function setStage(args) {
|
|
1719
2095
|
const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1720
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2096
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2097
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1721
2098
|
});
|
|
1722
2099
|
return commitSetStage(ctx, targetStage, reason, options?.actor);
|
|
1723
2100
|
}
|
|
@@ -1968,287 +2345,58 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
|
|
|
1968
2345
|
let count = 0;
|
|
1969
2346
|
for (; ; ) {
|
|
1970
2347
|
if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
|
|
1971
|
-
client,
|
|
1972
|
-
instanceId,
|
|
1973
|
-
...actor !== void 0 ? { options: { actor } } : {},
|
|
1974
|
-
...clientForGdr ? { clientForGdr } : {},
|
|
1975
|
-
...clock ? { clock } : {},
|
|
1976
|
-
...overlay ? { overlay } : {}
|
|
1977
|
-
})).fired) return count;
|
|
1978
|
-
if (count++, count >= CASCADE_LIMIT)
|
|
1979
|
-
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
|
|
1983
|
-
const parentRef = child.ancestors.at(-1);
|
|
1984
|
-
if (parentRef === void 0) return;
|
|
1985
|
-
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
1986
|
-
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
1987
|
-
const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
|
|
1988
|
-
if (stage === void 0) return;
|
|
1989
|
-
const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
|
|
1990
|
-
if (task?.subworkflows === void 0) return;
|
|
1991
|
-
const entry = parentCurrentTasks.find((e) => e.name === task.name);
|
|
1992
|
-
if (entry === void 0 || entry.status !== "active") return;
|
|
1993
|
-
const ctx = await buildEngineContext({
|
|
1994
|
-
client,
|
|
1995
|
-
clientForGdr,
|
|
1996
|
-
instance: parent,
|
|
1997
|
-
definition,
|
|
1998
|
-
...clock ? { clock } : {}
|
|
1999
|
-
}), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
2000
|
-
if (outcome !== void 0)
|
|
2001
|
-
return { parent, definition, stage, task, outcome };
|
|
2002
|
-
}
|
|
2003
|
-
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
2004
|
-
const instance = await client.getDocument(instanceId);
|
|
2005
|
-
if (!instance) return;
|
|
2006
|
-
const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
|
|
2007
|
-
if (found === void 0) return;
|
|
2008
|
-
const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
|
|
2009
|
-
client,
|
|
2010
|
-
clientForGdr: resolvedClientForGdr,
|
|
2011
|
-
instance: parent,
|
|
2012
|
-
definition,
|
|
2013
|
-
...clock ? { clock } : {}
|
|
2014
|
-
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
|
|
2015
|
-
mutEntry !== void 0 && (applyTaskStatusChange({
|
|
2016
|
-
entry: mutEntry,
|
|
2017
|
-
history: mutation.history,
|
|
2018
|
-
stage: stage.name,
|
|
2019
|
-
to: outcome,
|
|
2020
|
-
at: ctx.now,
|
|
2021
|
-
actor: gateActor(outcome)
|
|
2022
|
-
}), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
|
|
2023
|
-
}
|
|
2024
|
-
function startMutation(instance) {
|
|
2025
|
-
return {
|
|
2026
|
-
currentStage: instance.currentStage,
|
|
2027
|
-
// Deep-ish copy. Per-scope state entry arrays need their own copies
|
|
2028
|
-
// so ops can mutate without leaking into the source.
|
|
2029
|
-
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
2030
|
-
stages: instance.stages.map((s) => ({
|
|
2031
|
-
...s,
|
|
2032
|
-
state: (s.state ?? []).map((entry) => ({ ...entry })),
|
|
2033
|
-
tasks: s.tasks.map((t) => ({
|
|
2034
|
-
...t,
|
|
2035
|
-
...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
|
|
2036
|
-
}))
|
|
2037
|
-
})),
|
|
2038
|
-
pendingEffects: [...instance.pendingEffects],
|
|
2039
|
-
effectHistory: [...instance.effectHistory],
|
|
2040
|
-
effectsContext: [...instance.effectsContext],
|
|
2041
|
-
history: [...instance.history],
|
|
2042
|
-
lastChangedAt: instance.lastChangedAt,
|
|
2043
|
-
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
2044
|
-
...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
|
|
2045
|
-
pendingCreates: []
|
|
2046
|
-
};
|
|
2047
|
-
}
|
|
2048
|
-
function instanceStateFields(src) {
|
|
2049
|
-
const fields = {
|
|
2050
|
-
currentStage: src.currentStage,
|
|
2051
|
-
state: src.state,
|
|
2052
|
-
stages: src.stages,
|
|
2053
|
-
pendingEffects: src.pendingEffects,
|
|
2054
|
-
effectHistory: src.effectHistory,
|
|
2055
|
-
effectsContext: src.effectsContext,
|
|
2056
|
-
history: src.history
|
|
2057
|
-
};
|
|
2058
|
-
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
|
|
2059
|
-
}
|
|
2060
|
-
function materializeInstance(base, mutation) {
|
|
2061
|
-
return {
|
|
2062
|
-
...base,
|
|
2063
|
-
currentStage: mutation.currentStage,
|
|
2064
|
-
state: mutation.state,
|
|
2065
|
-
stages: mutation.stages
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
async function persist(ctx, mutation) {
|
|
2069
|
-
const set = {
|
|
2070
|
-
...instanceStateFields(mutation),
|
|
2071
|
-
lastChangedAt: ctx.now
|
|
2072
|
-
}, pendingCreates = mutation.pendingCreates;
|
|
2073
|
-
if (pendingCreates.length === 0)
|
|
2074
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
2075
|
-
const tx = ctx.client.transaction();
|
|
2076
|
-
for (const body of pendingCreates) tx.create(body);
|
|
2077
|
-
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
2078
|
-
const actorForPriming = void 0;
|
|
2079
|
-
for (const body of pendingCreates)
|
|
2080
|
-
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
2081
|
-
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
2082
|
-
if (!reloaded)
|
|
2083
|
-
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
2084
|
-
return reloaded;
|
|
2085
|
-
}
|
|
2086
|
-
function currentStageEntry(mutation) {
|
|
2087
|
-
const entry = findOpenStageEntry(mutation);
|
|
2088
|
-
if (entry === void 0)
|
|
2089
|
-
throw new Error(
|
|
2090
|
-
`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
|
|
2091
|
-
);
|
|
2092
|
-
return entry;
|
|
2093
|
-
}
|
|
2094
|
-
function currentTasks(mutation) {
|
|
2095
|
-
return currentStageEntry(mutation).tasks;
|
|
2096
|
-
}
|
|
2097
|
-
function findTaskInCurrentStage(ctx, taskName) {
|
|
2098
|
-
const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
|
|
2099
|
-
if (task === void 0)
|
|
2100
|
-
throw new Error(
|
|
2101
|
-
`Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
2102
|
-
);
|
|
2103
|
-
return { stage, task };
|
|
2104
|
-
}
|
|
2105
|
-
function requireMutationTaskEntry(mutation, task) {
|
|
2106
|
-
const mutEntry = currentTasks(mutation).find((t) => t.name === task);
|
|
2107
|
-
if (mutEntry === void 0)
|
|
2108
|
-
throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
|
|
2109
|
-
return mutEntry;
|
|
2110
|
-
}
|
|
2111
|
-
function findCurrentStageEntry(instance) {
|
|
2112
|
-
return findOpenStageEntry(instance);
|
|
2113
|
-
}
|
|
2114
|
-
function findCurrentTasks(instance) {
|
|
2115
|
-
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
2116
|
-
}
|
|
2117
|
-
async function completeEffect(args) {
|
|
2118
|
-
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2119
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2120
|
-
});
|
|
2121
|
-
return commitCompleteEffect(
|
|
2122
|
-
ctx,
|
|
2123
|
-
effectKey,
|
|
2124
|
-
status,
|
|
2125
|
-
outputs,
|
|
2126
|
-
detail,
|
|
2127
|
-
error,
|
|
2128
|
-
durationMs,
|
|
2129
|
-
options?.actor
|
|
2130
|
-
);
|
|
2131
|
-
}
|
|
2132
|
-
function buildEffectHistoryEntry(pending, outcome) {
|
|
2133
|
-
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
2134
|
-
return {
|
|
2135
|
-
_key: pending._key,
|
|
2136
|
-
name: pending.name,
|
|
2137
|
-
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
2138
|
-
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
2139
|
-
params: pending.params,
|
|
2140
|
-
origin: pending.origin,
|
|
2141
|
-
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
2142
|
-
ranAt,
|
|
2143
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
2144
|
-
status,
|
|
2145
|
-
...detail !== void 0 ? { detail } : {},
|
|
2146
|
-
...error !== void 0 ? { error } : {},
|
|
2147
|
-
...outputs !== void 0 ? { outputs } : {}
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
function upsertEffectsContext(mutation, effectName, outputs) {
|
|
2151
|
-
const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
|
|
2152
|
-
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
2153
|
-
}
|
|
2154
|
-
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
2155
|
-
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
2156
|
-
if (pending === void 0)
|
|
2157
|
-
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
2158
|
-
const mutation = startMutation(ctx.instance);
|
|
2159
|
-
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
2160
|
-
const ranAt = ctx.now;
|
|
2161
|
-
return mutation.effectHistory.push(
|
|
2162
|
-
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
2163
|
-
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
2164
|
-
_key: randomKey(),
|
|
2165
|
-
_type: "effectCompleted",
|
|
2166
|
-
at: ranAt,
|
|
2167
|
-
effectKey,
|
|
2168
|
-
effect: pending.name,
|
|
2169
|
-
status,
|
|
2170
|
-
...outputs !== void 0 ? { outputs } : {},
|
|
2171
|
-
...detail !== void 0 ? { detail } : {},
|
|
2172
|
-
...actor !== void 0 ? { actor } : {}
|
|
2173
|
-
}), await persist(ctx, mutation), { effectKey, status };
|
|
2174
|
-
}
|
|
2175
|
-
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
2176
|
-
const key = randomKey(), pending = {
|
|
2177
|
-
_key: key,
|
|
2178
|
-
_type: "pendingEffect",
|
|
2179
|
-
name: effect.name,
|
|
2180
|
-
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
2181
|
-
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
2182
|
-
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
2183
|
-
params,
|
|
2184
|
-
origin,
|
|
2185
|
-
...actor !== void 0 ? { actor } : {},
|
|
2186
|
-
queuedAt: now
|
|
2187
|
-
}, history = {
|
|
2188
|
-
_key: randomKey(),
|
|
2189
|
-
_type: "effectQueued",
|
|
2190
|
-
at: now,
|
|
2191
|
-
effectKey: key,
|
|
2192
|
-
effect: effect.name,
|
|
2193
|
-
origin
|
|
2194
|
-
};
|
|
2195
|
-
return { pending, history };
|
|
2196
|
-
}
|
|
2197
|
-
async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
|
|
2198
|
-
if (!effects || effects.length === 0) return;
|
|
2199
|
-
const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
|
|
2200
|
-
...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
|
|
2201
|
-
...actor !== void 0 ? { actor } : {},
|
|
2202
|
-
// Caller-supplied action params, readable in bindings as `$params.<name>`.
|
|
2203
|
-
vars: { params: opts?.callerParams ?? {} }
|
|
2204
|
-
});
|
|
2205
|
-
for (const effect of effects) {
|
|
2206
|
-
const resolved = await resolveBindings({
|
|
2207
|
-
bindings: effect.bindings,
|
|
2208
|
-
staticInput: effect.input,
|
|
2209
|
-
snapshot: ctx.snapshot,
|
|
2210
|
-
params
|
|
2211
|
-
}), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
|
|
2212
|
-
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
2348
|
+
client,
|
|
2349
|
+
instanceId,
|
|
2350
|
+
...actor !== void 0 ? { options: { actor } } : {},
|
|
2351
|
+
...clientForGdr ? { clientForGdr } : {},
|
|
2352
|
+
...clock ? { clock } : {},
|
|
2353
|
+
...overlay ? { overlay } : {}
|
|
2354
|
+
})).fired) return count;
|
|
2355
|
+
if (count++, count >= CASCADE_LIMIT)
|
|
2356
|
+
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
2213
2357
|
}
|
|
2214
2358
|
}
|
|
2215
|
-
async function
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2359
|
+
async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
|
|
2360
|
+
const parentRef = child.ancestors.at(-1);
|
|
2361
|
+
if (parentRef === void 0) return;
|
|
2362
|
+
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
2363
|
+
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
2364
|
+
const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
|
|
2365
|
+
if (stage === void 0) return;
|
|
2366
|
+
const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
|
|
2367
|
+
if (task?.subworkflows === void 0) return;
|
|
2368
|
+
const entry = parentCurrentTasks.find((e) => e.name === task.name);
|
|
2369
|
+
if (entry === void 0 || entry.status !== "active") return;
|
|
2370
|
+
const ctx = await buildEngineContext({
|
|
2371
|
+
client,
|
|
2372
|
+
clientForGdr,
|
|
2373
|
+
instance: parent,
|
|
2374
|
+
definition,
|
|
2375
|
+
...clock ? { clock } : {}
|
|
2376
|
+
}), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
2377
|
+
if (outcome !== void 0)
|
|
2378
|
+
return { parent, definition, stage, task, outcome };
|
|
2220
2379
|
}
|
|
2221
|
-
async function
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
)
|
|
2237
|
-
), mutation.pendingEffects = [], mutation.history.push({
|
|
2238
|
-
_key: randomKey(),
|
|
2239
|
-
_type: "aborted",
|
|
2240
|
-
at,
|
|
2380
|
+
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
2381
|
+
const instance = await client.getDocument(instanceId);
|
|
2382
|
+
if (!instance) return;
|
|
2383
|
+
const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
|
|
2384
|
+
if (found === void 0) return;
|
|
2385
|
+
const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
|
|
2386
|
+
client,
|
|
2387
|
+
clientForGdr: resolvedClientForGdr,
|
|
2388
|
+
instance: parent,
|
|
2389
|
+
definition,
|
|
2390
|
+
...clock ? { clock } : {}
|
|
2391
|
+
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
|
|
2392
|
+
mutEntry !== void 0 && (applyTaskStatusChange({
|
|
2393
|
+
entry: mutEntry,
|
|
2394
|
+
history: mutation.history,
|
|
2241
2395
|
stage: stage.name,
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
clientForGdr: ctx.clientForGdr,
|
|
2247
|
-
instance: ctx.instance,
|
|
2248
|
-
definition: ctx.definition,
|
|
2249
|
-
stageName: stage.name,
|
|
2250
|
-
now: ctx.now
|
|
2251
|
-
}), { fired: !0, stage: stage.name };
|
|
2396
|
+
to: outcome,
|
|
2397
|
+
at: ctx.now,
|
|
2398
|
+
actor: gateActor(outcome)
|
|
2399
|
+
}), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
|
|
2252
2400
|
}
|
|
2253
2401
|
const parsedFilters = /* @__PURE__ */ new Map();
|
|
2254
2402
|
async function matchesFilter(args) {
|
|
@@ -2344,93 +2492,6 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2344
2492
|
return;
|
|
2345
2493
|
}
|
|
2346
2494
|
}
|
|
2347
|
-
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
2348
|
-
function guardsForResource(client) {
|
|
2349
|
-
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
2350
|
-
}
|
|
2351
|
-
function instanceGuardQuery(instanceId) {
|
|
2352
|
-
return {
|
|
2353
|
-
query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
|
|
2354
|
-
params: { guardType: GUARD_DOC_TYPE, instanceId }
|
|
2355
|
-
};
|
|
2356
|
-
}
|
|
2357
|
-
function verdictGuardsForInstance(client, instanceId) {
|
|
2358
|
-
const { query, params } = instanceGuardQuery(instanceId);
|
|
2359
|
-
return client.fetch(query, params);
|
|
2360
|
-
}
|
|
2361
|
-
async function guardsForInstance(args) {
|
|
2362
|
-
const { client, clientForGdr, instance } = args, stateUris = [
|
|
2363
|
-
...collectEntryDocUris(instance.state),
|
|
2364
|
-
...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
|
|
2365
|
-
], clients = resourceClientMap(
|
|
2366
|
-
instance.workflowResource,
|
|
2367
|
-
client,
|
|
2368
|
-
clientForGdr,
|
|
2369
|
-
stateUris.map((uri) => tryParseGdr(uri))
|
|
2370
|
-
), { query, params } = instanceGuardQuery(instance._id);
|
|
2371
|
-
return queryGuardsAcross(clients.values(), query, params);
|
|
2372
|
-
}
|
|
2373
|
-
async function guardsForDefinition(args) {
|
|
2374
|
-
const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
|
|
2375
|
-
(def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
|
|
2376
|
-
), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs);
|
|
2377
|
-
return queryGuardsAcross(clients.values(), "*[_type == $t && sourceDefinition == $definition]", {
|
|
2378
|
-
t: GUARD_DOC_TYPE,
|
|
2379
|
-
definition
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
function guardIdRefs(definition) {
|
|
2383
|
-
return definition.stages.flatMap(
|
|
2384
|
-
(stage) => (stage.guards ?? []).flatMap(
|
|
2385
|
-
(guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
|
|
2386
|
-
)
|
|
2387
|
-
);
|
|
2388
|
-
}
|
|
2389
|
-
function staticIdRefGdr(idRef, stage, definition) {
|
|
2390
|
-
const read = /^\$state\.([\w-]+)/.exec(idRef);
|
|
2391
|
-
if (read === null) return;
|
|
2392
|
-
const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
|
|
2393
|
-
return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
|
|
2394
|
-
}
|
|
2395
|
-
function entriesInScope(stage, definition) {
|
|
2396
|
-
return [
|
|
2397
|
-
...definition.state ?? [],
|
|
2398
|
-
...stage.state ?? [],
|
|
2399
|
-
...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
|
|
2400
|
-
];
|
|
2401
|
-
}
|
|
2402
|
-
function gdrFromValue(value) {
|
|
2403
|
-
if (typeof value == "string") return tryParseGdr(value);
|
|
2404
|
-
if (typeof value == "object" && value !== null && "id" in value) {
|
|
2405
|
-
const id = value.id;
|
|
2406
|
-
return typeof id == "string" ? tryParseGdr(id) : void 0;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
function tryParseGdr(uri) {
|
|
2410
|
-
try {
|
|
2411
|
-
return parseGdr(uri);
|
|
2412
|
-
} catch {
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
|
|
2417
|
-
const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
|
|
2418
|
-
for (const parsed of gdrs) {
|
|
2419
|
-
if (parsed === void 0) continue;
|
|
2420
|
-
const key = resourceKey(resourceFromParsed(parsed));
|
|
2421
|
-
map.has(key) || map.set(key, clientForGdr(parsed));
|
|
2422
|
-
}
|
|
2423
|
-
return map;
|
|
2424
|
-
}
|
|
2425
|
-
async function queryGuardsAcross(clients, query, params) {
|
|
2426
|
-
const perResource = await Promise.all(
|
|
2427
|
-
[...clients].map((c) => c.fetch(query, params))
|
|
2428
|
-
);
|
|
2429
|
-
return dedupById(perResource.flat());
|
|
2430
|
-
}
|
|
2431
|
-
function dedupById(guards) {
|
|
2432
|
-
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
2433
|
-
}
|
|
2434
2495
|
async function evaluateInstance(args) {
|
|
2435
2496
|
const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2436
2497
|
validateTags(tags);
|
|
@@ -2701,11 +2762,51 @@ async function loadDefinitionVersions(client, definition, tags) {
|
|
|
2701
2762
|
{ definition, engineTags: tags }
|
|
2702
2763
|
);
|
|
2703
2764
|
}
|
|
2765
|
+
async function abortInstance(args) {
|
|
2766
|
+
const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2767
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2768
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2769
|
+
});
|
|
2770
|
+
return commitAbort(ctx, reason, options?.actor);
|
|
2771
|
+
}
|
|
2772
|
+
async function commitAbort(ctx, reason, actor) {
|
|
2773
|
+
if (ctx.instance.completedAt !== void 0)
|
|
2774
|
+
return { fired: !1 };
|
|
2775
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
|
|
2776
|
+
return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
|
|
2777
|
+
...mutation.pendingEffects.map(
|
|
2778
|
+
(pending) => buildEffectHistoryEntry(pending, {
|
|
2779
|
+
status: "failed",
|
|
2780
|
+
ranAt: at,
|
|
2781
|
+
actor,
|
|
2782
|
+
detail: "instance aborted before dispatch",
|
|
2783
|
+
error: void 0,
|
|
2784
|
+
durationMs: void 0,
|
|
2785
|
+
outputs: void 0
|
|
2786
|
+
})
|
|
2787
|
+
)
|
|
2788
|
+
), mutation.pendingEffects = [], mutation.history.push({
|
|
2789
|
+
_key: randomKey(),
|
|
2790
|
+
_type: "aborted",
|
|
2791
|
+
at,
|
|
2792
|
+
stage: stage.name,
|
|
2793
|
+
...reason !== void 0 ? { reason } : {},
|
|
2794
|
+
...actor !== void 0 ? { actor } : {}
|
|
2795
|
+
}), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
|
|
2796
|
+
client: ctx.client,
|
|
2797
|
+
clientForGdr: ctx.clientForGdr,
|
|
2798
|
+
instance: ctx.instance,
|
|
2799
|
+
definition: ctx.definition,
|
|
2800
|
+
stageName: stage.name,
|
|
2801
|
+
now: ctx.now
|
|
2802
|
+
}), { fired: !0, stage: stage.name };
|
|
2803
|
+
}
|
|
2704
2804
|
async function fireAction(args) {
|
|
2705
2805
|
const { client, instanceId, task, action, params, options } = args;
|
|
2706
2806
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2707
2807
|
const ctx = await loadContext(client, instanceId, {
|
|
2708
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2808
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2809
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2709
2810
|
});
|
|
2710
2811
|
try {
|
|
2711
2812
|
return await commitAction(ctx, task, action, params, options);
|
|
@@ -2831,6 +2932,15 @@ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay)
|
|
|
2831
2932
|
);
|
|
2832
2933
|
return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
|
|
2833
2934
|
}
|
|
2935
|
+
async function abortAndPropagate(args) {
|
|
2936
|
+
const { client, instanceId, reason, actor, clientForGdr, clock } = args, result = await abortInstance({
|
|
2937
|
+
client,
|
|
2938
|
+
instanceId,
|
|
2939
|
+
...reason !== void 0 ? { reason } : {},
|
|
2940
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
2941
|
+
});
|
|
2942
|
+
return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), result.fired;
|
|
2943
|
+
}
|
|
2834
2944
|
function buildClientForGdr(defaultClient, resolver) {
|
|
2835
2945
|
return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
|
|
2836
2946
|
}
|
|
@@ -2853,10 +2963,11 @@ function assertActionAllowed(evaluation, task, action) {
|
|
|
2853
2963
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
2854
2964
|
}
|
|
2855
2965
|
async function applyAction(args) {
|
|
2856
|
-
const { client, instance, task, action, params, actor, grants, clock } = args, options = {
|
|
2966
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
|
|
2857
2967
|
actor,
|
|
2858
2968
|
...grants !== void 0 ? { grants } : {},
|
|
2859
|
-
...clock !== void 0 ? { clock } : {}
|
|
2969
|
+
...clock !== void 0 ? { clock } : {},
|
|
2970
|
+
...clientForGdr !== void 0 ? { clientForGdr } : {}
|
|
2860
2971
|
};
|
|
2861
2972
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
2862
2973
|
client,
|
|
@@ -2867,6 +2978,78 @@ async function applyAction(args) {
|
|
|
2867
2978
|
options
|
|
2868
2979
|
})).ranOps;
|
|
2869
2980
|
}
|
|
2981
|
+
async function deleteDefinition(args) {
|
|
2982
|
+
const { client, tags, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tags);
|
|
2983
|
+
if (versions.length === 0)
|
|
2984
|
+
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2985
|
+
const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
|
|
2986
|
+
if (targets.length === 0)
|
|
2987
|
+
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
2988
|
+
const lastVersionGoes = targets.length === versions.length;
|
|
2989
|
+
await assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes);
|
|
2990
|
+
const instanceIds = await nonTerminalInstanceIds(client, tags, definition, version);
|
|
2991
|
+
if (instanceIds.length > 0 && cascade2 !== !0)
|
|
2992
|
+
throw new Error(
|
|
2993
|
+
`Cannot delete ${definition}: ${instanceIds.length} non-terminal instance(s) exist (${previewIds(instanceIds)}). Pass cascade to abort them first \u2014 instances are aborted in place, never deleted.`
|
|
2994
|
+
);
|
|
2995
|
+
const abortedInstanceIds = await abortInstances({
|
|
2996
|
+
client,
|
|
2997
|
+
instanceIds,
|
|
2998
|
+
reason,
|
|
2999
|
+
actor,
|
|
3000
|
+
clientForGdr,
|
|
3001
|
+
clock
|
|
3002
|
+
}), tx = client.transaction();
|
|
3003
|
+
for (const target of targets) tx.delete(target._id);
|
|
3004
|
+
await tx.commit();
|
|
3005
|
+
const deletedGuardCount = lastVersionGoes ? await deleteOrphanedDefinitionGuards({
|
|
3006
|
+
client,
|
|
3007
|
+
clientForGdr,
|
|
3008
|
+
workflowResource: args.workflowResource,
|
|
3009
|
+
definition,
|
|
3010
|
+
definitions: versions,
|
|
3011
|
+
tags
|
|
3012
|
+
}) : 0;
|
|
3013
|
+
return {
|
|
3014
|
+
name: definition,
|
|
3015
|
+
deletedVersions: targets.map((d) => d.version),
|
|
3016
|
+
abortedInstanceIds,
|
|
3017
|
+
deletedGuardCount
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
async function assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes) {
|
|
3021
|
+
const deployed = await client.fetch(
|
|
3022
|
+
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
|
|
3023
|
+
{ engineTags: tags }
|
|
3024
|
+
), targetIds = new Set(targets.map((d) => d._id)), targetVersions = new Set(targets.map((d) => d.version)), referrers = deployed.filter((d) => !targetIds.has(d._id)).filter(
|
|
3025
|
+
(d) => refsOf(d).some(
|
|
3026
|
+
(ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
|
|
3027
|
+
)
|
|
3028
|
+
);
|
|
3029
|
+
if (referrers.length > 0) {
|
|
3030
|
+
const names = referrers.map((d) => `${d.name} v${d.version}`).join(", ");
|
|
3031
|
+
throw new Error(
|
|
3032
|
+
`Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
async function nonTerminalInstanceIds(client, tags, definition, version) {
|
|
3037
|
+
const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
|
|
3038
|
+
return (await client.fetch(
|
|
3039
|
+
`*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
|
|
3040
|
+
{ definition, engineTags: tags, ...version !== void 0 ? { version } : {} }
|
|
3041
|
+
)).map((doc) => doc._id);
|
|
3042
|
+
}
|
|
3043
|
+
async function abortInstances(args) {
|
|
3044
|
+
const { client, instanceIds, reason, actor, clientForGdr, clock } = args, aborted = [];
|
|
3045
|
+
for (const instanceId of instanceIds)
|
|
3046
|
+
await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock }) && aborted.push(instanceId);
|
|
3047
|
+
return aborted;
|
|
3048
|
+
}
|
|
3049
|
+
function previewIds(ids) {
|
|
3050
|
+
const head = ids.slice(0, 3).join(", ");
|
|
3051
|
+
return ids.length > 3 ? `${head}, \u2026` : head;
|
|
3052
|
+
}
|
|
2870
3053
|
const workflow = {
|
|
2871
3054
|
/**
|
|
2872
3055
|
* Deploy a set of workflow definitions as one call. Each lands as a
|
|
@@ -2917,6 +3100,14 @@ const workflow = {
|
|
|
2917
3100
|
}
|
|
2918
3101
|
return hasWrites && await tx.commit(), { results };
|
|
2919
3102
|
},
|
|
3103
|
+
/**
|
|
3104
|
+
* Remove a deployed workflow definition (all versions, or one via
|
|
3105
|
+
* `version`). Refuses while non-terminal instances exist unless
|
|
3106
|
+
* `cascade` aborts them first — instances are never deleted, only
|
|
3107
|
+
* aborted in place; see {@link deleteDefinitionInternal} for the
|
|
3108
|
+
* full contract (spawn-referrer check, guard-doc housekeeping).
|
|
3109
|
+
*/
|
|
3110
|
+
deleteDefinition: async (args) => deleteDefinition(args),
|
|
2920
3111
|
/**
|
|
2921
3112
|
* Spawn a new workflow instance from a deployed definition.
|
|
2922
3113
|
*
|
|
@@ -3011,6 +3202,7 @@ const workflow = {
|
|
|
3011
3202
|
actor,
|
|
3012
3203
|
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3013
3204
|
clock,
|
|
3205
|
+
clientForGdr,
|
|
3014
3206
|
...params !== void 0 ? { params } : {}
|
|
3015
3207
|
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3016
3208
|
return {
|
|
@@ -3038,7 +3230,7 @@ const workflow = {
|
|
|
3038
3230
|
...detail !== void 0 ? { detail } : {},
|
|
3039
3231
|
...error !== void 0 ? { error } : {},
|
|
3040
3232
|
...durationMs !== void 0 ? { durationMs } : {},
|
|
3041
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3233
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3042
3234
|
});
|
|
3043
3235
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3044
3236
|
return {
|
|
@@ -3079,7 +3271,7 @@ const workflow = {
|
|
|
3079
3271
|
instanceId,
|
|
3080
3272
|
targetStage,
|
|
3081
3273
|
...reason !== void 0 ? { reason } : {},
|
|
3082
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3274
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3083
3275
|
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
3084
3276
|
return {
|
|
3085
3277
|
instance: await reload(client, instanceId, tags),
|
|
@@ -3089,24 +3281,18 @@ const workflow = {
|
|
|
3089
3281
|
},
|
|
3090
3282
|
/**
|
|
3091
3283
|
* Admin override — hard-stop an in-flight instance where it stands.
|
|
3092
|
-
* No stage move, no transition effects, pending effects cancelled;
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3095
|
-
* so a parent gate waiting on this child re-evaluates.
|
|
3284
|
+
* No stage move, no transition effects, pending effects cancelled;
|
|
3285
|
+
* see {@link abortAndPropagate} for the abort + ancestor-propagation
|
|
3286
|
+
* contract (propagated, not cascaded — the instance is terminal).
|
|
3096
3287
|
*/
|
|
3097
3288
|
abortInstance: async (args) => {
|
|
3098
3289
|
const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3099
3290
|
await reload(client, instanceId, tags);
|
|
3100
|
-
const
|
|
3101
|
-
|
|
3102
|
-
instanceId,
|
|
3103
|
-
...reason !== void 0 ? { reason } : {},
|
|
3104
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3105
|
-
});
|
|
3106
|
-
return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), {
|
|
3291
|
+
const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
|
|
3292
|
+
return {
|
|
3107
3293
|
instance: await reload(client, instanceId, tags),
|
|
3108
3294
|
cascaded: 0,
|
|
3109
|
-
fired
|
|
3295
|
+
fired
|
|
3110
3296
|
};
|
|
3111
3297
|
},
|
|
3112
3298
|
/**
|
|
@@ -3205,6 +3391,30 @@ const workflow = {
|
|
|
3205
3391
|
* `fireAction` to gate writes via the same logic.
|
|
3206
3392
|
*/
|
|
3207
3393
|
evaluate: async (args) => evaluateInstance(args),
|
|
3394
|
+
/**
|
|
3395
|
+
* Diagnose why an instance is or isn't progressing. Projects the
|
|
3396
|
+
* instance (the same read as `evaluate`) and classifies it — terminal,
|
|
3397
|
+
* `progressing`, `waiting` (an action is available — healthy), or `stuck`
|
|
3398
|
+
* with a structured cause — returning that verdict as a
|
|
3399
|
+
* {@link DiagnoseResult} alongside the evaluation it came from, so a
|
|
3400
|
+
* consumer can render the supporting evidence without a second projection.
|
|
3401
|
+
* Pure read.
|
|
3402
|
+
*/
|
|
3403
|
+
diagnose: async (args) => {
|
|
3404
|
+
const evaluation = await evaluateInstance(args);
|
|
3405
|
+
return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
|
|
3406
|
+
},
|
|
3407
|
+
/**
|
|
3408
|
+
* List the actions an actor could fire on an instance's current stage,
|
|
3409
|
+
* each flagged `allowed` (with a structured `disabledReason` when not).
|
|
3410
|
+
* Projects the instance from the actor's perspective and flattens its
|
|
3411
|
+
* tasks' actions. Returns the evaluation alongside the actions so a
|
|
3412
|
+
* consumer can read the instance/stage context. Pure read.
|
|
3413
|
+
*/
|
|
3414
|
+
availableActions: async (args) => {
|
|
3415
|
+
const evaluation = await evaluateInstance(args);
|
|
3416
|
+
return { evaluation, actions: availableActions(evaluation.currentStage.tasks) };
|
|
3417
|
+
},
|
|
3208
3418
|
/**
|
|
3209
3419
|
* Materialised spawned children of a parent instance.
|
|
3210
3420
|
*
|
|
@@ -3465,6 +3675,7 @@ function createInstanceSession(args) {
|
|
|
3465
3675
|
task,
|
|
3466
3676
|
action,
|
|
3467
3677
|
actor,
|
|
3678
|
+
clientForGdr,
|
|
3468
3679
|
...grants !== void 0 ? { grants } : {},
|
|
3469
3680
|
...clock !== void 0 ? { clock } : {},
|
|
3470
3681
|
...params !== void 0 ? { params } : {}
|
|
@@ -3520,8 +3731,11 @@ function createEngine(args) {
|
|
|
3520
3731
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3521
3732
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3522
3733
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
3734
|
+
diagnose: (rest) => workflow.diagnose(bind(rest)),
|
|
3735
|
+
availableActions: (rest) => workflow.availableActions(bind(rest)),
|
|
3523
3736
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
3524
3737
|
abortInstance: (rest) => workflow.abortInstance(bind(rest)),
|
|
3738
|
+
deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
|
|
3525
3739
|
getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
|
|
3526
3740
|
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3527
3741
|
instance: (instanceDoc, opts) => createInstanceSession({
|
|
@@ -3599,6 +3813,36 @@ function createEngine(args) {
|
|
|
3599
3813
|
})
|
|
3600
3814
|
};
|
|
3601
3815
|
}
|
|
3816
|
+
function docIdFor(def, target) {
|
|
3817
|
+
return definitionDocId(target.workflowResource, canonicalTag(target.tags), def.name, def.version);
|
|
3818
|
+
}
|
|
3819
|
+
function diffStatus(existing, expected) {
|
|
3820
|
+
return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
|
|
3821
|
+
}
|
|
3822
|
+
function diffEntry(def, existingRaw, target) {
|
|
3823
|
+
const docId = docIdFor(def, target), expected = {
|
|
3824
|
+
...def,
|
|
3825
|
+
_id: docId,
|
|
3826
|
+
_type: schema.WORKFLOW_DEFINITION_TYPE,
|
|
3827
|
+
tags: target.tags
|
|
3828
|
+
}, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
|
|
3829
|
+
return {
|
|
3830
|
+
name: def.name,
|
|
3831
|
+
version: def.version,
|
|
3832
|
+
status,
|
|
3833
|
+
docId,
|
|
3834
|
+
expected,
|
|
3835
|
+
...existing !== void 0 ? { existing } : {}
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
async function computeDiffEntries(client, defs, target) {
|
|
3839
|
+
const entries = [];
|
|
3840
|
+
for (const def of defs) {
|
|
3841
|
+
const existingRaw = await client.getDocument(docIdFor(def, target)) ?? void 0;
|
|
3842
|
+
entries.push(diffEntry(def, existingRaw, target));
|
|
3843
|
+
}
|
|
3844
|
+
return entries;
|
|
3845
|
+
}
|
|
3602
3846
|
const HISTORY_DISPLAY = {
|
|
3603
3847
|
stageEntered: {
|
|
3604
3848
|
title: "Stage entered",
|
|
@@ -3791,15 +4035,23 @@ exports.OP_DISPLAY = OP_DISPLAY;
|
|
|
3791
4035
|
exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
|
|
3792
4036
|
exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
|
|
3793
4037
|
exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
|
|
4038
|
+
exports.abortReason = abortReason;
|
|
4039
|
+
exports.actionVerdict = actionVerdict;
|
|
4040
|
+
exports.assigneesOf = assigneesOf;
|
|
4041
|
+
exports.availableActions = availableActions;
|
|
3794
4042
|
exports.buildSnapshot = buildSnapshot;
|
|
3795
4043
|
exports.canonicalTag = canonicalTag;
|
|
3796
4044
|
exports.compileGuard = compileGuard;
|
|
4045
|
+
exports.computeDiffEntries = computeDiffEntries;
|
|
3797
4046
|
exports.contentReleaseName = contentReleaseName;
|
|
3798
4047
|
exports.createEngine = createEngine;
|
|
3799
4048
|
exports.datasetResourceParts = datasetResourceParts;
|
|
3800
4049
|
exports.defaultLoggerFactory = defaultLoggerFactory;
|
|
3801
4050
|
exports.denyingGuards = denyingGuards;
|
|
3802
4051
|
exports.deployStageGuards = deployStageGuards;
|
|
4052
|
+
exports.diagnoseInputFromEvaluation = diagnoseInputFromEvaluation;
|
|
4053
|
+
exports.diagnoseInstance = diagnoseInstance;
|
|
4054
|
+
exports.diffEntry = diffEntry;
|
|
3803
4055
|
exports.displayDescription = displayDescription;
|
|
3804
4056
|
exports.displayTitle = displayTitle;
|
|
3805
4057
|
exports.effectsContextMap = effectsContextMap;
|
|
@@ -3818,6 +4070,7 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
|
|
|
3818
4070
|
exports.isGdr = isGdr;
|
|
3819
4071
|
exports.isTerminalStage = isTerminalStage;
|
|
3820
4072
|
exports.lakeGuardId = lakeGuardId;
|
|
4073
|
+
exports.openStage = openStage;
|
|
3821
4074
|
exports.parseGdr = parseGdr;
|
|
3822
4075
|
exports.readsRaw = readsRaw;
|
|
3823
4076
|
exports.refCanvas = refCanvas;
|