@lostgradient/weft 0.12.0 → 0.13.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.
Files changed (191) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api.d.ts +2 -0
  3. package/dist/cli/api.js +3 -4
  4. package/dist/cli/generated/operation-catalog.snapshot.json +508 -12
  5. package/dist/cli/generated/operation-client.generated.d.ts +118 -10
  6. package/dist/cli/generated/operation-client.generated.js +74 -1
  7. package/dist/cli/operation-catalog-snapshot.d.ts +11 -2
  8. package/dist/cli/operation-catalog-snapshot.js +14 -5
  9. package/dist/cli/operation-client-runtime.d.ts +29 -0
  10. package/dist/client/client-storage.d.ts +24 -0
  11. package/dist/client/client-storage.js +14 -0
  12. package/dist/client/handle-delegation.d.ts +3 -3
  13. package/dist/client/handle-delegation.js +2 -2
  14. package/dist/client/http-client-requests.d.ts +5 -3
  15. package/dist/client/http-client-requests.js +20 -2
  16. package/dist/client/http-client-storage.d.ts +3 -0
  17. package/dist/client/http-client-storage.js +110 -0
  18. package/dist/client/http-client.d.ts +9 -6
  19. package/dist/client/http-client.js +13 -18
  20. package/dist/client/http-operations.d.ts +3 -1
  21. package/dist/client/http-operations.js +77 -1
  22. package/dist/client/http-request.d.ts +15 -8
  23. package/dist/client/http-request.js +19 -7
  24. package/dist/client/in-process-operations.d.ts +2 -2
  25. package/dist/client/in-process-operations.js +2 -2
  26. package/dist/client/index.d.ts +5 -1
  27. package/dist/client/index.js +13 -2
  28. package/dist/client/interface.d.ts +26 -26
  29. package/dist/client/local.d.ts +9 -6
  30. package/dist/client/local.js +9 -4
  31. package/dist/client/search-params.js +10 -6
  32. package/dist/client/start-body.d.ts +3 -1
  33. package/dist/client/start-body.js +9 -0
  34. package/dist/core/bulk-workflow-filter.d.ts +2 -1
  35. package/dist/core/bulk-workflow-filter.js +5 -1
  36. package/dist/core/engine/async-activity-records.d.ts +12 -1
  37. package/dist/core/engine/async-activity-records.js +81 -2
  38. package/dist/core/engine/bulk-operations-purge.js +16 -1
  39. package/dist/core/engine/bulk-operations-shared.js +2 -0
  40. package/dist/core/engine/callback-creators-schedule.d.ts +2 -1
  41. package/dist/core/engine/callback-creators-schedule.js +3 -3
  42. package/dist/core/engine/checkpoint-reads.js +11 -1
  43. package/dist/core/engine/child-workflow.js +15 -6
  44. package/dist/core/engine/construction.js +3 -0
  45. package/dist/core/engine/disposal.js +2 -0
  46. package/dist/core/engine/engine-internal-types.d.ts +4 -1
  47. package/dist/core/engine/fenced-write.d.ts +5 -3
  48. package/dist/core/engine/finalizer-status.d.ts +10 -0
  49. package/dist/core/engine/finalizer-status.js +82 -0
  50. package/dist/core/engine/index.d.ts +26 -2
  51. package/dist/core/engine/index.js +71 -8
  52. package/dist/core/engine/internals.d.ts +3 -0
  53. package/dist/core/engine/lease-health.d.ts +64 -0
  54. package/dist/core/engine/lease-health.js +0 -0
  55. package/dist/core/engine/lease-manager.d.ts +4 -2
  56. package/dist/core/engine/lease-manager.js +32 -4
  57. package/dist/core/engine/lifecycle/recovered-services.js +0 -2
  58. package/dist/core/engine/lifecycle/shared.d.ts +11 -8
  59. package/dist/core/engine/lifecycle/start-batch.js +12 -0
  60. package/dist/core/engine/lifecycle/start.d.ts +1 -1
  61. package/dist/core/engine/lifecycle/start.js +34 -5
  62. package/dist/core/engine/operations-coordination.d.ts +1 -12
  63. package/dist/core/engine/operations-coordination.js +57 -10
  64. package/dist/core/engine/operations-speculate.d.ts +1 -1
  65. package/dist/core/engine/operations-speculate.js +19 -5
  66. package/dist/core/engine/registration.js +14 -0
  67. package/dist/core/engine/schedule-handle.d.ts +4 -4
  68. package/dist/core/engine/schedule-handle.js +2 -2
  69. package/dist/core/engine/schedule-overlap.d.ts +11 -0
  70. package/dist/core/engine/schedule-overlap.js +50 -0
  71. package/dist/core/engine/schedule-run.d.ts +11 -3
  72. package/dist/core/engine/schedule-run.js +27 -4
  73. package/dist/core/engine/schedule-timer.js +10 -1
  74. package/dist/core/engine/schedules.d.ts +5 -4
  75. package/dist/core/engine/schedules.js +62 -56
  76. package/dist/core/engine/state-utilities.js +1 -1
  77. package/dist/core/engine/storage-io.d.ts +12 -0
  78. package/dist/core/engine/storage-io.js +49 -10
  79. package/dist/core/engine/termination/finalizer-claim.d.ts +8 -6
  80. package/dist/core/engine/termination/finalizer-claim.js +2 -1
  81. package/dist/core/engine/termination/finalizer.js +10 -14
  82. package/dist/core/engine/timeline-coordinator-constants.d.ts +3 -0
  83. package/dist/core/engine/timeline-coordinator-constants.js +1 -0
  84. package/dist/core/engine/timeline-coordinator-detail.d.ts +13 -0
  85. package/dist/core/engine/timeline-coordinator-detail.js +76 -0
  86. package/dist/core/engine/validation/schedule-options.d.ts +8 -1
  87. package/dist/core/engine/validation/schedule-options.js +18 -0
  88. package/dist/core/engine/validation/schedule.d.ts +1 -1
  89. package/dist/core/engine/validation/schedule.js +27 -3
  90. package/dist/core/engine/validation.js +66 -2
  91. package/dist/core/engine/workflow-state-stream.d.ts +5 -1
  92. package/dist/core/engine/workflow-state-stream.js +37 -14
  93. package/dist/core/events/schedule-events.d.ts +3 -4
  94. package/dist/core/events/workflow-events.d.ts +2 -1
  95. package/dist/core/list-filter-validation.d.ts +3 -0
  96. package/dist/core/list-filter-validation.js +11 -0
  97. package/dist/core/registry-snapshot.d.ts +32 -0
  98. package/dist/core/registry-snapshot.js +28 -0
  99. package/dist/core/review/index.d.ts +4 -4
  100. package/dist/core/types/activity.d.ts +52 -5
  101. package/dist/core/types/bulk.d.ts +1 -0
  102. package/dist/core/types/list-options.d.ts +6 -0
  103. package/dist/core/types/options.d.ts +6 -5
  104. package/dist/core/types/reviews.d.ts +2 -2
  105. package/dist/core/types/schedules.d.ts +45 -2
  106. package/dist/core/types/services-resolution.d.ts +5 -5
  107. package/dist/core/types/state.d.ts +64 -0
  108. package/dist/core/types/workflow-log.d.ts +3 -2
  109. package/dist/core/types/workflow-observability.d.ts +50 -0
  110. package/dist/core/types/workflow-observability.js +0 -0
  111. package/dist/core/types/workflow-registry.d.ts +7 -0
  112. package/dist/core/types.d.ts +1 -0
  113. package/dist/core/types.js +1 -0
  114. package/dist/http.js +2 -2
  115. package/dist/index.d.ts +4 -3
  116. package/dist/indexeddb.js +1 -1
  117. package/dist/server/access-policy-metadata.d.ts +92 -0
  118. package/dist/server/access-policy-metadata.js +78 -0
  119. package/dist/server/fault-to-http.d.ts +3 -7
  120. package/dist/server/fault-to-http.js +2 -34
  121. package/dist/server/handler/index.js +1 -1
  122. package/dist/server/handler/route-dispatch.d.ts +10 -3
  123. package/dist/server/handler/route-dispatch.js +19 -4
  124. package/dist/server/index.d.ts +1 -1
  125. package/dist/server/index.js +1 -0
  126. package/dist/server/interactive-operations.js +2 -0
  127. package/dist/server/openapi-error-responses.js +55 -4
  128. package/dist/server/openapi.d.ts +3 -1
  129. package/dist/server/openapi.js +4 -0
  130. package/dist/server/openrpc-document-schema.d.ts +64 -0
  131. package/dist/server/openrpc-document-schema.js +6 -26
  132. package/dist/server/openrpc.d.ts +4 -0
  133. package/dist/server/openrpc.js +9 -33
  134. package/dist/server/operation-fault.d.ts +24 -7
  135. package/dist/server/operation-fault.js +65 -13
  136. package/dist/server/operations/aggregate-workflows.d.ts +6 -0
  137. package/dist/server/operations/aggregate-workflows.js +3 -2
  138. package/dist/server/operations/async-activity.d.ts +14 -0
  139. package/dist/server/operations/async-activity.js +88 -4
  140. package/dist/server/operations/bulk-filter-helpers.d.ts +3 -0
  141. package/dist/server/operations/bulk-filter-helpers.js +27 -3
  142. package/dist/server/operations/bulk-mutate-workflow-tags.d.ts +6 -0
  143. package/dist/server/operations/bulk-signal-workflows.d.ts +6 -0
  144. package/dist/server/operations/get-stream-chunks.js +2 -7
  145. package/dist/server/operations/get-system-lease.d.ts +19 -0
  146. package/dist/server/operations/get-system-lease.js +48 -0
  147. package/dist/server/operations/get-workflow-observability.d.ts +24 -0
  148. package/dist/server/operations/get-workflow-observability.js +80 -0
  149. package/dist/server/operations/get-workflow-result.js +2 -2
  150. package/dist/server/operations/get-workflow.d.ts +2 -2
  151. package/dist/server/operations/list-filter-query-extractor.js +15 -0
  152. package/dist/server/operations/list-workflows.d.ts +6 -0
  153. package/dist/server/operations/list-workflows.js +12 -3
  154. package/dist/server/operations/operation-helpers.d.ts +9 -18
  155. package/dist/server/operations/operation-helpers.js +5 -23
  156. package/dist/server/operations/purge-workflows.d.ts +3 -0
  157. package/dist/server/operations/recover-all.js +2 -1
  158. package/dist/server/operations/single-workflow-tag-mutation.js +2 -2
  159. package/dist/server/operations/sse-stream.js +2 -7
  160. package/dist/server/operations/storage-capabilities.d.ts +34 -0
  161. package/dist/server/operations/storage-capabilities.js +38 -0
  162. package/dist/server/operations/storage.js +8 -17
  163. package/dist/server/operations/stream-workflow-sse.js +3 -9
  164. package/dist/server/operations/update-schedule.d.ts +8 -0
  165. package/dist/server/operations/update-schedule.js +70 -7
  166. package/dist/server/operations/workflow-events-sse.js +2 -2
  167. package/dist/server/rest-binding.d.ts +7 -6
  168. package/dist/server/rest-bindings.d.ts +2 -13
  169. package/dist/server/rest-bindings.js +19 -10
  170. package/dist/server/task-queue.d.ts +7 -5
  171. package/dist/storage/capabilities.d.ts +1 -1
  172. package/dist/storage/cloudflare-value-codec.d.ts +5 -3
  173. package/dist/storage/cloudflare-value-codec.js +1 -1
  174. package/dist/storage/compressed-storage.js +1 -1
  175. package/dist/storage/index.d.ts +7 -1
  176. package/dist/storage/interface.d.ts +16 -4
  177. package/dist/storage/interface.js +1 -1
  178. package/dist/storage/key-prefixes.d.ts +1 -1
  179. package/dist/storage/key-prefixes.js +4 -0
  180. package/dist/storage/lmdb.js +1 -1
  181. package/dist/storage/memory.js +1 -1
  182. package/dist/storage/neon.js +4 -4
  183. package/dist/storage/postgres.js +4 -4
  184. package/dist/storage/resolve.js +1 -1
  185. package/dist/storage/scoped-storage.js +1 -1
  186. package/dist/storage/testing.js +1 -1
  187. package/dist/storage/turso.js +2 -2
  188. package/dist/version.d.ts +1 -1
  189. package/dist/version.js +1 -1
  190. package/dist/web-extension.js +1 -1
  191. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  import { KEYS } from "../../storage/interface.js";
