@sanity/workflow-engine 0.32.0 → 0.33.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 CHANGED
@@ -81,6 +81,10 @@ function effectiveEditable(baseline, override) {
81
81
 
82
82
  const DEFAULT_EDIT_MODE = "set";
83
83
 
84
+ function _fieldTargetLabel(target) {
85
+ return [ target.scope, target.activity, target.field ].filter(Boolean).join(".");
86
+ }
87
+
84
88
  function fieldSites(definition, stage) {
85
89
  const sites = [];
86
90
  for (const entry of definition.fields ?? []) sites.push({
@@ -1057,6 +1061,7 @@ function describeSiteHeading(address, ctx) {
1057
1061
  }
1058
1062
 
1059
1063
  case "action":
1064
+ case "action-when":
1060
1065
  {
1061
1066
  const title = actionTitle(address, ctx), activity = activityTitle(address.activity, ctx);
1062
1067
  return groqConditionDescribe.phrase(id, {
@@ -1064,7 +1069,7 @@ function describeSiteHeading(address, ctx) {
1064
1069
  title: title,
1065
1070
  activity: activity
1066
1071
  },
1067
- text: `action ${groqConditionDescribe.quoted(title)} on ${groqConditionDescribe.quoted(activity)} is available when:`
1072
+ text: `action ${groqConditionDescribe.quoted(title)} on ${groqConditionDescribe.quoted(activity)} ${address.kind === "action" ? "is available" : "fires automatically"} when:`
1068
1073
  });
1069
1074
  }
1070
1075
 
@@ -1091,18 +1096,6 @@ function describeSiteHeading(address, ctx) {
1091
1096
  });
1092
1097
  }
1093
1098
 
1094
- case "action-when":
1095
- {
1096
- const title = actionTitle(address, ctx), activity = activityTitle(address.activity, ctx);
1097
- return groqConditionDescribe.phrase(id, {
1098
- params: {
1099
- title: title,
1100
- activity: activity
1101
- },
1102
- text: `action ${groqConditionDescribe.quoted(title)} on ${groqConditionDescribe.quoted(activity)} fires automatically when:`
1103
- });
1104
- }
1105
-
1106
1099
  case "editable-field":
1107
1100
  {
1108
1101
  const field = fieldTitleOf({
@@ -1146,6 +1139,7 @@ function describeSite(site, ctx) {
1146
1139
  }
1147
1140
 
1148
1141
  case "action":
1142
+ case "action-when":
1149
1143
  {
1150
1144
  const title = actionTitle(site, ctx), activity = activityTitle(site.activity, ctx);
1151
1145
  return groqConditionDescribe.phrase(id, {
@@ -1153,7 +1147,7 @@ function describeSite(site, ctx) {
1153
1147
  title: title,
1154
1148
  activity: activity
1155
1149
  },
1156
- text: `the ${groqConditionDescribe.quoted(title)} action on ${groqConditionDescribe.quoted(activity)}`
1150
+ text: `the ${groqConditionDescribe.quoted(title)} ${site.kind === "action" ? "action" : "trigger"} on ${groqConditionDescribe.quoted(activity)}`
1157
1151
  });
1158
1152
  }
1159
1153
 
@@ -1169,18 +1163,6 @@ function describeSite(site, ctx) {
1169
1163
  });
1170
1164
  }
1171
1165
 
1172
- case "action-when":
1173
- {
1174
- const title = actionTitle(site, ctx), activity = activityTitle(site.activity, ctx);
1175
- return groqConditionDescribe.phrase(id, {
1176
- params: {
1177
- title: title,
1178
- activity: activity
1179
- },
1180
- text: `the ${groqConditionDescribe.quoted(title)} trigger on ${groqConditionDescribe.quoted(activity)}`
1181
- });
1182
- }
1183
-
1184
1166
  case "activity-filter":
1185
1167
  {
1186
1168
  const activity = activityTitle(site.activity, ctx);
@@ -1238,10 +1220,10 @@ function unsatisfiedTransitionSummaries(evaluation) {
1238
1220
  definition: evaluation.definition
1239
1221
  };
1240
1222
  return evaluation.currentStage.transitions.filter(t => !t.whenSatisfied).flatMap(t => {
1241
- const summary = describeCondition(t.insight, ctx).summary;
1242
- return summary !== void 0 ? [ {
1223
+ const summary2 = describeCondition(t.insight, ctx).summary;
1224
+ return summary2 !== void 0 ? [ {
1243
1225
  transition: t.transition.name,
1244
- summary: summary
1226
+ summary: summary2
1245
1227
  } ] : [];
1246
1228
  });
1247
1229
  }
@@ -2872,8 +2854,8 @@ class CascadeLimitError extends invariants.WorkflowError {
2872
2854
 
2873
2855
  const FIELD_OP_TYPES = [ "field.set", "field.setIfMissing", "field.unset", "field.append", "field.inc", "field.dec", "field.updateWhere", "field.removeWhere" ];
2874
2856
 
2875
- function isFieldOp(summary) {
2876
- return FIELD_OP_TYPES.includes(summary.opType);
2857
+ function isFieldOp(summary2) {
2858
+ return FIELD_OP_TYPES.includes(summary2.opType);
2877
2859
  }
2878
2860
 
2879
2861
  function validateActionParams({action: action, activityName: activityName, callerParams: callerParams}) {
@@ -2952,7 +2934,7 @@ async function runOps(args) {
2952
2934
  if (ops === void 0 || ops.length === 0) return [];
2953
2935
  const opsFromDefinition = origin.effect === void 0 && origin.edit !== !0, summaries = [];
2954
2936
  for (const op of ops) {
2955
- const summary = await applyOp(op, {
2937
+ const summary2 = await applyOp(op, {
2956
2938
  mutation: mutation,
2957
2939
  stage: stage,
2958
2940
  activityName: origin.activity,
@@ -2966,9 +2948,9 @@ async function runOps(args) {
2966
2948
  memberRoles: args.memberRoles,
2967
2949
  roleAliases: args.roleAliases
2968
2950
  });
2969
- summaries.push(summary), shouldRecordOpApplied(summary) && mutation.history.push(opAppliedEntry({
2951
+ summaries.push(summary2), shouldRecordOpApplied(summary2) && mutation.history.push(opAppliedEntry({
2970
2952
  origin: origin,
2971
- summary: summary,
2953
+ summary: summary2,
2972
2954
  stage: stage,
2973
2955
  actor: actor,
2974
2956
  now: now
@@ -2977,12 +2959,12 @@ async function runOps(args) {
2977
2959
  return summaries;
2978
2960
  }
2979
2961
 
2980
- function shouldRecordOpApplied(summary) {
2981
- return summary.opType !== "field.setIfMissing" || summary.resolved !== void 0;
2962
+ function shouldRecordOpApplied(summary2) {
2963
+ return summary2.opType !== "field.setIfMissing" || summary2.resolved !== void 0;
2982
2964
  }
2983
2965
 
2984
2966
  function opAppliedEntry(args) {
2985
- const {origin: origin, summary: summary, stage: stage, actor: actor, now: now} = args;
2967
+ const {origin: origin, summary: summary2, stage: stage, actor: actor, now: now} = args;
2986
2968
  return {
2987
2969
  _key: randomKey(),
2988
2970
  _type: "opApplied",
@@ -3000,12 +2982,12 @@ function opAppliedEntry(args) {
3000
2982
  ...origin.effect !== void 0 ? {
3001
2983
  effect: origin.effect
3002
2984
  } : {},
3003
- opType: summary.opType,
3004
- ...summary.target !== void 0 ? {
3005
- target: summary.target
2985
+ opType: summary2.opType,
2986
+ ...summary2.target !== void 0 ? {
2987
+ target: summary2.target
3006
2988
  } : {},
3007
- ...summary.resolved !== void 0 ? {
3008
- resolved: summary.resolved
2989
+ ...summary2.resolved !== void 0 ? {
2990
+ resolved: summary2.resolved
3009
2991
  } : {},
3010
2992
  ...actor !== void 0 ? {
3011
2993
  actor: actor
@@ -3782,7 +3764,13 @@ function siblingConfig(target) {
3782
3764
  };
3783
3765
  }
3784
3766
 
3785
- const wallClock = () => /* @__PURE__ */ (new Date).toISOString();
3767
+ const wallClock = () => /* @__PURE__ */ (new Date).toISOString(), tick = ms => new Promise(resolve => {
3768
+ setTimeout(resolve, ms);
3769
+ }), wallSleeper = async ms => {
3770
+ let left = ms;
3771
+ for (;left > 2147483647; ) await tick(2147483647), left -= 2147483647;
3772
+ await tick(left);
3773
+ };
3786
3774
 
3787
3775
  function addMs(instant, ms) {
3788
3776
  return new Date(Date.parse(instant) + ms).toISOString();
@@ -3792,7 +3780,7 @@ function hasPassed(instant, now) {
3792
3780
  return Date.parse(instant) <= Date.parse(now);
3793
3781
  }
3794
3782
 
3795
- const DATA_MODEL_VERSION = 9, DATA_MODEL_MIN_READER = 4, DATA_MODEL_MAX_READER = 9, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/workflows/prerelease";
3783
+ const DATA_MODEL_VERSION = 10, DATA_MODEL_MIN_READER = 4, DATA_MODEL_MAX_READER = 10, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/workflows/prerelease";
3796
3784
 
3797
3785
  class ReaderModelAcknowledgementError extends invariants.WorkflowError {
3798
3786
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -3948,6 +3936,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
3948
3936
  compatibility: "reader-floor",
3949
3937
  applicability: "detectable",
3950
3938
  summary: "Singular assignee fields use member lists with at most one user and any number of roles."
3939
+ }), Object.freeze({
3940
+ id: "required-content-references",
3941
+ introducedInModel: 10,
3942
+ minReaderModel: 10,
3943
+ documentTypes: Object.freeze([ "definition", "instance" ]),
3944
+ compatibility: "reader-floor",
3945
+ applicability: "detectable",
3946
+ summary: "Required content references must remain readable after initialization before actions or transitions advance."
3947
+ }), Object.freeze({
3948
+ id: "effect-retry-policy",
3949
+ introducedInModel: 10,
3950
+ minReaderModel: 10,
3951
+ documentTypes: Object.freeze([ "definition", "instance" ]),
3952
+ compatibility: "reader-floor",
3953
+ applicability: "detectable",
3954
+ summary: "An effect node declares a bounded retry policy the engine enforces on every runtime."
3951
3955
  }) ]);
3952
3956
 
3953
3957
  function recordOf(value) {
@@ -4079,6 +4083,10 @@ function hasSingularAssigneeLists(document) {
4079
4083
  return typeof snapshot?.modelVersion == "number" && isAssignmentListModel(snapshot.modelVersion) && persistedFieldEntries(document).some(entry => entryHasKind(entry, "assignee"));
4080
4084
  }
4081
4085
 
4086
+ function hasEffectRetryPolicy(document) {
4087
+ return persistedDefinitionTree(document)?.actions.some(action => recordsAt(action, "effects").some(effect => recordOf(effect.retry) !== void 0)) ?? !1;
4088
+ }
4089
+
4082
4090
  function isMutationGuardAction(action) {
4083
4091
  return typeof action == "string" && invariants.MUTATION_GUARD_ACTIONS.some(candidate => candidate === action);
4084
4092
  }
@@ -4090,6 +4098,10 @@ function hasSplitGuardIdSpaces(document) {
4090
4098
  })) ?? !1;
4091
4099
  }
4092
4100
 
4101
+ function hasRequiredContentReferences(document) {
4102
+ return persistedDefinitionTree(document)?.roots.some(root => recordsAt(root, "fields").some(field => field.required === !0 && typeof field.type == "string" && invariants.refKindAcceptsTypes(field.type))) ?? !1;
4103
+ }
4104
+
4093
4105
  const featureDetectors = {
4094
4106
  "governed-model-stamps": () => !0,
4095
4107
  "subject-field-kind": document => hasFieldKind(document, "subject"),
@@ -4104,6 +4116,8 @@ const featureDetectors = {
4104
4116
  "role-constrained-assignment-fields": hasRoleConstrainedAssignments,
4105
4117
  "singular-assignee-lists": hasSingularAssigneeLists,
4106
4118
  "split-guard-id-spaces": hasSplitGuardIdSpaces,
4119
+ "required-content-references": hasRequiredContentReferences,
4120
+ "effect-retry-policy": hasEffectRetryPolicy,
4107
4121
  "node-semantics": hasNodeSemantics,
4108
4122
  "field-patch-ops": document => hasPersistedOpType(document, [ "field.inc", "field.dec", "field.setIfMissing" ]),
4109
4123
  "attributes-condition-var": () => !0
@@ -4340,9 +4354,13 @@ function abortReason(instance) {
4340
4354
  }
4341
4355
 
4342
4356
  function diagnoseInputFromEvaluation(evaluation) {
4357
+ if (evaluation.missingDocuments?.length && evaluation.blockingMissingDocuments === void 0) throw new Error("Missing blocking document evidence in workflow evaluation");
4343
4358
  const stage = openStage(evaluation.instance);
4344
4359
  return {
4345
4360
  instance: evaluation.instance,
4361
+ ...evaluation.missingDocuments !== void 0 ? {
4362
+ missingDocuments: evaluation.blockingMissingDocuments ?? []
4363
+ } : {},
4346
4364
  activities: evaluation.currentStage.activities,
4347
4365
  transitions: evaluation.currentStage.transitions,
4348
4366
  assignees: Object.fromEntries(evaluation.currentStage.activities.map(t => [ t.activity.name, assigneesOf(stage, t.activity.name) ]))
@@ -4461,7 +4479,7 @@ function diagnoseInstance(input) {
4461
4479
  at: instance.completedAt,
4462
4480
  ...liveChildrenField(instance)
4463
4481
  };
4464
- const cause = stuckFromDocumentState(input) ?? noTransitionFiresCause(input);
4482
+ const cause = stuckFromDocumentState(input) ?? missingDocumentCause(input.missingDocuments) ?? noTransitionFiresCause(input);
4465
4483
  return cause !== void 0 ? {
4466
4484
  state: "stuck",
4467
4485
  cause: cause
@@ -4470,10 +4488,27 @@ function diagnoseInstance(input) {
4470
4488
  };
4471
4489
  }
4472
4490
 
4491
+ function missingDocumentCause(documents = []) {
4492
+ return documents.length > 0 ? {
4493
+ kind: "document-missing",
4494
+ documents: documents
4495
+ } : void 0;
4496
+ }
4497
+
4498
+ function _additionalMissingDocuments(diagnosis, documents) {
4499
+ return diagnosis.state === "stuck" && diagnosis.cause.kind !== "document-missing" ? documents ?? [] : [];
4500
+ }
4501
+
4473
4502
  const RUNNABLE_VERBS = /* @__PURE__ */ new Set([ "set-stage", "abort", "drain-effects", "reset-activity" ]);
4474
4503
 
4475
4504
  function remediationsForCause(cause) {
4476
4505
  switch (cause.kind) {
4506
+ case "document-missing":
4507
+ return [ {
4508
+ verb: "abort",
4509
+ rationale: "Required content cannot be read. Investigate the referenced documents, or abort the instance if it is no longer needed."
4510
+ } ];
4511
+
4477
4512
  case "failed-effect":
4478
4513
  return [ {
4479
4514
  verb: "retry-effect",
@@ -4568,12 +4603,12 @@ function joinCacheFor(client) {
4568
4603
  }
4569
4604
 
4570
4605
  function isClientProjectMember(value) {
4571
- return invariants.isRecord(value) && typeof value.id == "string";
4606
+ return invariants.isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean");
4572
4607
  }
4573
4608
 
4574
- async function requestProjectMembers(request, projectId) {
4609
+ async function _requestProjectMembers(request, projectId) {
4575
4610
  const project = await request({
4576
- uri: `/projects/${encodeURIComponent(projectId)}`
4611
+ url: `/projects/${encodeURIComponent(projectId)}`
4577
4612
  }), members = invariants.isRecord(project) ? project.members : void 0;
4578
4613
  if (!Array.isArray(members) || !members.every(isClientProjectMember)) throw new Error(`Project member directory response is invalid for project "${projectId}"`);
4579
4614
  return members;
@@ -4581,7 +4616,7 @@ async function requestProjectMembers(request, projectId) {
4581
4616
 
4582
4617
  async function requestProjectUsers(args) {
4583
4618
  const {request: request, projectId: projectId, memberIds: memberIds} = args, response = await request({
4584
- uri: `/projects/${encodeURIComponent(projectId)}/users/${memberIds.map(encodeURIComponent).join(",")}`
4619
+ url: `/projects/${encodeURIComponent(projectId)}/users/${memberIds.map(encodeURIComponent).join(",")}`
4585
4620
  }), rows = Array.isArray(response) ? response : [ response ];
4586
4621
  if (!rows.every(isClientProjectUser)) throw new Error(`Project user directory response is invalid for project "${projectId}"`);
4587
4622
  const requested = new Set(memberIds);
@@ -4589,7 +4624,7 @@ async function requestProjectUsers(args) {
4589
4624
  }
4590
4625
 
4591
4626
  async function requestProjectMemberDirectory(args) {
4592
- const {request: request, projectId: projectId} = args, members = await requestProjectMembers(request, projectId), memberIds = members.map(member => member.id).filter(id => invariants.classifyPrincipalId(id).namespace === "project");
4627
+ const {request: request, projectId: projectId} = args, members = await _requestProjectMembers(request, projectId), memberIds = members.map(member => member.id).filter(id => invariants.classifyPrincipalId(id).namespace === "project");
4593
4628
  if (memberIds.length === 0) return {
4594
4629
  members: members,
4595
4630
  globalUsers: []
@@ -4664,7 +4699,7 @@ function clientProjectUserDirectory(client, projectId) {
4664
4699
  });
4665
4700
  try {
4666
4701
  const response = await client.request({
4667
- uri: `/projects/${encodeURIComponent(projectId)}/users/${encodeURIComponent(id)}`
4702
+ url: `/projects/${encodeURIComponent(projectId)}/users/${encodeURIComponent(id)}`
4668
4703
  }), candidate = Array.isArray(response) ? response[0] : response;
4669
4704
  return candidate == null ? {
4670
4705
  status: "missing"
@@ -4776,8 +4811,30 @@ function collectWatchRefs(instance) {
4776
4811
  }
4777
4812
 
4778
4813
  function collectSubjectRefs(instance) {
4779
- const stage = invariants.findOpenStageEntry(instance), activityFields = stage?.activities.map(activity => activity.fields) ?? [];
4780
- return [ ...entryDocRefs(instance.fields), ...entryDocRefs(stage?.fields), ...activityFields.flatMap(entryDocRefs), ...entryReleaseRefs(instance.fields), ...entryReleaseRefs(stage?.fields), ...activityFields.flatMap(entryReleaseRefs) ];
4814
+ const entries = activeFieldEntries(instance).map(({entry: entry}) => entry);
4815
+ return [ ...entryDocRefs(entries), ...entryReleaseRefs(entries) ];
4816
+ }
4817
+
4818
+ function activeFieldEntries(instance) {
4819
+ const stage = invariants.findOpenStageEntry(instance);
4820
+ return [ ...scopedEntries(instance.fields, {
4821
+ scope: "workflow"
4822
+ }), ...scopedEntries(stage?.fields, {
4823
+ scope: "stage"
4824
+ }), ...(stage?.activities ?? []).flatMap(activity => scopedEntries(activity.fields, {
4825
+ scope: "activity",
4826
+ activity: activity.name
4827
+ })) ];
4828
+ }
4829
+
4830
+ function scopedEntries(entries, scope) {
4831
+ return (entries ?? []).map(entry => ({
4832
+ target: {
4833
+ ...scope,
4834
+ field: entry.name
4835
+ },
4836
+ entry: entry
4837
+ }));
4781
4838
  }
4782
4839
 
4783
4840
  function foreignSubjectRefs(instance) {
@@ -4878,6 +4935,10 @@ function entrySubjectRefs(entries) {
4878
4935
  return fieldEntries(entries).flatMap(entry => (typeof entry._type == "string" && invariants.isSingleDocRefKind(entry._type) || entry._type === "release.ref") && invariants.isGdr(entry.value) ? [ entry.value ] : []);
4879
4936
  }
4880
4937
 
4938
+ function missingEntryDocRefs(entries, knownIds) {
4939
+ return entryDocRefs(entries).filter(reference => !knownIds.has(reference.id));
4940
+ }
4941
+
4881
4942
  function fieldEntries(entries) {
4882
4943
  return Array.isArray(entries) ? entries.filter(entry => !!entry && typeof entry == "object") : [];
4883
4944
  }
@@ -6148,7 +6209,7 @@ async function resolveThroughDirectory(args) {
6148
6209
  if (safeMisses.length === 0 || client.request === void 0) return out;
6149
6210
  try {
6150
6211
  const response = await client.request({
6151
- uri: `/projects/${encodeURIComponent(projectId)}/users/${safeMisses.map(encodeURIComponent).join(",")}`,
6212
+ url: `/projects/${encodeURIComponent(projectId)}/users/${safeMisses.map(encodeURIComponent).join(",")}`,
6152
6213
  tag: REQUEST_TAG.accessResolveActor
6153
6214
  });
6154
6215
  ingestDirectoryRows({
@@ -6348,6 +6409,12 @@ function memoizedBy(compute) {
6348
6409
  };
6349
6410
  }
6350
6411
 
6412
+ function chunks(values, size) {
6413
+ return Array.from({
6414
+ length: Math.ceil(values.length / size)
6415
+ }, (_, index) => values.slice(index * size, (index + 1) * size));
6416
+ }
6417
+
6351
6418
  async function hydrateSnapshot(args) {
6352
6419
  const {client: client, clientForGdr: clientForGdr, instance: instance, overlay: overlay} = args, loaded = [], visited = /* @__PURE__ */ new Set;
6353
6420
  loaded.push({
@@ -6458,12 +6525,6 @@ function groupReads(reads) {
6458
6525
  return [ ...byClient.values() ].flatMap(groups => [ ...groups.values() ]);
6459
6526
  }
6460
6527
 
6461
- function chunks(values, size) {
6462
- return Array.from({
6463
- length: Math.ceil(values.length / size)
6464
- }, (_, index) => values.slice(index * size, (index + 1) * size));
6465
- }
6466
-
6467
6528
  function collectEntryDocUris(resolvedFieldEntries) {
6468
6529
  return entryDocRefs(resolvedFieldEntries).map(ref => ref.id);
6469
6530
  }
@@ -7683,6 +7744,29 @@ async function refreshStageGuards({ctx: ctx, mutation: mutation, stageName: stag
7683
7744
  });
7684
7745
  }
7685
7746
 
7747
+ function missingRequiredDocuments({instance: instance, definition: definition, snapshot: snapshot}) {
7748
+ return (definition.fields ?? []).filter(field => field.required === !0 && invariants.refKindAcceptsTypes(field.type)).flatMap(field => {
7749
+ const entry = instance.fields.find(candidate => candidate.name === field.name && candidate._type === field.type);
7750
+ return missingEntryDocRefs(entry === void 0 ? [] : [ entry ], snapshot.knownIds).map(reference => ({
7751
+ target: {
7752
+ scope: "workflow",
7753
+ field: field.name
7754
+ },
7755
+ reference: reference
7756
+ }));
7757
+ });
7758
+ }
7759
+
7760
+ function requiredDocumentRequirements(args) {
7761
+ return [ ...new Set(missingRequiredDocuments(args).map(({target: target}) => target.field)) ].map(field => {
7762
+ const title = args.definition.fields?.find(entry => entry.name === field)?.title ?? field;
7763
+ return {
7764
+ name: `field:${field}`,
7765
+ title: `Documents selected for “${title}” must be readable`
7766
+ };
7767
+ });
7768
+ }
7769
+
7686
7770
  async function discoverItems(args) {
7687
7771
  const {client: client, groq: groq, params: params, perspective: perspective} = args;
7688
7772
  return client.fetch(groq, paramsForLake(params), {
@@ -8259,7 +8343,7 @@ async function persistStageMove({ctx: ctx, mutation: mutation, exitedStage: exit
8259
8343
  }
8260
8344
 
8261
8345
  async function pickTransition(ctx, stage) {
8262
- for (const candidate of stage.transitions ?? []) {
8346
+ if (!(missingRequiredDocuments(ctx).length > 0)) for (const candidate of stage.transitions ?? []) {
8263
8347
  const outcome = await ctxEvaluateConditionOutcome({
8264
8348
  ctx: ctx,
8265
8349
  condition: candidate.when
@@ -8361,7 +8445,7 @@ const actorCache = /* @__PURE__ */ new WeakMap, grantsCache = /* @__PURE__ */ ne
8361
8445
 
8362
8446
  async function resolveAccess(taggedClient, args = {}) {
8363
8447
  const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
8364
- if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ uri: '/users/me' })`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
8448
+ if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({url: '/users/me'})`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
8365
8449
  const grantsPromise = args.grantsFromPath !== void 0 ? cachedGrants({
8366
8450
  client: client,
8367
8451
  requestFn: requestFn,
@@ -8430,20 +8514,32 @@ function cachedByKey(args) {
8430
8514
  }
8431
8515
 
8432
8516
  async function fetchActor(client, requestFn) {
8433
- const [resourceUser, globalUser] = await Promise.all([ fetchCurrentUser(requestFn, "the workflow resource host"), fetchGlobalUser(client) ]), resourceId = usableId(resourceUser), globalHostId = usableId(globalUser.user), carriedId = invariants.firstCarriedGlobalId([ globalHostId, resourceId ]), sessionId = carriedId ?? globalHostId ?? resourceId;
8517
+ const resourceUser = await fetchCurrentUser(requestFn, "the workflow resource host"), resourceId = usableId(resourceUser), resourceGlobalId = invariants.firstCarriedGlobalId([ resourceId ]) ?? invariants.directoryBridgeId(resourceUser?.sanityUserId);
8518
+ if (resourceGlobalId !== void 0) return identityFromUsers({
8519
+ id: resourceGlobalId,
8520
+ resourceUser: resourceUser
8521
+ });
8522
+ const globalUser = await fetchGlobalUser(client), globalHostId = usableId(globalUser.user), carriedId = invariants.firstCarriedGlobalId([ globalHostId ]), sessionId = carriedId ?? globalHostId ?? resourceId;
8434
8523
  if (sessionId === void 0) return;
8435
8524
  const bridge = carriedId === void 0 ? await bridgeProjectPrincipal({
8436
8525
  client: client,
8437
8526
  sessionId: sessionId,
8438
8527
  resourceId: resourceId
8439
8528
  }) : NO_BRIDGE, id = bridge.status === "resolved" ? bridge.globalId : sessionId;
8440
- refuseProjectScopedActor({
8529
+ return refuseProjectScopedActor({
8441
8530
  id: id,
8442
8531
  globalUser: globalUser,
8443
8532
  globalHostId: globalHostId,
8444
8533
  bridgeFailure: bridge.status === "unavailable" ? bridge : void 0
8534
+ }), identityFromUsers({
8535
+ id: id,
8536
+ resourceUser: resourceUser,
8537
+ globalUser: globalUser.user
8445
8538
  });
8446
- const roleNames = roleNamesFor(resourceUser, globalUser);
8539
+ }
8540
+
8541
+ function identityFromUsers(args) {
8542
+ const {id: id, resourceUser: resourceUser, globalUser: globalUser} = args, resourceId = usableId(resourceUser), roleNames = roleNamesFor(resourceUser, globalUser);
8447
8543
  return {
8448
8544
  actor: {
8449
8545
  kind: "person",
@@ -8459,7 +8555,7 @@ async function fetchActor(client, requestFn) {
8459
8555
  }
8460
8556
 
8461
8557
  function roleNamesFor(resourceUser, globalUser) {
8462
- return (resourceUser?.roles?.length ? resourceUser : globalUser.user)?.roles?.map(r => r.name).filter(n => !!n) ?? [];
8558
+ return (resourceUser?.roles?.length ? resourceUser : globalUser)?.roles?.map(r => r.name).filter(n => !!n) ?? [];
8463
8559
  }
8464
8560
 
8465
8561
  const NO_BRIDGE = {
@@ -8523,11 +8619,11 @@ function usableId(user) {
8523
8619
  async function fetchCurrentUser(requestFn, hostDescription) {
8524
8620
  try {
8525
8621
  return await requestFn({
8526
- uri: "/users/me",
8622
+ url: "/users/me",
8527
8623
  tag: REQUEST_TAG.accessResolveActor
8528
8624
  });
8529
8625
  } catch (err) {
8530
- throw new Error(`workflow: /users/me request against ${hostDescription} failed. The engine resolves the actor from the client's token via \`client.request({ uri: '/users/me' })\`. Check the token/connectivity.`, {
8626
+ throw new Error(`workflow: /users/me request against ${hostDescription} failed. The engine resolves the actor from the client's token via \`client.request({url: '/users/me'})\`. Check the token/connectivity.`, {
8531
8627
  cause: err
8532
8628
  });
8533
8629
  }
@@ -8544,7 +8640,7 @@ async function fetchGlobalUser(client) {
8544
8640
  };
8545
8641
  try {
8546
8642
  const user = await sibling.request({
8547
- uri: "/users/me",
8643
+ url: "/users/me",
8548
8644
  tag: REQUEST_TAG.accessResolveActor
8549
8645
  }), id = usableId(user);
8550
8646
  return sibling.requireGlobalPrincipal && id !== void 0 && invariants.classifyPrincipalId(id).namespace === "project" ? {
@@ -8667,7 +8763,7 @@ async function resolveOrganizationId(args) {
8667
8763
  load: async () => {
8668
8764
  try {
8669
8765
  const project = await requestFn({
8670
- uri: `/projects/${encodeURIComponent(projectId)}`,
8766
+ url: `/projects/${encodeURIComponent(projectId)}`,
8671
8767
  tag: REQUEST_TAG.accessResolveOrg
8672
8768
  });
8673
8769
  if (!invariants.isRecord(project)) return;
@@ -8692,15 +8788,17 @@ function httpStatusOf(err) {
8692
8788
  return typeof status == "number" ? status : void 0;
8693
8789
  }
8694
8790
 
8791
+ function attributesRequestOf(client) {
8792
+ const version = client.config?.().apiVersion, request = typeof client.withConfig == "function" || version === void 0 || version === ENGINE_API_VERSION ? lazyRequest(pinApiVersion(client)) : void 0;
8793
+ return request === void 0 && console.warn("workflow: skipped org attributes fetch; the client cannot issue API-version-pinned requests. Advisory $attributes stays undefined (conditions referencing it fail closed)."),
8794
+ request;
8795
+ }
8796
+
8695
8797
  async function fetchAttributesCached(args) {
8696
- const {client: client, orgId: orgId} = args, sibling = globalHostRequestOf(client);
8697
- if ("reason" in sibling) {
8698
- console.warn(`workflow: skipped org attributes fetch (${sibling.reason}); advisory $attributes stays undefined (conditions referencing it fail closed).`);
8699
- return;
8700
- }
8701
- try {
8702
- const response = await sibling.request({
8703
- uri: `/organizations/${encodeURIComponent(orgId)}/users/me/attributes?limit=${ATTRIBUTES_FETCH_LIMIT}`,
8798
+ const {client: client, orgId: orgId} = args, request = attributesRequestOf(client);
8799
+ if (request !== void 0) try {
8800
+ const response = await request({
8801
+ url: `/organizations/${encodeURIComponent(orgId)}/users/me/attributes?limit=${ATTRIBUTES_FETCH_LIMIT}`,
8704
8802
  tag: REQUEST_TAG.accessAttributes
8705
8803
  }), envelope = invariants.isRecord(response) ? response : void 0;
8706
8804
  if (envelope?.hasMore === !0) {
@@ -8814,6 +8912,15 @@ async function resolveActionCommit({ctx: ctx, activityName: activityName, action
8814
8912
  actionName: actionName,
8815
8913
  status: entry?.status
8816
8914
  });
8915
+ const unmetRequirements = requiredDocumentRequirements(ctx);
8916
+ if (unmetRequirements.length > 0) throw new ActionDisabledError({
8917
+ activity: activityName,
8918
+ action: actionName,
8919
+ reason: {
8920
+ kind: "requirements-unmet",
8921
+ unmetRequirements: unmetRequirements
8922
+ }
8923
+ });
8817
8924
  const params = validateActionParams({
8818
8925
  action: action,
8819
8926
  activityName: activityName,
@@ -8970,13 +9077,13 @@ async function runTriggeredActions({ctx: ctx, mutation: mutation, stage: stage})
8970
9077
  async function runActivityTriggers({ctx: ctx, mutation: mutation, activity: activity}) {
8971
9078
  let fires = 0, ranFieldOps = !1;
8972
9079
  for (const action of activity.actions ?? []) {
8973
- if (!invariants.isCascadeFired(action)) continue;
8974
9080
  const entry = currentActivities(mutation).find(e => e.name === activity.name);
8975
9081
  if (entry === void 0 || entry.status !== "active") break;
8976
- if ((entry.firedActions ?? []).includes(action.name) || !await triggerIsLive({
9082
+ if (!await triggerIsLive({
8977
9083
  ctx: ctx,
8978
9084
  activity: activity,
8979
- action: action
9085
+ action: action,
9086
+ entry: entry
8980
9087
  })) continue;
8981
9088
  const result = await fireTriggeredAction({
8982
9089
  ctx: ctx,
@@ -8993,8 +9100,12 @@ async function runActivityTriggers({ctx: ctx, mutation: mutation, activity: acti
8993
9100
  };
8994
9101
  }
8995
9102
 
8996
- async function triggerIsLive({ctx: ctx, activity: activity, action: action}) {
8997
- if (!tokenMayExecute({
9103
+ function isUnfiredTrigger(action, entry) {
9104
+ return invariants.isCascadeFired(action) && !(entry.firedActions ?? []).includes(action.name);
9105
+ }
9106
+
9107
+ async function triggerIsLive({ctx: ctx, activity: activity, action: action, entry: entry}) {
9108
+ if (!isUnfiredTrigger(action, entry) || missingRequiredDocuments(ctx).length > 0 || !tokenMayExecute({
8998
9109
  actor: ctx.actor,
8999
9110
  action: action,
9000
9111
  ctx: ctx
@@ -10098,7 +10209,7 @@ function staleClaimReason(args) {
10098
10209
  if (isClaimExpired(claim, args.now)) return "lease-expired";
10099
10210
  }
10100
10211
 
10101
- function renewClaimLease(args) {
10212
+ function renewClaimLease$1(args) {
10102
10213
  const {mutation: mutation, effectKey: effectKey, now: now, leaseMs: leaseMs} = args;
10103
10214
  mutation.pendingEffects = mutation.pendingEffects.map(entry => entry._key === effectKey && entry.claim !== void 0 ? {
10104
10215
  ...entry,
@@ -10135,7 +10246,7 @@ async function commitReport({ctx: ctx, effectKey: effectKey, claimToken: claimTo
10135
10246
  mutation: mutation,
10136
10247
  record: requestRecord,
10137
10248
  now: ctx.now
10138
- }), renewClaimLease({
10249
+ }), renewClaimLease$1({
10139
10250
  mutation: mutation,
10140
10251
  effectKey: effectKey,
10141
10252
  now: ctx.now,
@@ -10224,6 +10335,100 @@ async function commitResetActivity({ctx: ctx, activity: activity, to: to, reques
10224
10335
  };
10225
10336
  }
10226
10337
 
10338
+ const AvailabilityResponse = v__namespace.object({
10339
+ documents: v__namespace.array(v__namespace.object({
10340
+ _id: v__namespace.string()
10341
+ })),
10342
+ omitted: v__namespace.array(v__namespace.object({
10343
+ id: v__namespace.string(),
10344
+ reason: v__namespace.picklist([ "existence", "permission" ])
10345
+ }))
10346
+ }), AVAILABILITY_BATCH_SIZE = 100, AVAILABILITY_CONCURRENCY = 4, pendingReads = /* @__PURE__ */ new WeakMap;
10347
+
10348
+ function groupsFor(documents, clientForGdr) {
10349
+ const groups = /* @__PURE__ */ new Map;
10350
+ for (const {reference: reference} of documents) {
10351
+ const parsed = invariants.parseGdr(reference.id), resource = invariants.resourceFromParsed(parsed), key = invariants.resourceGdr(resource), group = groups.get(key) ?? {
10352
+ client: clientForGdr(parsed),
10353
+ resource: resource,
10354
+ documents: /* @__PURE__ */ new Map
10355
+ };
10356
+ group.documents.set(reference.id, parsed.documentId), groups.set(key, group);
10357
+ }
10358
+ return [ ...groups.values() ];
10359
+ }
10360
+
10361
+ async function readGroup(group) {
10362
+ let response;
10363
+ try {
10364
+ const url = documentUrl(group);
10365
+ url !== void 0 && group.client.request !== void 0 ? response = await readResponse(group.client, url) : logUnavailable({
10366
+ resource: group.resource,
10367
+ reason: "unsupported-client"
10368
+ });
10369
+ } catch (error) {
10370
+ logUnavailable({
10371
+ resource: group.resource,
10372
+ reason: v__namespace.isValiError(error) ? "invalid-response" : "request-failed",
10373
+ error: error
10374
+ });
10375
+ }
10376
+ return [ ...group.documents ].map(([uri, id]) => [ uri, availabilityOf(response, id) ]);
10377
+ }
10378
+
10379
+ function logUnavailable({resource: resource, reason: reason, error: error}) {
10380
+ const statusCode = typeof error == "object" && error !== null && "statusCode" in error ? error.statusCode : void 0;
10381
+ console.warn("[workflows] Document availability could not be confirmed.", {
10382
+ resource: invariants.resourceGdr(resource),
10383
+ reason: reason,
10384
+ ...typeof statusCode == "number" ? {
10385
+ statusCode: statusCode
10386
+ } : {}
10387
+ });
10388
+ }
10389
+
10390
+ function availabilityOf(response, id) {
10391
+ if (response === void 0) return "unreadable";
10392
+ const omissions = response.omitted.filter(entry => idUtils.getPublishedId(idUtils.DocumentId(entry.id)) === id);
10393
+ return omissions.some(entry => entry.reason === "permission") ? "unreadable" : response.documents.some(doc => idUtils.getPublishedId(idUtils.DocumentId(doc._id)) === id) ? "outside-perspective" : omissions.some(entry => entry.id === id && entry.reason === "existence") ? "deleted" : "unreadable";
10394
+ }
10395
+
10396
+ function documentUrl(group) {
10397
+ const ids = [ ...new Set(group.documents.values()) ].sort().map(encodeURIComponent).join(",");
10398
+ let url;
10399
+ if ("getDataUrl" in group.client && group.client.getDataUrl !== void 0) url = group.client.getDataUrl("doc", ids); else {
10400
+ if (group.resource.type !== "dataset") return;
10401
+ const {dataset: dataset} = invariants.datasetResourceParts(group.resource.id);
10402
+ url = `/data/doc/${dataset}/${ids}`;
10403
+ }
10404
+ return `${url}?includeAllVersions=true`;
10405
+ }
10406
+
10407
+ function readResponse(client, url) {
10408
+ const identity = unwrapRequestTag(client), pending = pendingReads.get(identity) ?? /* @__PURE__ */ new Map;
10409
+ pendingReads.set(identity, pending);
10410
+ const existing = pending.get(url);
10411
+ if (existing !== void 0) return existing;
10412
+ const result = client.request({
10413
+ url: url
10414
+ }).then(response => v__namespace.parse(AvailabilityResponse, response)).finally(() => pending.delete(url));
10415
+ return pending.set(url, result), result;
10416
+ }
10417
+
10418
+ function documentAvailabilityReader(args) {
10419
+ return async documents => {
10420
+ const result = /* @__PURE__ */ new Map, groups = groupsFor(documents, args.clientForGdr).flatMap(group => chunks([ ...group.documents ], AVAILABILITY_BATCH_SIZE).map(entries => ({
10421
+ ...group,
10422
+ documents: new Map(entries)
10423
+ })));
10424
+ for (const wave of chunks(groups, AVAILABILITY_CONCURRENCY)) {
10425
+ const rows = await Promise.all(wave.map(readGroup));
10426
+ for (const [uri, availability] of rows.flat()) result.set(uri, availability);
10427
+ }
10428
+ return result;
10429
+ };
10430
+ }
10431
+
10227
10432
  const REPLAY_SURFACE = {
10228
10433
  allows: () => !0,
10229
10434
  description: "any resource (in-memory replay; the committing surface enforces)"
@@ -10375,6 +10580,80 @@ function fabricatable(assignment) {
10375
10580
  return assignment.target.path.every(segment => typeof segment != "number" || segment <= groqConditionDescribe.MAX_COUNTERFACTUAL_INDEX);
10376
10581
  }
10377
10582
 
10583
+ function missingDocumentsOf({instance: instance, snapshot: snapshot, definition: definition}) {
10584
+ const stage = findStageNode({
10585
+ definition: definition,
10586
+ stageName: instance.currentStage
10587
+ });
10588
+ return activeFieldEntries(instance).flatMap(({target: target, entry: entry}) => {
10589
+ const declarations = {
10590
+ workflow: definition?.fields,
10591
+ stage: stage?.fields,
10592
+ activity: stage?.activities?.find(activity => activity.name === target.activity)?.fields
10593
+ }, title = entry.title ?? declarations[target.scope ?? "workflow"]?.find(field => field.name === target.field)?.title;
10594
+ return missingEntryDocRefs([ entry ], snapshot.knownIds).map(reference => ({
10595
+ target: target,
10596
+ reference: reference,
10597
+ ...title !== void 0 ? {
10598
+ title: title
10599
+ } : {}
10600
+ }));
10601
+ });
10602
+ }
10603
+
10604
+ function blockingConditions(evaluation) {
10605
+ const transitions = evaluation.currentStage.transitions.some(entry => entry.whenSatisfied) ? [] : evaluation.currentStage.transitions;
10606
+ return [ ...evaluation.currentStage.activities.filter(entry => entry.status === "active" && (entry.activity.actions ?? []).length > 0).flatMap(entry => (entry.unmetRequirements ?? []).map(requirement => ({
10607
+ insight: entry.requirementInsights?.[requirement.name],
10608
+ activity: entry.activity.name
10609
+ }))), ...transitions.map(entry => ({
10610
+ insight: entry.insight
10611
+ })) ];
10612
+ }
10613
+
10614
+ function readsDocumentContent({entry: entry, read: read, atom: atom}) {
10615
+ return groqConditionDescribe.atomReadsDataset(atom) ? !0 : invariants.isSingleDocRefKind(entry._type) && read.path.length > 1 && read.path[1] !== "_id" && read.path[1] !== "_type";
10616
+ }
10617
+
10618
+ function contentTargets({atoms: atoms, fields: fields, activity: activity}) {
10619
+ return atoms.flatMap(atom => atom.reads.flatMap(read => {
10620
+ if (read.variable !== "fields") return [];
10621
+ const field = fields.findLast(({entry: entry, target: target}) => entry.name === read.path[0] && (target.scope !== "activity" || target.activity === activity));
10622
+ return field !== void 0 && readsDocumentContent({
10623
+ entry: field.entry,
10624
+ read: read,
10625
+ atom: atom
10626
+ }) ? [ _fieldTargetLabel(field.target) ] : [];
10627
+ }));
10628
+ }
10629
+
10630
+ async function blockingAtoms(condition, args) {
10631
+ const atoms = condition.insight?.blockedBy.map(({atom: atom}) => atom) ?? [], names = [ ...new Set(atoms.flatMap(atom => atom.reads).filter(read => read.path.length === 0 && args.definition.predicates?.[read.variable] !== void 0).map(read => read.variable)) ];
10632
+ if (names.length === 0) return atoms;
10633
+ const site = args.sites.find(entry => entry.insight === condition.insight);
10634
+ if (site === void 0) throw new Error("Missing evaluated scope for a blocking condition");
10635
+ const dependencies = await Promise.all(names.map(async name => {
10636
+ const predicate = args.definition.predicates?.[name];
10637
+ if (predicate === void 0) throw new Error(`Missing predicate "${name}"`);
10638
+ return (await groqConditionDescribe.explainCondition({
10639
+ condition: site.params[name] === !0 ? `!(${predicate})` : predicate,
10640
+ dataset: args.snapshot.docs,
10641
+ params: site.params
10642
+ })).blockedBy.map(({atom: atom}) => atom);
10643
+ }));
10644
+ return [ ...atoms, ...dependencies.flat() ];
10645
+ }
10646
+
10647
+ async function blockingMissingDocuments(args) {
10648
+ if (!args.missingDocuments?.length) return [];
10649
+ const fields = activeFieldEntries(args.instance), targets = (await Promise.all(blockingConditions(args).map(async condition => contentTargets({
10650
+ atoms: await blockingAtoms(condition, args),
10651
+ fields: fields,
10652
+ activity: condition.activity
10653
+ })))).flat(), blocked = /* @__PURE__ */ new Set([ ...targets, ...missingRequiredDocuments(args).map(({target: target}) => _fieldTargetLabel(target)) ]);
10654
+ return args.missingDocuments.filter(({target: target}) => blocked.has(_fieldTargetLabel(target)));
10655
+ }
10656
+
10378
10657
  async function subjectResourceGrants(args) {
10379
10658
  const {clientForGdr: clientForGdr, instance: instance} = args, clients = /* @__PURE__ */ new Map;
10380
10659
  for (const {parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
@@ -10437,6 +10716,9 @@ async function evaluateInstance(args) {
10437
10716
  guards: guards,
10438
10717
  now: now,
10439
10718
  resourceGrants: resourceGrants,
10719
+ documentAvailability: documentAvailabilityReader({
10720
+ clientForGdr: clientForGdr
10721
+ }),
10440
10722
  guardDereference: clientGuardDereference(client),
10441
10723
  ...localPrincipalId !== void 0 ? {
10442
10724
  localPrincipalId: localPrincipalId
@@ -10544,7 +10826,11 @@ async function evaluateFromSnapshot(args) {
10544
10826
  }), subjectPermissionReason = subjectDenials.length > 0 ? {
10545
10827
  kind: "subject-permission-denied",
10546
10828
  denied: subjectDenials
10547
- } : void 0, sites = [], activityEvaluations = [];
10829
+ } : void 0, sites = [], activityEvaluations = [], requiredDocuments = requiredDocumentRequirements({
10830
+ instance: instance,
10831
+ definition: definition,
10832
+ snapshot: snapshot
10833
+ });
10548
10834
  for (const activity of stage.activities ?? []) activityEvaluations.push(await evaluateActivity({
10549
10835
  activity: activity,
10550
10836
  statusEntry: currentActivityEntries.find(t => t.name === activity.name),
@@ -10555,36 +10841,23 @@ async function evaluateFromSnapshot(args) {
10555
10841
  stageHasExits: !isTerminalStage(stage),
10556
10842
  guardDenial: guardDenial,
10557
10843
  subjectPermissionReason: subjectPermissionReason,
10844
+ requiredDocuments: requiredDocuments,
10558
10845
  sites: sites,
10559
10846
  autonomy: activityAutonomyOf(stageAutonomy2, activity.name),
10560
10847
  fireConsequence: fireConsequence
10561
10848
  }));
10562
- const cascadeParams = await renderConditionScope(scopeSource), transitionEvaluations = [];
10563
- for (const transition of stage.transitions ?? []) {
10564
- const insight = await explainSite({
10565
- site: {
10566
- kind: "transition",
10567
- transition: transition.name
10568
- },
10569
- condition: transition.when,
10570
- params: cascadeParams,
10571
- snapshot: snapshot,
10572
- sites: sites
10573
- });
10574
- transitionEvaluations.push({
10575
- transition: transition,
10576
- whenSatisfied: insight.outcome === "satisfied",
10577
- unevaluable: insight.outcome === "unevaluable",
10578
- insight: insight
10579
- });
10580
- }
10581
- const currentStage = {
10849
+ const cascadeParams = await renderConditionScope(scopeSource), transitionEvaluations = await evaluateTransitions({
10850
+ stage: stage,
10851
+ cascadeParams: cascadeParams,
10852
+ snapshot: snapshot,
10853
+ sites: sites
10854
+ }), currentStage = {
10582
10855
  stage: stage,
10583
10856
  ...semanticsOf(stage),
10584
10857
  activities: activityEvaluations,
10585
10858
  transitions: transitionEvaluations,
10586
10859
  autonomy: stageAutonomy2
10587
- }, pendingOnYou = activityEvaluations.filter(t => t.pendingOnActor), canInteract = activityEvaluations.some(t => t.actions.some(a => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableFields = await evaluateEditableFields({
10860
+ }, missingDocuments = await readMissingDocuments(args), pendingOnYou = activityEvaluations.filter(t => t.pendingOnActor), canInteract = activityEvaluations.some(t => t.actions.some(a => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableFields = await evaluateEditableFields({
10588
10861
  instance: instance,
10589
10862
  definition: definition,
10590
10863
  stage: stage,
@@ -10611,6 +10884,17 @@ async function evaluateFromSnapshot(args) {
10611
10884
  sites: sites,
10612
10885
  snapshot: snapshot
10613
10886
  }),
10887
+ ...missingDocuments.length > 0 ? {
10888
+ missingDocuments: missingDocuments,
10889
+ blockingMissingDocuments: await blockingMissingDocuments({
10890
+ instance: instance,
10891
+ definition: definition,
10892
+ currentStage: currentStage,
10893
+ missingDocuments: missingDocuments,
10894
+ snapshot: snapshot,
10895
+ sites: sites
10896
+ })
10897
+ } : {},
10614
10898
  autonomy: autonomy,
10615
10899
  ...nextEvaluationAt !== void 0 ? {
10616
10900
  nextEvaluationAt: nextEvaluationAt
@@ -10618,6 +10902,42 @@ async function evaluateFromSnapshot(args) {
10618
10902
  };
10619
10903
  }
10620
10904
 
10905
+ async function readMissingDocuments(args) {
10906
+ const missingDocuments = missingDocumentsOf(args);
10907
+ if (args.documentAvailability === void 0 || missingDocuments.length === 0) return missingDocuments;
10908
+ const availability = await args.documentAvailability(missingDocuments);
10909
+ return missingDocuments.map(document => {
10910
+ const verdict = availability.get(document.reference.id);
10911
+ return verdict === void 0 ? document : {
10912
+ ...document,
10913
+ availability: verdict
10914
+ };
10915
+ });
10916
+ }
10917
+
10918
+ async function evaluateTransitions(args) {
10919
+ const {stage: stage, cascadeParams: cascadeParams, snapshot: snapshot, sites: sites} = args, evaluations = [];
10920
+ for (const transition of stage.transitions ?? []) {
10921
+ const insight = await explainSite({
10922
+ site: {
10923
+ kind: "transition",
10924
+ transition: transition.name
10925
+ },
10926
+ condition: transition.when,
10927
+ params: cascadeParams,
10928
+ snapshot: snapshot,
10929
+ sites: sites
10930
+ });
10931
+ evaluations.push({
10932
+ transition: transition,
10933
+ whenSatisfied: insight.outcome === "satisfied",
10934
+ unevaluable: insight.outcome === "unevaluable",
10935
+ insight: insight
10936
+ });
10937
+ }
10938
+ return evaluations;
10939
+ }
10940
+
10621
10941
  const AUTONOMY_CACHE = /* @__PURE__ */ new WeakMap;
10622
10942
 
10623
10943
  function autonomyOf(definition) {
@@ -10628,54 +10948,56 @@ function autonomyOf(definition) {
10628
10948
  }
10629
10949
 
10630
10950
  async function evaluateEditableFields(args) {
10631
- const {instance: instance, definition: definition, stage: stage, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, guardDenial: guardDenial, sites: sites} = args, fields = [];
10632
- for (const site of editableFieldsInStage(definition, stage)) {
10633
- const window = fieldWindowOpen(instance, site), insight = await editPredicateInsight({
10634
- site: site,
10635
- snapshot: snapshot,
10636
- scope: scope,
10637
- scopeForActivity: scopeForActivity,
10638
- sites: sites
10639
- }), predicateSatisfied = insight === void 0 || insight.outcome === "satisfied", reason = editDisabledReason({
10640
- effective: site.effective,
10641
- instance: instance,
10642
- window: window,
10643
- guardDenial: guardDenial,
10644
- predicateSatisfied: predicateSatisfied
10645
- }), value = readFieldValue(instance, site);
10646
- fields.push({
10647
- scope: site.scope,
10648
- ...site.activity !== void 0 ? {
10649
- activity: site.activity
10650
- } : {},
10651
- name: site.name,
10652
- type: site.type,
10653
- ...site.title !== void 0 ? {
10654
- title: site.title
10655
- } : {},
10656
- ...site.validation !== void 0 ? {
10657
- validation: site.validation
10658
- } : {},
10659
- ...site.roles !== void 0 ? {
10660
- roles: site.roles,
10661
- ...definition.roleAliases !== void 0 ? {
10662
- roleAliases: definition.roleAliases
10663
- } : {}
10664
- } : {},
10665
- value: value,
10666
- editable: reason === void 0,
10667
- ...reason !== void 0 ? {
10668
- disabledReason: reason
10669
- } : {},
10670
- ...fieldProvenance(instance, site.ref),
10671
- ...insight !== void 0 ? {
10672
- insight: insight
10673
- } : {}
10674
- });
10675
- }
10951
+ const fields = [];
10952
+ for (const site of editableFieldsInStage(args.definition, args.stage)) fields.push(await evaluateEditableField(args, site));
10676
10953
  return fields;
10677
10954
  }
10678
10955
 
10956
+ async function evaluateEditableField(args, site) {
10957
+ const {instance: instance, definition: definition, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, guardDenial: guardDenial, sites: sites} = args, window = fieldWindowOpen(instance, site), insight = await editPredicateInsight({
10958
+ site: site,
10959
+ snapshot: snapshot,
10960
+ scope: scope,
10961
+ scopeForActivity: scopeForActivity,
10962
+ sites: sites
10963
+ }), predicateSatisfied = insight === void 0 || insight.outcome === "satisfied", reason = editDisabledReason({
10964
+ effective: site.effective,
10965
+ instance: instance,
10966
+ window: window,
10967
+ guardDenial: guardDenial,
10968
+ predicateSatisfied: predicateSatisfied
10969
+ });
10970
+ return {
10971
+ scope: site.scope,
10972
+ ...site.activity !== void 0 ? {
10973
+ activity: site.activity
10974
+ } : {},
10975
+ name: site.name,
10976
+ type: site.type,
10977
+ ...site.title !== void 0 ? {
10978
+ title: site.title
10979
+ } : {},
10980
+ ...site.validation !== void 0 ? {
10981
+ validation: site.validation
10982
+ } : {},
10983
+ ...site.roles !== void 0 ? {
10984
+ roles: site.roles,
10985
+ ...definition.roleAliases !== void 0 ? {
10986
+ roleAliases: definition.roleAliases
10987
+ } : {}
10988
+ } : {},
10989
+ value: readFieldValue(instance, site),
10990
+ editable: reason === void 0,
10991
+ ...reason !== void 0 ? {
10992
+ disabledReason: reason
10993
+ } : {},
10994
+ ...fieldProvenance(instance, site.ref),
10995
+ ...insight !== void 0 ? {
10996
+ insight: insight
10997
+ } : {}
10998
+ };
10999
+ }
11000
+
10679
11001
  async function editPredicateInsight(args) {
10680
11002
  const {site: site, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, sites: sites} = args;
10681
11003
  if (typeof site.effective != "string") return;
@@ -10699,14 +11021,10 @@ async function editPredicateInsight(args) {
10699
11021
  async function forecastSubjectDenials(args) {
10700
11022
  const {instance: instance, snapshot: snapshot, resourceGrants: resourceGrants} = args;
10701
11023
  if (resourceGrants === void 0 || resourceGrants.size === 0) return [];
10702
- const denials = [], seen = /* @__PURE__ */ new Set;
10703
- for (const {ref: ref, parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
10704
- if (seen.has(ref.id)) continue;
10705
- seen.add(ref.id);
10706
- const access = resourceGrants.get(invariants.resourceGdr(resource));
10707
- if (access === void 0) continue;
10708
- const doc = snapshot.docs.find(d => d._id === ref.id);
10709
- doc !== void 0 && await subjectUpdateAllowed({
11024
+ const denials = [], subjects = new Map(foreignSubjectRefs(instance).map(subject => [ subject.ref.id, subject ]));
11025
+ for (const {ref: ref, parsed: parsed, resource: resource} of subjects.values()) {
11026
+ const access = resourceGrants.get(invariants.resourceGdr(resource)), doc = snapshot.docs.find(d => d._id === ref.id);
11027
+ access === void 0 || doc === void 0 || await subjectUpdateAllowed({
10710
11028
  doc: doc,
10711
11029
  parsed: parsed,
10712
11030
  grants: access.grants,
@@ -10739,13 +11057,13 @@ async function subjectUpdateAllowed(args) {
10739
11057
  }
10740
11058
 
10741
11059
  async function evaluateActivity(args) {
10742
- const {activity: activity, statusEntry: statusEntry, instance: instance, snapshot: snapshot, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, stageHasExits: stageHasExits, guardDenial: guardDenial, subjectPermissionReason: subjectPermissionReason, sites: sites, autonomy: autonomy, fireConsequence: fireConsequence} = args, status = statusEntry?.status ?? "skipped", assigned = activityScope.assigned === !0, {unmetRequirements: unmetRequirements, ...conditionInsights} = await explainActivityConditions({
11060
+ const {activity: activity, statusEntry: statusEntry, instance: instance, snapshot: snapshot, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, stageHasExits: stageHasExits, guardDenial: guardDenial, subjectPermissionReason: subjectPermissionReason, requiredDocuments: requiredDocuments, sites: sites, autonomy: autonomy, fireConsequence: fireConsequence} = args, status = statusEntry?.status ?? "skipped", assigned = activityScope.assigned === !0, {unmetRequirements: activityRequirements, ...conditionInsights} = await explainActivityConditions({
10743
11061
  activity: activity,
10744
11062
  activityScope: activityScope,
10745
11063
  cascadeActivityScope: cascadeActivityScope,
10746
11064
  snapshot: snapshot,
10747
11065
  sites: sites
10748
- }), requirementsReason = unmetRequirements.length > 0 ? {
11066
+ }), unmetRequirements = [ ...requiredDocuments, ...activityRequirements ], requirementsReason = unmetRequirements.length > 0 ? {
10749
11067
  kind: "requirements-unmet",
10750
11068
  unmetRequirements: unmetRequirements
10751
11069
  } : void 0, actions = [];
@@ -11121,6 +11439,46 @@ function claimReleasedEntry(args) {
11121
11439
  };
11122
11440
  }
11123
11441
 
11442
+ const RENEW_MAX_ATTEMPTS = 3;
11443
+
11444
+ async function claimOf(args) {
11445
+ const {client: client, tag: tag, instanceId: instanceId, effectKey: effectKey} = args, instance = await reload({
11446
+ client: client,
11447
+ instanceId: instanceId,
11448
+ tag: tag
11449
+ });
11450
+ return {
11451
+ instance: instance,
11452
+ claim: instance.pendingEffects.find(e => e._key === effectKey)?.claim
11453
+ };
11454
+ }
11455
+
11456
+ async function claimStillHeld(args) {
11457
+ const {claimToken: claimToken, ...where} = args, {claim: claim} = await claimOf(where);
11458
+ return claim?.claimToken === claimToken;
11459
+ }
11460
+
11461
+ async function renewClaimLease(args) {
11462
+ const {client: client, tag: tag, instanceId: instanceId, effectKey: effectKey, claimToken: claimToken, holdMs: holdMs, clock: clock} = args;
11463
+ for (let attempt = 1; attempt <= RENEW_MAX_ATTEMPTS; attempt++) {
11464
+ const {instance: instance, claim: claim} = await claimOf({
11465
+ client: client,
11466
+ tag: tag,
11467
+ instanceId: instanceId,
11468
+ effectKey: effectKey
11469
+ });
11470
+ if (claim?.claimToken !== claimToken) return;
11471
+ const until = addMs(clock(), holdMs);
11472
+ try {
11473
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({
11474
+ [`pendingEffects[_key=="${effectKey}"].claim.leaseExpiresAt`]: until
11475
+ }).commit(SYNC_COMMIT), until;
11476
+ } catch (error) {
11477
+ if (!isRevisionConflict(error)) throw error;
11478
+ }
11479
+ }
11480
+ }
11481
+
11124
11482
  const SWEEP_MAX_ATTEMPTS = 5;
11125
11483
 
11126
11484
  async function sweepStaleClaims(args) {
@@ -11168,7 +11526,7 @@ function isProjectRole(value) {
11168
11526
 
11169
11527
  async function projectRoleNames(request, projectId) {
11170
11528
  const result = await request({
11171
- uri: `/projects/${encodeURIComponent(projectId)}/roles`
11529
+ url: `/projects/${encodeURIComponent(projectId)}/roles`
11172
11530
  });
11173
11531
  if (!Array.isArray(result) || !result.every(isProjectRole)) throw new Error(`Project role catalog response is invalid for project "${projectId}"`);
11174
11532
  return new Set(result.map(role => role.name));
@@ -11364,7 +11722,7 @@ async function validateDefinitionRoles(args) {
11364
11722
  const references = args.definitions.flatMap(definitionRoleReferences);
11365
11723
  if (references.length === 0) /* @__PURE__ */ return new Map;
11366
11724
  if (args.client.request === void 0) throw new Error("workflow.deployDefinitions: definitions reference project roles, but the client cannot read the project role catalog");
11367
- const projectId = roleCatalogProjectId(args.client, args.workflowResource), [known, members] = await Promise.all([ projectRoleNames(args.client.request, projectId), requestProjectMembers(args.client.request, projectId) ]), unknown = references.filter(reference => !known.has(reference.role));
11725
+ const projectId = roleCatalogProjectId(args.client, args.workflowResource), [known, members] = await Promise.all([ projectRoleNames(args.client.request, projectId), _requestProjectMembers(args.client.request, projectId) ]), unknown = references.filter(reference => !known.has(reference.role));
11368
11726
  if (unknown.length > 0) {
11369
11727
  const lines = unknown.map(({definition: definition, path: path, role: role}) => ` - ${definition}: ${path} references unknown role "${role}"`);
11370
11728
  throw new Error(`workflow.deployDefinitions: unknown project roles for project "${projectId}":\n${lines.join(`\n`)}`);
@@ -11494,7 +11852,7 @@ function refsOf(def) {
11494
11852
  }
11495
11853
 
11496
11854
  function hashDefinitionContent(def) {
11497
- const canonical = stableStringify(def);
11855
+ const canonical = stableStringify(withoutAuthoringRuntime(def));
11498
11856
  let hash = 0xcbf29ce484222325n;
11499
11857
  for (let i = 0; i < canonical.length; i++) hash ^= BigInt(canonical.charCodeAt(i)),
11500
11858
  hash = hash * 0x100000001b3n & 0xffffffffffffffffn;
@@ -11507,7 +11865,7 @@ function planDefinitionDeploy({def: def, latest: latest, target: target}) {
11507
11865
  definition: def,
11508
11866
  workflowResource: target.workflowResource
11509
11867
  });
11510
- const expanded = expandResourceAliases(def, target.resourceAliases), contentHash = hashDefinitionContent(expanded), unchanged = latest !== void 0 && latest.contentHash === contentHash, version = unchanged ? latest.version : (latest?.version ?? 0) + 1, docId = invariants.definitionDocId({
11868
+ const expanded = withoutAuthoringRuntime(expandResourceAliases(def, target.resourceAliases)), contentHash = hashDefinitionContent(expanded), unchanged = latest !== void 0 && latest.contentHash === contentHash, version = unchanged ? latest.version : (latest?.version ?? 0) + 1, docId = invariants.definitionDocId({
11511
11869
  tag: target.tag,
11512
11870
  definition: expanded.name,
11513
11871
  version: version
@@ -11564,10 +11922,17 @@ function isDocumentEnvelopeKey(key) {
11564
11922
  return key.startsWith("_") || DEPLOY_STAMPED_KEYS.has(key);
11565
11923
  }
11566
11924
 
11925
+ const AUTHORING_RUNTIME_KEY = "runtime";
11926
+
11927
+ function withoutAuthoringRuntime(def) {
11928
+ const content = Object.entries(def).filter(([key]) => key !== AUTHORING_RUNTIME_KEY);
11929
+ return Object.fromEntries(content);
11930
+ }
11931
+
11567
11932
  function parseDefinitionInput(def, caller) {
11568
11933
  const label = invariants.labelFor(caller, def);
11569
11934
  if (def._type !== void 0 && def._type !== invariants.WORKFLOW_DEFINITION_TYPE) throw new Error(`${label}: expected a workflow definition or a "${invariants.WORKFLOW_DEFINITION_TYPE}" document, got _type "${String(def._type)}"`);
11570
- const content = Object.fromEntries(Object.entries(def).filter(([key]) => !isDocumentEnvelopeKey(key)));
11935
+ const content = Object.fromEntries(Object.entries(withoutAuthoringRuntime(def)).filter(([key]) => !isDocumentEnvelopeKey(key)));
11571
11936
  return invariants.parseStoredDefinition(content, label);
11572
11937
  }
11573
11938
 
@@ -13487,7 +13852,7 @@ function pendingEntry(args) {
13487
13852
  }
13488
13853
 
13489
13854
  function createEffectCommitQueue(args) {
13490
- const {effectKey: effectKey, claimToken: claimToken, commit: commit} = args, pending = [];
13855
+ const {effectKey: effectKey, claimToken: claimToken, attempt: attempt, commit: commit} = args, pending = [];
13491
13856
  let admitted = 0, dead, closed = !1, tail = Promise.resolve();
13492
13857
  function assertOpen() {
13493
13858
  if (dead !== void 0) throw dead;
@@ -13542,7 +13907,7 @@ function createEffectCommitQueue(args) {
13542
13907
  value: entry.latestValue
13543
13908
  }
13544
13909
  } ],
13545
- idempotencyKey: `effect:${effectKey}:${claimToken}:progress:${coalesceKey}:${admitted}`,
13910
+ idempotencyKey: `effect:${effectKey}:${claimToken}:${attempt}:progress:${coalesceKey}:${admitted}`,
13546
13911
  coalesceKey: coalesceKey,
13547
13912
  latestValue: value
13548
13913
  });
@@ -13552,6 +13917,29 @@ function createEffectCommitQueue(args) {
13552
13917
  };
13553
13918
  }
13554
13919
 
13920
+ function effectRetryPolicy(definition, effectName) {
13921
+ return findEffect(definition, effectName)?.retry;
13922
+ }
13923
+
13924
+ function retryWindowClosed(policy, elapsedMs) {
13925
+ return policy.expiryMs !== void 0 && elapsedMs >= policy.expiryMs;
13926
+ }
13927
+
13928
+ function nextRetryDelayMs(policy, at) {
13929
+ if (at.attempt >= policy.attempts) return;
13930
+ const delay = invariants.effectBackoffMs(policy, at.attempt);
13931
+ return retryWindowClosed(policy, at.elapsedMs + delay) ? void 0 : delay;
13932
+ }
13933
+
13934
+ function retryFailedDetail(args) {
13935
+ const window = args.windowClosed ? ", retry window expired" : "";
13936
+ return `failed after ${args.at.attempt} of ${args.attempts} attempts${window}: ${args.error.message}`;
13937
+ }
13938
+
13939
+ function retrySucceededDetail(args) {
13940
+ if (args.at.attempt !== 1) return `succeeded on attempt ${args.at.attempt} of ${args.attempts}`;
13941
+ }
13942
+
13555
13943
  class MissingHandlerError extends invariants.WorkflowError {
13556
13944
  info;
13557
13945
  constructor(args) {
@@ -13634,7 +14022,7 @@ function isCancelledCompletion(data) {
13634
14022
  }
13635
14023
 
13636
14024
  async function drainEffectsInternal(args) {
13637
- const {tag: tag, workflowResource: workflowResource, instanceId: instanceId, handlers: handlers, missingHandler: missingHandler, logger: logger, handlerClient: rawHandlerClient, handlerResourceClients: rawHandlerResourceClients} = args, cascadeTelemetry = drainCascadeTelemetry(args.telemetry), {client: client, resourceClients: resourceClients} = taggedScope(args, REQUEST_TAG.drain), leaseMs = args.leaseMs ?? DEFAULT_EFFECT_LEASE_MS, clock = args.clock ?? wallClock, handlerClient = effectHandlerClient(rawHandlerClient), handlerResourceClients = effectHandlerResolver(rawHandlerResourceClients), routeGdr = buildClientForGdr({
14025
+ const {tag: tag, workflowResource: workflowResource, instanceId: instanceId, handlers: handlers, missingHandler: missingHandler, logger: logger, handlerClient: rawHandlerClient, handlerResourceClients: rawHandlerResourceClients} = args, cascadeTelemetry = drainCascadeTelemetry(args.telemetry), {client: client, resourceClients: resourceClients} = taggedScope(args, REQUEST_TAG.drain), leaseMs = args.leaseMs ?? DEFAULT_EFFECT_LEASE_MS, clock = args.clock ?? wallClock, sleep = args.sleep ?? wallSleeper, handlerClient = effectHandlerClient(rawHandlerClient), handlerResourceClients = effectHandlerResolver(rawHandlerResourceClients), routeGdr = buildClientForGdr({
13638
14026
  client: handlerClient,
13639
14027
  workflowResource: workflowResource,
13640
14028
  resourceClients: handlerResourceClients
@@ -13660,6 +14048,7 @@ async function drainEffectsInternal(args) {
13660
14048
  log: log,
13661
14049
  leaseMs: leaseMs,
13662
14050
  clock: clock,
14051
+ sleep: sleep,
13663
14052
  ...executionContext !== void 0 ? {
13664
14053
  executionContext: executionContext
13665
14054
  } : {},
@@ -13667,14 +14056,14 @@ async function drainEffectsInternal(args) {
13667
14056
  drainerActor: drainerActor,
13668
14057
  clientFor: clientFor,
13669
14058
  buckets: buckets,
13670
- skippedKeys: /* @__PURE__ */ new Set
14059
+ handledKeys: /* @__PURE__ */ new Set
13671
14060
  };
13672
14061
  for (;await drainOneCandidate(pass); ) ;
13673
14062
  return buckets;
13674
14063
  }
13675
14064
 
13676
14065
  async function drainOneCandidate(pass) {
13677
- const {client: client, tag: tag, instanceId: instanceId, buckets: buckets, skippedKeys: skippedKeys} = pass, now = pass.clock(), before = await reload({
14066
+ const {client: client, tag: tag, instanceId: instanceId, buckets: buckets, handledKeys: handledKeys} = pass, now = pass.clock(), before = await reload({
13678
14067
  client: client,
13679
14068
  instanceId: instanceId,
13680
14069
  tag: tag
@@ -13683,7 +14072,7 @@ async function drainOneCandidate(pass) {
13683
14072
  const candidate = findClaimableCandidate({
13684
14073
  instance: before,
13685
14074
  now: now,
13686
- skippedKeys: skippedKeys
14075
+ handledKeys: handledKeys
13687
14076
  });
13688
14077
  if (candidate === void 0) return !1;
13689
14078
  const handler = pass.handlers[candidate.name];
@@ -13692,7 +14081,7 @@ async function drainOneCandidate(pass) {
13692
14081
  candidate: candidate,
13693
14082
  instanceId: instanceId,
13694
14083
  log: pass.log
13695
- }), buckets.skipped.push(candidate), skippedKeys.add(candidate._key), !0;
14084
+ }), buckets.skipped.push(candidate), handledKeys.add(candidate._key), !0;
13696
14085
  const claimed = await claimPendingEffect({
13697
14086
  client: client,
13698
14087
  instance: before,
@@ -13705,11 +14094,15 @@ async function drainOneCandidate(pass) {
13705
14094
  } : {}
13706
14095
  });
13707
14096
  if (claimed === void 0) return !0;
13708
- const outcome = await dispatchAndReport({
14097
+ const retry = effectRetryPolicy(parseDefinitionSnapshot(before), candidate.name), outcome = await dispatchAndReport({
13709
14098
  handler: handler,
13710
14099
  candidate: candidate,
13711
14100
  claimToken: claimed.claimToken,
14101
+ ...retry !== void 0 ? {
14102
+ retry: retry
14103
+ } : {},
13712
14104
  leaseMs: pass.leaseMs,
14105
+ sleep: pass.sleep,
13713
14106
  client: client,
13714
14107
  handlerClient: pass.handlerClient,
13715
14108
  tag: tag,
@@ -13727,50 +14120,80 @@ async function drainOneCandidate(pass) {
13727
14120
  telemetry: pass.telemetry,
13728
14121
  clock: pass.clock
13729
14122
  });
13730
- return buckets[outcome].push(candidate), !0;
14123
+ return buckets[outcome].push(candidate), outcome === "lost" && handledKeys.add(candidate._key),
14124
+ !0;
13731
14125
  }
13732
14126
 
13733
- function findClaimableCandidate({instance: instance, now: now, skippedKeys: skippedKeys}) {
13734
- return instance.pendingEffects.find(e => (e.claim === void 0 || isClaimExpired(e.claim, now)) && !skippedKeys.has(e._key));
14127
+ function findClaimableCandidate({instance: instance, now: now, handledKeys: handledKeys}) {
14128
+ return instance.pendingEffects.find(e => (e.claim === void 0 || isClaimExpired(e.claim, now)) && !handledKeys.has(e._key));
13735
14129
  }
13736
14130
 
13737
14131
  async function dispatchAndReport(args) {
13738
- const {handler: handler, candidate: candidate, claimToken: claimToken, leaseMs: leaseMs, client: client, handlerClient: handlerClient, tag: tag, workflowResource: workflowResource, resourceClients: resourceClients, instanceId: instanceId, clientFor: clientFor, logger: logger, log: log, executionContext: executionContext, telemetry: telemetry, clock: clock} = args, commitQueue = createEffectCommitQueue({
13739
- effectKey: candidate._key,
13740
- claimToken: claimToken,
13741
- commit: async req => {
13742
- await workflow.commitEffectOps({
13743
- client: client,
13744
- tag: tag,
13745
- workflowResource: workflowResource,
13746
- ...resourceClients !== void 0 ? {
13747
- resourceClients: resourceClients
13748
- } : {},
13749
- ...executionContext !== void 0 ? {
13750
- executionContext: executionContext
13751
- } : {},
13752
- ...telemetry !== void 0 ? {
13753
- telemetry: telemetry
13754
- } : {},
13755
- instanceId: instanceId,
13756
- effectKey: candidate._key,
13757
- claimToken: claimToken,
13758
- ops: req.ops,
13759
- idempotencyKey: req.idempotencyKey,
13760
- leaseMs: leaseMs,
13761
- clock: clock
13762
- });
13763
- }
13764
- }), {outputs: outputs, ops: ops, dispatchError: dispatchError} = await dispatchEffect({
14132
+ const {handler: handler, candidate: candidate, claimToken: claimToken, retry: retry, leaseMs: leaseMs, sleep: sleep, client: client, handlerClient: handlerClient, tag: tag, workflowResource: workflowResource, resourceClients: resourceClients, instanceId: instanceId, clientFor: clientFor, logger: logger, log: log, executionContext: executionContext, telemetry: telemetry, clock: clock} = args, attemptOnce = attempt => dispatchEffect({
13765
14133
  handler: handler,
13766
14134
  candidate: candidate,
13767
- queue: commitQueue,
14135
+ queue: createEffectCommitQueue({
14136
+ effectKey: candidate._key,
14137
+ claimToken: claimToken,
14138
+ attempt: attempt,
14139
+ commit: async req => {
14140
+ await workflow.commitEffectOps({
14141
+ client: client,
14142
+ tag: tag,
14143
+ workflowResource: workflowResource,
14144
+ ...resourceClients !== void 0 ? {
14145
+ resourceClients: resourceClients
14146
+ } : {},
14147
+ ...executionContext !== void 0 ? {
14148
+ executionContext: executionContext
14149
+ } : {},
14150
+ ...telemetry !== void 0 ? {
14151
+ telemetry: telemetry
14152
+ } : {},
14153
+ instanceId: instanceId,
14154
+ effectKey: candidate._key,
14155
+ claimToken: claimToken,
14156
+ ops: req.ops,
14157
+ idempotencyKey: req.idempotencyKey,
14158
+ leaseMs: leaseMs,
14159
+ clock: clock
14160
+ });
14161
+ }
14162
+ }),
13768
14163
  ctx: {
13769
14164
  client: handlerClient,
13770
14165
  clientFor: clientFor,
13771
14166
  instanceId: instanceId,
13772
14167
  logger: logger
13773
14168
  }
14169
+ }), claimed = {
14170
+ client: client,
14171
+ tag: tag,
14172
+ instanceId: instanceId,
14173
+ effectKey: candidate._key,
14174
+ claimToken: claimToken
14175
+ }, run = await runRetryPolicy({
14176
+ attemptOnce: attemptOnce,
14177
+ ...retry !== void 0 ? {
14178
+ retry: retry
14179
+ } : {},
14180
+ clock: clock,
14181
+ sleep: sleep,
14182
+ holdThrough: holdMs => renewClaimLease({
14183
+ ...claimed,
14184
+ holdMs: holdMs,
14185
+ clock: clock
14186
+ }),
14187
+ stillHeld: () => claimStillHeld(claimed),
14188
+ leaseMs: leaseMs
14189
+ });
14190
+ if (run.abandoned !== void 0) return log.warn(run.abandoned === "lease" ? `Effect "${candidate.name}" could not hold its claim between retry attempts — the run stopped with attempts left` : `Effect "${candidate.name}" was settled by another party during a retry backoff — the run stopped without dispatching again`, {
14191
+ effectKey: candidate._key,
14192
+ instanceId: instanceId
14193
+ }), "lost";
14194
+ const {outputs: outputs, ops: ops, dispatchError: dispatchError} = run.dispatch, detail = retry === void 0 ? void 0 : completionDetail({
14195
+ run: run,
14196
+ retry: retry
13774
14197
  });
13775
14198
  try {
13776
14199
  await reportEffectOutcome({
@@ -13791,6 +14214,9 @@ async function dispatchAndReport(args) {
13791
14214
  outputs: outputs,
13792
14215
  ops: ops,
13793
14216
  dispatchError: dispatchError,
14217
+ ...detail !== void 0 ? {
14218
+ detail: detail
14219
+ } : {},
13794
14220
  clock: clock
13795
14221
  });
13796
14222
  } catch (error) {
@@ -13803,6 +14229,71 @@ async function dispatchAndReport(args) {
13803
14229
  return dispatchError === void 0 ? "drained" : "failed";
13804
14230
  }
13805
14231
 
14232
+ async function runRetryPolicy(args) {
14233
+ const {attemptOnce: attemptOnce, retry: retry, clock: clock} = args, startedAt = Date.parse(clock()), elapsed = () => Date.parse(clock()) - startedAt;
14234
+ let attempt = 1, dispatch = await attemptOnce(attempt);
14235
+ for (;dispatch.dispatchError !== void 0 && retry !== void 0; ) {
14236
+ const at = {
14237
+ attempt: attempt,
14238
+ elapsedMs: elapsed()
14239
+ }, stop = await waitForNextAttempt({
14240
+ retry: retry,
14241
+ at: at,
14242
+ elapsed: elapsed,
14243
+ pace: args
14244
+ });
14245
+ if (stop !== void 0) return {
14246
+ ...stop,
14247
+ dispatch: dispatch,
14248
+ at: at
14249
+ };
14250
+ attempt += 1, dispatch = await attemptOnce(attempt);
14251
+ }
14252
+ return {
14253
+ dispatch: dispatch,
14254
+ at: {
14255
+ attempt: attempt,
14256
+ elapsedMs: elapsed()
14257
+ },
14258
+ windowClosed: !1
14259
+ };
14260
+ }
14261
+
14262
+ async function waitForNextAttempt(args) {
14263
+ const {retry: retry, at: at, elapsed: elapsed} = args, {clock: clock, sleep: sleep, holdThrough: holdThrough, stillHeld: stillHeld, leaseMs: leaseMs} = args.pace, delay = nextRetryDelayMs(retry, at);
14264
+ if (delay === void 0) return {
14265
+ windowClosed: at.attempt < retry.attempts
14266
+ };
14267
+ const heldUntil = await holdThrough(delay + leaseMs);
14268
+ if (heldUntil === void 0 || hasPassed(heldUntil, addMs(clock(), delay))) return {
14269
+ windowClosed: !1,
14270
+ abandoned: "lease"
14271
+ };
14272
+ if (retryWindowClosed(retry, elapsed() + delay)) return {
14273
+ windowClosed: !0
14274
+ };
14275
+ if (await sleep(delay), !await stillHeld()) return {
14276
+ windowClosed: !1,
14277
+ abandoned: "settled"
14278
+ };
14279
+ if (retryWindowClosed(retry, elapsed())) return {
14280
+ windowClosed: !0
14281
+ };
14282
+ }
14283
+
14284
+ function completionDetail(args) {
14285
+ const {run: run, retry: retry} = args, {dispatchError: dispatchError} = run.dispatch;
14286
+ return dispatchError === void 0 ? retrySucceededDetail({
14287
+ at: run.at,
14288
+ attempts: retry.attempts
14289
+ }) : retryFailedDetail({
14290
+ at: run.at,
14291
+ attempts: retry.attempts,
14292
+ error: dispatchError,
14293
+ windowClosed: run.windowClosed
14294
+ });
14295
+ }
14296
+
13806
14297
  async function reportEffectOutcome(args) {
13807
14298
  const {outputs: outputs, ops: ops, dispatchError: dispatchError, resourceClients: resourceClients, ...rest} = args;
13808
14299
  await workflow.completeEffect({
@@ -14093,6 +14584,9 @@ function createInstanceSession(args) {
14093
14584
  definition: definitionOf(),
14094
14585
  actor: actor,
14095
14586
  snapshot: snapshotFrom(held, normalizedSelf),
14587
+ documentAvailability: documentAvailabilityReader({
14588
+ clientForGdr: evalScope.clientForGdr
14589
+ }),
14096
14590
  guards: guards,
14097
14591
  resourceGrants: resourceGrants,
14098
14592
  guardDereference: clientGuardDereference(evalScope.client),
@@ -14506,6 +15000,9 @@ function createEngine(args) {
14506
15000
  telemetry: telemetry,
14507
15001
  ...args.effects?.leaseMs !== void 0 ? {
14508
15002
  leaseMs: args.effects.leaseMs
15003
+ } : {},
15004
+ ...args.sleep !== void 0 ? {
15005
+ sleep: args.sleep
14509
15006
  } : {}
14510
15007
  }), drained = result.drained;
14511
15008
  return (drained.length > 0 || emptyDrainDue()) && telemetry.log(WorkflowEffectsDrained, {
@@ -14527,27 +15024,43 @@ function createEngine(args) {
14527
15024
  const CONSENT_TAG = "telemetry.consent", BATCH_TAG = "telemetry.batch";
14528
15025
 
14529
15026
  function createTelemetryIntake(args) {
14530
- const {client: client, projectId: projectId, denied: denied = !1} = args;
15027
+ const {client: client, projectId: projectId, denied: denied = !1, context: context} = args;
14531
15028
  let cachedConsent;
14532
15029
  return {
14533
15030
  resolveConsent: async () => denied ? {
14534
15031
  status: "denied"
14535
15032
  } : (cachedConsent !== void 0 || (cachedConsent = await client.request({
14536
- uri: "/intake/telemetry-status",
15033
+ url: "/intake/telemetry-status",
14537
15034
  tag: CONSENT_TAG
14538
15035
  })), cachedConsent),
14539
15036
  sendEvents: batch => client.request({
14540
- uri: "/intake/batch",
15037
+ url: "/intake/batch",
14541
15038
  method: "POST",
14542
15039
  body: {
14543
15040
  projectId: projectId,
14544
- batch: batch
15041
+ batch: context === void 0 ? batch : batch.map(event => withContext(event, context))
14545
15042
  },
14546
15043
  tag: BATCH_TAG
14547
15044
  })
14548
15045
  };
14549
15046
  }
14550
15047
 
15048
+ function withContext(event, context) {
15049
+ if (!invariants.isRecord(event)) return event;
15050
+ const existing = invariants.isRecord(event.context) ? event.context : {};
15051
+ return {
15052
+ ...event,
15053
+ context: {
15054
+ ...existing,
15055
+ ...context
15056
+ }
15057
+ };
15058
+ }
15059
+
15060
+ function _resolveTelemetryEnvironment(nodeEnv, fallback) {
15061
+ return nodeEnv === "production" ? "production" : nodeEnv === "development" || nodeEnv === "test" ? "development" : fallback;
15062
+ }
15063
+
14551
15064
  function isTelemetryEnvDenied(env) {
14552
15065
  return isTrueish(env.CI) || isTrueish(env.DO_NOT_TRACK);
14553
15066
  }
@@ -14570,6 +15083,16 @@ function isTrueish(value) {
14570
15083
  return normalized !== "0" && normalized !== "false";
14571
15084
  }
14572
15085
 
15086
+ function summary(documents, deleted) {
15087
+ if (documents.length === 0) return "";
15088
+ const titles = [ ...new Set(documents.map(document => document.title ?? document.target.field)) ].map(title => `“${title}”`).join(", ");
15089
+ return documents.length === 1 ? `A document selected for ${titles} ${deleted ? "has been deleted" : "can’t be read by this workflow"}` : `${documents.length} documents selected for ${titles} ${deleted ? "have been deleted" : "can’t be read by this workflow"}`;
15090
+ }
15091
+
15092
+ function _missingDocumentsSummary(documents) {
15093
+ return [ summary(documents.filter(document => document.availability === "deleted"), !0), summary(documents.filter(document => document.availability !== "deleted"), !1) ].filter(Boolean).join(". ");
15094
+ }
15095
+
14573
15096
  function groupSitesOf(definition) {
14574
15097
  const sites = [], workflow2 = declarationIndex({
14575
15098
  groups: definition.groups,
@@ -15463,6 +15986,16 @@ exports.WorkflowStageTransitioned = WorkflowStageTransitioned;
15463
15986
 
15464
15987
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
15465
15988
 
15989
+ exports._additionalMissingDocuments = _additionalMissingDocuments;
15990
+
15991
+ exports._fieldTargetLabel = _fieldTargetLabel;
15992
+
15993
+ exports._missingDocumentsSummary = _missingDocumentsSummary;
15994
+
15995
+ exports._requestProjectMembers = _requestProjectMembers;
15996
+
15997
+ exports._resolveTelemetryEnvironment = _resolveTelemetryEnvironment;
15998
+
15466
15999
  exports.abortReason = abortReason;
15467
16000
 
15468
16001
  exports.acceptsDocumentType = acceptsDocumentType;
@@ -15575,6 +16108,8 @@ exports.documentStuckCause = documentStuckCause;
15575
16108
 
15576
16109
  exports.effectOutputsMap = effectOutputsMap;
15577
16110
 
16111
+ exports.effectSites = effectSites;
16112
+
15578
16113
  exports.entryDocRefs = entryDocRefs;
15579
16114
 
15580
16115
  exports.evaluateFromSnapshot = evaluateFromSnapshot;