@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.js
CHANGED
|
@@ -246,6 +246,20 @@ function effectBindings(effects) {
|
|
|
246
246
|
(e) => Object.entries(e.bindings ?? {}).map(([k, groq]) => [`${e.name}.${k}`, groq])
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
|
+
function actionVerdict(task, action) {
|
|
250
|
+
return {
|
|
251
|
+
task: task.task.name,
|
|
252
|
+
taskStatus: task.status,
|
|
253
|
+
action: action.action.name,
|
|
254
|
+
title: action.action.title,
|
|
255
|
+
allowed: action.allowed,
|
|
256
|
+
disabledReason: action.disabledReason,
|
|
257
|
+
params: action.action.params ?? []
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function availableActions(tasks) {
|
|
261
|
+
return tasks.flatMap((t) => t.actions.map((a) => actionVerdict(t, a)));
|
|
262
|
+
}
|
|
249
263
|
const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
|
|
250
264
|
class MutationGuardDeniedError extends Error {
|
|
251
265
|
denied;
|
|
@@ -440,79 +454,63 @@ function stripStateForLake(value) {
|
|
|
440
454
|
function bareId(id) {
|
|
441
455
|
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
442
456
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
448
|
-
return { doc: compileGuard({
|
|
449
|
-
id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
|
|
450
|
-
resourceType: resource.type,
|
|
451
|
-
resourceId: resource.id,
|
|
452
|
-
owner: GUARD_OWNER,
|
|
453
|
-
sourceInstanceId: instance._id,
|
|
454
|
-
sourceDefinition: instance.definition,
|
|
455
|
-
sourceStage: stageName,
|
|
456
|
-
name: guard.name,
|
|
457
|
-
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
458
|
-
match: {
|
|
459
|
-
...types !== void 0 ? { types } : {},
|
|
460
|
-
idRefs: bareIdRefs(targets),
|
|
461
|
-
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
462
|
-
actions: guard.match.actions
|
|
463
|
-
},
|
|
464
|
-
predicate: guard.predicate ?? "",
|
|
465
|
-
metadata: resolveMetadata(guard.metadata, ctx)
|
|
466
|
-
}), routeGdr: targets[0].parsed };
|
|
457
|
+
function abortReason(instance) {
|
|
458
|
+
return instance.history.find(
|
|
459
|
+
(h) => h._type === "aborted"
|
|
460
|
+
)?.reason;
|
|
467
461
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
await client.patch(doc._id).set(body).commit();
|
|
462
|
+
function diagnoseInputFromEvaluation(evaluation) {
|
|
463
|
+
return {
|
|
464
|
+
instance: evaluation.instance,
|
|
465
|
+
tasks: evaluation.currentStage.tasks,
|
|
466
|
+
transitions: evaluation.currentStage.transitions
|
|
467
|
+
};
|
|
475
468
|
}
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
for (const guard of stage?.guards ?? []) {
|
|
479
|
-
const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
|
|
480
|
-
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
481
|
-
}
|
|
482
|
-
return out;
|
|
469
|
+
function openStage(instance) {
|
|
470
|
+
return findOpenStageEntry(instance);
|
|
483
471
|
}
|
|
484
|
-
|
|
485
|
-
return
|
|
472
|
+
function assigneesOf(stage, taskName) {
|
|
473
|
+
return (stage?.tasks.find((t) => t.name === taskName)?.state ?? []).filter((s) => s._type === "assignees").flatMap((s) => s.value);
|
|
486
474
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
|
|
490
|
-
for (const { client, doc } of resolvedStageGuards(args))
|
|
491
|
-
await upsertGuard(client, doc);
|
|
475
|
+
function isResolved(status) {
|
|
476
|
+
return status === "done" || status === "skipped";
|
|
492
477
|
}
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
for (const { client, doc } of resolvedStageGuards(args))
|
|
497
|
-
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
478
|
+
function failedEffectCause(input) {
|
|
479
|
+
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));
|
|
480
|
+
return effect ? { kind: "failed-effect", effect } : void 0;
|
|
498
481
|
}
|
|
499
|
-
function
|
|
500
|
-
const
|
|
501
|
-
return
|
|
482
|
+
function hungEffectCause(input) {
|
|
483
|
+
const effect = input.instance.pendingEffects.find((e) => e.claim !== void 0);
|
|
484
|
+
return effect ? { kind: "hung-effect", effect } : void 0;
|
|
502
485
|
}
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
return
|
|
486
|
+
function failedTaskCause(input) {
|
|
487
|
+
const failed = input.tasks.find((t) => t.status === "failed");
|
|
488
|
+
return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
|
|
506
489
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
490
|
+
function waitingState(input) {
|
|
491
|
+
const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
|
|
492
|
+
if (active !== void 0)
|
|
493
|
+
return {
|
|
494
|
+
state: "waiting",
|
|
495
|
+
task: active.task.name,
|
|
496
|
+
assignees: assigneesOf(openStage(input.instance), active.task.name),
|
|
497
|
+
actions: (active.task.actions ?? []).map((a) => a.name)
|
|
498
|
+
};
|
|
512
499
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
500
|
+
function noTransitionFiresCause(input) {
|
|
501
|
+
if (input.transitions.length !== 0 && !input.transitions.some((t) => t.filterSatisfied) && input.tasks.every((t) => isResolved(t.status)))
|
|
502
|
+
return { kind: "no-transition-fires" };
|
|
503
|
+
}
|
|
504
|
+
function diagnoseInstance(input) {
|
|
505
|
+
const { instance } = input;
|
|
506
|
+
if (instance.abortedAt !== void 0) {
|
|
507
|
+
const reason = abortReason(instance);
|
|
508
|
+
return { state: "aborted", at: instance.abortedAt, ...reason !== void 0 ? { reason } : {} };
|
|
509
|
+
}
|
|
510
|
+
if (instance.completedAt !== void 0)
|
|
511
|
+
return { state: "completed", at: instance.completedAt };
|
|
512
|
+
const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
|
|
513
|
+
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
|
|
516
514
|
}
|
|
517
515
|
function buildSnapshot(args) {
|
|
518
516
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
@@ -631,156 +629,357 @@ function resourceFromParsed(parsed) {
|
|
|
631
629
|
function collectEntryDocUris(resolvedStateEntries) {
|
|
632
630
|
return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
|
|
633
631
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
issues;
|
|
638
|
-
constructor(args) {
|
|
639
|
-
const issueText = args.issues.join("; ");
|
|
640
|
-
super(
|
|
641
|
-
`State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
|
|
642
|
-
), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
const GdrShape = v.looseObject({
|
|
646
|
-
id: v.pipe(
|
|
647
|
-
v.string(),
|
|
648
|
-
v.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
649
|
-
),
|
|
650
|
-
type: v.pipe(v.string(), v.minLength(1))
|
|
651
|
-
}), ReleaseRefShape = v.looseObject({
|
|
652
|
-
id: v.pipe(
|
|
653
|
-
v.string(),
|
|
654
|
-
v.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
655
|
-
),
|
|
656
|
-
type: v.literal("system.release"),
|
|
657
|
-
releaseName: v.pipe(v.string(), v.minLength(1))
|
|
658
|
-
}), ActorShape = v.looseObject({
|
|
659
|
-
kind: v.picklist(["user", "ai", "system"]),
|
|
660
|
-
id: v.pipe(v.string(), v.minLength(1)),
|
|
661
|
-
roles: v.optional(v.array(v.string())),
|
|
662
|
-
onBehalfOf: v.optional(v.string())
|
|
663
|
-
}), AssigneeShape = v.union([
|
|
664
|
-
v.looseObject({ type: v.literal("user"), id: v.pipe(v.string(), v.minLength(1)) }),
|
|
665
|
-
v.looseObject({ type: v.literal("role"), role: v.pipe(v.string(), v.minLength(1)) })
|
|
666
|
-
]), ChecklistItemShape = v.looseObject({
|
|
667
|
-
label: v.pipe(v.string(), v.minLength(1)),
|
|
668
|
-
done: v.boolean(),
|
|
669
|
-
doneBy: v.optional(v.string()),
|
|
670
|
-
doneAt: v.optional(v.string()),
|
|
671
|
-
_key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
672
|
-
}), NoteItemShape = v.pipe(
|
|
673
|
-
v.looseObject({
|
|
674
|
-
_key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
675
|
-
}),
|
|
676
|
-
v.check(
|
|
677
|
-
(val) => typeof val == "object" && val !== null && !Array.isArray(val),
|
|
678
|
-
"must be an object"
|
|
679
|
-
)
|
|
680
|
-
), NullableString = v.union([v.null(), v.string()]), NullableNumber = v.union([v.null(), v.number()]), NullableBoolean = v.union([v.null(), v.boolean()]), NullableDateTime = v.union([
|
|
681
|
-
v.null(),
|
|
682
|
-
v.pipe(
|
|
683
|
-
v.string(),
|
|
684
|
-
v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
685
|
-
)
|
|
686
|
-
]), NullableUrl = NullableString, valueSchemas = {
|
|
687
|
-
"doc.ref": v.union([v.null(), GdrShape]),
|
|
688
|
-
"doc.refs": v.array(GdrShape),
|
|
689
|
-
"release.ref": v.union([v.null(), ReleaseRefShape]),
|
|
690
|
-
query: v.any(),
|
|
691
|
-
"value.string": NullableString,
|
|
692
|
-
"value.url": NullableUrl,
|
|
693
|
-
"value.number": NullableNumber,
|
|
694
|
-
"value.boolean": NullableBoolean,
|
|
695
|
-
"value.dateTime": NullableDateTime,
|
|
696
|
-
"value.actor": v.union([v.null(), ActorShape]),
|
|
697
|
-
checklist: v.array(ChecklistItemShape),
|
|
698
|
-
notes: v.array(NoteItemShape),
|
|
699
|
-
assignees: v.array(AssigneeShape)
|
|
700
|
-
}, itemSchemas = {
|
|
701
|
-
"doc.refs": GdrShape,
|
|
702
|
-
checklist: ChecklistItemShape,
|
|
703
|
-
notes: NoteItemShape,
|
|
704
|
-
assignees: AssigneeShape
|
|
705
|
-
};
|
|
706
|
-
function isAppendable(entryType) {
|
|
707
|
-
return entryType in itemSchemas;
|
|
632
|
+
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
633
|
+
function guardsForResource(client) {
|
|
634
|
+
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
708
635
|
}
|
|
709
|
-
function
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
entryName: args.entryName,
|
|
715
|
-
issues: [`unknown state entry type ${args.entryType}`],
|
|
716
|
-
mode: "value"
|
|
717
|
-
});
|
|
718
|
-
const result = v.safeParse(schema, args.value);
|
|
719
|
-
if (!result.success)
|
|
720
|
-
throw new StateValueShapeError({
|
|
721
|
-
entryType: args.entryType,
|
|
722
|
-
entryName: args.entryName,
|
|
723
|
-
issues: formatIssues(result.issues),
|
|
724
|
-
mode: "value"
|
|
725
|
-
});
|
|
636
|
+
function instanceGuardQuery(instanceId) {
|
|
637
|
+
return {
|
|
638
|
+
query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
|
|
639
|
+
params: { guardType: GUARD_DOC_TYPE, instanceId }
|
|
640
|
+
};
|
|
726
641
|
}
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
entryType: args.entryType,
|
|
731
|
-
entryName: args.entryName,
|
|
732
|
-
issues: [`state entry type ${args.entryType} does not support append`],
|
|
733
|
-
mode: "item"
|
|
734
|
-
});
|
|
735
|
-
const schema = itemSchemas[args.entryType], result = v.safeParse(schema, args.item);
|
|
736
|
-
if (!result.success)
|
|
737
|
-
throw new StateValueShapeError({
|
|
738
|
-
entryType: args.entryType,
|
|
739
|
-
entryName: args.entryName,
|
|
740
|
-
issues: formatIssues(result.issues),
|
|
741
|
-
mode: "item"
|
|
742
|
-
});
|
|
642
|
+
function verdictGuardsForInstance(client, instanceId) {
|
|
643
|
+
const { query, params } = instanceGuardQuery(instanceId);
|
|
644
|
+
return client.fetch(query, params);
|
|
743
645
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
646
|
+
async function guardsForInstance(args) {
|
|
647
|
+
const { client, clientForGdr, instance } = args, stateUris = [
|
|
648
|
+
...collectEntryDocUris(instance.state),
|
|
649
|
+
...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
|
|
650
|
+
], clients = resourceClientMap(
|
|
651
|
+
instance.workflowResource,
|
|
652
|
+
client,
|
|
653
|
+
clientForGdr,
|
|
654
|
+
stateUris.map((uri) => tryParseGdr(uri))
|
|
655
|
+
), { query, params } = instanceGuardQuery(instance._id);
|
|
656
|
+
return queryGuardsAcross(clients.values(), query, params);
|
|
749
657
|
}
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (entry._type === "release.ref" && entry.value !== null) {
|
|
754
|
-
const releaseName = entry.value.releaseName;
|
|
755
|
-
if (typeof releaseName == "string" && releaseName.length > 0)
|
|
756
|
-
return [releaseName];
|
|
757
|
-
}
|
|
758
|
-
}
|
|
658
|
+
async function guardsForDefinition(args) {
|
|
659
|
+
const perClient = await guardsForDefinitionByClient(args);
|
|
660
|
+
return dedupById(perClient.flatMap((entry) => entry.guards));
|
|
759
661
|
}
|
|
760
|
-
async function
|
|
761
|
-
const {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
662
|
+
async function guardsForDefinitionByClient(args) {
|
|
663
|
+
const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
|
|
664
|
+
(def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
|
|
665
|
+
), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs), query = "*[_type == $t && sourceDefinition == $definition]", params = { t: GUARD_DOC_TYPE, definition };
|
|
666
|
+
return Promise.all(
|
|
667
|
+
[...clients.values()].map(async (resourceClient) => ({
|
|
668
|
+
client: resourceClient,
|
|
669
|
+
guards: await resourceClient.fetch(query, params)
|
|
670
|
+
}))
|
|
671
|
+
);
|
|
769
672
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
function defaultEntryValue(entryType) {
|
|
777
|
-
return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
|
|
673
|
+
function guardIdRefs(definition) {
|
|
674
|
+
return definition.stages.flatMap(
|
|
675
|
+
(stage) => (stage.guards ?? []).flatMap(
|
|
676
|
+
(guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
|
|
677
|
+
)
|
|
678
|
+
);
|
|
778
679
|
}
|
|
779
|
-
function
|
|
780
|
-
const
|
|
781
|
-
|
|
680
|
+
function staticIdRefGdr(idRef, stage, definition) {
|
|
681
|
+
const read = /^\$state\.([\w-]+)/.exec(idRef);
|
|
682
|
+
if (read === null) return;
|
|
683
|
+
const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
|
|
684
|
+
return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
|
|
782
685
|
}
|
|
783
|
-
function
|
|
686
|
+
function entriesInScope(stage, definition) {
|
|
687
|
+
return [
|
|
688
|
+
...definition.state ?? [],
|
|
689
|
+
...stage.state ?? [],
|
|
690
|
+
...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
function gdrFromValue(value) {
|
|
694
|
+
if (typeof value == "string") return tryParseGdr(value);
|
|
695
|
+
if (typeof value == "object" && value !== null && "id" in value) {
|
|
696
|
+
const id = value.id;
|
|
697
|
+
return typeof id == "string" ? tryParseGdr(id) : void 0;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function tryParseGdr(uri) {
|
|
701
|
+
try {
|
|
702
|
+
return parseGdr(uri);
|
|
703
|
+
} catch {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
|
|
708
|
+
const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
|
|
709
|
+
for (const parsed of gdrs) {
|
|
710
|
+
if (parsed === void 0) continue;
|
|
711
|
+
const key = resourceKey(resourceFromParsed(parsed));
|
|
712
|
+
map.has(key) || map.set(key, clientForGdr(parsed));
|
|
713
|
+
}
|
|
714
|
+
return map;
|
|
715
|
+
}
|
|
716
|
+
async function queryGuardsAcross(clients, query, params) {
|
|
717
|
+
const perResource = await Promise.all(
|
|
718
|
+
[...clients].map((c) => c.fetch(query, params))
|
|
719
|
+
);
|
|
720
|
+
return dedupById(perResource.flat());
|
|
721
|
+
}
|
|
722
|
+
function dedupById(guards) {
|
|
723
|
+
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
724
|
+
}
|
|
725
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
726
|
+
function validateTags(tags) {
|
|
727
|
+
if (tags.length === 0)
|
|
728
|
+
throw new Error("tags: must be a non-empty array");
|
|
729
|
+
for (const tag of tags)
|
|
730
|
+
if (!TAG_RE.test(tag))
|
|
731
|
+
throw new Error(
|
|
732
|
+
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
function canonicalTag(tags) {
|
|
736
|
+
return tags[0];
|
|
737
|
+
}
|
|
738
|
+
function tagScopeFilter(param = "engineTags") {
|
|
739
|
+
return `count(tags[@ in $${param}]) > 0`;
|
|
740
|
+
}
|
|
741
|
+
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
742
|
+
function resolveGuard(guard, instance, stageName, now) {
|
|
743
|
+
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
744
|
+
if (targets === null) return null;
|
|
745
|
+
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
746
|
+
return { doc: compileGuard({
|
|
747
|
+
id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
|
|
748
|
+
resourceType: resource.type,
|
|
749
|
+
resourceId: resource.id,
|
|
750
|
+
owner: GUARD_OWNER,
|
|
751
|
+
sourceInstanceId: instance._id,
|
|
752
|
+
sourceDefinition: instance.definition,
|
|
753
|
+
sourceStage: stageName,
|
|
754
|
+
name: guard.name,
|
|
755
|
+
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
756
|
+
match: {
|
|
757
|
+
...types !== void 0 ? { types } : {},
|
|
758
|
+
idRefs: bareIdRefs(targets),
|
|
759
|
+
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
760
|
+
actions: guard.match.actions
|
|
761
|
+
},
|
|
762
|
+
predicate: guard.predicate ?? "",
|
|
763
|
+
metadata: resolveMetadata(guard.metadata, ctx)
|
|
764
|
+
}), routeGdr: targets[0].parsed };
|
|
765
|
+
}
|
|
766
|
+
async function upsertGuard(client, doc) {
|
|
767
|
+
if (!await client.getDocument(doc._id)) {
|
|
768
|
+
await client.create(doc);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
772
|
+
await client.patch(doc._id).set(body).commit();
|
|
773
|
+
}
|
|
774
|
+
function resolvedStageGuards(args) {
|
|
775
|
+
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
776
|
+
for (const guard of stage?.guards ?? []) {
|
|
777
|
+
const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
|
|
778
|
+
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
async function committedInstance(args) {
|
|
783
|
+
return await args.client.getDocument(args.instance._id) ?? void 0;
|
|
784
|
+
}
|
|
785
|
+
async function deployStageGuards(args) {
|
|
786
|
+
const live = await committedInstance(args);
|
|
787
|
+
if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
|
|
788
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
789
|
+
await upsertGuard(client, doc);
|
|
790
|
+
}
|
|
791
|
+
async function retractStageGuards(args) {
|
|
792
|
+
const live = await committedInstance(args);
|
|
793
|
+
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
794
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
795
|
+
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
796
|
+
}
|
|
797
|
+
async function deleteOrphanedDefinitionGuards(args) {
|
|
798
|
+
const { client, clientForGdr, workflowResource, definition, definitions, tags } = args, perClient = await guardsForDefinitionByClient({
|
|
799
|
+
client,
|
|
800
|
+
clientForGdr,
|
|
801
|
+
workflowResource,
|
|
802
|
+
definition,
|
|
803
|
+
definitions
|
|
804
|
+
}), ownPartitionPrefix = `${canonicalTag(tags)}.`;
|
|
805
|
+
let count = 0;
|
|
806
|
+
for (const { client: resourceClient, guards } of perClient) {
|
|
807
|
+
const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
|
|
808
|
+
if (own.length === 0) continue;
|
|
809
|
+
const tx = resourceClient.transaction();
|
|
810
|
+
for (const guard of own) tx.delete(guard._id);
|
|
811
|
+
await tx.commit(), count += own.length;
|
|
812
|
+
}
|
|
813
|
+
return count;
|
|
814
|
+
}
|
|
815
|
+
function randomKey(length = 12) {
|
|
816
|
+
const bytes = new Uint8Array(length);
|
|
817
|
+
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
818
|
+
}
|
|
819
|
+
async function evaluateCondition(args) {
|
|
820
|
+
const { condition, snapshot, params } = args;
|
|
821
|
+
return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
|
|
822
|
+
}
|
|
823
|
+
async function evaluatePredicates(args) {
|
|
824
|
+
const out = {};
|
|
825
|
+
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
826
|
+
out[name] = !!await runGroq(groq, args.params, args.snapshot);
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
async function runGroq(groq, params, snapshot) {
|
|
830
|
+
const tree = parse(groq, { params });
|
|
831
|
+
return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
832
|
+
}
|
|
833
|
+
class StateValueShapeError extends Error {
|
|
834
|
+
entryType;
|
|
835
|
+
entryName;
|
|
836
|
+
issues;
|
|
837
|
+
constructor(args) {
|
|
838
|
+
const issueText = args.issues.join("; ");
|
|
839
|
+
super(
|
|
840
|
+
`State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
|
|
841
|
+
), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const GdrShape = v.looseObject({
|
|
845
|
+
id: v.pipe(
|
|
846
|
+
v.string(),
|
|
847
|
+
v.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
848
|
+
),
|
|
849
|
+
type: v.pipe(v.string(), v.minLength(1))
|
|
850
|
+
}), ReleaseRefShape = v.looseObject({
|
|
851
|
+
id: v.pipe(
|
|
852
|
+
v.string(),
|
|
853
|
+
v.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
854
|
+
),
|
|
855
|
+
type: v.literal("system.release"),
|
|
856
|
+
releaseName: v.pipe(v.string(), v.minLength(1))
|
|
857
|
+
}), ActorShape = v.looseObject({
|
|
858
|
+
kind: v.picklist(["user", "ai", "system"]),
|
|
859
|
+
id: v.pipe(v.string(), v.minLength(1)),
|
|
860
|
+
roles: v.optional(v.array(v.string())),
|
|
861
|
+
onBehalfOf: v.optional(v.string())
|
|
862
|
+
}), AssigneeShape = v.union([
|
|
863
|
+
v.looseObject({ type: v.literal("user"), id: v.pipe(v.string(), v.minLength(1)) }),
|
|
864
|
+
v.looseObject({ type: v.literal("role"), role: v.pipe(v.string(), v.minLength(1)) })
|
|
865
|
+
]), ChecklistItemShape = v.looseObject({
|
|
866
|
+
label: v.pipe(v.string(), v.minLength(1)),
|
|
867
|
+
done: v.boolean(),
|
|
868
|
+
doneBy: v.optional(v.string()),
|
|
869
|
+
doneAt: v.optional(v.string()),
|
|
870
|
+
_key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
871
|
+
}), NoteItemShape = v.pipe(
|
|
872
|
+
v.looseObject({
|
|
873
|
+
_key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
874
|
+
}),
|
|
875
|
+
v.check(
|
|
876
|
+
(val) => typeof val == "object" && val !== null && !Array.isArray(val),
|
|
877
|
+
"must be an object"
|
|
878
|
+
)
|
|
879
|
+
), NullableString = v.union([v.null(), v.string()]), NullableNumber = v.union([v.null(), v.number()]), NullableBoolean = v.union([v.null(), v.boolean()]), NullableDateTime = v.union([
|
|
880
|
+
v.null(),
|
|
881
|
+
v.pipe(
|
|
882
|
+
v.string(),
|
|
883
|
+
v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
884
|
+
)
|
|
885
|
+
]), NullableUrl = NullableString, valueSchemas = {
|
|
886
|
+
"doc.ref": v.union([v.null(), GdrShape]),
|
|
887
|
+
"doc.refs": v.array(GdrShape),
|
|
888
|
+
"release.ref": v.union([v.null(), ReleaseRefShape]),
|
|
889
|
+
query: v.any(),
|
|
890
|
+
"value.string": NullableString,
|
|
891
|
+
"value.url": NullableUrl,
|
|
892
|
+
"value.number": NullableNumber,
|
|
893
|
+
"value.boolean": NullableBoolean,
|
|
894
|
+
"value.dateTime": NullableDateTime,
|
|
895
|
+
"value.actor": v.union([v.null(), ActorShape]),
|
|
896
|
+
checklist: v.array(ChecklistItemShape),
|
|
897
|
+
notes: v.array(NoteItemShape),
|
|
898
|
+
assignees: v.array(AssigneeShape)
|
|
899
|
+
}, itemSchemas = {
|
|
900
|
+
"doc.refs": GdrShape,
|
|
901
|
+
checklist: ChecklistItemShape,
|
|
902
|
+
notes: NoteItemShape,
|
|
903
|
+
assignees: AssigneeShape
|
|
904
|
+
};
|
|
905
|
+
function isAppendable(entryType) {
|
|
906
|
+
return entryType in itemSchemas;
|
|
907
|
+
}
|
|
908
|
+
function validateStateValue(args) {
|
|
909
|
+
const schema = valueSchemas[args.entryType];
|
|
910
|
+
if (schema === void 0)
|
|
911
|
+
throw new StateValueShapeError({
|
|
912
|
+
entryType: args.entryType,
|
|
913
|
+
entryName: args.entryName,
|
|
914
|
+
issues: [`unknown state entry type ${args.entryType}`],
|
|
915
|
+
mode: "value"
|
|
916
|
+
});
|
|
917
|
+
const result = v.safeParse(schema, args.value);
|
|
918
|
+
if (!result.success)
|
|
919
|
+
throw new StateValueShapeError({
|
|
920
|
+
entryType: args.entryType,
|
|
921
|
+
entryName: args.entryName,
|
|
922
|
+
issues: formatIssues(result.issues),
|
|
923
|
+
mode: "value"
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function validateStateAppendItem(args) {
|
|
927
|
+
if (!isAppendable(args.entryType))
|
|
928
|
+
throw new StateValueShapeError({
|
|
929
|
+
entryType: args.entryType,
|
|
930
|
+
entryName: args.entryName,
|
|
931
|
+
issues: [`state entry type ${args.entryType} does not support append`],
|
|
932
|
+
mode: "item"
|
|
933
|
+
});
|
|
934
|
+
const schema = itemSchemas[args.entryType], result = v.safeParse(schema, args.item);
|
|
935
|
+
if (!result.success)
|
|
936
|
+
throw new StateValueShapeError({
|
|
937
|
+
entryType: args.entryType,
|
|
938
|
+
entryName: args.entryName,
|
|
939
|
+
issues: formatIssues(result.issues),
|
|
940
|
+
mode: "item"
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
function formatIssues(issues) {
|
|
944
|
+
return issues.map((i) => {
|
|
945
|
+
const keys = i.path?.map((p) => p.key) ?? [];
|
|
946
|
+
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
function derivePerspectiveFromState(entries) {
|
|
950
|
+
if (entries !== void 0) {
|
|
951
|
+
for (const entry of entries)
|
|
952
|
+
if (entry._type === "release.ref" && entry.value !== null) {
|
|
953
|
+
const releaseName = entry.value.releaseName;
|
|
954
|
+
if (typeof releaseName == "string" && releaseName.length > 0)
|
|
955
|
+
return [releaseName];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async function resolveDeclaredState(args) {
|
|
960
|
+
const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
|
|
961
|
+
if (entryDefs === void 0 || entryDefs.length === 0) return [];
|
|
962
|
+
const out = [];
|
|
963
|
+
for (const entry of entryDefs) {
|
|
964
|
+
const scopedCtx = { ...ctx, resolvedState: out };
|
|
965
|
+
out.push(await resolveOneEntry(entry, initialState, scopedCtx, randomKey2));
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
970
|
+
"doc.refs",
|
|
971
|
+
"checklist",
|
|
972
|
+
"notes",
|
|
973
|
+
"assignees"
|
|
974
|
+
]);
|
|
975
|
+
function defaultEntryValue(entryType) {
|
|
976
|
+
return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
|
|
977
|
+
}
|
|
978
|
+
function resolveInitValue(entry, initialState, defaultValue) {
|
|
979
|
+
const initMatch = initialState.find((s) => s.name === entry.name && s.type === entry.type);
|
|
980
|
+
return initMatch === void 0 ? defaultValue : (assertInitValueShape(entry, initMatch.value), initMatch.value);
|
|
981
|
+
}
|
|
982
|
+
function resolveStateReadValue(source, ctx, defaultValue) {
|
|
784
983
|
const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.name === source.state), targetValue = target === void 0 ? void 0 : target.value;
|
|
785
984
|
return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
|
|
786
985
|
}
|
|
@@ -963,77 +1162,30 @@ async function resolveStageStateEntries(args) {
|
|
|
963
1162
|
workflowResource: instance.workflowResource,
|
|
964
1163
|
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
965
1164
|
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
966
|
-
workflowState: instance.state ?? [],
|
|
967
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
968
|
-
},
|
|
969
|
-
randomKey
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
async function resolveTaskStateEntries(args) {
|
|
973
|
-
const { client, instance, stage, task, now } = args;
|
|
974
|
-
return resolveDeclaredState({
|
|
975
|
-
entryDefs: task.state,
|
|
976
|
-
initialState: [],
|
|
977
|
-
ctx: {
|
|
978
|
-
client,
|
|
979
|
-
now,
|
|
980
|
-
selfId: instance._id,
|
|
981
|
-
tags: instance.tags ?? [],
|
|
982
|
-
stageName: stage.name,
|
|
983
|
-
workflowResource: instance.workflowResource,
|
|
984
|
-
taskName: task.name,
|
|
985
|
-
workflowState: instance.state ?? [],
|
|
986
|
-
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
987
|
-
},
|
|
988
|
-
randomKey
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
async function resolveBindings(args) {
|
|
992
|
-
const resolved = {};
|
|
993
|
-
for (const [key, groq] of Object.entries(args.bindings ?? {}))
|
|
994
|
-
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
995
|
-
return { ...resolved, ...args.staticInput };
|
|
996
|
-
}
|
|
997
|
-
function effectsContextEntry(name, value) {
|
|
998
|
-
const _key = randomKey();
|
|
999
|
-
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 };
|
|
1000
|
-
}
|
|
1001
|
-
function effectsContextJsonEntry(name, value) {
|
|
1002
|
-
return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
|
|
1003
|
-
}
|
|
1004
|
-
function buildInstanceBase(args) {
|
|
1005
|
-
const { id, now, actor, perspective } = args;
|
|
1006
|
-
return {
|
|
1007
|
-
_id: id,
|
|
1008
|
-
_type: WORKFLOW_INSTANCE_TYPE,
|
|
1009
|
-
_rev: "",
|
|
1010
|
-
_createdAt: now,
|
|
1011
|
-
_updatedAt: now,
|
|
1012
|
-
tags: args.tags,
|
|
1013
|
-
workflowResource: args.workflowResource,
|
|
1014
|
-
definition: args.definitionName,
|
|
1015
|
-
pinnedVersion: args.pinnedVersion,
|
|
1016
|
-
definitionSnapshot: JSON.stringify(args.definition),
|
|
1017
|
-
state: args.state,
|
|
1018
|
-
effectsContext: args.effectsContext,
|
|
1019
|
-
ancestors: args.ancestors,
|
|
1020
|
-
...perspective !== void 0 ? { perspective } : {},
|
|
1021
|
-
currentStage: args.initialStage,
|
|
1022
|
-
stages: [],
|
|
1023
|
-
pendingEffects: [],
|
|
1024
|
-
effectHistory: [],
|
|
1025
|
-
history: [
|
|
1026
|
-
{
|
|
1027
|
-
_key: randomKey(),
|
|
1028
|
-
_type: "stageEntered",
|
|
1029
|
-
at: now,
|
|
1030
|
-
stage: args.initialStage,
|
|
1031
|
-
...actor !== void 0 ? { actor } : {}
|
|
1032
|
-
}
|
|
1033
|
-
],
|
|
1034
|
-
startedAt: now,
|
|
1035
|
-
lastChangedAt: now
|
|
1036
|
-
};
|
|
1165
|
+
workflowState: instance.state ?? [],
|
|
1166
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1167
|
+
},
|
|
1168
|
+
randomKey
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
async function resolveTaskStateEntries(args) {
|
|
1172
|
+
const { client, instance, stage, task, now } = args;
|
|
1173
|
+
return resolveDeclaredState({
|
|
1174
|
+
entryDefs: task.state,
|
|
1175
|
+
initialState: [],
|
|
1176
|
+
ctx: {
|
|
1177
|
+
client,
|
|
1178
|
+
now,
|
|
1179
|
+
selfId: instance._id,
|
|
1180
|
+
tags: instance.tags ?? [],
|
|
1181
|
+
stageName: stage.name,
|
|
1182
|
+
workflowResource: instance.workflowResource,
|
|
1183
|
+
taskName: task.name,
|
|
1184
|
+
workflowState: instance.state ?? [],
|
|
1185
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
1186
|
+
},
|
|
1187
|
+
randomKey
|
|
1188
|
+
});
|
|
1037
1189
|
}
|
|
1038
1190
|
class ActionParamsInvalidError extends Error {
|
|
1039
1191
|
action;
|
|
@@ -1154,21 +1306,98 @@ function stageTransitionHistory(args) {
|
|
|
1154
1306
|
}
|
|
1155
1307
|
];
|
|
1156
1308
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
)
|
|
1309
|
+
function startMutation(instance) {
|
|
1310
|
+
return {
|
|
1311
|
+
currentStage: instance.currentStage,
|
|
1312
|
+
// Deep-ish copy. Per-scope state entry arrays need their own copies
|
|
1313
|
+
// so ops can mutate without leaking into the source.
|
|
1314
|
+
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
1315
|
+
stages: instance.stages.map((s) => ({
|
|
1316
|
+
...s,
|
|
1317
|
+
state: (s.state ?? []).map((entry) => ({ ...entry })),
|
|
1318
|
+
tasks: s.tasks.map((t) => ({
|
|
1319
|
+
...t,
|
|
1320
|
+
...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
|
|
1321
|
+
}))
|
|
1322
|
+
})),
|
|
1323
|
+
pendingEffects: [...instance.pendingEffects],
|
|
1324
|
+
effectHistory: [...instance.effectHistory],
|
|
1325
|
+
effectsContext: [...instance.effectsContext],
|
|
1326
|
+
history: [...instance.history],
|
|
1327
|
+
lastChangedAt: instance.lastChangedAt,
|
|
1328
|
+
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
1329
|
+
...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
|
|
1330
|
+
pendingCreates: []
|
|
1331
|
+
};
|
|
1166
1332
|
}
|
|
1167
|
-
function
|
|
1168
|
-
|
|
1333
|
+
function instanceStateFields(src) {
|
|
1334
|
+
const fields = {
|
|
1335
|
+
currentStage: src.currentStage,
|
|
1336
|
+
state: src.state,
|
|
1337
|
+
stages: src.stages,
|
|
1338
|
+
pendingEffects: src.pendingEffects,
|
|
1339
|
+
effectHistory: src.effectHistory,
|
|
1340
|
+
effectsContext: src.effectsContext,
|
|
1341
|
+
history: src.history
|
|
1342
|
+
};
|
|
1343
|
+
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
|
|
1169
1344
|
}
|
|
1170
|
-
function
|
|
1171
|
-
return
|
|
1345
|
+
function materializeInstance(base, mutation) {
|
|
1346
|
+
return {
|
|
1347
|
+
...base,
|
|
1348
|
+
currentStage: mutation.currentStage,
|
|
1349
|
+
state: mutation.state,
|
|
1350
|
+
stages: mutation.stages
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
async function persist(ctx, mutation) {
|
|
1354
|
+
const set = {
|
|
1355
|
+
...instanceStateFields(mutation),
|
|
1356
|
+
lastChangedAt: ctx.now
|
|
1357
|
+
}, pendingCreates = mutation.pendingCreates;
|
|
1358
|
+
if (pendingCreates.length === 0)
|
|
1359
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1360
|
+
const tx = ctx.client.transaction();
|
|
1361
|
+
for (const body of pendingCreates) tx.create(body);
|
|
1362
|
+
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1363
|
+
const actorForPriming = void 0;
|
|
1364
|
+
for (const body of pendingCreates)
|
|
1365
|
+
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);
|
|
1366
|
+
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1367
|
+
if (!reloaded)
|
|
1368
|
+
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
1369
|
+
return reloaded;
|
|
1370
|
+
}
|
|
1371
|
+
function currentStageEntry(mutation) {
|
|
1372
|
+
const entry = findOpenStageEntry(mutation);
|
|
1373
|
+
if (entry === void 0)
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
|
|
1376
|
+
);
|
|
1377
|
+
return entry;
|
|
1378
|
+
}
|
|
1379
|
+
function currentTasks(mutation) {
|
|
1380
|
+
return currentStageEntry(mutation).tasks;
|
|
1381
|
+
}
|
|
1382
|
+
function findTaskInCurrentStage(ctx, taskName) {
|
|
1383
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
|
|
1384
|
+
if (task === void 0)
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
1387
|
+
);
|
|
1388
|
+
return { stage, task };
|
|
1389
|
+
}
|
|
1390
|
+
function requireMutationTaskEntry(mutation, task) {
|
|
1391
|
+
const mutEntry = currentTasks(mutation).find((t) => t.name === task);
|
|
1392
|
+
if (mutEntry === void 0)
|
|
1393
|
+
throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
|
|
1394
|
+
return mutEntry;
|
|
1395
|
+
}
|
|
1396
|
+
function findCurrentStageEntry(instance) {
|
|
1397
|
+
return findOpenStageEntry(instance);
|
|
1398
|
+
}
|
|
1399
|
+
function findCurrentTasks(instance) {
|
|
1400
|
+
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
1172
1401
|
}
|
|
1173
1402
|
function definitionLookupGroq(explicit) {
|
|
1174
1403
|
const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
|
|
@@ -1186,6 +1415,47 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
1186
1415
|
return defaultClient;
|
|
1187
1416
|
}
|
|
1188
1417
|
}
|
|
1418
|
+
function effectsContextEntry(name, value) {
|
|
1419
|
+
const _key = randomKey();
|
|
1420
|
+
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 };
|
|
1421
|
+
}
|
|
1422
|
+
function effectsContextJsonEntry(name, value) {
|
|
1423
|
+
return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
|
|
1424
|
+
}
|
|
1425
|
+
function buildInstanceBase(args) {
|
|
1426
|
+
const { id, now, actor, perspective } = args;
|
|
1427
|
+
return {
|
|
1428
|
+
_id: id,
|
|
1429
|
+
_type: WORKFLOW_INSTANCE_TYPE,
|
|
1430
|
+
_rev: "",
|
|
1431
|
+
_createdAt: now,
|
|
1432
|
+
_updatedAt: now,
|
|
1433
|
+
tags: args.tags,
|
|
1434
|
+
workflowResource: args.workflowResource,
|
|
1435
|
+
definition: args.definitionName,
|
|
1436
|
+
pinnedVersion: args.pinnedVersion,
|
|
1437
|
+
definitionSnapshot: JSON.stringify(args.definition),
|
|
1438
|
+
state: args.state,
|
|
1439
|
+
effectsContext: args.effectsContext,
|
|
1440
|
+
ancestors: args.ancestors,
|
|
1441
|
+
...perspective !== void 0 ? { perspective } : {},
|
|
1442
|
+
currentStage: args.initialStage,
|
|
1443
|
+
stages: [],
|
|
1444
|
+
pendingEffects: [],
|
|
1445
|
+
effectHistory: [],
|
|
1446
|
+
history: [
|
|
1447
|
+
{
|
|
1448
|
+
_key: randomKey(),
|
|
1449
|
+
_type: "stageEntered",
|
|
1450
|
+
at: now,
|
|
1451
|
+
stage: args.initialStage,
|
|
1452
|
+
...actor !== void 0 ? { actor } : {}
|
|
1453
|
+
}
|
|
1454
|
+
],
|
|
1455
|
+
startedAt: now,
|
|
1456
|
+
lastChangedAt: now
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1189
1459
|
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";
|
|
1190
1460
|
function gateActor(outcome) {
|
|
1191
1461
|
return {
|
|
@@ -1364,6 +1634,111 @@ async function subworkflowRows(ctx, refs) {
|
|
|
1364
1634
|
status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
|
|
1365
1635
|
}));
|
|
1366
1636
|
}
|
|
1637
|
+
async function resolveBindings(args) {
|
|
1638
|
+
const resolved = {};
|
|
1639
|
+
for (const [key, groq] of Object.entries(args.bindings ?? {}))
|
|
1640
|
+
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
1641
|
+
return { ...resolved, ...args.staticInput };
|
|
1642
|
+
}
|
|
1643
|
+
async function completeEffect(args) {
|
|
1644
|
+
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1645
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1646
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1647
|
+
});
|
|
1648
|
+
return commitCompleteEffect(
|
|
1649
|
+
ctx,
|
|
1650
|
+
effectKey,
|
|
1651
|
+
status,
|
|
1652
|
+
outputs,
|
|
1653
|
+
detail,
|
|
1654
|
+
error,
|
|
1655
|
+
durationMs,
|
|
1656
|
+
options?.actor
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
function buildEffectHistoryEntry(pending, outcome) {
|
|
1660
|
+
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
1661
|
+
return {
|
|
1662
|
+
_key: pending._key,
|
|
1663
|
+
name: pending.name,
|
|
1664
|
+
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
1665
|
+
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
1666
|
+
params: pending.params,
|
|
1667
|
+
origin: pending.origin,
|
|
1668
|
+
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
1669
|
+
ranAt,
|
|
1670
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
1671
|
+
status,
|
|
1672
|
+
...detail !== void 0 ? { detail } : {},
|
|
1673
|
+
...error !== void 0 ? { error } : {},
|
|
1674
|
+
...outputs !== void 0 ? { outputs } : {}
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function upsertEffectsContext(mutation, effectName, outputs) {
|
|
1678
|
+
const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
|
|
1679
|
+
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
1680
|
+
}
|
|
1681
|
+
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
1682
|
+
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
1683
|
+
if (pending === void 0)
|
|
1684
|
+
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
1685
|
+
const mutation = startMutation(ctx.instance);
|
|
1686
|
+
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1687
|
+
const ranAt = ctx.now;
|
|
1688
|
+
return mutation.effectHistory.push(
|
|
1689
|
+
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1690
|
+
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
1691
|
+
_key: randomKey(),
|
|
1692
|
+
_type: "effectCompleted",
|
|
1693
|
+
at: ranAt,
|
|
1694
|
+
effectKey,
|
|
1695
|
+
effect: pending.name,
|
|
1696
|
+
status,
|
|
1697
|
+
...outputs !== void 0 ? { outputs } : {},
|
|
1698
|
+
...detail !== void 0 ? { detail } : {},
|
|
1699
|
+
...actor !== void 0 ? { actor } : {}
|
|
1700
|
+
}), await persist(ctx, mutation), { effectKey, status };
|
|
1701
|
+
}
|
|
1702
|
+
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1703
|
+
const key = randomKey(), pending = {
|
|
1704
|
+
_key: key,
|
|
1705
|
+
_type: "pendingEffect",
|
|
1706
|
+
name: effect.name,
|
|
1707
|
+
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
1708
|
+
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
1709
|
+
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
1710
|
+
params,
|
|
1711
|
+
origin,
|
|
1712
|
+
...actor !== void 0 ? { actor } : {},
|
|
1713
|
+
queuedAt: now
|
|
1714
|
+
}, history = {
|
|
1715
|
+
_key: randomKey(),
|
|
1716
|
+
_type: "effectQueued",
|
|
1717
|
+
at: now,
|
|
1718
|
+
effectKey: key,
|
|
1719
|
+
effect: effect.name,
|
|
1720
|
+
origin
|
|
1721
|
+
};
|
|
1722
|
+
return { pending, history };
|
|
1723
|
+
}
|
|
1724
|
+
async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
|
|
1725
|
+
if (!effects || effects.length === 0) return;
|
|
1726
|
+
const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
|
|
1727
|
+
...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
|
|
1728
|
+
...actor !== void 0 ? { actor } : {},
|
|
1729
|
+
// Caller-supplied action params, readable in bindings as `$params.<name>`.
|
|
1730
|
+
vars: { params: opts?.callerParams ?? {} }
|
|
1731
|
+
});
|
|
1732
|
+
for (const effect of effects) {
|
|
1733
|
+
const resolved = await resolveBindings({
|
|
1734
|
+
bindings: effect.bindings,
|
|
1735
|
+
staticInput: effect.input,
|
|
1736
|
+
snapshot: ctx.snapshot,
|
|
1737
|
+
params
|
|
1738
|
+
}), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
|
|
1739
|
+
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1367
1742
|
function resolveStaticSource(src, ctx) {
|
|
1368
1743
|
switch (src.type) {
|
|
1369
1744
|
case "literal":
|
|
@@ -1593,7 +1968,8 @@ function evalOpPredicate(p, row, ctx) {
|
|
|
1593
1968
|
}
|
|
1594
1969
|
async function invokeTask(args) {
|
|
1595
1970
|
const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1596
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
1971
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1972
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1597
1973
|
});
|
|
1598
1974
|
return commitInvoke(ctx, taskName, options?.actor);
|
|
1599
1975
|
}
|
|
@@ -1701,7 +2077,8 @@ function isTerminalStage(stage) {
|
|
|
1701
2077
|
}
|
|
1702
2078
|
async function setStage(args) {
|
|
1703
2079
|
const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1704
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2080
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2081
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1705
2082
|
});
|
|
1706
2083
|
return commitSetStage(ctx, targetStage, reason, options?.actor);
|
|
1707
2084
|
}
|
|
@@ -1952,287 +2329,58 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
|
|
|
1952
2329
|
let count = 0;
|
|
1953
2330
|
for (; ; ) {
|
|
1954
2331
|
if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
|
|
1955
|
-
client,
|
|
1956
|
-
instanceId,
|
|
1957
|
-
...actor !== void 0 ? { options: { actor } } : {},
|
|
1958
|
-
...clientForGdr ? { clientForGdr } : {},
|
|
1959
|
-
...clock ? { clock } : {},
|
|
1960
|
-
...overlay ? { overlay } : {}
|
|
1961
|
-
})).fired) return count;
|
|
1962
|
-
if (count++, count >= CASCADE_LIMIT)
|
|
1963
|
-
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
|
|
1967
|
-
const parentRef = child.ancestors.at(-1);
|
|
1968
|
-
if (parentRef === void 0) return;
|
|
1969
|
-
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
1970
|
-
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
1971
|
-
const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
|
|
1972
|
-
if (stage === void 0) return;
|
|
1973
|
-
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);
|
|
1974
|
-
if (task?.subworkflows === void 0) return;
|
|
1975
|
-
const entry = parentCurrentTasks.find((e) => e.name === task.name);
|
|
1976
|
-
if (entry === void 0 || entry.status !== "active") return;
|
|
1977
|
-
const ctx = await buildEngineContext({
|
|
1978
|
-
client,
|
|
1979
|
-
clientForGdr,
|
|
1980
|
-
instance: parent,
|
|
1981
|
-
definition,
|
|
1982
|
-
...clock ? { clock } : {}
|
|
1983
|
-
}), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
1984
|
-
if (outcome !== void 0)
|
|
1985
|
-
return { parent, definition, stage, task, outcome };
|
|
1986
|
-
}
|
|
1987
|
-
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
1988
|
-
const instance = await client.getDocument(instanceId);
|
|
1989
|
-
if (!instance) return;
|
|
1990
|
-
const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
|
|
1991
|
-
if (found === void 0) return;
|
|
1992
|
-
const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
|
|
1993
|
-
client,
|
|
1994
|
-
clientForGdr: resolvedClientForGdr,
|
|
1995
|
-
instance: parent,
|
|
1996
|
-
definition,
|
|
1997
|
-
...clock ? { clock } : {}
|
|
1998
|
-
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
|
|
1999
|
-
mutEntry !== void 0 && (applyTaskStatusChange({
|
|
2000
|
-
entry: mutEntry,
|
|
2001
|
-
history: mutation.history,
|
|
2002
|
-
stage: stage.name,
|
|
2003
|
-
to: outcome,
|
|
2004
|
-
at: ctx.now,
|
|
2005
|
-
actor: gateActor(outcome)
|
|
2006
|
-
}), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
|
|
2007
|
-
}
|
|
2008
|
-
function startMutation(instance) {
|
|
2009
|
-
return {
|
|
2010
|
-
currentStage: instance.currentStage,
|
|
2011
|
-
// Deep-ish copy. Per-scope state entry arrays need their own copies
|
|
2012
|
-
// so ops can mutate without leaking into the source.
|
|
2013
|
-
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
2014
|
-
stages: instance.stages.map((s) => ({
|
|
2015
|
-
...s,
|
|
2016
|
-
state: (s.state ?? []).map((entry) => ({ ...entry })),
|
|
2017
|
-
tasks: s.tasks.map((t) => ({
|
|
2018
|
-
...t,
|
|
2019
|
-
...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
|
|
2020
|
-
}))
|
|
2021
|
-
})),
|
|
2022
|
-
pendingEffects: [...instance.pendingEffects],
|
|
2023
|
-
effectHistory: [...instance.effectHistory],
|
|
2024
|
-
effectsContext: [...instance.effectsContext],
|
|
2025
|
-
history: [...instance.history],
|
|
2026
|
-
lastChangedAt: instance.lastChangedAt,
|
|
2027
|
-
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
2028
|
-
...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
|
|
2029
|
-
pendingCreates: []
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
function instanceStateFields(src) {
|
|
2033
|
-
const fields = {
|
|
2034
|
-
currentStage: src.currentStage,
|
|
2035
|
-
state: src.state,
|
|
2036
|
-
stages: src.stages,
|
|
2037
|
-
pendingEffects: src.pendingEffects,
|
|
2038
|
-
effectHistory: src.effectHistory,
|
|
2039
|
-
effectsContext: src.effectsContext,
|
|
2040
|
-
history: src.history
|
|
2041
|
-
};
|
|
2042
|
-
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
|
|
2043
|
-
}
|
|
2044
|
-
function materializeInstance(base, mutation) {
|
|
2045
|
-
return {
|
|
2046
|
-
...base,
|
|
2047
|
-
currentStage: mutation.currentStage,
|
|
2048
|
-
state: mutation.state,
|
|
2049
|
-
stages: mutation.stages
|
|
2050
|
-
};
|
|
2051
|
-
}
|
|
2052
|
-
async function persist(ctx, mutation) {
|
|
2053
|
-
const set = {
|
|
2054
|
-
...instanceStateFields(mutation),
|
|
2055
|
-
lastChangedAt: ctx.now
|
|
2056
|
-
}, pendingCreates = mutation.pendingCreates;
|
|
2057
|
-
if (pendingCreates.length === 0)
|
|
2058
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
2059
|
-
const tx = ctx.client.transaction();
|
|
2060
|
-
for (const body of pendingCreates) tx.create(body);
|
|
2061
|
-
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
2062
|
-
const actorForPriming = void 0;
|
|
2063
|
-
for (const body of pendingCreates)
|
|
2064
|
-
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);
|
|
2065
|
-
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
2066
|
-
if (!reloaded)
|
|
2067
|
-
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
2068
|
-
return reloaded;
|
|
2069
|
-
}
|
|
2070
|
-
function currentStageEntry(mutation) {
|
|
2071
|
-
const entry = findOpenStageEntry(mutation);
|
|
2072
|
-
if (entry === void 0)
|
|
2073
|
-
throw new Error(
|
|
2074
|
-
`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
|
|
2075
|
-
);
|
|
2076
|
-
return entry;
|
|
2077
|
-
}
|
|
2078
|
-
function currentTasks(mutation) {
|
|
2079
|
-
return currentStageEntry(mutation).tasks;
|
|
2080
|
-
}
|
|
2081
|
-
function findTaskInCurrentStage(ctx, taskName) {
|
|
2082
|
-
const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
|
|
2083
|
-
if (task === void 0)
|
|
2084
|
-
throw new Error(
|
|
2085
|
-
`Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
2086
|
-
);
|
|
2087
|
-
return { stage, task };
|
|
2088
|
-
}
|
|
2089
|
-
function requireMutationTaskEntry(mutation, task) {
|
|
2090
|
-
const mutEntry = currentTasks(mutation).find((t) => t.name === task);
|
|
2091
|
-
if (mutEntry === void 0)
|
|
2092
|
-
throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
|
|
2093
|
-
return mutEntry;
|
|
2094
|
-
}
|
|
2095
|
-
function findCurrentStageEntry(instance) {
|
|
2096
|
-
return findOpenStageEntry(instance);
|
|
2097
|
-
}
|
|
2098
|
-
function findCurrentTasks(instance) {
|
|
2099
|
-
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
2100
|
-
}
|
|
2101
|
-
async function completeEffect(args) {
|
|
2102
|
-
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2103
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2104
|
-
});
|
|
2105
|
-
return commitCompleteEffect(
|
|
2106
|
-
ctx,
|
|
2107
|
-
effectKey,
|
|
2108
|
-
status,
|
|
2109
|
-
outputs,
|
|
2110
|
-
detail,
|
|
2111
|
-
error,
|
|
2112
|
-
durationMs,
|
|
2113
|
-
options?.actor
|
|
2114
|
-
);
|
|
2115
|
-
}
|
|
2116
|
-
function buildEffectHistoryEntry(pending, outcome) {
|
|
2117
|
-
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
2118
|
-
return {
|
|
2119
|
-
_key: pending._key,
|
|
2120
|
-
name: pending.name,
|
|
2121
|
-
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
2122
|
-
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
2123
|
-
params: pending.params,
|
|
2124
|
-
origin: pending.origin,
|
|
2125
|
-
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
2126
|
-
ranAt,
|
|
2127
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
2128
|
-
status,
|
|
2129
|
-
...detail !== void 0 ? { detail } : {},
|
|
2130
|
-
...error !== void 0 ? { error } : {},
|
|
2131
|
-
...outputs !== void 0 ? { outputs } : {}
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
function upsertEffectsContext(mutation, effectName, outputs) {
|
|
2135
|
-
const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
|
|
2136
|
-
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
2137
|
-
}
|
|
2138
|
-
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
2139
|
-
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
2140
|
-
if (pending === void 0)
|
|
2141
|
-
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
2142
|
-
const mutation = startMutation(ctx.instance);
|
|
2143
|
-
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
2144
|
-
const ranAt = ctx.now;
|
|
2145
|
-
return mutation.effectHistory.push(
|
|
2146
|
-
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
2147
|
-
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
2148
|
-
_key: randomKey(),
|
|
2149
|
-
_type: "effectCompleted",
|
|
2150
|
-
at: ranAt,
|
|
2151
|
-
effectKey,
|
|
2152
|
-
effect: pending.name,
|
|
2153
|
-
status,
|
|
2154
|
-
...outputs !== void 0 ? { outputs } : {},
|
|
2155
|
-
...detail !== void 0 ? { detail } : {},
|
|
2156
|
-
...actor !== void 0 ? { actor } : {}
|
|
2157
|
-
}), await persist(ctx, mutation), { effectKey, status };
|
|
2158
|
-
}
|
|
2159
|
-
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
2160
|
-
const key = randomKey(), pending = {
|
|
2161
|
-
_key: key,
|
|
2162
|
-
_type: "pendingEffect",
|
|
2163
|
-
name: effect.name,
|
|
2164
|
-
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
2165
|
-
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
2166
|
-
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
2167
|
-
params,
|
|
2168
|
-
origin,
|
|
2169
|
-
...actor !== void 0 ? { actor } : {},
|
|
2170
|
-
queuedAt: now
|
|
2171
|
-
}, history = {
|
|
2172
|
-
_key: randomKey(),
|
|
2173
|
-
_type: "effectQueued",
|
|
2174
|
-
at: now,
|
|
2175
|
-
effectKey: key,
|
|
2176
|
-
effect: effect.name,
|
|
2177
|
-
origin
|
|
2178
|
-
};
|
|
2179
|
-
return { pending, history };
|
|
2180
|
-
}
|
|
2181
|
-
async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
|
|
2182
|
-
if (!effects || effects.length === 0) return;
|
|
2183
|
-
const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
|
|
2184
|
-
...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
|
|
2185
|
-
...actor !== void 0 ? { actor } : {},
|
|
2186
|
-
// Caller-supplied action params, readable in bindings as `$params.<name>`.
|
|
2187
|
-
vars: { params: opts?.callerParams ?? {} }
|
|
2188
|
-
});
|
|
2189
|
-
for (const effect of effects) {
|
|
2190
|
-
const resolved = await resolveBindings({
|
|
2191
|
-
bindings: effect.bindings,
|
|
2192
|
-
staticInput: effect.input,
|
|
2193
|
-
snapshot: ctx.snapshot,
|
|
2194
|
-
params
|
|
2195
|
-
}), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
|
|
2196
|
-
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
2332
|
+
client,
|
|
2333
|
+
instanceId,
|
|
2334
|
+
...actor !== void 0 ? { options: { actor } } : {},
|
|
2335
|
+
...clientForGdr ? { clientForGdr } : {},
|
|
2336
|
+
...clock ? { clock } : {},
|
|
2337
|
+
...overlay ? { overlay } : {}
|
|
2338
|
+
})).fired) return count;
|
|
2339
|
+
if (count++, count >= CASCADE_LIMIT)
|
|
2340
|
+
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
2197
2341
|
}
|
|
2198
2342
|
}
|
|
2199
|
-
async function
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2343
|
+
async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
|
|
2344
|
+
const parentRef = child.ancestors.at(-1);
|
|
2345
|
+
if (parentRef === void 0) return;
|
|
2346
|
+
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
2347
|
+
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
2348
|
+
const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
|
|
2349
|
+
if (stage === void 0) return;
|
|
2350
|
+
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);
|
|
2351
|
+
if (task?.subworkflows === void 0) return;
|
|
2352
|
+
const entry = parentCurrentTasks.find((e) => e.name === task.name);
|
|
2353
|
+
if (entry === void 0 || entry.status !== "active") return;
|
|
2354
|
+
const ctx = await buildEngineContext({
|
|
2355
|
+
client,
|
|
2356
|
+
clientForGdr,
|
|
2357
|
+
instance: parent,
|
|
2358
|
+
definition,
|
|
2359
|
+
...clock ? { clock } : {}
|
|
2360
|
+
}), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
2361
|
+
if (outcome !== void 0)
|
|
2362
|
+
return { parent, definition, stage, task, outcome };
|
|
2204
2363
|
}
|
|
2205
|
-
async function
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
)
|
|
2221
|
-
), mutation.pendingEffects = [], mutation.history.push({
|
|
2222
|
-
_key: randomKey(),
|
|
2223
|
-
_type: "aborted",
|
|
2224
|
-
at,
|
|
2364
|
+
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
2365
|
+
const instance = await client.getDocument(instanceId);
|
|
2366
|
+
if (!instance) return;
|
|
2367
|
+
const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
|
|
2368
|
+
if (found === void 0) return;
|
|
2369
|
+
const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
|
|
2370
|
+
client,
|
|
2371
|
+
clientForGdr: resolvedClientForGdr,
|
|
2372
|
+
instance: parent,
|
|
2373
|
+
definition,
|
|
2374
|
+
...clock ? { clock } : {}
|
|
2375
|
+
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
|
|
2376
|
+
mutEntry !== void 0 && (applyTaskStatusChange({
|
|
2377
|
+
entry: mutEntry,
|
|
2378
|
+
history: mutation.history,
|
|
2225
2379
|
stage: stage.name,
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
clientForGdr: ctx.clientForGdr,
|
|
2231
|
-
instance: ctx.instance,
|
|
2232
|
-
definition: ctx.definition,
|
|
2233
|
-
stageName: stage.name,
|
|
2234
|
-
now: ctx.now
|
|
2235
|
-
}), { fired: !0, stage: stage.name };
|
|
2380
|
+
to: outcome,
|
|
2381
|
+
at: ctx.now,
|
|
2382
|
+
actor: gateActor(outcome)
|
|
2383
|
+
}), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
|
|
2236
2384
|
}
|
|
2237
2385
|
const parsedFilters = /* @__PURE__ */ new Map();
|
|
2238
2386
|
async function matchesFilter(args) {
|
|
@@ -2328,93 +2476,6 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2328
2476
|
return;
|
|
2329
2477
|
}
|
|
2330
2478
|
}
|
|
2331
|
-
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
2332
|
-
function guardsForResource(client) {
|
|
2333
|
-
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
2334
|
-
}
|
|
2335
|
-
function instanceGuardQuery(instanceId) {
|
|
2336
|
-
return {
|
|
2337
|
-
query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
|
|
2338
|
-
params: { guardType: GUARD_DOC_TYPE, instanceId }
|
|
2339
|
-
};
|
|
2340
|
-
}
|
|
2341
|
-
function verdictGuardsForInstance(client, instanceId) {
|
|
2342
|
-
const { query, params } = instanceGuardQuery(instanceId);
|
|
2343
|
-
return client.fetch(query, params);
|
|
2344
|
-
}
|
|
2345
|
-
async function guardsForInstance(args) {
|
|
2346
|
-
const { client, clientForGdr, instance } = args, stateUris = [
|
|
2347
|
-
...collectEntryDocUris(instance.state),
|
|
2348
|
-
...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
|
|
2349
|
-
], clients = resourceClientMap(
|
|
2350
|
-
instance.workflowResource,
|
|
2351
|
-
client,
|
|
2352
|
-
clientForGdr,
|
|
2353
|
-
stateUris.map((uri) => tryParseGdr(uri))
|
|
2354
|
-
), { query, params } = instanceGuardQuery(instance._id);
|
|
2355
|
-
return queryGuardsAcross(clients.values(), query, params);
|
|
2356
|
-
}
|
|
2357
|
-
async function guardsForDefinition(args) {
|
|
2358
|
-
const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
|
|
2359
|
-
(def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
|
|
2360
|
-
), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs);
|
|
2361
|
-
return queryGuardsAcross(clients.values(), "*[_type == $t && sourceDefinition == $definition]", {
|
|
2362
|
-
t: GUARD_DOC_TYPE,
|
|
2363
|
-
definition
|
|
2364
|
-
});
|
|
2365
|
-
}
|
|
2366
|
-
function guardIdRefs(definition) {
|
|
2367
|
-
return definition.stages.flatMap(
|
|
2368
|
-
(stage) => (stage.guards ?? []).flatMap(
|
|
2369
|
-
(guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
|
|
2370
|
-
)
|
|
2371
|
-
);
|
|
2372
|
-
}
|
|
2373
|
-
function staticIdRefGdr(idRef, stage, definition) {
|
|
2374
|
-
const read = /^\$state\.([\w-]+)/.exec(idRef);
|
|
2375
|
-
if (read === null) return;
|
|
2376
|
-
const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
|
|
2377
|
-
return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
|
|
2378
|
-
}
|
|
2379
|
-
function entriesInScope(stage, definition) {
|
|
2380
|
-
return [
|
|
2381
|
-
...definition.state ?? [],
|
|
2382
|
-
...stage.state ?? [],
|
|
2383
|
-
...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
|
|
2384
|
-
];
|
|
2385
|
-
}
|
|
2386
|
-
function gdrFromValue(value) {
|
|
2387
|
-
if (typeof value == "string") return tryParseGdr(value);
|
|
2388
|
-
if (typeof value == "object" && value !== null && "id" in value) {
|
|
2389
|
-
const id = value.id;
|
|
2390
|
-
return typeof id == "string" ? tryParseGdr(id) : void 0;
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
function tryParseGdr(uri) {
|
|
2394
|
-
try {
|
|
2395
|
-
return parseGdr(uri);
|
|
2396
|
-
} catch {
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
|
|
2401
|
-
const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
|
|
2402
|
-
for (const parsed of gdrs) {
|
|
2403
|
-
if (parsed === void 0) continue;
|
|
2404
|
-
const key = resourceKey(resourceFromParsed(parsed));
|
|
2405
|
-
map.has(key) || map.set(key, clientForGdr(parsed));
|
|
2406
|
-
}
|
|
2407
|
-
return map;
|
|
2408
|
-
}
|
|
2409
|
-
async function queryGuardsAcross(clients, query, params) {
|
|
2410
|
-
const perResource = await Promise.all(
|
|
2411
|
-
[...clients].map((c) => c.fetch(query, params))
|
|
2412
|
-
);
|
|
2413
|
-
return dedupById(perResource.flat());
|
|
2414
|
-
}
|
|
2415
|
-
function dedupById(guards) {
|
|
2416
|
-
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
2417
|
-
}
|
|
2418
2479
|
async function evaluateInstance(args) {
|
|
2419
2480
|
const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2420
2481
|
validateTags(tags);
|
|
@@ -2685,11 +2746,51 @@ async function loadDefinitionVersions(client, definition, tags) {
|
|
|
2685
2746
|
{ definition, engineTags: tags }
|
|
2686
2747
|
);
|
|
2687
2748
|
}
|
|
2749
|
+
async function abortInstance(args) {
|
|
2750
|
+
const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2751
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2752
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2753
|
+
});
|
|
2754
|
+
return commitAbort(ctx, reason, options?.actor);
|
|
2755
|
+
}
|
|
2756
|
+
async function commitAbort(ctx, reason, actor) {
|
|
2757
|
+
if (ctx.instance.completedAt !== void 0)
|
|
2758
|
+
return { fired: !1 };
|
|
2759
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
|
|
2760
|
+
return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
|
|
2761
|
+
...mutation.pendingEffects.map(
|
|
2762
|
+
(pending) => buildEffectHistoryEntry(pending, {
|
|
2763
|
+
status: "failed",
|
|
2764
|
+
ranAt: at,
|
|
2765
|
+
actor,
|
|
2766
|
+
detail: "instance aborted before dispatch",
|
|
2767
|
+
error: void 0,
|
|
2768
|
+
durationMs: void 0,
|
|
2769
|
+
outputs: void 0
|
|
2770
|
+
})
|
|
2771
|
+
)
|
|
2772
|
+
), mutation.pendingEffects = [], mutation.history.push({
|
|
2773
|
+
_key: randomKey(),
|
|
2774
|
+
_type: "aborted",
|
|
2775
|
+
at,
|
|
2776
|
+
stage: stage.name,
|
|
2777
|
+
...reason !== void 0 ? { reason } : {},
|
|
2778
|
+
...actor !== void 0 ? { actor } : {}
|
|
2779
|
+
}), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
|
|
2780
|
+
client: ctx.client,
|
|
2781
|
+
clientForGdr: ctx.clientForGdr,
|
|
2782
|
+
instance: ctx.instance,
|
|
2783
|
+
definition: ctx.definition,
|
|
2784
|
+
stageName: stage.name,
|
|
2785
|
+
now: ctx.now
|
|
2786
|
+
}), { fired: !0, stage: stage.name };
|
|
2787
|
+
}
|
|
2688
2788
|
async function fireAction(args) {
|
|
2689
2789
|
const { client, instanceId, task, action, params, options } = args;
|
|
2690
2790
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2691
2791
|
const ctx = await loadContext(client, instanceId, {
|
|
2692
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
2792
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
2793
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
2693
2794
|
});
|
|
2694
2795
|
try {
|
|
2695
2796
|
return await commitAction(ctx, task, action, params, options);
|
|
@@ -2815,6 +2916,15 @@ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay)
|
|
|
2815
2916
|
);
|
|
2816
2917
|
return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
|
|
2817
2918
|
}
|
|
2919
|
+
async function abortAndPropagate(args) {
|
|
2920
|
+
const { client, instanceId, reason, actor, clientForGdr, clock } = args, result = await abortInstance({
|
|
2921
|
+
client,
|
|
2922
|
+
instanceId,
|
|
2923
|
+
...reason !== void 0 ? { reason } : {},
|
|
2924
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
2925
|
+
});
|
|
2926
|
+
return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), result.fired;
|
|
2927
|
+
}
|
|
2818
2928
|
function buildClientForGdr(defaultClient, resolver) {
|
|
2819
2929
|
return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
|
|
2820
2930
|
}
|
|
@@ -2837,10 +2947,11 @@ function assertActionAllowed(evaluation, task, action) {
|
|
|
2837
2947
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
2838
2948
|
}
|
|
2839
2949
|
async function applyAction(args) {
|
|
2840
|
-
const { client, instance, task, action, params, actor, grants, clock } = args, options = {
|
|
2950
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
|
|
2841
2951
|
actor,
|
|
2842
2952
|
...grants !== void 0 ? { grants } : {},
|
|
2843
|
-
...clock !== void 0 ? { clock } : {}
|
|
2953
|
+
...clock !== void 0 ? { clock } : {},
|
|
2954
|
+
...clientForGdr !== void 0 ? { clientForGdr } : {}
|
|
2844
2955
|
};
|
|
2845
2956
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
2846
2957
|
client,
|
|
@@ -2851,6 +2962,78 @@ async function applyAction(args) {
|
|
|
2851
2962
|
options
|
|
2852
2963
|
})).ranOps;
|
|
2853
2964
|
}
|
|
2965
|
+
async function deleteDefinition(args) {
|
|
2966
|
+
const { client, tags, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tags);
|
|
2967
|
+
if (versions.length === 0)
|
|
2968
|
+
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2969
|
+
const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
|
|
2970
|
+
if (targets.length === 0)
|
|
2971
|
+
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
2972
|
+
const lastVersionGoes = targets.length === versions.length;
|
|
2973
|
+
await assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes);
|
|
2974
|
+
const instanceIds = await nonTerminalInstanceIds(client, tags, definition, version);
|
|
2975
|
+
if (instanceIds.length > 0 && cascade2 !== !0)
|
|
2976
|
+
throw new Error(
|
|
2977
|
+
`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.`
|
|
2978
|
+
);
|
|
2979
|
+
const abortedInstanceIds = await abortInstances({
|
|
2980
|
+
client,
|
|
2981
|
+
instanceIds,
|
|
2982
|
+
reason,
|
|
2983
|
+
actor,
|
|
2984
|
+
clientForGdr,
|
|
2985
|
+
clock
|
|
2986
|
+
}), tx = client.transaction();
|
|
2987
|
+
for (const target of targets) tx.delete(target._id);
|
|
2988
|
+
await tx.commit();
|
|
2989
|
+
const deletedGuardCount = lastVersionGoes ? await deleteOrphanedDefinitionGuards({
|
|
2990
|
+
client,
|
|
2991
|
+
clientForGdr,
|
|
2992
|
+
workflowResource: args.workflowResource,
|
|
2993
|
+
definition,
|
|
2994
|
+
definitions: versions,
|
|
2995
|
+
tags
|
|
2996
|
+
}) : 0;
|
|
2997
|
+
return {
|
|
2998
|
+
name: definition,
|
|
2999
|
+
deletedVersions: targets.map((d) => d.version),
|
|
3000
|
+
abortedInstanceIds,
|
|
3001
|
+
deletedGuardCount
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
async function assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes) {
|
|
3005
|
+
const deployed = await client.fetch(
|
|
3006
|
+
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
|
|
3007
|
+
{ engineTags: tags }
|
|
3008
|
+
), 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(
|
|
3009
|
+
(d) => refsOf(d).some(
|
|
3010
|
+
(ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
|
|
3011
|
+
)
|
|
3012
|
+
);
|
|
3013
|
+
if (referrers.length > 0) {
|
|
3014
|
+
const names = referrers.map((d) => `${d.name} v${d.version}`).join(", ");
|
|
3015
|
+
throw new Error(
|
|
3016
|
+
`Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function nonTerminalInstanceIds(client, tags, definition, version) {
|
|
3021
|
+
const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
|
|
3022
|
+
return (await client.fetch(
|
|
3023
|
+
`*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
|
|
3024
|
+
{ definition, engineTags: tags, ...version !== void 0 ? { version } : {} }
|
|
3025
|
+
)).map((doc) => doc._id);
|
|
3026
|
+
}
|
|
3027
|
+
async function abortInstances(args) {
|
|
3028
|
+
const { client, instanceIds, reason, actor, clientForGdr, clock } = args, aborted = [];
|
|
3029
|
+
for (const instanceId of instanceIds)
|
|
3030
|
+
await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock }) && aborted.push(instanceId);
|
|
3031
|
+
return aborted;
|
|
3032
|
+
}
|
|
3033
|
+
function previewIds(ids) {
|
|
3034
|
+
const head = ids.slice(0, 3).join(", ");
|
|
3035
|
+
return ids.length > 3 ? `${head}, \u2026` : head;
|
|
3036
|
+
}
|
|
2854
3037
|
const workflow = {
|
|
2855
3038
|
/**
|
|
2856
3039
|
* Deploy a set of workflow definitions as one call. Each lands as a
|
|
@@ -2901,6 +3084,14 @@ const workflow = {
|
|
|
2901
3084
|
}
|
|
2902
3085
|
return hasWrites && await tx.commit(), { results };
|
|
2903
3086
|
},
|
|
3087
|
+
/**
|
|
3088
|
+
* Remove a deployed workflow definition (all versions, or one via
|
|
3089
|
+
* `version`). Refuses while non-terminal instances exist unless
|
|
3090
|
+
* `cascade` aborts them first — instances are never deleted, only
|
|
3091
|
+
* aborted in place; see {@link deleteDefinitionInternal} for the
|
|
3092
|
+
* full contract (spawn-referrer check, guard-doc housekeeping).
|
|
3093
|
+
*/
|
|
3094
|
+
deleteDefinition: async (args) => deleteDefinition(args),
|
|
2904
3095
|
/**
|
|
2905
3096
|
* Spawn a new workflow instance from a deployed definition.
|
|
2906
3097
|
*
|
|
@@ -2995,6 +3186,7 @@ const workflow = {
|
|
|
2995
3186
|
actor,
|
|
2996
3187
|
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
2997
3188
|
clock,
|
|
3189
|
+
clientForGdr,
|
|
2998
3190
|
...params !== void 0 ? { params } : {}
|
|
2999
3191
|
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3000
3192
|
return {
|
|
@@ -3022,7 +3214,7 @@ const workflow = {
|
|
|
3022
3214
|
...detail !== void 0 ? { detail } : {},
|
|
3023
3215
|
...error !== void 0 ? { error } : {},
|
|
3024
3216
|
...durationMs !== void 0 ? { durationMs } : {},
|
|
3025
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3217
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3026
3218
|
});
|
|
3027
3219
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3028
3220
|
return {
|
|
@@ -3063,7 +3255,7 @@ const workflow = {
|
|
|
3063
3255
|
instanceId,
|
|
3064
3256
|
targetStage,
|
|
3065
3257
|
...reason !== void 0 ? { reason } : {},
|
|
3066
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3258
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3067
3259
|
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
3068
3260
|
return {
|
|
3069
3261
|
instance: await reload(client, instanceId, tags),
|
|
@@ -3073,24 +3265,18 @@ const workflow = {
|
|
|
3073
3265
|
},
|
|
3074
3266
|
/**
|
|
3075
3267
|
* Admin override — hard-stop an in-flight instance where it stands.
|
|
3076
|
-
* No stage move, no transition effects, pending effects cancelled;
|
|
3077
|
-
*
|
|
3078
|
-
*
|
|
3079
|
-
* so a parent gate waiting on this child re-evaluates.
|
|
3268
|
+
* No stage move, no transition effects, pending effects cancelled;
|
|
3269
|
+
* see {@link abortAndPropagate} for the abort + ancestor-propagation
|
|
3270
|
+
* contract (propagated, not cascaded — the instance is terminal).
|
|
3080
3271
|
*/
|
|
3081
3272
|
abortInstance: async (args) => {
|
|
3082
3273
|
const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3083
3274
|
await reload(client, instanceId, tags);
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
instanceId,
|
|
3087
|
-
...reason !== void 0 ? { reason } : {},
|
|
3088
|
-
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3089
|
-
});
|
|
3090
|
-
return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), {
|
|
3275
|
+
const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
|
|
3276
|
+
return {
|
|
3091
3277
|
instance: await reload(client, instanceId, tags),
|
|
3092
3278
|
cascaded: 0,
|
|
3093
|
-
fired
|
|
3279
|
+
fired
|
|
3094
3280
|
};
|
|
3095
3281
|
},
|
|
3096
3282
|
/**
|
|
@@ -3189,6 +3375,30 @@ const workflow = {
|
|
|
3189
3375
|
* `fireAction` to gate writes via the same logic.
|
|
3190
3376
|
*/
|
|
3191
3377
|
evaluate: async (args) => evaluateInstance(args),
|
|
3378
|
+
/**
|
|
3379
|
+
* Diagnose why an instance is or isn't progressing. Projects the
|
|
3380
|
+
* instance (the same read as `evaluate`) and classifies it — terminal,
|
|
3381
|
+
* `progressing`, `waiting` (an action is available — healthy), or `stuck`
|
|
3382
|
+
* with a structured cause — returning that verdict as a
|
|
3383
|
+
* {@link DiagnoseResult} alongside the evaluation it came from, so a
|
|
3384
|
+
* consumer can render the supporting evidence without a second projection.
|
|
3385
|
+
* Pure read.
|
|
3386
|
+
*/
|
|
3387
|
+
diagnose: async (args) => {
|
|
3388
|
+
const evaluation = await evaluateInstance(args);
|
|
3389
|
+
return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
|
|
3390
|
+
},
|
|
3391
|
+
/**
|
|
3392
|
+
* List the actions an actor could fire on an instance's current stage,
|
|
3393
|
+
* each flagged `allowed` (with a structured `disabledReason` when not).
|
|
3394
|
+
* Projects the instance from the actor's perspective and flattens its
|
|
3395
|
+
* tasks' actions. Returns the evaluation alongside the actions so a
|
|
3396
|
+
* consumer can read the instance/stage context. Pure read.
|
|
3397
|
+
*/
|
|
3398
|
+
availableActions: async (args) => {
|
|
3399
|
+
const evaluation = await evaluateInstance(args);
|
|
3400
|
+
return { evaluation, actions: availableActions(evaluation.currentStage.tasks) };
|
|
3401
|
+
},
|
|
3192
3402
|
/**
|
|
3193
3403
|
* Materialised spawned children of a parent instance.
|
|
3194
3404
|
*
|
|
@@ -3449,6 +3659,7 @@ function createInstanceSession(args) {
|
|
|
3449
3659
|
task,
|
|
3450
3660
|
action,
|
|
3451
3661
|
actor,
|
|
3662
|
+
clientForGdr,
|
|
3452
3663
|
...grants !== void 0 ? { grants } : {},
|
|
3453
3664
|
...clock !== void 0 ? { clock } : {},
|
|
3454
3665
|
...params !== void 0 ? { params } : {}
|
|
@@ -3504,8 +3715,11 @@ function createEngine(args) {
|
|
|
3504
3715
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3505
3716
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3506
3717
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
3718
|
+
diagnose: (rest) => workflow.diagnose(bind(rest)),
|
|
3719
|
+
availableActions: (rest) => workflow.availableActions(bind(rest)),
|
|
3507
3720
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
3508
3721
|
abortInstance: (rest) => workflow.abortInstance(bind(rest)),
|
|
3722
|
+
deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
|
|
3509
3723
|
getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
|
|
3510
3724
|
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3511
3725
|
instance: (instanceDoc, opts) => createInstanceSession({
|
|
@@ -3583,6 +3797,36 @@ function createEngine(args) {
|
|
|
3583
3797
|
})
|
|
3584
3798
|
};
|
|
3585
3799
|
}
|
|
3800
|
+
function docIdFor(def, target) {
|
|
3801
|
+
return definitionDocId(target.workflowResource, canonicalTag(target.tags), def.name, def.version);
|
|
3802
|
+
}
|
|
3803
|
+
function diffStatus(existing, expected) {
|
|
3804
|
+
return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
|
|
3805
|
+
}
|
|
3806
|
+
function diffEntry(def, existingRaw, target) {
|
|
3807
|
+
const docId = docIdFor(def, target), expected = {
|
|
3808
|
+
...def,
|
|
3809
|
+
_id: docId,
|
|
3810
|
+
_type: WORKFLOW_DEFINITION_TYPE,
|
|
3811
|
+
tags: target.tags
|
|
3812
|
+
}, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
|
|
3813
|
+
return {
|
|
3814
|
+
name: def.name,
|
|
3815
|
+
version: def.version,
|
|
3816
|
+
status,
|
|
3817
|
+
docId,
|
|
3818
|
+
expected,
|
|
3819
|
+
...existing !== void 0 ? { existing } : {}
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
async function computeDiffEntries(client, defs, target) {
|
|
3823
|
+
const entries = [];
|
|
3824
|
+
for (const def of defs) {
|
|
3825
|
+
const existingRaw = await client.getDocument(docIdFor(def, target)) ?? void 0;
|
|
3826
|
+
entries.push(diffEntry(def, existingRaw, target));
|
|
3827
|
+
}
|
|
3828
|
+
return entries;
|
|
3829
|
+
}
|
|
3586
3830
|
const HISTORY_DISPLAY = {
|
|
3587
3831
|
stageEntered: {
|
|
3588
3832
|
title: "Stage entered",
|
|
@@ -3776,15 +4020,23 @@ export {
|
|
|
3776
4020
|
WORKFLOW_DEFINITION_TYPE,
|
|
3777
4021
|
WORKFLOW_INSTANCE_TYPE,
|
|
3778
4022
|
WorkflowStateDivergedError,
|
|
4023
|
+
abortReason,
|
|
4024
|
+
actionVerdict,
|
|
4025
|
+
assigneesOf,
|
|
4026
|
+
availableActions,
|
|
3779
4027
|
buildSnapshot,
|
|
3780
4028
|
canonicalTag,
|
|
3781
4029
|
compileGuard,
|
|
4030
|
+
computeDiffEntries,
|
|
3782
4031
|
contentReleaseName,
|
|
3783
4032
|
createEngine,
|
|
3784
4033
|
datasetResourceParts,
|
|
3785
4034
|
defaultLoggerFactory,
|
|
3786
4035
|
denyingGuards,
|
|
3787
4036
|
deployStageGuards,
|
|
4037
|
+
diagnoseInputFromEvaluation,
|
|
4038
|
+
diagnoseInstance,
|
|
4039
|
+
diffEntry,
|
|
3788
4040
|
displayDescription,
|
|
3789
4041
|
displayTitle,
|
|
3790
4042
|
effectsContextMap,
|
|
@@ -3803,6 +4055,7 @@ export {
|
|
|
3803
4055
|
isGdr,
|
|
3804
4056
|
isTerminalStage,
|
|
3805
4057
|
lakeGuardId,
|
|
4058
|
+
openStage,
|
|
3806
4059
|
parseGdr,
|
|
3807
4060
|
readsRaw,
|
|
3808
4061
|
refCanvas,
|