2
2
  import { decode } from "../codec.js";
3
3
  import { WorkflowNotRegisteredError } from "./errors.js";
4
+ import { commitFencedEngineWrite } from "./fenced-write.js";
4
5
  import { ScheduleHandle } from "./schedule-handle.js";
5
6
  import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
6
7
  import { getNextScheduleOccurrence } from "./schedule-occurrence.js";
8
+ import { applyBlockedScheduleOccurrence, drainQueuedScheduleRun } from "./schedule-overlap.js";
7
9
  import { decodeScheduleRunMetadata } from "./schedule-run-metadata.js";
8
10
  import {
9
11
  clearScheduleCurrentWorkflow,
@@ -11,14 +13,20 @@ import {
11
13
  matchesScheduleFilter,
12
14
  paginateScheduleSummaries
13
15
  } from "./state-utilities.js";
14
- import { loadScheduleState, requireScheduleState, writeScheduleState } from "./storage-io.js";
16
+ import {
17
+ loadScheduleState,
18
+ requireScheduleState,
19
+ runSerializedScheduleStateOperation,
20
+ writeScheduleState
21
+ } from "./storage-io.js";
15
22
  import {
16
23
  coerceScheduleId,
17
24
  decodeScheduleState,
18
25
  isValidScheduleIdentifier,
19
26
  normalizeScheduleFilter,
20
27
  normalizeScheduleOptions,
21
- normalizeScheduleSpec
28
+ normalizeScheduleSpec,
29
+ normalizeScheduleUpdateOptions
22
30
  } from "./validation/schedule.js";
23
31
  export { startScheduledRun } from "./schedule-run.js";
24
32
  export { handleScheduleTimer } from "./schedule-timer.js";
@@ -48,7 +56,7 @@ export async function schedule(internals, type, input, spec, options) {
48
56
  updatedAt: now,
49
57
  nextFireAt: getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now),
50
58
  missedFireCount: 0,
51
- queuedRuns: 0
59
+ queuedRuns: []
52
60
  };
