@ferricstore/ferricstore 0.12.1 → 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 (98) hide show
  1. package/README.md +2 -2
  2. package/dist/index.cjs +591 -133
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +61 -7
  5. package/dist/index.d.ts +61 -7
  6. package/dist/index.js +586 -129
  7. package/dist/index.js.map +1 -1
  8. package/dist/langgraph.cjs.map +1 -1
  9. package/dist/langgraph.d.cts +1 -1
  10. package/dist/langgraph.d.ts +1 -1
  11. package/dist/langgraph.js.map +1 -1
  12. package/dist/{outcomes-DmBwnq0Y.d.cts → outcomes-CcX956h4.d.cts} +20 -1
  13. package/dist/{outcomes-BbFDp3AH.d.ts → outcomes-DLJYTrX8.d.ts} +20 -1
  14. package/docs/api/assets/hierarchy.js +1 -1
  15. package/docs/api/assets/navigation.js +1 -1
  16. package/docs/api/assets/search.js +1 -1
  17. package/docs/api/classes/ConnectionClosedError.html +1 -1
  18. package/docs/api/classes/FerricStoreClient.html +68 -23
  19. package/docs/api/classes/FerricStoreError.html +1 -1
  20. package/docs/api/classes/FlowAlreadyExistsError.html +1 -1
  21. package/docs/api/classes/FlowBatchError.html +1 -1
  22. package/docs/api/classes/FlowNotFoundError.html +1 -1
  23. package/docs/api/classes/FlowQueryError.html +1 -1
  24. package/docs/api/classes/FlowWrongStateError.html +1 -1
  25. package/docs/api/classes/HTTPTransportError.html +1 -1
  26. package/docs/api/classes/InvalidCommandError.html +1 -1
  27. package/docs/api/classes/LeaseRenewalError.html +3 -3
  28. package/docs/api/classes/LockHeldError.html +1 -1
  29. package/docs/api/classes/LockNotOwnedError.html +1 -1
  30. package/docs/api/classes/OverloadedError.html +1 -1
  31. package/docs/api/classes/QueueCompletionError.html +1 -1
  32. package/docs/api/classes/RequestTimeoutError.html +1 -1
  33. package/docs/api/classes/RerouteError.html +1 -1
  34. package/docs/api/classes/StaleLeaseError.html +1 -1
  35. package/docs/api/classes/StalePolicyGenerationError.html +1 -1
  36. package/docs/api/classes/Workflow.html +19 -19
  37. package/docs/api/classes/WorkflowClient.html +4 -4
  38. package/docs/api/classes/WorkflowContext.html +74 -36
  39. package/docs/api/classes/WorkflowFlowCommands.html +26 -22
  40. package/docs/api/classes/WorkflowWorker.html +6 -6
  41. package/docs/api/functions/complete.html +1 -1
  42. package/docs/api/functions/fail.html +1 -1
  43. package/docs/api/functions/isAlreadyAppliedOutcome.html +41 -0
  44. package/docs/api/functions/isOutcome.html +1 -1
  45. package/docs/api/functions/retry.html +1 -1
  46. package/docs/api/functions/transition.html +1 -1
  47. package/docs/api/hierarchy.html +55 -47
  48. package/docs/api/index.html +2 -2
  49. package/docs/api/interfaces/AdminListOptions.html +1 -1
  50. package/docs/api/interfaces/AdvanceOptions.html +141 -0
  51. package/docs/api/interfaces/AlreadyAppliedOutcome.html +69 -0
  52. package/docs/api/interfaces/AppliedStepOutcome.html +87 -0
  53. package/docs/api/interfaces/CancelOptions.html +6 -6
  54. package/docs/api/interfaces/CompleteOptions.html +4 -4
  55. package/docs/api/interfaces/CompleteOutcome.html +13 -13
  56. package/docs/api/interfaces/CreateManyOptions.html +1 -1
  57. package/docs/api/interfaces/EffectCompensateOptions.html +1 -1
  58. package/docs/api/interfaces/EffectConfirmOptions.html +1 -1
  59. package/docs/api/interfaces/EffectFailOptions.html +1 -1
  60. package/docs/api/interfaces/EffectStatusOptions.html +1 -1
  61. package/docs/api/interfaces/FailOptions.html +4 -4
  62. package/docs/api/interfaces/FailOutcome.html +13 -13
  63. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +1 -1
  64. package/docs/api/interfaces/FerricStoreClientOptions.html +1 -1
  65. package/docs/api/interfaces/FlowQueryCountResult.html +1 -1
  66. package/docs/api/interfaces/FlowQueryIndexBuild.html +1 -1
  67. package/docs/api/interfaces/FlowQueryIndexProgress.html +1 -1
  68. package/docs/api/interfaces/FlowQueryIndexValidation.html +1 -1
  69. package/docs/api/interfaces/FlowQueryRecordsResult.html +1 -1
  70. package/docs/api/interfaces/HistoryOptions.html +16 -16
  71. package/docs/api/interfaces/InvocationCreateOptions.html +1 -1
  72. package/docs/api/interfaces/LeaseMutationOptions.html +1 -1
  73. package/docs/api/interfaces/LimitListOptions.html +1 -1
  74. package/docs/api/interfaces/MutateOptions.html +1 -1
  75. package/docs/api/interfaces/NamedValueMutation.html +9 -9
  76. package/docs/api/interfaces/ReadOptions.html +12 -12
  77. package/docs/api/interfaces/ReclaimOptions.html +1 -1
  78. package/docs/api/interfaces/ReplayedStepResult.html +73 -0
  79. package/docs/api/interfaces/RequestContextOptions.html +1 -1
  80. package/docs/api/interfaces/RetryOptions.html +4 -4
  81. package/docs/api/interfaces/RetryOutcome.html +13 -13
  82. package/docs/api/interfaces/RunStepsItem.html +3 -3
  83. package/docs/api/interfaces/RunStepsManyOptions.html +11 -11
  84. package/docs/api/interfaces/SearchOptions.html +13 -13
  85. package/docs/api/interfaces/StepContinueOptions.html +1 -1
  86. package/docs/api/interfaces/StepOptions.html +159 -0
  87. package/docs/api/interfaces/StepResult.html +67 -0
  88. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  89. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  90. package/docs/api/interfaces/TransitionOptions.html +1 -1
  91. package/docs/api/interfaces/TransitionOutcome.html +14 -14
  92. package/docs/api/modules.html +6 -6
  93. package/docs/api/types/ConnectionRequestDisposition.html +1 -1
  94. package/docs/api/types/ManagementPairs.html +1 -1
  95. package/docs/api/types/Outcome.html +1 -1
  96. package/docs/api/types/SearchStateMeta.html +1 -1
  97. package/docs/api/types/WorkflowStepResult.html +38 -0
  98. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -89,6 +89,7 @@ __export(index_exports, {
89
89
  complete: () => complete,
90
90
  fail: () => fail2,
91
91
  httpCommandDisposition: () => httpCommandDisposition,
92
+ isAlreadyAppliedOutcome: () => isAlreadyAppliedOutcome,
92
93
  isOutcome: () => isOutcome,
93
94
  isReconnectableClosedConnectionError: () => isReconnectableClosedConnectionError,
94
95
  mapException: () => mapException,
@@ -9122,6 +9123,8 @@ var HTTPTransport = class {
9122
9123
  const timer = timeoutMs == null ? void 0 : setLongTimeout(() => controller.abort(requestTimeoutReason), timeoutMs);
9123
9124
  timer?.unref();
9124
9125
  try {
9126
+ const response = await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9127
+ if (response.status !== 401 || this.config.headers.authorization == null) return response;
9125
9128
  return await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9126
9129
  } catch (error) {
9127
9130
  if (controller.signal.aborted) {
@@ -18598,12 +18601,20 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18598
18601
  return this.records(items);
18599
18602
  }
18600
18603
  async claimJobs(type, options) {
18601
- return await this.claimDue(type, {
18604
+ const requestedState = options.state;
18605
+ const includeState = options.includeState ?? requestedState == null;
18606
+ const jobs = await this.claimDue(type, {
18602
18607
  ...options,
18603
- includeState: options.includeState ?? false,
18608
+ includeState,
18604
18609
  jobOnly: true,
18605
18610
  limit: options.limit ?? 100
18606
18611
  });
18612
+ if (!includeState && requestedState != null) {
18613
+ for (const job of jobs) {
18614
+ if (job[CLAIMED_ITEM_WIRE] != null) job.runState ??= requestedState;
18615
+ }
18616
+ }
18617
+ return jobs;
18607
18618
  }
18608
18619
  async reclaim(type, options) {
18609
18620
  const captured = snapshotClaimOptions(options);
@@ -18626,7 +18637,7 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18626
18637
  this.appendPartitionOptions(args, captured);
18627
18638
  append(args, "PRIORITY", captured.priority);
18628
18639
  if (jobOnly) {
18629
- append(args, "RETURN", compactClaimReturnMode2(false, captured.includeAttributes === true));
18640
+ append(args, "RETURN", compactClaimReturnMode2(true, captured.includeAttributes === true));
18630
18641
  }
18631
18642
  appendPayloadRead(args, captured.payload, captured.payloadMaxBytes);
18632
18643
  appendValueReturn(args, { values: captured.values, valueMaxBytes: captured.valueMaxBytes });
@@ -19007,6 +19018,254 @@ var FerricStoreProducerClient = class extends FerricStoreClaimClient {
19007
19018
  }
19008
19019
  };
19009
19020
 
19021
+ // src/client-durable-step.ts
19022
+ var import_node_crypto2 = require("crypto");
19023
+
19024
+ // src/workflow-utilities.ts
19025
+ function resolveWorkflowStates(configured, registrationFor) {
19026
+ const stateNames = [...new Set(configured)];
19027
+ if (stateNames.length === 0) throw new Error("Workflow worker requires at least one state");
19028
+ return stateNames.map((stateName) => {
19029
+ const registration = registrationFor(stateName);
19030
+ if (registration == null) {
19031
+ throw new Error(`No handler registered for workflow state '${stateName}'`);
19032
+ }
19033
+ return { registration, stateName };
19034
+ });
19035
+ }
19036
+ function normalizeStateMode(mode) {
19037
+ if (mode === "fifo" || mode === "parallel") return mode;
19038
+ throw new Error("state mode must be 'fifo' or 'parallel'");
19039
+ }
19040
+ function valueRefToString(value) {
19041
+ if (typeof value === "string") return value;
19042
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
19043
+ if (typeof value === "object" && value != null && Object.hasOwn(value, "ref")) {
19044
+ const ref = value.ref;
19045
+ return typeof ref === "string" ? ref : Buffer.isBuffer(ref) ? ref.toString("utf8") : void 0;
19046
+ }
19047
+ return void 0;
19048
+ }
19049
+
19050
+ // src/client-durable-step.ts
19051
+ var DURABLE_STEP_VALUE_PREFIX = "__ferricstore_step__:sha256:";
19052
+ function durableMutationMayHaveCommitted(error) {
19053
+ if (error instanceof ConnectionClosedError || error instanceof RequestTimeoutError) {
19054
+ return error.requestDisposition === "possibly_sent";
19055
+ }
19056
+ if (error instanceof HTTPTransportError) return error.safeToRetry !== true;
19057
+ if (error instanceof FlowNotFoundError || error instanceof FlowWrongStateError || error instanceof InvalidCommandError || error instanceof StaleLeaseError) {
19058
+ return false;
19059
+ }
19060
+ if (error instanceof FerricStoreError && error.safeToRetry === true) return false;
19061
+ return true;
19062
+ }
19063
+ async function advanceClaim(client, job, options) {
19064
+ assertDurableClaim(job);
19065
+ assertNonEmptyString(options.toState, "toState");
19066
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19067
+ const response = await client.stepContinue(job.id, {
19068
+ attributesDelete: options.attributesDelete,
19069
+ attributesMerge: options.attributesMerge,
19070
+ dropValues: options.dropValues,
19071
+ fencingToken: job.fencingToken,
19072
+ fromState: job.runState,
19073
+ leaseMs: options.leaseMs,
19074
+ leaseToken: job.leaseToken,
19075
+ nowMs: options.nowMs,
19076
+ overrideValues: options.overrideValues,
19077
+ partitionKey,
19078
+ payload: options.payload,
19079
+ returnJob: true,
19080
+ stateMeta: options.stateMeta,
19081
+ type: job.type,
19082
+ toState: options.toState,
19083
+ valueRefs: options.valueRefs,
19084
+ values: options.values
19085
+ });
19086
+ const refreshed = response;
19087
+ if (refreshed[CLAIMED_ITEM_WIRE] != null) {
19088
+ if (refreshed.runState != null && refreshed.runState !== options.toState) {
19089
+ throw new FerricStoreError("compact continuation response returned the wrong runState");
19090
+ }
19091
+ refreshed.runState ??= options.toState;
19092
+ if (refreshed.type == null && job.type != null) refreshed.type = job.type;
19093
+ }
19094
+ assertContinuationClaim(job, refreshed, options.toState);
19095
+ return refreshed;
19096
+ }
19097
+ async function runDurableStep(client, job, options, hooks = {}) {
19098
+ assertDurableClaim(job);
19099
+ const name = options.name;
19100
+ const run2 = options.run;
19101
+ const continuation = snapshotAdvanceOptions(options);
19102
+ assertNonEmptyString(name, "name");
19103
+ assertNonEmptyString(continuation.toState, "toState");
19104
+ if (typeof run2 !== "function") throw new TypeError("run must be a function");
19105
+ const resultName = durableStepValueName(name);
19106
+ assertStepValueIsReserved(continuation, resultName);
19107
+ const leaseMs = continuation.leaseMs ?? 3e4;
19108
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19109
+ const validated = await client.extendLease(job.id, {
19110
+ fencingToken: job.fencingToken,
19111
+ leaseMs,
19112
+ leaseToken: job.leaseToken,
19113
+ nowMs: continuation.nowMs,
19114
+ partitionKey
19115
+ });
19116
+ assertValidatedLease(job, validated);
19117
+ if (validated.valueRefs != null && Object.hasOwn(validated.valueRefs, resultName)) {
19118
+ if (validated.runState !== continuation.toState) {
19119
+ throw new FerricStoreError("committed durable step result does not match the requested target state");
19120
+ }
19121
+ const ref = valueRefToString(validated.valueRefs[resultName]);
19122
+ if (ref == null) {
19123
+ throw new FerricStoreError("committed durable step result has invalid reference metadata");
19124
+ }
19125
+ const entry = (await valueMGetEntries(client, [ref]))[0];
19126
+ if (entry?.found !== true) {
19127
+ throw new FerricStoreError("committed durable step result is missing");
19128
+ }
19129
+ const replayedJob = claimedItemFromRecord(validated);
19130
+ hooks.replayed?.(replayedJob);
19131
+ return { job: replayedJob, result: entry.value };
19132
+ }
19133
+ const result = normalizeStepResult(client, await run2());
19134
+ await hooks.beforeCommit?.();
19135
+ let refreshed;
19136
+ try {
19137
+ refreshed = await advanceClaim(client, validated, {
19138
+ ...continuation,
19139
+ leaseMs,
19140
+ values: { ...continuation.values, [resultName]: result }
19141
+ });
19142
+ } catch (error) {
19143
+ hooks.commitFailed?.(error);
19144
+ throw error;
19145
+ }
19146
+ hooks.committed?.(refreshed);
19147
+ return { job: refreshed, result };
19148
+ }
19149
+ function normalizeStepResult(client, value) {
19150
+ return client.codec.decode(client.codec.encode(value));
19151
+ }
19152
+ function durableStepValueName(name) {
19153
+ const digest = (0, import_node_crypto2.createHash)("sha256").update(name, "utf8").digest("hex");
19154
+ return `${DURABLE_STEP_VALUE_PREFIX}${digest}`;
19155
+ }
19156
+ function snapshotAdvanceOptions(options) {
19157
+ return {
19158
+ ...options.attributesDelete == null ? {} : { attributesDelete: [...options.attributesDelete] },
19159
+ ...options.attributesMerge == null ? {} : { attributesMerge: { ...options.attributesMerge } },
19160
+ ...options.dropValues == null ? {} : { dropValues: [...options.dropValues] },
19161
+ ...options.leaseMs == null ? {} : { leaseMs: options.leaseMs },
19162
+ ...options.nowMs == null ? {} : { nowMs: options.nowMs },
19163
+ ...options.overrideValues == null ? {} : { overrideValues: [...options.overrideValues] },
19164
+ ...options.payload === void 0 ? {} : { payload: options.payload },
19165
+ ...options.stateMeta == null ? {} : { stateMeta: { ...options.stateMeta } },
19166
+ toState: options.toState,
19167
+ ...options.valueRefs == null ? {} : { valueRefs: { ...options.valueRefs } },
19168
+ ...options.values == null ? {} : { values: { ...options.values } }
19169
+ };
19170
+ }
19171
+ function assertDurableClaim(job) {
19172
+ if (typeof job !== "object" || job == null) throw new TypeError("job must be a claimed Flow");
19173
+ assertNonEmptyString(job.id, "job.id");
19174
+ assertNonEmptyString(job.runState, "job.runState");
19175
+ if (!Buffer.isBuffer(job.leaseToken) || job.leaseToken.byteLength === 0) {
19176
+ throw new TypeError("job.leaseToken must be a non-empty Buffer");
19177
+ }
19178
+ if ((typeof job.fencingToken !== "number" || !Number.isSafeInteger(job.fencingToken)) && typeof job.fencingToken !== "bigint") {
19179
+ throw new TypeError("job.fencingToken must be an integer");
19180
+ }
19181
+ if (fencingValue(job.fencingToken) <= 0n) {
19182
+ throw new TypeError("job.fencingToken must be a positive integer");
19183
+ }
19184
+ if (job.state !== "running") {
19185
+ throw new TypeError("job.state must be running");
19186
+ }
19187
+ }
19188
+ function assertValidatedLease(expected, actual) {
19189
+ try {
19190
+ assertDurableClaim(actual);
19191
+ } catch (error) {
19192
+ throw new FerricStoreError("lease validation returned an invalid claim", { cause: error });
19193
+ }
19194
+ if (actual.id !== expected.id) throw leaseValidationMismatch("id");
19195
+ if (normalizedPartition(actual.partitionKey) !== normalizedPartition(expected.partitionKey)) {
19196
+ throw leaseValidationMismatch("partition");
19197
+ }
19198
+ if (!actual.leaseToken.equals(expected.leaseToken)) throw leaseValidationMismatch("lease token");
19199
+ if (fencingValue(actual.fencingToken) !== fencingValue(expected.fencingToken)) {
19200
+ throw leaseValidationMismatch("fencing token");
19201
+ }
19202
+ if (actual.state !== expected.state) throw leaseValidationMismatch("physical state");
19203
+ if (actual.runState !== expected.runState) throw leaseValidationMismatch("runState");
19204
+ }
19205
+ function assertContinuationClaim(previous, refreshed, toState) {
19206
+ try {
19207
+ assertDurableClaim(refreshed);
19208
+ } catch (error) {
19209
+ throw new FerricStoreError("continuation response returned an invalid claim", { cause: error });
19210
+ }
19211
+ if (refreshed.id !== previous.id) throw continuationMismatch("id");
19212
+ if (normalizedPartition(refreshed.partitionKey) !== normalizedPartition(previous.partitionKey)) {
19213
+ throw continuationMismatch("partition");
19214
+ }
19215
+ if (refreshed.leaseToken.equals(previous.leaseToken)) {
19216
+ throw continuationMismatch("lease token did not change");
19217
+ }
19218
+ if (fencingValue(refreshed.fencingToken) <= fencingValue(previous.fencingToken)) {
19219
+ throw continuationMismatch("fencing token did not increase");
19220
+ }
19221
+ if (refreshed.state !== "running") throw continuationMismatch("physical state");
19222
+ if (refreshed.runState !== toState) throw continuationMismatch("runState");
19223
+ }
19224
+ function leaseValidationMismatch(field3) {
19225
+ return new FerricStoreError(`lease validation returned a mismatched ${field3}`);
19226
+ }
19227
+ function continuationMismatch(field3) {
19228
+ return new FerricStoreError(`continuation response returned a mismatched ${field3}`);
19229
+ }
19230
+ function normalizedPartition(value) {
19231
+ return value == null || value === "" ? void 0 : value;
19232
+ }
19233
+ function fencingValue(value) {
19234
+ return typeof value === "bigint" ? value : BigInt(value);
19235
+ }
19236
+ function assertNonEmptyString(value, name) {
19237
+ if (typeof value !== "string" || value.length === 0) {
19238
+ throw new TypeError(`${name} must be a non-empty string`);
19239
+ }
19240
+ }
19241
+ function assertStepValueIsReserved(options, resultName) {
19242
+ if (options.values != null && Object.hasOwn(options.values, resultName)) {
19243
+ throw new TypeError("values cannot mutate the reserved durable step result");
19244
+ }
19245
+ if (options.valueRefs != null && Object.hasOwn(options.valueRefs, resultName)) {
19246
+ throw new TypeError("valueRefs cannot mutate the reserved durable step result");
19247
+ }
19248
+ if (options.dropValues?.includes(resultName) === true) {
19249
+ throw new TypeError("dropValues cannot mutate the reserved durable step result");
19250
+ }
19251
+ if (options.overrideValues?.includes(resultName) === true) {
19252
+ throw new TypeError("overrideValues cannot mutate the reserved durable step result");
19253
+ }
19254
+ }
19255
+ function claimedItemFromRecord(record) {
19256
+ return {
19257
+ ...record.attributes == null ? {} : { attributes: record.attributes },
19258
+ fencingToken: record.fencingToken,
19259
+ id: record.id,
19260
+ leaseToken: Buffer.from(record.leaseToken),
19261
+ ...record.partitionKey === "" ? {} : { partitionKey: record.partitionKey },
19262
+ ...record.payload === void 0 ? {} : { payload: record.payload },
19263
+ ...record.runState == null ? {} : { runState: record.runState },
19264
+ state: record.state,
19265
+ type: record.type
19266
+ };
19267
+ }
19268
+
19010
19269
  // src/client.ts
19011
19270
  var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClient {
19012
19271
  static async fromUrl(url, options = {}) {
@@ -19121,6 +19380,10 @@ var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClie
19121
19380
  appendNamedValues(args, this.codec, options);
19122
19381
  return await this.recordOrGet(await this.commandArgs(args), id, partitionKey);
19123
19382
  }
19383
+ /**
19384
+ * @deprecated Low-level compatibility API. Use {@link advance} for state-only
19385
+ * continuation or {@link step} for a durable closure.
19386
+ */
19124
19387
  async stepContinue(id, options) {
19125
19388
  const partitionKey = options.partitionKey;
19126
19389
  const returnJob = options.returnJob === true;
@@ -19148,6 +19411,18 @@ var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClie
19148
19411
  const response = await this.commandArgs(args);
19149
19412
  return returnJob ? claimedItemFromResp(response, this.codec, { type }) : await this.recordOrGet(response, id, partitionKey);
19150
19413
  }
19414
+ /** Advance a claimed Flow and return its renewed claim. */
19415
+ async advance(job, options) {
19416
+ return await advanceClaim(this, job, options);
19417
+ }
19418
+ /**
19419
+ * Run a closure after validating the claim, then atomically journal its
19420
+ * result and advance the Flow. A committed result is replayed without
19421
+ * invoking the closure again.
19422
+ */
19423
+ async step(job, options) {
19424
+ return await runDurableStep(this, job, options);
19425
+ }
19151
19426
  async runStepsMany(items, options) {
19152
19427
  if (items.length === 0) return Buffer.from("OK");
19153
19428
  const hasStates = options.states != null;
@@ -19397,6 +19672,8 @@ function validateUnicodeScalarText2(value) {
19397
19672
 
19398
19673
  // src/outcomes.ts
19399
19674
  var OUTCOME_BRAND = /* @__PURE__ */ Symbol("ferricstore.outcome");
19675
+ var ALREADY_APPLIED_BRAND = /* @__PURE__ */ Symbol("ferricstore.alreadyApplied");
19676
+ var REPLAYED_STEP_BRAND = /* @__PURE__ */ Symbol("ferricstore.replayedStep");
19400
19677
  function transition(toState, options = {}) {
19401
19678
  return brandOutcome({ ...options, kind: "transition", toState });
19402
19679
  }
@@ -19409,6 +19686,22 @@ function retry(options = {}) {
19409
19686
  function fail2(options = {}) {
19410
19687
  return brandOutcome({ ...options, kind: "fail" });
19411
19688
  }
19689
+ function alreadyApplied(job) {
19690
+ return brandAlreadyApplied({ job, kind: "already_applied" });
19691
+ }
19692
+ function appliedStep(job, result) {
19693
+ return brandAlreadyApplied({ applied: true, job, kind: "already_applied", result });
19694
+ }
19695
+ function replayedStep(job, result) {
19696
+ const replayed = { applied: false, job, result };
19697
+ Object.defineProperty(replayed, REPLAYED_STEP_BRAND, {
19698
+ configurable: false,
19699
+ enumerable: false,
19700
+ value: true,
19701
+ writable: false
19702
+ });
19703
+ return replayed;
19704
+ }
19412
19705
  function isOutcome(value) {
19413
19706
  if (typeof value !== "object" || value == null || !Object.hasOwn(value, OUTCOME_BRAND) || value[OUTCOME_BRAND] !== true) {
19414
19707
  return false;
@@ -19419,6 +19712,12 @@ function isOutcome(value) {
19419
19712
  }
19420
19713
  return outcome.kind === "complete" || outcome.kind === "retry" || outcome.kind === "fail";
19421
19714
  }
19715
+ function isAlreadyAppliedOutcome(value) {
19716
+ return typeof value === "object" && value != null && Object.hasOwn(value, ALREADY_APPLIED_BRAND) && value[ALREADY_APPLIED_BRAND] === true && value.kind === "already_applied";
19717
+ }
19718
+ function isReplayedStepResult(value) {
19719
+ return typeof value === "object" && value != null && Object.hasOwn(value, REPLAYED_STEP_BRAND) && value[REPLAYED_STEP_BRAND] === true && value.applied === false;
19720
+ }
19422
19721
  function brandOutcome(outcome) {
19423
19722
  Object.defineProperty(outcome, "kind", {
19424
19723
  configurable: false,
@@ -19442,6 +19741,21 @@ function brandOutcome(outcome) {
19442
19741
  });
19443
19742
  return outcome;
19444
19743
  }
19744
+ function brandAlreadyApplied(outcome) {
19745
+ Object.defineProperty(outcome, "kind", {
19746
+ configurable: false,
19747
+ enumerable: true,
19748
+ value: "already_applied",
19749
+ writable: false
19750
+ });
19751
+ Object.defineProperty(outcome, ALREADY_APPLIED_BRAND, {
19752
+ configurable: false,
19753
+ enumerable: false,
19754
+ value: true,
19755
+ writable: false
19756
+ });
19757
+ return outcome;
19758
+ }
19445
19759
 
19446
19760
  // src/worker-config.ts
19447
19761
  function snapshotWorkerConfig(options) {
@@ -19458,8 +19772,7 @@ function snapshotWorkerConfig(options) {
19458
19772
  return Object.freeze(snapshot);
19459
19773
  }
19460
19774
 
19461
- // src/worker-internal.ts
19462
- var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19775
+ // src/worker-lease-guard.ts
19463
19776
  var LeaseRenewalError = class extends Error {
19464
19777
  constructor(cause) {
19465
19778
  super("FerricStore lease renewal failed", { cause });
@@ -19470,37 +19783,58 @@ var LeaseRenewalGuard = class {
19470
19783
  constructor(client, leasedJob, leaseMs, options) {
19471
19784
  this.client = client;
19472
19785
  this.leaseMs = leaseMs;
19473
- this.job = snapshotLeasedJob(leasedJob);
19474
- if (options.leaseRenewal === false) {
19475
- return;
19476
- }
19477
- const intervalMs = positiveInteger4(options.leaseRenewIntervalMs, Math.max(1, Math.trunc(leaseMs / 2)));
19478
- this.timer = setLongInterval(() => this.renew(), intervalMs);
19479
- this.timer.unref();
19786
+ this.currentJob = snapshotLeasedJob(leasedJob);
19787
+ this.renewalEnabled = options.leaseRenewal !== false;
19788
+ this.intervalMs = positiveInteger4(options.leaseRenewIntervalMs, Math.max(1, Math.trunc(leaseMs / 2)));
19789
+ this.startRenewal();
19480
19790
  }
19481
19791
  client;
19482
19792
  leaseMs;
19483
19793
  error;
19484
19794
  inFlight;
19485
- job;
19795
+ currentJob;
19796
+ intervalMs;
19797
+ renewalEnabled;
19798
+ state = "active";
19486
19799
  timer;
19800
+ get job() {
19801
+ return this.currentJob;
19802
+ }
19487
19803
  assertActive() {
19488
- if (this.error != null) {
19489
- throw this.error;
19490
- }
19804
+ if (this.error != null) throw this.error;
19491
19805
  }
19492
19806
  async stop() {
19493
- if (this.timer != null) {
19494
- this.timer.cancel();
19495
- this.timer = void 0;
19496
- }
19807
+ this.state = "stopped";
19808
+ this.cancelRenewal();
19497
19809
  await this.inFlight;
19498
19810
  this.assertActive();
19499
19811
  }
19812
+ async pauseForLeaseMutation() {
19813
+ if (this.state === "stopped") throw new Error("lease renewal guard is stopped");
19814
+ if (this.state === "paused") return;
19815
+ this.state = "paused";
19816
+ this.cancelRenewal();
19817
+ await this.inFlight;
19818
+ this.assertActive();
19819
+ }
19820
+ resumeWith(job) {
19821
+ if (this.state !== "paused") throw new Error("lease renewal guard is not paused");
19822
+ this.currentJob = snapshotLeasedJob(job);
19823
+ this.state = "active";
19824
+ this.startRenewal();
19825
+ }
19826
+ cancelRenewal() {
19827
+ if (this.timer == null) return;
19828
+ this.timer.cancel();
19829
+ this.timer = void 0;
19830
+ }
19831
+ startRenewal() {
19832
+ if (!this.renewalEnabled || this.state !== "active") return;
19833
+ this.timer = setLongInterval(() => this.renew(), this.intervalMs);
19834
+ this.timer.unref();
19835
+ }
19500
19836
  renew() {
19501
- if (this.error != null || this.inFlight != null) {
19502
- return;
19503
- }
19837
+ if (this.state !== "active" || this.error != null || this.inFlight != null) return;
19504
19838
  const renewal = this.client.extendLease(this.job.id, {
19505
19839
  fencingToken: this.job.fencingToken,
19506
19840
  leaseMs: this.leaseMs,
@@ -19511,17 +19845,12 @@ var LeaseRenewalGuard = class {
19511
19845
  () => void 0,
19512
19846
  (error) => {
19513
19847
  this.error = new LeaseRenewalError(error);
19514
- if (this.timer != null) {
19515
- this.timer.cancel();
19516
- this.timer = void 0;
19517
- }
19848
+ this.cancelRenewal();
19518
19849
  }
19519
19850
  );
19520
19851
  this.inFlight = renewal;
19521
19852
  void renewal.finally(() => {
19522
- if (this.inFlight === renewal) {
19523
- this.inFlight = void 0;
19524
- }
19853
+ if (this.inFlight === renewal) this.inFlight = void 0;
19525
19854
  });
19526
19855
  }
19527
19856
  };
@@ -19545,8 +19874,14 @@ function snapshotLeasedJob(job) {
19545
19874
  }
19546
19875
  return snapshot;
19547
19876
  }
19877
+ function positiveInteger4(value, fallback) {
19878
+ return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
19879
+ }
19880
+
19881
+ // src/worker-internal.ts
19882
+ var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19548
19883
  function workerConcurrency(options) {
19549
- return positiveInteger4(options.concurrency ?? options.workers, 1);
19884
+ return positiveInteger5(options.concurrency ?? options.workers, 1);
19550
19885
  }
19551
19886
  function workerRefillStrategy(options) {
19552
19887
  return options.refillStrategy === "wave" ? "wave" : "continuous";
@@ -19589,18 +19924,18 @@ function workerErrorPayload(error, options, codec) {
19589
19924
  }
19590
19925
  function workerBatchSize(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19591
19926
  return Math.min(
19592
- positiveInteger4(options.batchSize, 10),
19593
- positiveInteger4(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
19927
+ positiveInteger5(options.batchSize, 10),
19928
+ positiveInteger5(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
19594
19929
  );
19595
19930
  }
19596
19931
  function workerClaimLimit(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19597
19932
  return Math.min(workerBatchSize(options, flowManyBatchLimit), workerConcurrency(options));
19598
19933
  }
19599
19934
  function workerDrainBatches(options) {
19600
- return positiveInteger4(options.claimDrainBatches, 1);
19935
+ return positiveInteger5(options.claimDrainBatches, 1);
19601
19936
  }
19602
19937
  function workerLeaseMs(options, fallback = 3e4) {
19603
- return positiveInteger4(options.leaseMs, positiveInteger4(fallback, 3e4));
19938
+ return positiveInteger5(options.leaseMs, positiveInteger5(fallback, 3e4));
19604
19939
  }
19605
19940
  function workerIdleSleepMs(options) {
19606
19941
  return nonNegativeDuration(options.idleSleepMs, 250);
@@ -19612,7 +19947,7 @@ function workerClaimBlockMs(options, useBlocking) {
19612
19947
  if (options.signal == null || !Number.isFinite(options.blockMs) || options.blockMs < 0) {
19613
19948
  return options.blockMs;
19614
19949
  }
19615
- const abortPollMs = positiveInteger4(options.abortPollMs, 1e3);
19950
+ const abortPollMs = positiveInteger5(options.abortPollMs, 1e3);
19616
19951
  return options.blockMs === 0 ? abortPollMs : Math.min(Math.trunc(options.blockMs), abortPollMs);
19617
19952
  }
19618
19953
  function workerMaxIdleSleepMs(options, idleSleepMs = workerIdleSleepMs(options)) {
@@ -19623,8 +19958,8 @@ function nextWorkerIdleSleepMs(currentIdleSleepMs, maxIdleSleepMs) {
19623
19958
  return Math.min(maxIdleSleepMs, Math.max(1, currentIdleSleepMs * 2));
19624
19959
  }
19625
19960
  async function runContinuousWorkerPool(options) {
19626
- const concurrency = positiveInteger4(options.concurrency, 1);
19627
- const maxClaimSize = positiveInteger4(options.maxClaimSize, concurrency);
19961
+ const concurrency = positiveInteger5(options.concurrency, 1);
19962
+ const maxClaimSize = positiveInteger5(options.maxClaimSize, concurrency);
19628
19963
  const initialIdleSleepMs = nonNegativeDuration(options.idleSleepMs, 250);
19629
19964
  const maxIdleSleepMs = Math.max(
19630
19965
  initialIdleSleepMs,
@@ -19746,7 +20081,7 @@ function normalizeWorkflowWorkerConfig(options) {
19746
20081
  ...captured
19747
20082
  });
19748
20083
  }
19749
- function positiveInteger4(value, fallback) {
20084
+ function positiveInteger5(value, fallback) {
19750
20085
  return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
19751
20086
  }
19752
20087
  function nonNegativeInteger5(value, fallback) {
@@ -19799,7 +20134,7 @@ async function waitForRefillWindow(delayMs, signal) {
19799
20134
  }
19800
20135
 
19801
20136
  // src/queue.ts
19802
- var import_node_crypto2 = require("crypto");
20137
+ var import_node_crypto3 = require("crypto");
19803
20138
 
19804
20139
  // src/queue-completion-batcher.ts
19805
20140
  function queueClaimOptions(queue, options, overrides = {}) {
@@ -20664,7 +20999,7 @@ var Queue = class {
20664
20999
  this.client = client;
20665
21000
  this.type = captured.type;
20666
21001
  this.state = captured.state ?? "queued";
20667
- this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto2.randomUUID)()}`;
21002
+ this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto3.randomUUID)()}`;
20668
21003
  }
20669
21004
  async enqueue(id, options = {}) {
20670
21005
  return await this.client.enqueue(id, {
@@ -20686,53 +21021,55 @@ var Queue = class {
20686
21021
  };
20687
21022
 
20688
21023
  // src/workflow.ts
20689
- var import_node_crypto3 = require("crypto");
21024
+ var import_node_crypto4 = require("crypto");
20690
21025
 
20691
- // src/workflow-utilities.ts
20692
- function resolveWorkflowStates(configured, registrationFor) {
20693
- const stateNames = [...new Set(configured)];
20694
- if (stateNames.length === 0) throw new Error("Workflow worker requires at least one state");
20695
- return stateNames.map((stateName) => {
20696
- const registration = registrationFor(stateName);
20697
- if (registration == null) {
20698
- throw new Error(`No handler registered for workflow state '${stateName}'`);
20699
- }
20700
- return { registration, stateName };
21026
+ // src/workflow-registration.ts
21027
+ function createWorkflowStateRegistration(name, handler, options, defaultValueMaxBytes) {
21028
+ const own2 = { ...options };
21029
+ const claimValues = own2.claimValues == null ? void 0 : snapshotOwnStringArray(own2.claimValues, "claimValues");
21030
+ const retryPolicy = own2.retryPolicy == null ? void 0 : Object.freeze({ ...own2.retryPolicy });
21031
+ return Object.freeze({
21032
+ claimPayload: own2.claimPayload ?? true,
21033
+ claimRecord: own2.claimRecord ?? true,
21034
+ exceptionPolicy: normalizeExceptionPolicy(own2.exceptionPolicy),
21035
+ handler,
21036
+ leaseMs: own2.leaseMs ?? 3e4,
21037
+ ...own2.mode == null ? {} : { mode: normalizeStateMode(own2.mode) },
21038
+ name,
21039
+ returnRecord: own2.returnRecord ?? false,
21040
+ valueMaxBytes: own2.valueMaxBytes ?? defaultValueMaxBytes,
21041
+ ...claimValues == null ? {} : { claimValues },
21042
+ ...retryPolicy == null ? {} : { retryPolicy }
20701
21043
  });
20702
21044
  }
20703
- function normalizeStateMode(mode) {
20704
- if (mode === "fifo" || mode === "parallel") return mode;
20705
- throw new Error("state mode must be 'fifo' or 'parallel'");
20706
- }
20707
- function valueRefToString(value) {
20708
- if (typeof value === "string") return value;
20709
- if (Buffer.isBuffer(value)) return value.toString("utf8");
20710
- if (typeof value === "object" && value != null && Object.hasOwn(value, "ref")) {
20711
- const ref = value.ref;
20712
- return typeof ref === "string" ? ref : Buffer.isBuffer(ref) ? ref.toString("utf8") : void 0;
20713
- }
20714
- return void 0;
20715
- }
20716
21045
 
20717
21046
  // src/workflow-context.ts
20718
21047
  var MISSING_VALUE = /* @__PURE__ */ Symbol("ferricstore.missingValue");
20719
21048
  var WorkflowContext = class {
20720
- workflow;
20721
- job;
20722
- stateName;
20723
- flow;
20724
- leaseJob;
20725
- valueCache = /* @__PURE__ */ new Map();
20726
- constructor(workflow, job, stateName, leaseJob = job) {
21049
+ constructor(workflow, job, stateName, leaseJob = job, mutationCoordinator) {
21050
+ this.mutationCoordinator = mutationCoordinator;
20727
21051
  this.workflow = workflow;
20728
21052
  this.job = job;
20729
21053
  this.leaseJob = leaseJob;
20730
- this.stateName = stateName;
21054
+ this.currentStateName = stateName;
20731
21055
  this.flow = new WorkflowFlowCommands(this);
20732
21056
  }
21057
+ mutationCoordinator;
21058
+ workflow;
21059
+ job;
21060
+ flow;
21061
+ appliedMutation = false;
21062
+ currentStateName;
21063
+ leaseJob;
21064
+ mutationFailure;
21065
+ mutationPhase = "idle";
21066
+ valueCache = /* @__PURE__ */ new Map();
20733
21067
  get client() {
20734
21068
  return this.workflow.client;
20735
21069
  }
21070
+ get stateName() {
21071
+ return this.currentStateName;
21072
+ }
20736
21073
  get id() {
20737
21074
  return this.leaseJob.id;
20738
21075
  }
@@ -20743,7 +21080,7 @@ var WorkflowContext = class {
20743
21080
  return this.leaseJob.state;
20744
21081
  }
20745
21082
  get logicalState() {
20746
- return this.stateName;
21083
+ return this.currentStateName;
20747
21084
  }
20748
21085
  get partitionKey() {
20749
21086
  return this.leaseJob.partitionKey;
@@ -20781,6 +21118,53 @@ var WorkflowContext = class {
20781
21118
  get correlationId() {
20782
21119
  return "correlationId" in this.job ? this.job.correlationId : void 0;
20783
21120
  }
21121
+ async advance(toState, options = {}) {
21122
+ if (typeof toState !== "string" || toState.length === 0) {
21123
+ throw new TypeError("toState must be a non-empty string");
21124
+ }
21125
+ const current = this.durableClaim();
21126
+ await this.beforeCommit();
21127
+ let refreshed;
21128
+ try {
21129
+ refreshed = await advanceClaim(this.client, current, { ...options, toState });
21130
+ } catch (error) {
21131
+ this.commitFailed(error);
21132
+ throw error;
21133
+ }
21134
+ this.committed(refreshed);
21135
+ return alreadyApplied(refreshed);
21136
+ }
21137
+ async step(options) {
21138
+ let applied = false;
21139
+ const stepped = await runDurableStep(this.client, this.durableClaim(), options, {
21140
+ beforeCommit: async () => await this.beforeCommit(),
21141
+ commitFailed: (error) => this.commitFailed(error),
21142
+ committed: (job) => {
21143
+ applied = true;
21144
+ this.committed(job);
21145
+ },
21146
+ replayed: (job) => this.replayed(job)
21147
+ });
21148
+ return applied ? appliedStep(stepped.job, stepped.result) : replayedStep(stepped.job, stepped.result);
21149
+ }
21150
+ /** @internal */
21151
+ get hasAppliedMutation() {
21152
+ return this.appliedMutation;
21153
+ }
21154
+ /** @internal */
21155
+ get uncertainMutationFailure() {
21156
+ return this.mutationPhase === "uncertain" ? this.mutationFailure : void 0;
21157
+ }
21158
+ /** @internal */
21159
+ get hasUncertainMutation() {
21160
+ return this.mutationPhase === "uncertain";
21161
+ }
21162
+ /** @internal */
21163
+ assertAppliedOutcome(outcome) {
21164
+ if (!this.appliedMutation || !outcome.job.leaseToken.equals(this.leaseJob.leaseToken) || outcome.job.fencingToken !== this.leaseJob.fencingToken) {
21165
+ throw new Error("already-applied outcome does not match the workflow's refreshed claim");
21166
+ }
21167
+ }
20784
21168
  async value(name, defaultValue, options = {}) {
20785
21169
  const useLocalCache = options.localCache ?? this.workflow.valueConfig.localCache;
20786
21170
  if (useLocalCache && this.valueCache.has(name)) {
@@ -20860,7 +21244,49 @@ var WorkflowContext = class {
20860
21244
  return result;
20861
21245
  }
20862
21246
  valueMaxBytes() {
20863
- return this.workflow.stateRegistration(this.stateName)?.valueMaxBytes;
21247
+ return this.workflow.stateRegistration(this.currentStateName)?.valueMaxBytes;
21248
+ }
21249
+ durableClaim() {
21250
+ const runState = this.leaseJob.runState ?? (this.leaseJob[CLAIMED_ITEM_WIRE] == null ? void 0 : this.currentStateName);
21251
+ if (typeof runState !== "string" || runState.length === 0) {
21252
+ throw new TypeError("job.runState must be a non-empty string");
21253
+ }
21254
+ return {
21255
+ ...this.leaseJob,
21256
+ leaseToken: Buffer.from(this.leaseJob.leaseToken),
21257
+ runState
21258
+ };
21259
+ }
21260
+ async beforeCommit() {
21261
+ if (this.mutationPhase !== "idle") {
21262
+ throw new Error("a durable workflow mutation is already in progress or has an uncertain result");
21263
+ }
21264
+ await this.mutationCoordinator?.pause();
21265
+ this.mutationPhase = "committing";
21266
+ }
21267
+ commitFailed(error) {
21268
+ if (!durableMutationMayHaveCommitted(error)) {
21269
+ this.mutationFailure = void 0;
21270
+ this.mutationPhase = "idle";
21271
+ this.mutationCoordinator?.resume(this.leaseJob);
21272
+ return;
21273
+ }
21274
+ this.mutationFailure = error;
21275
+ this.mutationPhase = "uncertain";
21276
+ }
21277
+ committed(job) {
21278
+ this.acceptJob(job);
21279
+ this.appliedMutation = true;
21280
+ this.mutationCoordinator?.resume(job);
21281
+ }
21282
+ replayed(job) {
21283
+ this.acceptJob(job);
21284
+ }
21285
+ acceptJob(job) {
21286
+ this.leaseJob = job;
21287
+ this.currentStateName = job.runState ?? this.currentStateName;
21288
+ this.mutationFailure = void 0;
21289
+ this.mutationPhase = "idle";
20864
21290
  }
20865
21291
  };
20866
21292
  var WorkflowFlowCommands = class {
@@ -20898,6 +21324,7 @@ var WorkflowFlowCommands = class {
20898
21324
  toState
20899
21325
  });
20900
21326
  }
21327
+ /** @deprecated Use `ctx.advance(toState)`. */
20901
21328
  async stepContinue(toState, options = {}) {
20902
21329
  return await this.ctx.client.stepContinue(this.ctx.id, {
20903
21330
  ...options,
@@ -21060,24 +21487,77 @@ async function applyFail(client, ctx, outcome, returnRecord) {
21060
21487
  });
21061
21488
  }
21062
21489
 
21063
- // src/workflow-registration.ts
21064
- function createWorkflowStateRegistration(name, handler, options, defaultValueMaxBytes) {
21065
- const own2 = { ...options };
21066
- const claimValues = own2.claimValues == null ? void 0 : snapshotOwnStringArray(own2.claimValues, "claimValues");
21067
- const retryPolicy = own2.retryPolicy == null ? void 0 : Object.freeze({ ...own2.retryPolicy });
21068
- return Object.freeze({
21069
- claimPayload: own2.claimPayload ?? true,
21070
- claimRecord: own2.claimRecord ?? true,
21071
- exceptionPolicy: normalizeExceptionPolicy(own2.exceptionPolicy),
21072
- handler,
21073
- leaseMs: own2.leaseMs ?? 3e4,
21074
- ...own2.mode == null ? {} : { mode: normalizeStateMode(own2.mode) },
21075
- name,
21076
- returnRecord: own2.returnRecord ?? false,
21077
- valueMaxBytes: own2.valueMaxBytes ?? defaultValueMaxBytes,
21078
- ...claimValues == null ? {} : { claimValues },
21079
- ...retryPolicy == null ? {} : { retryPolicy }
21490
+ // src/workflow-job-execution.ts
21491
+ async function executeWorkflowJob(workflow, workerOptions, job, registration, guard) {
21492
+ guard.assertActive();
21493
+ const ctx = new WorkflowContext(workflow, job, registration.name, guard.job, {
21494
+ pause: async () => await guard.pauseForLeaseMutation(),
21495
+ resume: (refreshed) => guard.resumeWith(refreshed)
21080
21496
  });
21497
+ let value;
21498
+ try {
21499
+ value = await registration.handler(ctx);
21500
+ } catch (error) {
21501
+ await guard.stop();
21502
+ if (ctx.hasUncertainMutation) throw asError(error, "durable workflow mutation result is uncertain");
21503
+ await applyHandlerError(workflow, workerOptions, ctx, registration, error);
21504
+ return;
21505
+ }
21506
+ await guard.stop();
21507
+ if (ctx.hasUncertainMutation) {
21508
+ throw asError(ctx.uncertainMutationFailure, "durable workflow mutation result is uncertain");
21509
+ }
21510
+ if (isAlreadyAppliedOutcome(value)) {
21511
+ ctx.assertAppliedOutcome(value);
21512
+ await releaseAppliedMutation(workflow, ctx);
21513
+ return;
21514
+ }
21515
+ if (isReplayedStepResult(value)) {
21516
+ await applyWorkflowOutcome(
21517
+ workflow.client,
21518
+ ctx,
21519
+ complete({ result: value.result }),
21520
+ registration.returnRecord
21521
+ );
21522
+ return;
21523
+ }
21524
+ if (isOutcome(value)) {
21525
+ await applyWorkflowOutcome(workflow.client, ctx, value, registration.returnRecord);
21526
+ return;
21527
+ }
21528
+ if (ctx.hasAppliedMutation) {
21529
+ await releaseAppliedMutation(workflow, ctx);
21530
+ return;
21531
+ }
21532
+ await applyWorkflowOutcome(
21533
+ workflow.client,
21534
+ ctx,
21535
+ complete({ result: value }),
21536
+ registration.returnRecord
21537
+ );
21538
+ }
21539
+ function asError(value, message) {
21540
+ return value instanceof Error ? value : new Error(message, { cause: value });
21541
+ }
21542
+ async function releaseAppliedMutation(workflow, ctx) {
21543
+ await workflow.client.transition(ctx.id, {
21544
+ fencingToken: ctx.fencingToken,
21545
+ fromState: ctx.state,
21546
+ leaseToken: ctx.leaseToken,
21547
+ partitionKey: ctx.partitionKey,
21548
+ toState: ctx.logicalState
21549
+ });
21550
+ }
21551
+ async function applyHandlerError(workflow, workerOptions, ctx, registration, error) {
21552
+ const policy = workerOptions.exceptionPolicy ?? registration.exceptionPolicy;
21553
+ if (policy === "raise") throw error;
21554
+ const encodedError = workerErrorPayload(error, workerOptions, workflow.client.codec);
21555
+ await applyWorkflowOutcome(
21556
+ workflow.client,
21557
+ ctx,
21558
+ policy === "fail" ? fail2({ error: encodedError }) : retry({ error: encodedError }),
21559
+ registration.returnRecord
21560
+ );
21081
21561
  }
21082
21562
 
21083
21563
  // src/workflow.ts
@@ -21106,7 +21586,7 @@ var Workflow = class {
21106
21586
  localCache: captured.valueConfig?.localCache ?? false,
21107
21587
  valueMaxBytes: captured.valueConfig?.valueMaxBytes
21108
21588
  };
21109
- this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto3.randomUUID)()}`;
21589
+ this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto4.randomUUID)()}`;
21110
21590
  }
21111
21591
  state(name, handler, options = {}) {
21112
21592
  this.states.set(name, createWorkflowStateRegistration(
@@ -21207,6 +21687,10 @@ var WorkflowWorker = class {
21207
21687
  claimed += jobs.length;
21208
21688
  const guards = jobs.map((job) => new LeaseRenewalGuard(this.workflow.client, job, leaseMs, this.options));
21209
21689
  let cursor = 0;
21690
+ let batchFailure;
21691
+ const recordFailure = (error) => {
21692
+ batchFailure ??= { error };
21693
+ };
21210
21694
  const runNext = async () => {
21211
21695
  while (cursor < jobs.length) {
21212
21696
  const index = cursor;
@@ -21217,19 +21701,22 @@ var WorkflowWorker = class {
21217
21701
  try {
21218
21702
  await this.applyJob(job, registration, guard);
21219
21703
  applied += 1;
21704
+ } catch (error) {
21705
+ recordFailure(error);
21220
21706
  } finally {
21221
- await guard.stop();
21707
+ try {
21708
+ await guard.stop();
21709
+ } catch (error) {
21710
+ recordFailure(error);
21711
+ }
21222
21712
  }
21223
21713
  }
21224
21714
  };
21225
21715
  try {
21226
- const handlers = await Promise.allSettled(
21716
+ await Promise.all(
21227
21717
  Array.from({ length: Math.min(concurrency, jobs.length) }, () => runNext())
21228
21718
  );
21229
- const failed = handlers.find((handler) => handler.status === "rejected");
21230
- if (failed != null) {
21231
- throw failed.reason;
21232
- }
21719
+ if (batchFailure != null) throwError(batchFailure.error);
21233
21720
  } finally {
21234
21721
  await Promise.allSettled(
21235
21722
  guards.slice(cursor).map(async (guard) => await guard.stop())
@@ -21340,42 +21827,12 @@ var WorkflowWorker = class {
21340
21827
  });
21341
21828
  }
21342
21829
  async applyJob(job, registration, guard) {
21343
- guard.assertActive();
21344
- const ctx = new WorkflowContext(this.workflow, job, registration.name, guard.job);
21345
- let outcome;
21346
- try {
21347
- const value = await registration.handler(ctx);
21348
- outcome = isOutcome(value) ? value : complete({ result: value });
21349
- } catch (error) {
21350
- await guard.stop();
21351
- await this.applyHandlerError(ctx, registration, error);
21352
- return;
21353
- }
21354
- await guard.stop();
21355
- await applyWorkflowOutcome(this.workflow.client, ctx, outcome, registration.returnRecord);
21356
- }
21357
- async applyHandlerError(ctx, registration, error) {
21358
- const policy = this.options.exceptionPolicy ?? registration.exceptionPolicy;
21359
- if (policy === "raise") {
21360
- throw error;
21361
- }
21362
- if (policy === "fail") {
21363
- await applyWorkflowOutcome(
21364
- this.workflow.client,
21365
- ctx,
21366
- fail2({ error: workerErrorPayload(error, this.options, this.workflow.client.codec) }),
21367
- registration.returnRecord
21368
- );
21369
- return;
21370
- }
21371
- await applyWorkflowOutcome(
21372
- this.workflow.client,
21373
- ctx,
21374
- retry({ error: workerErrorPayload(error, this.options, this.workflow.client.codec) }),
21375
- registration.returnRecord
21376
- );
21830
+ await executeWorkflowJob(this.workflow, this.options, job, registration, guard);
21377
21831
  }
21378
21832
  };
21833
+ function throwError(value) {
21834
+ throw value instanceof Error ? value : new Error("workflow worker failed", { cause: value });
21835
+ }
21379
21836
 
21380
21837
  // src/version.ts
21381
21838
  var FERRICSTORE_SDK_VERSION = "0.12.0";
@@ -21442,6 +21899,7 @@ var FERRICSTORE_NATIVE_PROTOCOL_VERSION = 1;
21442
21899
  complete,
21443
21900
  fail,
21444
21901
  httpCommandDisposition,
21902
+ isAlreadyAppliedOutcome,
21445
21903
  isOutcome,
21446
21904
  isReconnectableClosedConnectionError,
21447
21905
  mapException,