53
61
  await writeScheduleState(internals, state);
54
62
  return new ScheduleHandle(scheduleId, internals.engine);
@@ -64,8 +72,7 @@ export async function listSchedules(internals, filter) {
64
72
  const state = decodeScheduleState(value);
65
73
  if (!state || !matchesScheduleFilter(state, normalizedFilter))
66
74
  continue;
67
- const { input: _input, ...summary } = state;
68
- items.push(summary);
75
+ items.push(toScheduleSummary(state));
69
76
  }
70
77
  return paginateScheduleSummaries(items, normalizedFilter);
71
78
  }
@@ -93,7 +100,7 @@ async function hasCurrentScheduleTimer(internals, state) {
93
100
  }
94
101
  export function toScheduleSummary(state) {
95
102
  const { input: _input, ...summary } = state;
96
- return summary;
103
+ return { ...summary, queuedRuns: summary.queuedRuns.map((queuedRun) => ({ ...queuedRun })) };
97
104
  }
98
105
  export async function pauseSchedule(internals, scheduleId) {
99
106
  const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), state = await requireScheduleState(internals, normalizedScheduleId);
@@ -105,7 +112,7 @@ export async function pauseSchedule(internals, scheduleId) {
105
112
  status: "paused",
106
113
  updatedAt: now,
107
114
  nextFireAt: getNextScheduleOccurrence(state, now),
108
- queuedRuns: 0
115
+ queuedRuns: []
109
116
  };
110
117
  await writeScheduleState(internals, updatedState, { includeTimer: !1 });
111
118
  }
@@ -132,23 +139,30 @@ export async function cancelSchedule(internals, scheduleId) {
132
139
  status: "cancelled",
133
140
  updatedAt: internals.options.getNow(),
134
141
  nextFireAt: null,
135
- queuedRuns: 0
142
+ queuedRuns: []
136
143
  };
137
144
  await writeScheduleState(internals, updatedState, { includeTimer: !1 });
138
145
  }
139
- export async function updateSchedule(internals, scheduleId, newSpec) {
140
- const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), normalizedSpec = normalizeScheduleSpec(newSpec), state = await requireScheduleState(internals, normalizedScheduleId), now = internals.options.getNow(), {
141
- cronExpression: _droppedCron,
142
- intervalMs: _droppedInterval,
143
- ...stateWithoutCadence
144
- } = state, cadenceFields = normalizedSpec.kind === "interval" ? { intervalMs: normalizedSpec.intervalMs } : { cronExpression: normalizedSpec.cronExpression }, anchorFields = normalizedSpec.kind === "interval" ? { createdAt: now } : {}, updatedState = {
145
- ...stateWithoutCadence,
146
- ...cadenceFields,
147
- ...anchorFields,
148
- updatedAt: now,
149
- nextFireAt: state.status === "cancelled" ? null : getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now)
150
- };
151
- await writeScheduleState(internals, updatedState, { includeTimer: state.status === "active" });
146
+ export async function updateSchedule(internals, scheduleId, newSpec, options) {
147
+ const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), normalizedSpec = normalizeScheduleSpec(newSpec), normalizedOptions = normalizeScheduleUpdateOptions(options);
148
+ await runSerializedScheduleStateOperation(internals, normalizedScheduleId, async () => {
149
+ const state = await requireScheduleState(internals, normalizedScheduleId), now = internals.options.getNow(), {
150
+ cronExpression: _droppedCron,
151
+ intervalMs: _droppedInterval,
152
+ ...stateWithoutCadence
153
+ } = state, cadenceFields = normalizedSpec.kind === "interval" ? { intervalMs: normalizedSpec.intervalMs } : { cronExpression: normalizedSpec.cronExpression }, anchorFields = normalizedSpec.kind === "interval" ? { createdAt: now } : {}, updatedState = {
154
+ ...stateWithoutCadence,
155
+ ...normalizedOptions,
156
+ ...cadenceFields,
157
+ ...anchorFields,
158
+ updatedAt: now,
159
+ nextFireAt: state.status === "cancelled" ? null : getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now)
160
+ };
161
+ await writeScheduleState(internals, updatedState, {
162
+ includeTimer: state.status === "active",
163
+ replaceTimerFrom: state
164
+ });
165
+ });
152
166
  }
153
167
  function scheduledRunOccupiesSlot(currentWorkflowState) {
154
168
  const status = currentWorkflowState?.status;
@@ -169,31 +183,20 @@ export async function refreshScheduledWorkflowState(internals, state, callbacks)
169
183
  function hasActiveScheduledWorkflow(currentWorkflowState) {
170
184
  return scheduledRunOccupiesSlot(currentWorkflowState);
171
185
  }
172
- async function applyBlockedScheduleOccurrence(state, hasActiveWorkflow, callbacks, occurrence) {
173
- if (!hasActiveWorkflow)
174
- return { ...state, currentWorkflowId: await callbacks.startScheduledRun(state, occurrence) };
175
- if (state.overlap === "cancel-running") {
176
- if (state.currentWorkflowId) {
177
- callbacks.getWorkflowResult(state.currentWorkflowId).catch(() => {});
178
- await callbacks.cancelWorkflow(state.currentWorkflowId);
179
- }
180
- const stateForStart = clearScheduleCurrentWorkflow(state);
181
- return {
182
- ...state,
183
- currentWorkflowId: await callbacks.startScheduledRun(stateForStart, occurrence)
184
- };
186
+ export async function applyScheduleOccurrence(internals, state, callbacks, occurrence) {
187
+ const { state: refreshedState, currentWorkflowState } = await callbacks.refreshScheduledWorkflowState(state);
188
+ let stateForOccurrence = refreshedState, hasActiveWorkflow = hasActiveScheduledWorkflow(currentWorkflowState);
189
+ if (!hasActiveWorkflow && stateForOccurrence.queuedRuns.length > 0) {
190
+ stateForOccurrence = await drainQueuedScheduleRun(stateForOccurrence, callbacks);
191
+ hasActiveWorkflow = !0;
185
192
  }
186
- if (state.overlap === "queue")
187
- return { ...state, queuedRuns: state.queuedRuns + 1 };
188
- return state;
189
- }
190
- export async function applyScheduleOccurrence(_internals, state, callbacks, occurrence) {
191
- const { state: refreshedState, currentWorkflowState } = await callbacks.refreshScheduledWorkflowState(state), hasActiveWorkflow = hasActiveScheduledWorkflow(currentWorkflowState);
192
- if (refreshedState.overlap === "allow") {
193
- await callbacks.startScheduledRun(refreshedState, occurrence);
194
- return refreshedState;
193
+ if (stateForOccurrence.overlap === "allow") {
194
+ await callbacks.startScheduledRun(stateForOccurrence, {
195
+ ...occurrence !== void 0 && { occurrence }
196
+ });
197
+ return stateForOccurrence;
195
198
  }
196
- return applyBlockedScheduleOccurrence(refreshedState, hasActiveWorkflow, callbacks, occurrence);
199
+ return applyBlockedScheduleOccurrence(internals, stateForOccurrence, hasActiveWorkflow, callbacks, occurrence);
197
200
  }
198
201
  export async function settleBackfillScheduleState(internals, state, callbacks) {
199
202
  if (!state.currentWorkflowId)
@@ -208,26 +211,29 @@ export async function handleScheduledWorkflowTerminal(internals, workflowId, cal
208
211
  const scheduleRunBytes = await internals.storage.get(KEYS.scheduleRun(workflowId));
209
212
  if (!scheduleRunBytes)
210
213
  return;
211
- await internals.storage.delete(KEYS.scheduleRun(workflowId));
212
214
  const metadata = decodeScheduleRunMetadata(scheduleRunBytes);
213
- if (metadata === null || !isValidScheduleIdentifier(metadata.id))
215
+ if (metadata === null || !isValidScheduleIdentifier(metadata.id)) {
216
+ await deleteTransientScheduleRunMetadata(internals, workflowId);
214
217
  return;
218
+ }
215
219
  const scheduleId = metadata.id, state = await loadScheduleState(internals, scheduleId);
216
- if (!state || state.currentWorkflowId !== workflowId)
220
+ if (!state || state.currentWorkflowId !== workflowId) {
221
+ await deleteTransientScheduleRunMetadata(internals, workflowId);
217
222
  return;
223
+ }
218
224
  const now = internals.options.getNow(), clearedState = {
219
225
  ...clearScheduleCurrentWorkflow(state),
220
226
  updatedAt: now
221
227
  };
222
- if (clearedState.status === "active" && clearedState.overlap === "queue" && clearedState.queuedRuns > 0) {
223
- const nextWorkflowId = await callbacks.startScheduledRun(clearedState);
224
- await writeScheduleState(internals, {
225
- ...clearedState,
226
- currentWorkflowId: nextWorkflowId,
227
- queuedRuns: clearedState.queuedRuns - 1,
228
- updatedAt: now
229
- }, { includeTimer: !1 });
228
+ if (clearedState.status === "active" && clearedState.queuedRuns.length > 0) {
229
+ await drainQueuedScheduleRun({ ...clearedState, updatedAt: now }, callbacks, workflowId);
230
230
  return;
231
231
  }
232
- await writeScheduleState(internals, clearedState, { includeTimer: !1 });
232
+ await writeScheduleState(internals, clearedState, {
233
+ includeTimer: !1,
234
+ additionalOperations: [{ type: "delete", key: KEYS.scheduleRun(workflowId) }]
235
+ });
236
+ }
237
+ async function deleteTransientScheduleRunMetadata(internals, workflowId) {
238
+ await commitFencedEngineWrite(internals, [{ type: "delete", key: KEYS.scheduleRun(workflowId) }], [], () => Error(`Schedule-run cleanup for workflow "${workflowId}" lost its precondition.`));
233
239
  }
@@ -223,7 +223,7 @@ function matchesListFilterStatus(state, filter) {
223
223
  return (Array.isArray(filter.status) ? filter.status : [filter.status]).includes(state.status);
224
224
  }
225
225
  function matchesListFilterIdentity(state, filter) {
226
- return matchesListFilterStatus(state, filter) && (filter.type === void 0 || state.type === filter.type) && (filter.idPrefix === void 0 || state.id.startsWith(filter.idPrefix));
226
+ return matchesListFilterStatus(state, filter) && (filter.type === void 0 || state.type === filter.type) && (filter.parentWorkflowId === void 0 || state.parentWorkflowId === filter.parentWorkflowId) && (filter.parentWorkflowExecutionToken === void 0 || state.parentWorkflowExecutionToken === filter.parentWorkflowExecutionToken) && (filter.idPrefix === void 0 || state.id.startsWith(filter.idPrefix));
227
227
  }
228
228
  function matchesListFilterTimeRanges(state, filter) {
229
229
  if (filter.createdAt !== void 0 && !timestampInRange(state.createdAt, filter.createdAt))
@@ -3,6 +3,16 @@ import type { ScheduleState, WorkflowState } from '../types.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
4
  /** Run a workflow-state write after any earlier write for that workflow has settled. */
5
5
  export declare function runSerializedWorkflowStateWrite<TResult>(internals: EngineInternals, workflowId: string, writeOperation: () => Promise<TResult>): Promise<TResult>;
6
+ /**
7
+ * Serialize a schedule read-modify-write operation with its timer callback.
8
+ *
9
+ * Unlike workflow-state write serialization, a failed predecessor is propagated
10
+ * to callers that were already queued behind it. In particular, an update that
11
+ * arrives while a scanned timer callback is rearming must not delete the fired
12
+ * timer after that callback fails; rejecting the queued update leaves the durable
13
+ * timer available for the scheduler (or a lease successor) to retry.
14
+ */
15
+ export declare function runSerializedScheduleStateOperation<TResult>(internals: EngineInternals, scheduleId: string, operation: () => Promise<TResult>): Promise<TResult>;
6
16
  /** Load and decode persisted workflow state by workflow ID. */
7
17
  export declare function loadWorkflowState(internals: EngineInternals, workflowId: string): Promise<WorkflowState | null>;
8
18
  /** Load a terminal workflow result or throw the persisted terminal error. */
@@ -29,6 +39,8 @@ export declare function requireScheduleState(internals: EngineInternals, schedul
29
39
  /** Persist schedule state and optionally write the next schedule timer. */
30
40
  export declare function writeScheduleState(internals: EngineInternals, state: ScheduleState, options?: {
31
41
  includeTimer?: boolean;
42
+ replaceTimerFrom?: ScheduleState;
43
+ additionalOperations?: BatchOperation[];
32
44
  }): Promise<void>;
33
45
  /** Load persisted workflow start headers by workflow ID. */
34
46
  export declare function loadWorkflowStartHeaders(internals: EngineInternals, workflowId: string): Promise<Map<string, string> | undefined>;
@@ -30,6 +30,21 @@ export async function runSerializedWorkflowStateWrite(internals, workflowId, wri
30
30
  internals.workflowStateWriteChains.delete(workflowId);
31
31
  }
32
32
  }
33
+ export async function runSerializedScheduleStateOperation(internals, scheduleId, operation) {
34
+ const execution = (internals.scheduleStateOperationChains.get(scheduleId) ?? Promise.resolve()).then(operation), trackedExecution = execution.then(() => {
35
+ return;
36
+ });
37
+ trackedExecution.catch(() => {
38
+ return;
39
+ });
40
+ internals.scheduleStateOperationChains.set(scheduleId, trackedExecution);
41
+ try {
42
+ return await execution;
43
+ } finally {
44
+ if (internals.scheduleStateOperationChains.get(scheduleId) === trackedExecution)
45
+ internals.scheduleStateOperationChains.delete(scheduleId);
46
+ }
47
+ }
33
48
  export async function loadWorkflowState(internals, workflowId) {
34
49
  const bytes = await internals.storage.get(KEYS.workflow(workflowId));
35
50
  if (!bytes)
@@ -80,19 +95,43 @@ export async function requireScheduleState(internals, scheduleId) {
80
95
  throw Error(`Schedule "${scheduleId}" not found`);
81
96
  return state;
82
97
  }
98
+ function buildPreviousScheduleTimerDeleteOperations(previousState) {
99
+ if (previousState?.status !== "active" || previousState.nextFireAt === null)
100
+ return [];
101
+ return [
102
+ {
103
+ type: "delete",
104
+ key: KEYS.scheduleTick(resolveEffectiveScheduleFireAt(previousState, previousState.nextFireAt), previousState.id)
105
+ }
106
+ ];
107
+ }
108
+ function buildNextScheduleTimerOperations(state, includeTimer) {
109
+ if (!includeTimer || state.status !== "active" || state.nextFireAt === null)
110
+ return [];
111
+ return buildTimerBatchOperations({
112
+ id: createScheduleTimerId(state.id),
113
+ workflowId: state.id,
114
+ fireAt: resolveEffectiveScheduleFireAt(state, state.nextFireAt),
115
+ kind: "schedule"
116
+ });
117
+ }
118
+ function buildScheduleTimerReplacementOperations(state, includeTimer, previousState) {
119
+ const deleteOperations = buildPreviousScheduleTimerDeleteOperations(previousState), nextTimerOperations = buildNextScheduleTimerOperations(state, includeTimer);
120
+ if (nextTimerOperations.length > 0)
121
+ return [...deleteOperations, ...nextTimerOperations];
122
+ if (previousState === void 0)
123
+ return deleteOperations;
124
+ return [
125
+ ...deleteOperations,
126
+ { type: "delete", key: `timer-idx:${createScheduleTimerId(state.id)}` }
127
+ ];
128
+ }
83
129
  export async function writeScheduleState(internals, state, options) {
84
130
  const operations = [
85
131
  { type: "put", key: KEYS.schedule(state.id), value: encode(state) }
86
- ];
87
- if ((options?.includeTimer ?? state.status === "active") && state.status === "active" && state.nextFireAt !== null) {
88
- const effectiveFireAt = resolveEffectiveScheduleFireAt(state, state.nextFireAt);
89
- operations.push(...buildTimerBatchOperations({
90
- id: createScheduleTimerId(state.id),
91
- workflowId: state.id,
92
- fireAt: effectiveFireAt,
93
- kind: "schedule"
94
- }));
95
- }
132
+ ], includeTimer = options?.includeTimer ?? state.status === "active";
133
+ operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
134
+ operations.push(...options?.additionalOperations ?? []);
96
135
  await commitFencedEngineWrite(internals, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
97
136
  }
98
137
  export async function loadWorkflowStartHeaders(internals, workflowId) {
@@ -37,11 +37,11 @@ export declare function runningClaimIsStale(internals: EngineInternals, claim: T
37
37
  /**
38
38
  * Durable audit record written to {@link KEYS.teardownDeadLetter} when a workflow's
39
39
  * finalizer permanently fails — the retry horizon was reached, or the recorded resource
40
- * state vanished so the finalizer can never run. It is the supported operator surface
41
- * for a leaked external resource: raw storage inspection of the `wf-teardown-deadletter:`
42
- * key is the contract (in-process teardown events are best-effort and not durable, and
43
- * the record is excluded from the workflow purge delete-set so it survives after the
44
- * workflow record is gone).
40
+ * state vanished so the finalizer can never run. It is the supported durable operator
41
+ * record for a leaked external resource; {@link Engine.getFinalizerStatus} and the matching
42
+ * transport operation expose it without relying on best-effort teardown events. The record
43
+ * is excluded from the workflow purge delete-set so it survives after the workflow record
44
+ * is gone.
45
45
  */
46
46
  export interface TeardownDeadLetterRecord {
47
47
  /** The workflow type whose finalizer leaked. */
@@ -52,6 +52,8 @@ export interface TeardownDeadLetterRecord {
52
52
  attempts: number;
53
53
  /** Engine clock when the record was written. */
54
54
  deadLetteredAt: number;
55
+ /** Concrete run identity, present on records written from current workflow state. */
56
+ workflowExecutionToken?: string;
55
57
  /** The decoded `ctx.setFinalizerState` payload, when it was still recoverable. */
56
58
  finalizerInput?: unknown;
57
59
  }
@@ -107,4 +109,4 @@ export declare function settleOnRunningClaim(internals: EngineInternals, workflo
107
109
  export declare function deadLetterTeardown(internals: EngineInternals, workflowId: string, workflowType: string, attempts: number, expectedBytes: Uint8Array, details: {
108
110
  lastError: string;
109
111
  finalizerInput: unknown;
110
- }): Promise<boolean>;
112
+ }, workflowExecutionToken?: string): Promise<boolean>;
@@ -64,12 +64,13 @@ export async function settleOnRunningClaim(internals, workflowId, runningBytes,
64
64
  { key: KEYS.teardownOwed(workflowId), expectedValue: runningBytes }
65
65
  ]);
66
66
  }
67
- export async function deadLetterTeardown(internals, workflowId, workflowType, attempts, expectedBytes, details) {
67
+ export async function deadLetterTeardown(internals, workflowId, workflowType, attempts, expectedBytes, details, workflowExecutionToken) {
68
68
  const deadLetter = {
69
69
  type: workflowType,
70
70
  lastError: details.lastError,
71
71
  attempts,
72
72
  deadLetteredAt: internals.options.getNow(),
73
+ ...workflowExecutionToken === void 0 ? {} : { workflowExecutionToken },
73
74
  ...details.finalizerInput === void 0 ? {} : { finalizerInput: details.finalizerInput }
74
75
  };
75
76
  return settleOnRunningClaim(internals, workflowId, expectedBytes, [
@@ -1,6 +1,7 @@
1
1
  import { KEYS } from "../../../storage/interface.js";
2
2
  import { decode } from "../../codec.js";
3
3
  import { WorkflowTeardownEvent } from "../../events.js";
4
+ import { buildTeardownSuccessOperations } from "../finalizer-status.js";
4
5
  import { isTeardownClaim, parseTeardownTimerId } from "../state-utilities.js";
5
6
  import { runFinalizerActivity } from "./finalizer-activity.js";
6
7
  import {
@@ -21,10 +22,7 @@ export { teardownStaleThresholdMs } from "./finalizer-claim.js";
21
22
  const TERMINAL_STATUSES_OWED_TEARDOWN = new Set([
22
23
  "cancelled",
23
24
  "timed-out"
24
- ]);
25
- function errorMessage(error) {
26
- return error instanceof Error ? error.message : String(error);
27
- }
25
+ ]), errorMessage = (error) => error instanceof Error ? error.message : String(error);
28
26
  async function rearmOnLostSettle(internals, workflowId, token) {
29
27
  await rearmTeardownTimer(internals, workflowId, token, TEARDOWN_SELF_HEAL_DELAY_MS);
30
28
  }
@@ -63,7 +61,7 @@ async function resolveTeardownDrive(internals, workflowId, token, callbacks) {
63
61
  return { kind: "rearm", token };
64
62
  const finalizerStateBytes = await internals.storage.get(KEYS.finalizerState(workflowId));
65
63
  if (finalizerStateBytes === null) {
66
- await deadLetterMissingState(internals, workflowId, state.type, claim.attempts, markerBytes, callbacks);
64
+ await deadLetterMissingState(internals, workflowId, state.type, state.workflowExecutionToken, claim.attempts, markerBytes, callbacks);
67
65
  return { kind: "cleared" };
68
66
  }
69
67
  return {
@@ -71,12 +69,12 @@ async function resolveTeardownDrive(internals, workflowId, token, callbacks) {
71
69
  drive: { state, claim, markerBytes, finalizer, finalizerInput: decode(finalizerStateBytes) }
72
70
  };
73
71
  }
74
- async function deadLetterMissingState(internals, workflowId, workflowType, attempts, expectedBytes, callbacks) {
72
+ async function deadLetterMissingState(internals, workflowId, workflowType, workflowExecutionToken, attempts, expectedBytes, callbacks) {
75
73
  const lastError = "finalizer state missing \u2014 recorded resource cannot be torn down";
76
74
  if (await deadLetterTeardown(internals, workflowId, workflowType, attempts, expectedBytes, {
77
75
  lastError,
78
76
  finalizerInput: void 0
79
- }))
77
+ }, workflowExecutionToken))
80
78
  callbacks.dispatchEvent(new WorkflowTeardownEvent(workflowId, workflowType, "dead-lettered", attempts, lastError));
81
79
  }
82
80
  export async function runWorkflowFinalizer(internals, workflowId, timerId, callbacks) {
@@ -105,7 +103,7 @@ async function driveResolvedTeardown(internals, workflowId, token, drive, callba
105
103
  }
106
104
  const result = await runFinalizerActivity(finalizer, finalizerInput, attempt, internals.abortController.signal, state.workflowExecutionToken);
107
105
  if (result.ok) {
108
- await settleTeardownSuccess(internals, workflowId, state.type, token, attempt, runningBytes, callbacks);
106
+ await settleTeardownSuccess(internals, workflowId, state.type, state.workflowExecutionToken, token, attempt, runningBytes, callbacks);
109
107
  return;
110
108
  }
111
109
  if (result.abortedByShutdown) {
@@ -123,11 +121,9 @@ async function driveResolvedTeardown(internals, workflowId, token, drive, callba
123
121
  }
124
122
  await settleTeardownFailure(internals, workflowId, state, attempt, token, runningBytes, result.error, callbacks);
125
123
  }
126
- async function settleTeardownSuccess(internals, workflowId, workflowType, token, attempt, runningBytes, callbacks) {
127
- if (!await settleOnRunningClaim(internals, workflowId, runningBytes, [
128
- { type: "delete", key: KEYS.teardownOwed(workflowId) },
129
- { type: "delete", key: KEYS.finalizerState(workflowId) }
130
- ])) {
124
+ async function settleTeardownSuccess(internals, workflowId, workflowType, workflowExecutionToken, token, attempt, runningBytes, callbacks) {
125
+ const operations = buildTeardownSuccessOperations(workflowId, attempt, internals.options.getNow(), workflowExecutionToken);
126
+ if (!await settleOnRunningClaim(internals, workflowId, runningBytes, operations)) {
131
127
  await rearmOnLostSettle(internals, workflowId, token);
132
128
  return;
133
129
  }
@@ -140,7 +136,7 @@ async function settleTeardownFailure(internals, workflowId, state, attempt, toke
140
136
  if (!await deadLetterTeardown(internals, workflowId, state.type, attempt, runningBytes, {
141
137
  lastError: message,
142
138
  finalizerInput: finalizerStateBytes === null ? void 0 : decode(finalizerStateBytes)
143
- })) {
139
+ }, state.workflowExecutionToken)) {
144
140
  await rearmOnLostSettle(internals, workflowId, token);
145
141
  return;
146
142
  }
@@ -0,0 +1,3 @@
1
+ /** Prevent one fan-out operation from making a timeline record grow without bound. */
2
+ export declare const MAX_TIMELINE_COORDINATOR_DETAILS = 100;
3
+ export declare const MAX_TIMELINE_DETAIL_STRING_LENGTH = 512;
@@ -0,0 +1 @@
1
+ export const MAX_TIMELINE_COORDINATOR_DETAILS = 100, MAX_TIMELINE_DETAIL_STRING_LENGTH = 512;
@@ -0,0 +1,13 @@
1
+ import type { ContextOperationRequest } from '../context.ts';
2
+ import type { ParallelBranchSlot } from '../context/parallel-operations.ts';
3
+ import type { WorkflowTimelineOperationDetail } from '../types.ts';
4
+ import type { EngineInternals } from './internals.ts';
5
+ export declare function operationTimelineDetail(operation: ContextOperationRequest, index: number, outcome: WorkflowTimelineOperationDetail['outcome'], options?: {
6
+ error?: unknown;
7
+ key?: string;
8
+ }): WorkflowTimelineOperationDetail;
9
+ export declare function parallelTimelineDetails(operations: readonly ContextOperationRequest[], slots: readonly ParallelBranchSlot[], branchNames?: readonly string[]): WorkflowTimelineOperationDetail[];
10
+ export declare function raceTimelineDetails(operations: readonly ContextOperationRequest[], winnerIndex: number, branchNames?: readonly string[], winnerError?: unknown): WorkflowTimelineOperationDetail[];
11
+ export declare function runAllTimelineDetails(branchNames: readonly string[], slots: readonly ParallelBranchSlot[], branches: Record<string, readonly [Function] | readonly [Function, unknown]>): WorkflowTimelineOperationDetail[];
12
+ export declare function recordTimelineBranches(internals: EngineInternals, workflowId: string, details: readonly WorkflowTimelineOperationDetail[]): void;
13
+ export declare function recordTimelineSpeculation(internals: EngineInternals, workflowId: string, details: readonly WorkflowTimelineOperationDetail[], outcome: 'committed' | 'rolled-back'): void;
@@ -0,0 +1,76 @@
1
+ import { getTimelineOperationLabel, summarizeTimelineValue } from "./state-utilities.js";
2
+ import {
3
+ MAX_TIMELINE_COORDINATOR_DETAILS,
4
+ MAX_TIMELINE_DETAIL_STRING_LENGTH
5
+ } from "./timeline-coordinator-constants.js";
6
+ function boundedString(value) {
7
+ return value.length <= MAX_TIMELINE_DETAIL_STRING_LENGTH ? value : `${value.slice(0, MAX_TIMELINE_DETAIL_STRING_LENGTH - 1)}\u2026`;
8
+ }
9
+ function boundedErrorSummary(error) {
10
+ return boundedString(summarizeTimelineValue(error));
11
+ }
12
+ export function operationTimelineDetail(operation, index, outcome, options = {}) {
13
+ return {
14
+ index,
15
+ ...options.key === void 0 ? {} : { key: boundedString(options.key) },
16
+ operationId: boundedString(operation.operationId),
17
+ operationType: operation.type,
18
+ operationLabel: boundedString(getTimelineOperationLabel(operation)),
19
+ outcome,
20
+ ...options.error === void 0 ? {} : { errorSummary: boundedErrorSummary(options.error) }
21
+ };
22
+ }
23
+ export function parallelTimelineDetails(operations, slots, branchNames) {
24
+ return operations.map((operation, index) => {
25
+ const slot = slots[index];
26
+ return operationTimelineDetail(operation, index, slot?.status === "fulfilled" ? "fulfilled" : "rejected", {
27
+ ...branchNames?.[index] === void 0 ? {} : { key: branchNames[index] },
28
+ ...slot?.status === "rejected" ? { error: slot.reason } : {}
29
+ });
30
+ });
31
+ }
32
+ export function raceTimelineDetails(operations, winnerIndex, branchNames, winnerError) {
33
+ return operations.map((operation, index) => operationTimelineDetail(operation, index, index === winnerIndex ? "won" : "lost", {
34
+ ...branchNames?.[index] === void 0 ? {} : { key: branchNames[index] },
35
+ ...index === winnerIndex && winnerError !== void 0 ? { error: winnerError } : {}
36
+ }));
37
+ }
38
+ export function runAllTimelineDetails(branchNames, slots, branches) {
39
+ return branchNames.map((key, index) => {
40
+ const slot = slots[index], branchFunction = branches[key]?.[0];
41
+ return {
42
+ index,
43
+ key: boundedString(key),
44
+ operationId: boundedString(slot?.operationId ?? `run-all:${String(index)}:${key}`),
45
+ operationType: "activity",
46
+ operationLabel: boundedString(branchFunction?.name || key),
47
+ outcome: slot?.status === "fulfilled" ? "fulfilled" : "rejected",
48
+ ...slot?.status === "rejected" ? { errorSummary: boundedErrorSummary(slot.reason) } : {}
49
+ };
50
+ });
51
+ }
52
+ function boundedDetails(details) {
53
+ return {
54
+ details: details.slice(0, MAX_TIMELINE_COORDINATOR_DETAILS),
55
+ omitted: Math.max(0, details.length - MAX_TIMELINE_COORDINATOR_DETAILS)
56
+ };
57
+ }
58
+ export function recordTimelineBranches(internals, workflowId, details) {
59
+ const entry = internals.pendingTimelineEntries?.get(workflowId)?.entry;
60
+ if (entry === void 0)
61
+ return;
62
+ const bounded = boundedDetails(details);
63
+ entry.branches = bounded.details;
64
+ if (bounded.omitted > 0)
65
+ entry.branchesOmitted = bounded.omitted;
66
+ }
67
+ export function recordTimelineSpeculation(internals, workflowId, details, outcome) {
68
+ const entry = internals.pendingTimelineEntries?.get(workflowId)?.entry;
69
+ if (entry === void 0)
70
+ return;
71
+ const bounded = boundedDetails(details);
72
+ entry.children = bounded.details;
73
+ if (bounded.omitted > 0)
74
+ entry.childrenOmitted = bounded.omitted;
75
+ entry.speculationOutcome = outcome;
76
+ }
@@ -1,9 +1,16 @@
1
- import type { ScheduleOptions, ScheduleOverlapPolicy } from '../../types.ts';
1
+ import type { ScheduleOptions, ScheduleOverlapPolicy, ScheduleUpdateOptions } from '../../types.ts';
2
2
  export declare const SCHEDULE_OVERLAP_POLICIES: Set<ScheduleOverlapPolicy>;
3
3
  type NormalizedScheduleOptions = Required<Pick<ScheduleOptions, 'overlap' | 'backfill'>> & {
4
4
  id?: string;
5
5
  description?: string;
6
6
  jitterMs?: number;
7
7
  };
8
+ type NormalizedScheduleUpdateOptions = {
9
+ description?: string;
10
+ overlap?: ScheduleOverlapPolicy;
11
+ backfill?: boolean;
12
+ jitterMs?: number;
13
+ };
8
14
  export declare function normalizeScheduleOptions(options: ScheduleOptions | undefined): NormalizedScheduleOptions;
15
+ export declare function normalizeScheduleUpdateOptions(options: ScheduleUpdateOptions | undefined): NormalizedScheduleUpdateOptions;
9
16
  export {};
@@ -23,6 +23,24 @@ export function normalizeScheduleOptions(options) {
23
23
  }
24
24
  };
25
25
  }
26
+ export function normalizeScheduleUpdateOptions(options) {
27
+ if (options === void 0)
28
+ return {};
29
+ if (typeof options !== "object" || options === null)
30
+ throw Error("options must be an object when provided");
31
+ return {
32
+ ...options.description !== void 0 && {
33
+ description: normalizeScheduleDescription(options.description)
34
+ },
35
+ ...options.overlap !== void 0 && { overlap: normalizeScheduleOverlap(options.overlap) },
36
+ ...options.backfill !== void 0 && {
37
+ backfill: normalizeScheduleBackfill(options.backfill)
38
+ },
39
+ ...options.jitter !== void 0 && {
40
+ jitterMs: normalizeScheduleJitter(options.jitter, "options.jitter")
41
+ }
42
+ };
43
+ }
26
44
  function normalizeScheduleDescription(description) {
27
45
  if (typeof description !== "string")
28
46
  throw Error("options.description must be a string when provided");
@@ -1,5 +1,5 @@
1
1
  import type { ScheduleFilter, ScheduleOverlapPolicy, ScheduleSpec, ScheduleState, ScheduleStatus } from '../../types.ts';
2
- export { normalizeScheduleOptions, SCHEDULE_OVERLAP_POLICIES } from './schedule-options.ts';
2
+ export { normalizeScheduleOptions, normalizeScheduleUpdateOptions, SCHEDULE_OVERLAP_POLICIES, } from './schedule-options.ts';
3
3
  export declare const SCHEDULE_STATUSES: Set<ScheduleStatus>;
4
4
  export declare function isValidScheduleTimestamp(value: unknown): value is number;
5
5
  export declare function isValidScheduleStatus(value: unknown): value is ScheduleStatus;
@@ -4,7 +4,11 @@ import { parseCronExpression } from "../../schedule.js";
4
4
  import { parseDuration } from "../../scheduler.js";
5
5
  import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
6
6
  import { SCHEDULE_OVERLAP_POLICIES } from "./schedule-options.js";
7
- export { normalizeScheduleOptions, SCHEDULE_OVERLAP_POLICIES } from "./schedule-options.js";
7
+ export {
8
+ normalizeScheduleOptions,
9
+ normalizeScheduleUpdateOptions,
10
+ SCHEDULE_OVERLAP_POLICIES
11
+ } from "./schedule-options.js";
8
12
  export const SCHEDULE_STATUSES = new Set(["active", "paused", "cancelled"]);
9
13
  export function isValidScheduleTimestamp(value) {
10
14
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -206,11 +210,29 @@ function decodeScheduleCurrentWorkflowId(decoded, scheduleId) {
206
210
  }
207
211
  function decodeScheduleQueuedRuns(decoded, scheduleId) {
208
212
  const queuedRuns = decoded.queuedRuns;
209
- if (typeof queuedRuns !== "number" || !Number.isSafeInteger(queuedRuns) || queuedRuns < 0) {
213
+ if (!Array.isArray(queuedRuns)) {
210
214
  rejectInvalidScheduleRecord(scheduleId, "with invalid queuedRuns");
211
215
  return null;
212
216
  }
213
- return queuedRuns;
217
+ const result = [], workflowIds = new Set;
218
+ for (const queuedRun of queuedRuns) {
219
+ if (!isRecord(queuedRun)) {
220
+ rejectInvalidScheduleRecord(scheduleId, "with invalid queuedRuns");
221
+ return null;
222
+ }
223
+ const { workflowId, queuedAt, occurrence } = queuedRun;
224
+ if (!isValidScheduleIdentifier(workflowId) || !isValidScheduleTimestamp(queuedAt) || occurrence !== void 0 && !isValidScheduleTimestamp(occurrence)) {
225
+ rejectInvalidScheduleRecord(scheduleId, "with invalid queuedRuns");
226
+ return null;
227
+ }
228
+ if (workflowIds.has(workflowId)) {
229
+ rejectInvalidScheduleRecord(scheduleId, "with duplicate queued workflow ids");
230
+ return null;
231
+ }
232
+ workflowIds.add(workflowId);
233
+ result.push({ workflowId, queuedAt, ...occurrence !== void 0 && { occurrence } });
234
+ }
235
+ return result;
214
236
  }
215
237
  function decodeScheduleMissedFireCount(decoded, scheduleId) {
216
238
  const missedFireCount = decoded.missedFireCount;
@@ -263,6 +285,8 @@ export function decodeScheduleRuntimeFields(decoded, scheduleId) {
263
285
  const queueFields = decodeScheduleQueueFields(decoded, scheduleId);
264
286
  if (queueFields === null)
265
287
  return null;
288
+ if (currentWorkflow.value !== void 0 && queueFields.queuedRuns.some((queuedRun) => queuedRun.workflowId === currentWorkflow.value))
289
+ return rejectInvalidScheduleRecord(scheduleId, "whose current workflow is also queued");
266
290
  return {
267
291
  ...optionFields,
268
292
  ...timestamps,