@ferricstore/ferricstore 0.12.2 → 0.13.1

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 (110) hide show
  1. package/README.md +45 -3
  2. package/dist/index.cjs +804 -198
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +87 -9
  5. package/dist/index.d.ts +87 -9
  6. package/dist/index.js +798 -194
  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/openai-agents.cjs.map +1 -1
  13. package/dist/openai-agents.js.map +1 -1
  14. package/dist/{outcomes-DmBwnq0Y.d.cts → outcomes-CcX956h4.d.cts} +20 -1
  15. package/dist/{outcomes-BbFDp3AH.d.ts → outcomes-DLJYTrX8.d.ts} +20 -1
  16. package/docs/api/assets/hierarchy.js +1 -1
  17. package/docs/api/assets/highlight.css +9 -2
  18. package/docs/api/assets/navigation.js +1 -1
  19. package/docs/api/assets/search.js +1 -1
  20. package/docs/api/classes/ConnectionClosedError.html +5 -5
  21. package/docs/api/classes/FerricStoreClient.html +68 -23
  22. package/docs/api/classes/FerricStoreError.html +2 -1
  23. package/docs/api/classes/FlowAlreadyExistsError.html +3 -3
  24. package/docs/api/classes/FlowBatchError.html +1 -1
  25. package/docs/api/classes/FlowNotFoundError.html +3 -3
  26. package/docs/api/classes/FlowQueryError.html +1 -1
  27. package/docs/api/classes/FlowWrongStateError.html +3 -3
  28. package/docs/api/classes/HTTPTransportError.html +14 -8
  29. package/docs/api/classes/InvalidCommandError.html +3 -3
  30. package/docs/api/classes/LeaseRenewalError.html +3 -3
  31. package/docs/api/classes/LockHeldError.html +3 -3
  32. package/docs/api/classes/LockNotOwnedError.html +3 -3
  33. package/docs/api/classes/NativeAdapter.html +3 -3
  34. package/docs/api/classes/OverloadedError.html +5 -5
  35. package/docs/api/classes/QueueCompletionError.html +1 -1
  36. package/docs/api/classes/RequestNotSentError.html +225 -0
  37. package/docs/api/classes/RequestTimeoutError.html +6 -6
  38. package/docs/api/classes/RerouteError.html +3 -3
  39. package/docs/api/classes/StaleLeaseError.html +3 -3
  40. package/docs/api/classes/StalePolicyGenerationError.html +3 -3
  41. package/docs/api/classes/Workflow.html +19 -19
  42. package/docs/api/classes/WorkflowClient.html +4 -4
  43. package/docs/api/classes/WorkflowContext.html +73 -37
  44. package/docs/api/classes/WorkflowFlowCommands.html +26 -22
  45. package/docs/api/classes/WorkflowWorker.html +6 -6
  46. package/docs/api/functions/classifyServerError.html +1 -1
  47. package/docs/api/functions/complete.html +1 -1
  48. package/docs/api/functions/fail.html +1 -1
  49. package/docs/api/functions/isAlreadyAppliedOutcome.html +41 -0
  50. package/docs/api/functions/isOutcome.html +1 -1
  51. package/docs/api/functions/mapException.html +1 -1
  52. package/docs/api/functions/retry.html +1 -1
  53. package/docs/api/functions/transition.html +1 -1
  54. package/docs/api/hierarchy.html +56 -47
  55. package/docs/api/index.html +32 -4
  56. package/docs/api/interfaces/AdminListOptions.html +1 -1
  57. package/docs/api/interfaces/AdvanceOptions.html +145 -0
  58. package/docs/api/interfaces/AlreadyAppliedOutcome.html +69 -0
  59. package/docs/api/interfaces/AppliedStepOutcome.html +87 -0
  60. package/docs/api/interfaces/CancelOptions.html +13 -9
  61. package/docs/api/interfaces/CompleteOptions.html +13 -9
  62. package/docs/api/interfaces/CompleteOutcome.html +13 -13
  63. package/docs/api/interfaces/CreateManyOptions.html +1 -1
  64. package/docs/api/interfaces/EffectCompensateOptions.html +1 -1
  65. package/docs/api/interfaces/EffectConfirmOptions.html +1 -1
  66. package/docs/api/interfaces/EffectFailOptions.html +1 -1
  67. package/docs/api/interfaces/EffectStatusOptions.html +1 -1
  68. package/docs/api/interfaces/ExtendLeaseOptions.html +10 -8
  69. package/docs/api/interfaces/FailOptions.html +13 -9
  70. package/docs/api/interfaces/FailOutcome.html +13 -13
  71. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +1 -1
  72. package/docs/api/interfaces/FerricStoreClientOptions.html +1 -1
  73. package/docs/api/interfaces/FlowQueryCountResult.html +1 -1
  74. package/docs/api/interfaces/FlowQueryIndexBuild.html +1 -1
  75. package/docs/api/interfaces/FlowQueryIndexProgress.html +1 -1
  76. package/docs/api/interfaces/FlowQueryIndexValidation.html +1 -1
  77. package/docs/api/interfaces/FlowQueryRecordsResult.html +1 -1
  78. package/docs/api/interfaces/HistoryOptions.html +16 -16
  79. package/docs/api/interfaces/InvocationCreateOptions.html +1 -1
  80. package/docs/api/interfaces/LeaseMutationOptions.html +11 -7
  81. package/docs/api/interfaces/LimitListOptions.html +1 -1
  82. package/docs/api/interfaces/MutateOptions.html +8 -4
  83. package/docs/api/interfaces/NamedValueMutation.html +9 -9
  84. package/docs/api/interfaces/ReadOptions.html +12 -12
  85. package/docs/api/interfaces/ReclaimOptions.html +1 -1
  86. package/docs/api/interfaces/ReplayedStepResult.html +73 -0
  87. package/docs/api/interfaces/RequestContextOptions.html +1 -1
  88. package/docs/api/interfaces/RetryOptions.html +13 -9
  89. package/docs/api/interfaces/RetryOutcome.html +13 -13
  90. package/docs/api/interfaces/RunStepsItem.html +3 -3
  91. package/docs/api/interfaces/RunStepsManyOptions.html +11 -11
  92. package/docs/api/interfaces/SearchOptions.html +13 -13
  93. package/docs/api/interfaces/StepContinueOptions.html +16 -12
  94. package/docs/api/interfaces/StepOptions.html +166 -0
  95. package/docs/api/interfaces/StepResult.html +67 -0
  96. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  97. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  98. package/docs/api/interfaces/TransitionOptions.html +15 -11
  99. package/docs/api/interfaces/TransitionOutcome.html +14 -14
  100. package/docs/api/media/design.md +12 -1
  101. package/docs/api/modules.html +8 -8
  102. package/docs/api/types/ConnectionRequestDisposition.html +1 -1
  103. package/docs/api/types/ManagementPairs.html +1 -1
  104. package/docs/api/types/Outcome.html +1 -1
  105. package/docs/api/types/RequestDisposition.html +1 -1
  106. package/docs/api/types/SearchStateMeta.html +1 -1
  107. package/docs/api/types/WorkflowStepResult.html +38 -0
  108. package/docs/api/variables/FERRICSTORE_SDK_VERSION.html +1 -1
  109. package/docs/design.md +12 -1
  110. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -69,6 +69,7 @@ __export(index_exports, {
69
69
  QueueWorker: () => QueueWorker,
70
70
  RawCodec: () => RawCodec,
71
71
  ReconnectingExecutor: () => ReconnectingExecutor,
72
+ RequestNotSentError: () => RequestNotSentError,
72
73
  RequestTimeoutError: () => RequestTimeoutError,
73
74
  RerouteError: () => RerouteError,
74
75
  RoutingTopology: () => RoutingTopology,
@@ -89,6 +90,7 @@ __export(index_exports, {
89
90
  complete: () => complete,
90
91
  fail: () => fail2,
91
92
  httpCommandDisposition: () => httpCommandDisposition,
93
+ isAlreadyAppliedOutcome: () => isAlreadyAppliedOutcome,
92
94
  isOutcome: () => isOutcome,
93
95
  isReconnectableClosedConnectionError: () => isReconnectableClosedConnectionError,
94
96
  mapException: () => mapException,
@@ -130,9 +132,16 @@ var FerricStoreError = class extends Error {
130
132
  };
131
133
  var HTTPTransportError = class extends FerricStoreError {
132
134
  code = "http_transport";
135
+ requestDisposition;
133
136
  statusCode;
134
137
  constructor(message, options = {}) {
135
- super(message, options);
138
+ const disposition = options.requestDisposition;
139
+ super(message, {
140
+ ...options,
141
+ retryable: options.retryable ?? (disposition === "unsent" ? false : void 0),
142
+ safeToRetry: options.safeToRetry ?? (disposition === "unsent" ? true : disposition === "possibly_sent" ? false : void 0)
143
+ });
144
+ this.requestDisposition = disposition;
136
145
  this.statusCode = options.statusCode;
137
146
  }
138
147
  };
@@ -157,6 +166,20 @@ var RequestTimeoutError = class extends FerricStoreError {
157
166
  this.timeoutMs = timeoutMs;
158
167
  }
159
168
  };
169
+ var RequestNotSentError = class extends FerricStoreError {
170
+ code = "request_not_sent";
171
+ requestDisposition = "unsent";
172
+ constructor(message, options = {}) {
173
+ super(message, { ...options, retryable: false, safeToRetry: true });
174
+ }
175
+ };
176
+ function requestNotSentError(error, message) {
177
+ if (error instanceof RequestNotSentError) return error;
178
+ return new RequestNotSentError(
179
+ message ?? (error instanceof Error ? error.message : String(error)),
180
+ { cause: error, raw: error }
181
+ );
182
+ }
160
183
  var FlowNotFoundError = class extends FerricStoreError {
161
184
  code = "flow_not_found";
162
185
  };
@@ -6415,7 +6438,10 @@ function decodeCompactPipeline(data, budget, claimModes) {
6415
6438
  );
6416
6439
  offset = read.offset;
6417
6440
  } else {
6418
- throw new FerricStoreError("unknown compact pipeline status");
6441
+ throw new FerricStoreError(`unknown compact pipeline status ${status}`, {
6442
+ retryable: false,
6443
+ safeToRetry: false
6444
+ });
6419
6445
  }
6420
6446
  }
6421
6447
  if (offset !== data.byteLength) throw new FerricStoreError("trailing compact pipeline bytes");
@@ -6585,6 +6611,7 @@ var COUNTED_COMPACT_TAGS = /* @__PURE__ */ new Set([
6585
6611
  ]);
6586
6612
 
6587
6613
  // src/protocol-response.ts
6614
+ var KNOWN_NATIVE_RESPONSE_STATUSES = /* @__PURE__ */ new Set([0, 1, 2, 3, 4, 5, 6]);
6588
6615
  function decodeResponse(frame, expectedOpcode, hints = {}) {
6589
6616
  if (frame.opcode !== expectedOpcode) {
6590
6617
  throw new FerricStoreError(
@@ -6609,6 +6636,13 @@ function decodeResponse(frame, expectedOpcode, hints = {}) {
6609
6636
  customPayload
6610
6637
  );
6611
6638
  if (status === STATUS_OK) return value;
6639
+ if (!KNOWN_NATIVE_RESPONSE_STATUSES.has(status)) {
6640
+ throw new FerricStoreError(`unknown native response status ${status}`, {
6641
+ raw: value,
6642
+ retryable: false,
6643
+ safeToRetry: false
6644
+ });
6645
+ }
6612
6646
  const message = protocolErrorMessage(status, value);
6613
6647
  throw classifyServerError(message, value, void 0, status);
6614
6648
  }
@@ -6648,7 +6682,7 @@ function unwrapPipelineResponse(value, options = {}, expectedItems) {
6648
6682
  continue;
6649
6683
  }
6650
6684
  const error = classifyServerError(
6651
- protocolErrorMessage(status === "busy" ? 4 : 1, payload),
6685
+ protocolErrorMessage(pipelineStatusCode(status), payload),
6652
6686
  payload,
6653
6687
  void 0,
6654
6688
  status
@@ -6657,15 +6691,46 @@ function unwrapPipelineResponse(value, options = {}, expectedItems) {
6657
6691
  out[index] = error;
6658
6692
  continue;
6659
6693
  }
6694
+ if (pipelineStatusToken(item[0]) != null) {
6695
+ const unknown = pipelineStatusToken(item[0]) ?? "unknown";
6696
+ const error = new FerricStoreError(`unknown native pipeline status ${JSON.stringify(unknown)}`, {
6697
+ raw: item,
6698
+ retryable: false,
6699
+ safeToRetry: false
6700
+ });
6701
+ if (options.throwOnItemError !== false) throw error;
6702
+ out[index] = error;
6703
+ continue;
6704
+ }
6660
6705
  }
6661
6706
  out[index] = item;
6662
6707
  }
6663
6708
  return out;
6664
6709
  }
6665
6710
  function pipelineStatus(value) {
6711
+ const status = pipelineStatusToken(value);
6712
+ if (status == null) return null;
6713
+ return status === "ok" || status === "error" || status === "auth" || status === "noperm" || status === "busy" || status === "reroute" || status === "bad_request" ? status : null;
6714
+ }
6715
+ function pipelineStatusToken(value) {
6666
6716
  if (typeof value !== "string" && !import_node_buffer26.Buffer.isBuffer(value) && !(value instanceof Uint8Array)) return null;
6667
- const status = asText(value).toLowerCase();
6668
- return status === "ok" || status === "busy" || status === "error" ? status : null;
6717
+ return asText(value).toLowerCase();
6718
+ }
6719
+ function pipelineStatusCode(status) {
6720
+ switch (status) {
6721
+ case "error":
6722
+ return 1;
6723
+ case "auth":
6724
+ return 2;
6725
+ case "noperm":
6726
+ return 3;
6727
+ case "busy":
6728
+ return 4;
6729
+ case "reroute":
6730
+ return 5;
6731
+ case "bad_request":
6732
+ return 6;
6733
+ }
6669
6734
  }
6670
6735
  function decodeResponseValue(opcode, body, hints, allowCompact, customPayload) {
6671
6736
  if (customPayload && allowCompact) {
@@ -8325,6 +8390,22 @@ function plainRecord(value) {
8325
8390
  return value;
8326
8391
  }
8327
8392
 
8393
+ // src/native-command-preparation.ts
8394
+ function prepareNativeCommand(args, maxFrameBytes) {
8395
+ try {
8396
+ return buildProtocolCommand(args, maxFrameBytes);
8397
+ } catch (error) {
8398
+ throw requestNotSentError(error);
8399
+ }
8400
+ }
8401
+ function encodeNativeRequest(command, requestId, maxFrameBytes) {
8402
+ try {
8403
+ return encodeRequest(command, requestId, maxFrameBytes);
8404
+ } catch (error) {
8405
+ throw requestNotSentError(error);
8406
+ }
8407
+ }
8408
+
8328
8409
  // src/native-adapter.ts
8329
8410
  var NativeAdapter = class _NativeAdapter {
8330
8411
  socket;
@@ -8432,15 +8513,13 @@ var NativeAdapter = class _NativeAdapter {
8432
8513
  return await this.executeCommandArgs(args);
8433
8514
  }
8434
8515
  async executeCommandArgs(args) {
8435
- const command = buildProtocolCommand(args, this.capabilities.requestFrameBytes);
8516
+ const command = prepareNativeCommand(args, this.capabilities.requestFrameBytes);
8436
8517
  return await this.request(command);
8437
8518
  }
8438
8519
  /** @internal Execute raw command arguments on a topology-selected lane. */
8439
8520
  async executeCommandOnLane(args, laneId) {
8440
- return await this.executeProtocolCommand(
8441
- buildProtocolCommand(args, this.capabilities.requestFrameBytes),
8442
- laneId
8443
- );
8521
+ const command = prepareNativeCommand(args, this.capabilities.requestFrameBytes);
8522
+ return await this.executeProtocolCommand(command, laneId);
8444
8523
  }
8445
8524
  async executeProtocolCommand(command, laneId) {
8446
8525
  return await this.request(laneId == null ? command : { ...command, laneId });
@@ -8534,14 +8613,14 @@ var NativeAdapter = class _NativeAdapter {
8534
8613
  let frame;
8535
8614
  try {
8536
8615
  requestId = this.requestScheduler.nextRequestId();
8537
- frame = encodeRequest(
8616
+ frame = encodeNativeRequest(
8538
8617
  withFlowQueryDeadline(command, timeoutMs),
8539
8618
  requestId,
8540
8619
  this.capabilities.requestFrameBytes
8541
8620
  );
8542
8621
  } catch (error) {
8543
8622
  if (hasFlowControlCredit) this.flowControl.release(laneId);
8544
- return Promise.reject(error instanceof Error ? error : new FerricStoreError(String(error), { raw: error }));
8623
+ return Promise.reject(error instanceof Error ? error : requestNotSentError(error));
8545
8624
  }
8546
8625
  return new Promise((resolve, reject) => {
8547
8626
  const timer = timeoutMs == null ? void 0 : setLongTimeout(() => {
@@ -9116,12 +9195,16 @@ var HTTPTransport = class {
9116
9195
  #requests = /* @__PURE__ */ new Set();
9117
9196
  #closed = false;
9118
9197
  async post(body, timeoutMs) {
9119
- if (this.#closed) throw new HTTPTransportError("HTTP transport is closed");
9198
+ if (this.#closed) {
9199
+ throw new HTTPTransportError("HTTP transport is closed", { requestDisposition: "unsent" });
9200
+ }
9120
9201
  const controller = new AbortController();
9121
9202
  this.#requests.add(controller);
9122
9203
  const timer = timeoutMs == null ? void 0 : setLongTimeout(() => controller.abort(requestTimeoutReason), timeoutMs);
9123
9204
  timer?.unref();
9124
9205
  try {
9206
+ const response = await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9207
+ if (response.status !== 401 || this.config.headers.authorization == null) return response;
9125
9208
  return await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9126
9209
  } catch (error) {
9127
9210
  if (controller.signal.aborted) {
@@ -9131,9 +9214,22 @@ var HTTPTransport = class {
9131
9214
  raw: { retryable: true, safe_to_retry: false }
9132
9215
  });
9133
9216
  }
9134
- if (error instanceof HTTPTransportError || error instanceof RequestTimeoutError) throw error;
9217
+ if (error instanceof RequestTimeoutError) throw error;
9218
+ if (error instanceof HTTPTransportError) {
9219
+ if (error.requestDisposition != null) throw error;
9220
+ throw new HTTPTransportError(error.message, {
9221
+ cause: error.cause ?? error,
9222
+ raw: error.raw,
9223
+ requestDisposition: "possibly_sent",
9224
+ retryAfterMs: error.retryAfterMs,
9225
+ retryable: error.retryable,
9226
+ safeToRetry: false,
9227
+ statusCode: error.statusCode
9228
+ });
9229
+ }
9135
9230
  throw new HTTPTransportError("HTTP command request failed", {
9136
9231
  cause: error,
9232
+ requestDisposition: "possibly_sent",
9137
9233
  retryable: true,
9138
9234
  safeToRetry: false
9139
9235
  });
@@ -9145,7 +9241,9 @@ var HTTPTransport = class {
9145
9241
  async close() {
9146
9242
  if (this.#closed) return;
9147
9243
  this.#closed = true;
9148
- const error = new HTTPTransportError("HTTP transport is closed");
9244
+ const error = new HTTPTransportError("HTTP transport is closed", {
9245
+ requestDisposition: "possibly_sent"
9246
+ });
9149
9247
  for (const controller of this.#requests) controller.abort(error);
9150
9248
  this.#httpAgent.destroy();
9151
9249
  this.#httpsAgent.destroy();
@@ -9388,16 +9486,22 @@ var HTTPAdapter = class _HTTPAdapter {
9388
9486
  await this.#transport.close();
9389
9487
  }
9390
9488
  async executeBatch(commands) {
9391
- if (commands.length > this.#config.maxBatchItems) {
9392
- throw new HTTPTransportError("HTTP command batch exceeds maxBatchItems");
9393
- }
9394
- for (const command of commands) assertHTTPCommandSupported(command[0]);
9395
- const prepared = commands.map((command) => prepareHTTPCommand(command, this.#config.maxRequestBytes));
9396
- const body = encodeHTTPCommands(prepared.map((command) => command.encoded), this.#config.maxRequestBytes);
9397
- if (body.byteLength > this.#config.maxRequestBytes) {
9398
- throw new HTTPTransportError("HTTP command request exceeds maxRequestBytes");
9489
+ let body;
9490
+ let serverBlockMs;
9491
+ try {
9492
+ if (commands.length > this.#config.maxBatchItems) {
9493
+ throw new HTTPTransportError("HTTP command batch exceeds maxBatchItems");
9494
+ }
9495
+ for (const command of commands) assertHTTPCommandSupported(command[0]);
9496
+ const prepared = commands.map((command) => prepareHTTPCommand(command, this.#config.maxRequestBytes));
9497
+ body = encodeHTTPCommands(prepared.map((command) => command.encoded), this.#config.maxRequestBytes);
9498
+ if (body.byteLength > this.#config.maxRequestBytes) {
9499
+ throw new HTTPTransportError("HTTP command request exceeds maxRequestBytes");
9500
+ }
9501
+ serverBlockMs = combinedServerBlockMs(prepared.map((command) => command.serverBlockMs));
9502
+ } catch (error) {
9503
+ throw unsentHTTPError(error);
9399
9504
  }
9400
- const serverBlockMs = combinedServerBlockMs(prepared.map((command) => command.serverBlockMs));
9401
9505
  const response = await this.#transport.post(
9402
9506
  body,
9403
9507
  serverResponseTimeoutMs(this.#config.timeoutMs, serverBlockMs)
@@ -9454,16 +9558,32 @@ function commandError(value) {
9454
9558
  }
9455
9559
  function topLevelError(status, envelope, retryAfter) {
9456
9560
  const details = isRecord2(envelope.error) ? envelope.error : {};
9561
+ const code = typeof details.code === "string" ? details.code.toLowerCase() : void 0;
9457
9562
  const message = typeof details.message === "string" ? details.message : `HTTP command request failed with status ${status}`;
9458
9563
  const retryAfterMs2 = retryAfterMilliseconds(retryAfter);
9564
+ const ambiguousTimeout = status === 408 || code === "request_timeout";
9565
+ const safeToRetry = !ambiguousTimeout && (details.safe_to_retry === true || definitelyRejectedHTTPStatus(status));
9459
9566
  return new HTTPTransportError(message, {
9460
9567
  raw: details,
9461
9568
  retryable: status === 408 || status === 425 || status === 429 || status >= 500,
9462
9569
  retryAfterMs: retryAfterMs2,
9463
- safeToRetry: false,
9570
+ requestDisposition: "possibly_sent",
9571
+ safeToRetry,
9464
9572
  statusCode: status
9465
9573
  });
9466
9574
  }
9575
+ function definitelyRejectedHTTPStatus(status) {
9576
+ return status === 400 || status === 401 || status === 403 || status === 404 || status === 405 || status === 406 || status === 411 || status === 413 || status === 414 || status === 415 || status === 422 || status === 426 || status === 431;
9577
+ }
9578
+ function unsentHTTPError(error) {
9579
+ if (error instanceof HTTPTransportError && error.requestDisposition === "unsent") return error;
9580
+ const message = error instanceof Error ? error.message : String(error);
9581
+ return new HTTPTransportError(message, {
9582
+ cause: error,
9583
+ raw: error instanceof FerricStoreError ? error.raw ?? error : error,
9584
+ requestDisposition: "unsent"
9585
+ });
9586
+ }
9467
9587
  function retryAfterMilliseconds(value) {
9468
9588
  if (value == null) return void 0;
9469
9589
  if (/^\d+$/u.test(value)) {
@@ -9556,9 +9676,9 @@ var ReconnectingExecutor = class {
9556
9676
  const snapshot = snapshotPipelineCommands(commands);
9557
9677
  const snapshotOptions = snapshotPipelineOptions(options) ?? {};
9558
9678
  for (const command of snapshot) assertCommandHasStableConnectionState(command);
9559
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9679
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9560
9680
  const initialExecutor = await this.executorPromise;
9561
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9681
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9562
9682
  if (initialExecutor.executePipeline == null) {
9563
9683
  return await executeCommandArraysIndividually(
9564
9684
  async (command) => await this.executeCommandArgs(command),
@@ -9579,9 +9699,9 @@ var ReconnectingExecutor = class {
9579
9699
  const snapshot = snapshotPipelineCommands(commands);
9580
9700
  const snapshotOptions = snapshotPipelineOptions(options) ?? {};
9581
9701
  for (const command of snapshot) assertCommandHasStableConnectionState(command);
9582
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9702
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9583
9703
  const initialExecutor = await this.executorPromise;
9584
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9704
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9585
9705
  if (initialExecutor.executeFusedPipeline == null) return void 0;
9586
9706
  try {
9587
9707
  return await initialExecutor.executeFusedPipeline(snapshot, snapshotOptions);
@@ -9614,7 +9734,7 @@ var ReconnectingExecutor = class {
9614
9734
  return;
9615
9735
  }
9616
9736
  this.closed = true;
9617
- this.retryAbortController.abort(new FerricStoreError("FerricStore client is closed"));
9737
+ this.retryAbortController.abort(new RequestNotSentError("FerricStore client is closed"));
9618
9738
  const executorPromise = this.executorPromise;
9619
9739
  const reconnectPromise = this.reconnectPromise;
9620
9740
  this.closePromise = (async () => {
@@ -9636,7 +9756,7 @@ var ReconnectingExecutor = class {
9636
9756
  let executor = await this.executorPromise;
9637
9757
  let retries = 0;
9638
9758
  for (; ; ) {
9639
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9759
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9640
9760
  try {
9641
9761
  return await operation(executor);
9642
9762
  } catch (error) {
@@ -9665,17 +9785,17 @@ var ReconnectingExecutor = class {
9665
9785
  try {
9666
9786
  await sleep(Math.min(this.maxDelayMs, base + jitter), this.retryAbortController.signal);
9667
9787
  } catch (error) {
9668
- if (this.closed) throw new FerricStoreError("FerricStore client is closed", { cause: error });
9788
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed", { cause: error });
9669
9789
  throw error;
9670
9790
  }
9671
9791
  }
9672
9792
  async reconnect(staleExecutor) {
9673
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9793
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9674
9794
  const competingReconnect = this.reconnectPromise;
9675
9795
  if (competingReconnect != null) return await competingReconnect;
9676
9796
  const observedExecutorPromise = this.executorPromise;
9677
9797
  const currentExecutor = await observedExecutorPromise.catch(() => void 0);
9678
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9798
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9679
9799
  if (this.reconnectPromise != null) return await this.reconnectPromise;
9680
9800
  if (this.executorPromise !== observedExecutorPromise) return await this.executorPromise;
9681
9801
  if (currentExecutor != null && currentExecutor !== staleExecutor) return currentExecutor;
@@ -9684,7 +9804,7 @@ var ReconnectingExecutor = class {
9684
9804
  const nextExecutor = await this.createExecutor(this.retryAbortController.signal);
9685
9805
  if (this.closed) {
9686
9806
  await Promise.resolve(nextExecutor.close?.()).catch(() => void 0);
9687
- throw new FerricStoreError("FerricStore client is closed");
9807
+ throw new RequestNotSentError("FerricStore client is closed");
9688
9808
  }
9689
9809
  this.executorPromise = Promise.resolve(nextExecutor);
9690
9810
  return nextExecutor;
@@ -11094,7 +11214,7 @@ var TopologyNativeAdapterPool = class _TopologyNativeAdapterPool {
11094
11214
  );
11095
11215
  }
11096
11216
  assertOpen() {
11097
- if (this.closed) throw new FerricStoreError("FerricStore topology adapter pool is closed");
11217
+ if (this.closed) throw new RequestNotSentError("FerricStore topology adapter pool is closed");
11098
11218
  }
11099
11219
  };
11100
11220
 
@@ -12220,7 +12340,7 @@ var AutoBatchExecutor = class {
12220
12340
  }
12221
12341
  async executeCommandArgs(args) {
12222
12342
  if (this.closed) {
12223
- throw new FerricStoreError("FerricStore client is closed");
12343
+ throw new RequestNotSentError("FerricStore client is closed");
12224
12344
  }
12225
12345
  const command = snapshotCommandArguments(args);
12226
12346
  if (this.executor.executePipeline == null) {
@@ -12240,7 +12360,7 @@ var AutoBatchExecutor = class {
12240
12360
  if (orderingBarrier != null) {
12241
12361
  await orderingBarrier;
12242
12362
  if (this.closed) {
12243
- throw new FerricStoreError("FerricStore client is closed");
12363
+ throw new RequestNotSentError("FerricStore client is closed");
12244
12364
  }
12245
12365
  }
12246
12366
  return await new Promise((resolve, reject) => {
@@ -12254,7 +12374,7 @@ var AutoBatchExecutor = class {
12254
12374
  }
12255
12375
  async executePipeline(commands, options) {
12256
12376
  if (this.closed) {
12257
- throw new FerricStoreError("FerricStore client is closed");
12377
+ throw new RequestNotSentError("FerricStore client is closed");
12258
12378
  }
12259
12379
  const snapshot = snapshotPipelineCommands(commands);
12260
12380
  const snapshotOptions = snapshotPipelineOptions(options);
@@ -12271,7 +12391,7 @@ var AutoBatchExecutor = class {
12271
12391
  }
12272
12392
  async executeFusedPipeline(commands, options) {
12273
12393
  if (this.closed) {
12274
- throw new FerricStoreError("FerricStore client is closed");
12394
+ throw new RequestNotSentError("FerricStore client is closed");
12275
12395
  }
12276
12396
  if (this.executor.executeFusedPipeline == null) return void 0;
12277
12397
  const snapshot = snapshotPipelineCommands(commands);
@@ -12282,7 +12402,7 @@ var AutoBatchExecutor = class {
12282
12402
  }
12283
12403
  async refreshTopology() {
12284
12404
  if (this.closed) {
12285
- throw new FerricStoreError("FerricStore client is closed");
12405
+ throw new RequestNotSentError("FerricStore client is closed");
12286
12406
  }
12287
12407
  const refreshTopology = this.executor.refreshTopology?.bind(this.executor);
12288
12408
  if (refreshTopology == null) {
@@ -12292,7 +12412,7 @@ var AutoBatchExecutor = class {
12292
12412
  }
12293
12413
  async route(key) {
12294
12414
  if (this.closed) {
12295
- throw new FerricStoreError("FerricStore client is closed");
12415
+ throw new RequestNotSentError("FerricStore client is closed");
12296
12416
  }
12297
12417
  const route = this.executor.route?.bind(this.executor);
12298
12418
  if (route == null) {
@@ -12312,7 +12432,7 @@ var AutoBatchExecutor = class {
12312
12432
  this.timer.cancel();
12313
12433
  this.timer = void 0;
12314
12434
  }
12315
- this.failPending(new Error("FerricStore client closed before auto-batch flush"));
12435
+ this.failPending(new RequestNotSentError("FerricStore client closed before auto-batch flush"));
12316
12436
  await this.orderingBarrier;
12317
12437
  await this.waitForInFlightBatches();
12318
12438
  await Promise.allSettled([...this.inFlightHelpers]);
@@ -12322,7 +12442,7 @@ var AutoBatchExecutor = class {
12322
12442
  }
12323
12443
  async executeAfterPendingBatches(operation) {
12324
12444
  if (this.closed) {
12325
- throw new FerricStoreError("FerricStore client is closed");
12445
+ throw new RequestNotSentError("FerricStore client is closed");
12326
12446
  }
12327
12447
  const previousBarrier = this.orderingBarrier;
12328
12448
  let releaseBarrier;
@@ -12343,7 +12463,7 @@ var AutoBatchExecutor = class {
12343
12463
  }
12344
12464
  executeHelper(operation) {
12345
12465
  if (this.closed) {
12346
- return Promise.reject(new FerricStoreError("FerricStore client is closed"));
12466
+ return Promise.reject(new RequestNotSentError("FerricStore client is closed"));
12347
12467
  }
12348
12468
  const task = Promise.resolve().then(operation);
12349
12469
  this.inFlightHelpers.add(task);
@@ -12365,7 +12485,7 @@ var AutoBatchExecutor = class {
12365
12485
  await previousBarrier;
12366
12486
  await this.flushNow();
12367
12487
  if (this.closed) {
12368
- throw new FerricStoreError("FerricStore client is closed");
12488
+ throw new RequestNotSentError("FerricStore client is closed");
12369
12489
  }
12370
12490
  dispatched = operation();
12371
12491
  } finally {
@@ -14704,7 +14824,7 @@ var FerricStoreClientBase = class {
14704
14824
  return validatePipelineResponse(results, snapshot.length);
14705
14825
  }
14706
14826
  async close() {
14707
- this.closeAbortController.abort(new FerricStoreError("FerricStore client is closed"));
14827
+ this.closeAbortController.abort(new RequestNotSentError("FerricStore client is closed"));
14708
14828
  this.closePromise ??= (async () => await this.executor.close?.())();
14709
14829
  await this.closePromise;
14710
14830
  }
@@ -17780,7 +17900,7 @@ async function executeProducerWriteWithBackpressure(execute, policy, signal) {
17780
17900
  function throwIfClosed(signal) {
17781
17901
  if (!signal.aborted) return;
17782
17902
  if (signal.reason instanceof Error) throw signal.reason;
17783
- throw new FerricStoreError("FerricStore client is closed", { raw: signal.reason });
17903
+ throw new RequestNotSentError("FerricStore client is closed", { raw: signal.reason });
17784
17904
  }
17785
17905
 
17786
17906
  // src/flow-policy.ts
@@ -18598,12 +18718,20 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18598
18718
  return this.records(items);
18599
18719
  }
18600
18720
  async claimJobs(type, options) {
18601
- return await this.claimDue(type, {
18721
+ const requestedState = options.state;
18722
+ const includeState = options.includeState ?? requestedState == null;
18723
+ const jobs = await this.claimDue(type, {
18602
18724
  ...options,
18603
- includeState: options.includeState ?? false,
18725
+ includeState,
18604
18726
  jobOnly: true,
18605
18727
  limit: options.limit ?? 100
18606
18728
  });
18729
+ if (!includeState && requestedState != null) {
18730
+ for (const job of jobs) {
18731
+ if (job[CLAIMED_ITEM_WIRE] != null) job.runState ??= requestedState;
18732
+ }
18733
+ }
18734
+ return jobs;
18607
18735
  }
18608
18736
  async reclaim(type, options) {
18609
18737
  const captured = snapshotClaimOptions(options);
@@ -18626,7 +18754,7 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18626
18754
  this.appendPartitionOptions(args, captured);
18627
18755
  append(args, "PRIORITY", captured.priority);
18628
18756
  if (jobOnly) {
18629
- append(args, "RETURN", compactClaimReturnMode2(false, captured.includeAttributes === true));
18757
+ append(args, "RETURN", compactClaimReturnMode2(true, captured.includeAttributes === true));
18630
18758
  }
18631
18759
  appendPayloadRead(args, captured.payload, captured.payloadMaxBytes);
18632
18760
  appendValueReturn(args, { values: captured.values, valueMaxBytes: captured.valueMaxBytes });
@@ -19007,6 +19135,291 @@ var FerricStoreProducerClient = class extends FerricStoreClaimClient {
19007
19135
  }
19008
19136
  };
19009
19137
 
19138
+ // src/client-durable-step.ts
19139
+ var import_node_crypto2 = require("crypto");
19140
+
19141
+ // src/workflow-utilities.ts
19142
+ function resolveWorkflowStates(configured, registrationFor) {
19143
+ const stateNames = [...new Set(configured)];
19144
+ if (stateNames.length === 0) throw new Error("Workflow worker requires at least one state");
19145
+ return stateNames.map((stateName) => {
19146
+ const registration = registrationFor(stateName);
19147
+ if (registration == null) {
19148
+ throw new Error(`No handler registered for workflow state '${stateName}'`);
19149
+ }
19150
+ return { registration, stateName };
19151
+ });
19152
+ }
19153
+ function normalizeStateMode(mode) {
19154
+ if (mode === "fifo" || mode === "parallel") return mode;
19155
+ throw new Error("state mode must be 'fifo' or 'parallel'");
19156
+ }
19157
+ function valueRefToString(value) {
19158
+ if (typeof value === "string") return value;
19159
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
19160
+ if (typeof value === "object" && value != null && Object.hasOwn(value, "ref")) {
19161
+ const ref = value.ref;
19162
+ return typeof ref === "string" ? ref : Buffer.isBuffer(ref) ? ref.toString("utf8") : void 0;
19163
+ }
19164
+ return void 0;
19165
+ }
19166
+
19167
+ // src/client-durable-step.ts
19168
+ var DURABLE_STEP_VALUE_PREFIX = "__ferricstore_step__:sha256:";
19169
+ function durableMutationMayHaveCommitted(error) {
19170
+ if (error instanceof ConnectionClosedError || error instanceof RequestTimeoutError) {
19171
+ return error.requestDisposition === "possibly_sent";
19172
+ }
19173
+ if (error instanceof HTTPTransportError) return error.safeToRetry !== true;
19174
+ if (error instanceof FlowNotFoundError || error instanceof FlowWrongStateError || error instanceof InvalidCommandError || error instanceof StaleLeaseError) {
19175
+ return false;
19176
+ }
19177
+ if (error instanceof FerricStoreError && error.safeToRetry === true) return false;
19178
+ return true;
19179
+ }
19180
+ async function advanceClaim(client, job, options) {
19181
+ assertDurableClaim(job);
19182
+ assertNonEmptyString(options.toState, "toState");
19183
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19184
+ const response = await client.stepContinue(job.id, {
19185
+ attributesDelete: options.attributesDelete,
19186
+ attributesMerge: options.attributesMerge,
19187
+ dropValues: options.dropValues,
19188
+ fencingToken: job.fencingToken,
19189
+ fromState: job.runState,
19190
+ leaseMs: options.leaseMs,
19191
+ leaseToken: job.leaseToken,
19192
+ nowMs: options.nowMs,
19193
+ overrideValues: options.overrideValues,
19194
+ partitionKey,
19195
+ payload: options.payload,
19196
+ returnJob: true,
19197
+ stateMeta: options.stateMeta,
19198
+ type: job.type,
19199
+ toState: options.toState,
19200
+ valueRefs: options.valueRefs,
19201
+ values: options.values
19202
+ });
19203
+ const refreshed = response;
19204
+ if (refreshed[CLAIMED_ITEM_WIRE] != null) {
19205
+ if (refreshed.runState != null && refreshed.runState !== options.toState) {
19206
+ throw new FerricStoreError("compact continuation response returned the wrong runState");
19207
+ }
19208
+ refreshed.runState ??= options.toState;
19209
+ if (refreshed.type == null && job.type != null) refreshed.type = job.type;
19210
+ }
19211
+ assertContinuationClaim(job, refreshed, options.toState);
19212
+ return refreshed;
19213
+ }
19214
+ async function runDurableStep(client, job, options, hooks = {}) {
19215
+ assertDurableClaim(job);
19216
+ const name = options.name;
19217
+ const run2 = options.run;
19218
+ const continuation = snapshotAdvanceOptions(options);
19219
+ assertNonEmptyString(name, "name");
19220
+ if (name.trim().length === 0) throw new TypeError("name must not be blank");
19221
+ if (!name.isWellFormed()) throw new TypeError("name must contain valid Unicode");
19222
+ assertNonEmptyString(continuation.toState, "toState");
19223
+ if (typeof run2 !== "function") throw new TypeError("run must be a function");
19224
+ const resultName = durableStepValueName(name);
19225
+ assertStepValueIsReserved(continuation, resultName);
19226
+ const leaseMs = continuation.leaseMs ?? 3e4;
19227
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19228
+ const validated = await client.extendLease(job.id, {
19229
+ fencingToken: job.fencingToken,
19230
+ leaseMs,
19231
+ leaseToken: job.leaseToken,
19232
+ nowMs: continuation.nowMs,
19233
+ partitionKey
19234
+ });
19235
+ assertValidatedLease(job, validated);
19236
+ if (validated.valueRefs != null && Object.hasOwn(validated.valueRefs, resultName)) {
19237
+ if (validated.runState !== continuation.toState) {
19238
+ throw new FerricStoreError("committed durable step result does not match the requested target state");
19239
+ }
19240
+ const ref = valueRefToString(validated.valueRefs[resultName]);
19241
+ if (ref == null) {
19242
+ throw new FerricStoreError("committed durable step result has invalid reference metadata");
19243
+ }
19244
+ const entry = (await valueMGetEntries(client, [ref]))[0];
19245
+ if (entry?.found !== true) {
19246
+ throw new FerricStoreError("committed durable step result is missing");
19247
+ }
19248
+ const replayedJob = claimedItemFromRecord(validated);
19249
+ notifyWithoutReplacingOutcome(hooks.replayed, replayedJob);
19250
+ return { job: replayedJob, result: entry.value };
19251
+ }
19252
+ const result = normalizeStepResult(client, await run2());
19253
+ await hooks.beforeCommit?.();
19254
+ let refreshed;
19255
+ try {
19256
+ refreshed = await advanceClaim(client, validated, {
19257
+ ...continuation,
19258
+ leaseMs,
19259
+ values: { ...continuation.values, [resultName]: result }
19260
+ });
19261
+ } catch (error) {
19262
+ notifyWithoutReplacingOutcome(hooks.commitFailed, error);
19263
+ throw error;
19264
+ }
19265
+ notifyWithoutReplacingOutcome(hooks.committed, refreshed);
19266
+ return { job: refreshed, result };
19267
+ }
19268
+ function notifyWithoutReplacingOutcome(callback, value) {
19269
+ try {
19270
+ callback?.(value);
19271
+ } catch {
19272
+ }
19273
+ }
19274
+ function normalizeStepResult(client, value) {
19275
+ return client.codec.decode(client.codec.encode(value));
19276
+ }
19277
+ function durableStepValueName(name) {
19278
+ const digest = (0, import_node_crypto2.createHash)("sha256").update(name, "utf8").digest("hex");
19279
+ return `${DURABLE_STEP_VALUE_PREFIX}${digest}`;
19280
+ }
19281
+ function snapshotAdvanceOptions(options) {
19282
+ return {
19283
+ ...options.attributesDelete == null ? {} : { attributesDelete: [...options.attributesDelete] },
19284
+ ...options.attributesMerge == null ? {} : { attributesMerge: { ...options.attributesMerge } },
19285
+ ...options.dropValues == null ? {} : { dropValues: [...options.dropValues] },
19286
+ ...options.leaseMs == null ? {} : { leaseMs: options.leaseMs },
19287
+ ...options.nowMs == null ? {} : { nowMs: options.nowMs },
19288
+ ...options.overrideValues == null ? {} : { overrideValues: [...options.overrideValues] },
19289
+ ...options.payload === void 0 ? {} : { payload: options.payload },
19290
+ ...options.stateMeta == null ? {} : { stateMeta: { ...options.stateMeta } },
19291
+ toState: options.toState,
19292
+ ...options.valueRefs == null ? {} : { valueRefs: { ...options.valueRefs } },
19293
+ ...options.values == null ? {} : { values: { ...options.values } }
19294
+ };
19295
+ }
19296
+ function assertDurableClaim(job) {
19297
+ if (typeof job !== "object" || job == null) throw new TypeError("job must be a claimed Flow");
19298
+ assertNonEmptyString(job.id, "job.id");
19299
+ assertNonEmptyString(job.runState, "job.runState");
19300
+ if (!Buffer.isBuffer(job.leaseToken) || job.leaseToken.byteLength === 0) {
19301
+ throw new TypeError("job.leaseToken must be a non-empty Buffer");
19302
+ }
19303
+ if ((typeof job.fencingToken !== "number" || !Number.isSafeInteger(job.fencingToken)) && typeof job.fencingToken !== "bigint") {
19304
+ throw new TypeError("job.fencingToken must be an integer");
19305
+ }
19306
+ if (fencingValue(job.fencingToken) <= 0n) {
19307
+ throw new TypeError("job.fencingToken must be a positive integer");
19308
+ }
19309
+ if (job.state !== "running") {
19310
+ throw new TypeError("job.state must be running");
19311
+ }
19312
+ }
19313
+ function assertValidatedLease(expected, actual) {
19314
+ try {
19315
+ assertDurableClaim(actual);
19316
+ } catch (error) {
19317
+ throw new FerricStoreError("lease validation returned an invalid claim", { cause: error });
19318
+ }
19319
+ if (actual.id !== expected.id) throw leaseValidationMismatch("id");
19320
+ if (normalizedPartition(actual.partitionKey) !== normalizedPartition(expected.partitionKey)) {
19321
+ throw leaseValidationMismatch("partition");
19322
+ }
19323
+ if (!actual.leaseToken.equals(expected.leaseToken)) throw leaseValidationMismatch("lease token");
19324
+ if (fencingValue(actual.fencingToken) !== fencingValue(expected.fencingToken)) {
19325
+ throw leaseValidationMismatch("fencing token");
19326
+ }
19327
+ if (actual.state !== expected.state) throw leaseValidationMismatch("physical state");
19328
+ if (actual.runState !== expected.runState) throw leaseValidationMismatch("runState");
19329
+ }
19330
+ function assertContinuationClaim(previous, refreshed, toState) {
19331
+ try {
19332
+ assertDurableClaim(refreshed);
19333
+ } catch (error) {
19334
+ throw new FerricStoreError("continuation response returned an invalid claim", { cause: error });
19335
+ }
19336
+ if (refreshed.id !== previous.id) throw continuationMismatch("id");
19337
+ if (normalizedPartition(refreshed.partitionKey) !== normalizedPartition(previous.partitionKey)) {
19338
+ throw continuationMismatch("partition");
19339
+ }
19340
+ if (refreshed.leaseToken.equals(previous.leaseToken)) {
19341
+ throw continuationMismatch("lease token did not change");
19342
+ }
19343
+ if (fencingValue(refreshed.fencingToken) <= fencingValue(previous.fencingToken)) {
19344
+ throw continuationMismatch("fencing token did not increase");
19345
+ }
19346
+ if (refreshed.state !== "running") throw continuationMismatch("physical state");
19347
+ if (refreshed.runState !== toState) throw continuationMismatch("runState");
19348
+ }
19349
+ function leaseValidationMismatch(field3) {
19350
+ return new FerricStoreError(`lease validation returned a mismatched ${field3}`);
19351
+ }
19352
+ function continuationMismatch(field3) {
19353
+ return new FerricStoreError(`continuation response returned a mismatched ${field3}`);
19354
+ }
19355
+ function normalizedPartition(value) {
19356
+ return value == null || value === "" ? void 0 : value;
19357
+ }
19358
+ function fencingValue(value) {
19359
+ return typeof value === "bigint" ? value : BigInt(value);
19360
+ }
19361
+ function assertNonEmptyString(value, name) {
19362
+ if (typeof value !== "string" || value.length === 0) {
19363
+ throw new TypeError(`${name} must be a non-empty string`);
19364
+ }
19365
+ }
19366
+ function assertStepValueIsReserved(options, resultName) {
19367
+ if (options.values != null && Object.hasOwn(options.values, resultName)) {
19368
+ throw new TypeError("values cannot mutate the reserved durable step result");
19369
+ }
19370
+ if (options.valueRefs != null && Object.hasOwn(options.valueRefs, resultName)) {
19371
+ throw new TypeError("valueRefs cannot mutate the reserved durable step result");
19372
+ }
19373
+ if (options.dropValues?.includes(resultName) === true) {
19374
+ throw new TypeError("dropValues cannot mutate the reserved durable step result");
19375
+ }
19376
+ if (options.overrideValues?.includes(resultName) === true) {
19377
+ throw new TypeError("overrideValues cannot mutate the reserved durable step result");
19378
+ }
19379
+ }
19380
+ function claimedItemFromRecord(record) {
19381
+ return {
19382
+ ...record.attributes == null ? {} : { attributes: record.attributes },
19383
+ fencingToken: record.fencingToken,
19384
+ id: record.id,
19385
+ leaseToken: Buffer.from(record.leaseToken),
19386
+ ...record.partitionKey === "" ? {} : { partitionKey: record.partitionKey },
19387
+ ...record.payload === void 0 ? {} : { payload: record.payload },
19388
+ ...record.runState == null ? {} : { runState: record.runState },
19389
+ state: record.state,
19390
+ type: record.type
19391
+ };
19392
+ }
19393
+
19394
+ // src/client-step-continue.ts
19395
+ function stepContinueArguments(id, options, codec) {
19396
+ try {
19397
+ const args = [
19398
+ "FLOW.STEP_CONTINUE",
19399
+ id,
19400
+ options.leaseToken,
19401
+ options.fromState,
19402
+ options.toState,
19403
+ "FENCING",
19404
+ options.fencingToken,
19405
+ "LEASE_MS",
19406
+ options.leaseMs ?? 3e4,
19407
+ "NOW",
19408
+ options.nowMs ?? nowMs()
19409
+ ];
19410
+ append(args, "PARTITION", options.partitionKey);
19411
+ append(args, "WORKER", options.worker);
19412
+ appendEncoded(args, "PAYLOAD", codec, options.payload);
19413
+ if (options.returnJob === true) args.push("RETURN", "JOBS_COMPACT");
19414
+ appendStateMeta(args, options.stateMeta);
19415
+ appendNamedValues(args, codec, options);
19416
+ appendAttributeMutations(args, options);
19417
+ return args;
19418
+ } catch (error) {
19419
+ throw requestNotSentError(error);
19420
+ }
19421
+ }
19422
+
19010
19423
  // src/client.ts
19011
19424
  var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClient {
19012
19425
  static async fromUrl(url, options = {}) {
@@ -19121,33 +19534,30 @@ var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClie
19121
19534
  appendNamedValues(args, this.codec, options);
19122
19535
  return await this.recordOrGet(await this.commandArgs(args), id, partitionKey);
19123
19536
  }
19537
+ /**
19538
+ * @deprecated Low-level compatibility API. Use {@link advance} for state-only
19539
+ * continuation or {@link step} for a durable closure.
19540
+ */
19124
19541
  async stepContinue(id, options) {
19125
19542
  const partitionKey = options.partitionKey;
19126
19543
  const returnJob = options.returnJob === true;
19127
19544
  const type = options.type;
19128
- const args = [
19129
- "FLOW.STEP_CONTINUE",
19130
- id,
19131
- options.leaseToken,
19132
- options.fromState,
19133
- options.toState,
19134
- "FENCING",
19135
- options.fencingToken,
19136
- "LEASE_MS",
19137
- options.leaseMs ?? 3e4,
19138
- "NOW",
19139
- options.nowMs ?? nowMs()
19140
- ];
19141
- append(args, "PARTITION", partitionKey);
19142
- append(args, "WORKER", options.worker);
19143
- appendEncoded(args, "PAYLOAD", this.codec, options.payload);
19144
- if (returnJob) args.push("RETURN", "JOBS_COMPACT");
19145
- appendStateMeta(args, options.stateMeta);
19146
- appendNamedValues(args, this.codec, options);
19147
- appendAttributeMutations(args, options);
19545
+ const args = stepContinueArguments(id, options, this.codec);
19148
19546
  const response = await this.commandArgs(args);
19149
19547
  return returnJob ? claimedItemFromResp(response, this.codec, { type }) : await this.recordOrGet(response, id, partitionKey);
19150
19548
  }
19549
+ /** Advance a claimed Flow and return its renewed claim. */
19550
+ async advance(job, options) {
19551
+ return await advanceClaim(this, job, options);
19552
+ }
19553
+ /**
19554
+ * Run a closure after validating the claim, then atomically journal its
19555
+ * result and advance the Flow. A committed result is replayed without
19556
+ * invoking the closure again.
19557
+ */
19558
+ async step(job, options) {
19559
+ return await runDurableStep(this, job, options);
19560
+ }
19151
19561
  async runStepsMany(items, options) {
19152
19562
  if (items.length === 0) return Buffer.from("OK");
19153
19563
  const hasStates = options.states != null;
@@ -19397,6 +19807,8 @@ function validateUnicodeScalarText2(value) {
19397
19807
 
19398
19808
  // src/outcomes.ts
19399
19809
  var OUTCOME_BRAND = /* @__PURE__ */ Symbol("ferricstore.outcome");
19810
+ var ALREADY_APPLIED_BRAND = /* @__PURE__ */ Symbol("ferricstore.alreadyApplied");
19811
+ var REPLAYED_STEP_BRAND = /* @__PURE__ */ Symbol("ferricstore.replayedStep");
19400
19812
  function transition(toState, options = {}) {
19401
19813
  return brandOutcome({ ...options, kind: "transition", toState });
19402
19814
  }
@@ -19409,6 +19821,22 @@ function retry(options = {}) {
19409
19821
  function fail2(options = {}) {
19410
19822
  return brandOutcome({ ...options, kind: "fail" });
19411
19823
  }
19824
+ function alreadyApplied(job) {
19825
+ return brandAlreadyApplied({ job, kind: "already_applied" });
19826
+ }
19827
+ function appliedStep(job, result) {
19828
+ return brandAlreadyApplied({ applied: true, job, kind: "already_applied", result });
19829
+ }
19830
+ function replayedStep(job, result) {
19831
+ const replayed = { applied: false, job, result };
19832
+ Object.defineProperty(replayed, REPLAYED_STEP_BRAND, {
19833
+ configurable: false,
19834
+ enumerable: false,
19835
+ value: true,
19836
+ writable: false
19837
+ });
19838
+ return replayed;
19839
+ }
19412
19840
  function isOutcome(value) {
19413
19841
  if (typeof value !== "object" || value == null || !Object.hasOwn(value, OUTCOME_BRAND) || value[OUTCOME_BRAND] !== true) {
19414
19842
  return false;
@@ -19419,6 +19847,12 @@ function isOutcome(value) {
19419
19847
  }
19420
19848
  return outcome.kind === "complete" || outcome.kind === "retry" || outcome.kind === "fail";
19421
19849
  }
19850
+ function isAlreadyAppliedOutcome(value) {
19851
+ return typeof value === "object" && value != null && Object.hasOwn(value, ALREADY_APPLIED_BRAND) && value[ALREADY_APPLIED_BRAND] === true && value.kind === "already_applied";
19852
+ }
19853
+ function isReplayedStepResult(value) {
19854
+ return typeof value === "object" && value != null && Object.hasOwn(value, REPLAYED_STEP_BRAND) && value[REPLAYED_STEP_BRAND] === true && value.applied === false;
19855
+ }
19422
19856
  function brandOutcome(outcome) {
19423
19857
  Object.defineProperty(outcome, "kind", {
19424
19858
  configurable: false,
@@ -19442,6 +19876,21 @@ function brandOutcome(outcome) {
19442
19876
  });
19443
19877
  return outcome;
19444
19878
  }
19879
+ function brandAlreadyApplied(outcome) {
19880
+ Object.defineProperty(outcome, "kind", {
19881
+ configurable: false,
19882
+ enumerable: true,
19883
+ value: "already_applied",
19884
+ writable: false
19885
+ });
19886
+ Object.defineProperty(outcome, ALREADY_APPLIED_BRAND, {
19887
+ configurable: false,
19888
+ enumerable: false,
19889
+ value: true,
19890
+ writable: false
19891
+ });
19892
+ return outcome;
19893
+ }
19445
19894
 
19446
19895
  // src/worker-config.ts
19447
19896
  function snapshotWorkerConfig(options) {
@@ -19458,8 +19907,7 @@ function snapshotWorkerConfig(options) {
19458
19907
  return Object.freeze(snapshot);
19459
19908
  }
19460
19909
 
19461
- // src/worker-internal.ts
19462
- var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19910
+ // src/worker-lease-guard.ts
19463
19911
  var LeaseRenewalError = class extends Error {
19464
19912
  constructor(cause) {
19465
19913
  super("FerricStore lease renewal failed", { cause });
@@ -19470,37 +19918,58 @@ var LeaseRenewalGuard = class {
19470
19918
  constructor(client, leasedJob, leaseMs, options) {
19471
19919
  this.client = client;
19472
19920
  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();
19921
+ this.currentJob = snapshotLeasedJob(leasedJob);
19922
+ this.renewalEnabled = options.leaseRenewal !== false;
19923
+ this.intervalMs = positiveInteger4(options.leaseRenewIntervalMs, Math.max(1, Math.trunc(leaseMs / 2)));
19924
+ this.startRenewal();
19480
19925
  }
19481
19926
  client;
19482
19927
  leaseMs;
19483
19928
  error;
19484
19929
  inFlight;
19485
- job;
19930
+ currentJob;
19931
+ intervalMs;
19932
+ renewalEnabled;
19933
+ state = "active";
19486
19934
  timer;
19935
+ get job() {
19936
+ return this.currentJob;
19937
+ }
19487
19938
  assertActive() {
19488
- if (this.error != null) {
19489
- throw this.error;
19490
- }
19939
+ if (this.error != null) throw this.error;
19491
19940
  }
19492
19941
  async stop() {
19493
- if (this.timer != null) {
19494
- this.timer.cancel();
19495
- this.timer = void 0;
19496
- }
19942
+ this.state = "stopped";
19943
+ this.cancelRenewal();
19944
+ await this.inFlight;
19945
+ this.assertActive();
19946
+ }
19947
+ async pauseForLeaseMutation() {
19948
+ if (this.state === "stopped") throw new Error("lease renewal guard is stopped");
19949
+ if (this.state === "paused") return;
19950
+ this.state = "paused";
19951
+ this.cancelRenewal();
19497
19952
  await this.inFlight;
19498
19953
  this.assertActive();
19499
19954
  }
19955
+ resumeWith(job) {
19956
+ if (this.state !== "paused") throw new Error("lease renewal guard is not paused");
19957
+ this.currentJob = snapshotLeasedJob(job);
19958
+ this.state = "active";
19959
+ this.startRenewal();
19960
+ }
19961
+ cancelRenewal() {
19962
+ if (this.timer == null) return;
19963
+ this.timer.cancel();
19964
+ this.timer = void 0;
19965
+ }
19966
+ startRenewal() {
19967
+ if (!this.renewalEnabled || this.state !== "active") return;
19968
+ this.timer = setLongInterval(() => this.renew(), this.intervalMs);
19969
+ this.timer.unref();
19970
+ }
19500
19971
  renew() {
19501
- if (this.error != null || this.inFlight != null) {
19502
- return;
19503
- }
19972
+ if (this.state !== "active" || this.error != null || this.inFlight != null) return;
19504
19973
  const renewal = this.client.extendLease(this.job.id, {
19505
19974
  fencingToken: this.job.fencingToken,
19506
19975
  leaseMs: this.leaseMs,
@@ -19511,17 +19980,12 @@ var LeaseRenewalGuard = class {
19511
19980
  () => void 0,
19512
19981
  (error) => {
19513
19982
  this.error = new LeaseRenewalError(error);
19514
- if (this.timer != null) {
19515
- this.timer.cancel();
19516
- this.timer = void 0;
19517
- }
19983
+ this.cancelRenewal();
19518
19984
  }
19519
19985
  );
19520
19986
  this.inFlight = renewal;
19521
19987
  void renewal.finally(() => {
19522
- if (this.inFlight === renewal) {
19523
- this.inFlight = void 0;
19524
- }
19988
+ if (this.inFlight === renewal) this.inFlight = void 0;
19525
19989
  });
19526
19990
  }
19527
19991
  };
@@ -19545,8 +20009,14 @@ function snapshotLeasedJob(job) {
19545
20009
  }
19546
20010
  return snapshot;
19547
20011
  }
20012
+ function positiveInteger4(value, fallback) {
20013
+ return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
20014
+ }
20015
+
20016
+ // src/worker-internal.ts
20017
+ var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19548
20018
  function workerConcurrency(options) {
19549
- return positiveInteger4(options.concurrency ?? options.workers, 1);
20019
+ return positiveInteger5(options.concurrency ?? options.workers, 1);
19550
20020
  }
19551
20021
  function workerRefillStrategy(options) {
19552
20022
  return options.refillStrategy === "wave" ? "wave" : "continuous";
@@ -19589,18 +20059,18 @@ function workerErrorPayload(error, options, codec) {
19589
20059
  }
19590
20060
  function workerBatchSize(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19591
20061
  return Math.min(
19592
- positiveInteger4(options.batchSize, 10),
19593
- positiveInteger4(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
20062
+ positiveInteger5(options.batchSize, 10),
20063
+ positiveInteger5(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
19594
20064
  );
19595
20065
  }
19596
20066
  function workerClaimLimit(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19597
20067
  return Math.min(workerBatchSize(options, flowManyBatchLimit), workerConcurrency(options));
19598
20068
  }
19599
20069
  function workerDrainBatches(options) {
19600
- return positiveInteger4(options.claimDrainBatches, 1);
20070
+ return positiveInteger5(options.claimDrainBatches, 1);
19601
20071
  }
19602
20072
  function workerLeaseMs(options, fallback = 3e4) {
19603
- return positiveInteger4(options.leaseMs, positiveInteger4(fallback, 3e4));
20073
+ return positiveInteger5(options.leaseMs, positiveInteger5(fallback, 3e4));
19604
20074
  }
19605
20075
  function workerIdleSleepMs(options) {
19606
20076
  return nonNegativeDuration(options.idleSleepMs, 250);
@@ -19612,7 +20082,7 @@ function workerClaimBlockMs(options, useBlocking) {
19612
20082
  if (options.signal == null || !Number.isFinite(options.blockMs) || options.blockMs < 0) {
19613
20083
  return options.blockMs;
19614
20084
  }
19615
- const abortPollMs = positiveInteger4(options.abortPollMs, 1e3);
20085
+ const abortPollMs = positiveInteger5(options.abortPollMs, 1e3);
19616
20086
  return options.blockMs === 0 ? abortPollMs : Math.min(Math.trunc(options.blockMs), abortPollMs);
19617
20087
  }
19618
20088
  function workerMaxIdleSleepMs(options, idleSleepMs = workerIdleSleepMs(options)) {
@@ -19623,8 +20093,8 @@ function nextWorkerIdleSleepMs(currentIdleSleepMs, maxIdleSleepMs) {
19623
20093
  return Math.min(maxIdleSleepMs, Math.max(1, currentIdleSleepMs * 2));
19624
20094
  }
19625
20095
  async function runContinuousWorkerPool(options) {
19626
- const concurrency = positiveInteger4(options.concurrency, 1);
19627
- const maxClaimSize = positiveInteger4(options.maxClaimSize, concurrency);
20096
+ const concurrency = positiveInteger5(options.concurrency, 1);
20097
+ const maxClaimSize = positiveInteger5(options.maxClaimSize, concurrency);
19628
20098
  const initialIdleSleepMs = nonNegativeDuration(options.idleSleepMs, 250);
19629
20099
  const maxIdleSleepMs = Math.max(
19630
20100
  initialIdleSleepMs,
@@ -19746,7 +20216,7 @@ function normalizeWorkflowWorkerConfig(options) {
19746
20216
  ...captured
19747
20217
  });
19748
20218
  }
19749
- function positiveInteger4(value, fallback) {
20219
+ function positiveInteger5(value, fallback) {
19750
20220
  return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
19751
20221
  }
19752
20222
  function nonNegativeInteger5(value, fallback) {
@@ -19799,7 +20269,7 @@ async function waitForRefillWindow(delayMs, signal) {
19799
20269
  }
19800
20270
 
19801
20271
  // src/queue.ts
19802
- var import_node_crypto2 = require("crypto");
20272
+ var import_node_crypto3 = require("crypto");
19803
20273
 
19804
20274
  // src/queue-completion-batcher.ts
19805
20275
  function queueClaimOptions(queue, options, overrides = {}) {
@@ -20664,7 +21134,7 @@ var Queue = class {
20664
21134
  this.client = client;
20665
21135
  this.type = captured.type;
20666
21136
  this.state = captured.state ?? "queued";
20667
- this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto2.randomUUID)()}`;
21137
+ this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto3.randomUUID)()}`;
20668
21138
  }
20669
21139
  async enqueue(id, options = {}) {
20670
21140
  return await this.client.enqueue(id, {
@@ -20686,53 +21156,59 @@ var Queue = class {
20686
21156
  };
20687
21157
 
20688
21158
  // src/workflow.ts
20689
- var import_node_crypto3 = require("crypto");
21159
+ var import_node_crypto4 = require("crypto");
20690
21160
 
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 };
21161
+ // src/workflow-registration.ts
21162
+ function createWorkflowStateRegistration(name, handler, options, defaultValueMaxBytes) {
21163
+ const own2 = { ...options };
21164
+ const claimValues = own2.claimValues == null ? void 0 : snapshotOwnStringArray(own2.claimValues, "claimValues");
21165
+ const retryPolicy = own2.retryPolicy == null ? void 0 : Object.freeze({ ...own2.retryPolicy });
21166
+ return Object.freeze({
21167
+ claimPayload: own2.claimPayload ?? true,
21168
+ claimRecord: own2.claimRecord ?? true,
21169
+ exceptionPolicy: normalizeExceptionPolicy(own2.exceptionPolicy),
21170
+ handler,
21171
+ leaseMs: own2.leaseMs ?? 3e4,
21172
+ ...own2.mode == null ? {} : { mode: normalizeStateMode(own2.mode) },
21173
+ name,
21174
+ returnRecord: own2.returnRecord ?? false,
21175
+ valueMaxBytes: own2.valueMaxBytes ?? defaultValueMaxBytes,
21176
+ ...claimValues == null ? {} : { claimValues },
21177
+ ...retryPolicy == null ? {} : { retryPolicy }
20701
21178
  });
20702
21179
  }
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
21180
 
20717
21181
  // src/workflow-context.ts
20718
21182
  var MISSING_VALUE = /* @__PURE__ */ Symbol("ferricstore.missingValue");
20719
21183
  var WorkflowContext = class {
20720
21184
  workflow;
20721
21185
  job;
20722
- stateName;
20723
21186
  flow;
21187
+ appliedMutation = false;
21188
+ currentStateName;
20724
21189
  leaseJob;
21190
+ mutationFailure;
21191
+ mutationPhase = "idle";
21192
+ mutationCoordinator;
20725
21193
  valueCache = /* @__PURE__ */ new Map();
20726
- constructor(workflow, job, stateName, leaseJob = job) {
21194
+ constructor(workflow, job, stateName) {
20727
21195
  this.workflow = workflow;
20728
21196
  this.job = job;
20729
- this.leaseJob = leaseJob;
20730
- this.stateName = stateName;
21197
+ this.leaseJob = job;
21198
+ this.currentStateName = stateName;
20731
21199
  this.flow = new WorkflowFlowCommands(this);
20732
21200
  }
21201
+ /** @internal */
21202
+ configureWorkerMutation(leaseJob, coordinator) {
21203
+ this.leaseJob = leaseJob;
21204
+ this.mutationCoordinator = coordinator;
21205
+ }
20733
21206
  get client() {
20734
21207
  return this.workflow.client;
20735
21208
  }
21209
+ get stateName() {
21210
+ return this.currentStateName;
21211
+ }
20736
21212
  get id() {
20737
21213
  return this.leaseJob.id;
20738
21214
  }
@@ -20743,7 +21219,7 @@ var WorkflowContext = class {
20743
21219
  return this.leaseJob.state;
20744
21220
  }
20745
21221
  get logicalState() {
20746
- return this.stateName;
21222
+ return this.currentStateName;
20747
21223
  }
20748
21224
  get partitionKey() {
20749
21225
  return this.leaseJob.partitionKey;
@@ -20781,6 +21257,53 @@ var WorkflowContext = class {
20781
21257
  get correlationId() {
20782
21258
  return "correlationId" in this.job ? this.job.correlationId : void 0;
20783
21259
  }
21260
+ async advance(toState, options = {}) {
21261
+ if (typeof toState !== "string" || toState.length === 0) {
21262
+ throw new TypeError("toState must be a non-empty string");
21263
+ }
21264
+ const current = this.durableClaim();
21265
+ await this.beforeCommit();
21266
+ let refreshed;
21267
+ try {
21268
+ refreshed = await advanceClaim(this.client, current, { ...options, toState });
21269
+ } catch (error) {
21270
+ this.commitFailed(error);
21271
+ throw error;
21272
+ }
21273
+ this.committed(refreshed);
21274
+ return alreadyApplied(refreshed);
21275
+ }
21276
+ async step(options) {
21277
+ let applied = false;
21278
+ const stepped = await runDurableStep(this.client, this.durableClaim(), options, {
21279
+ beforeCommit: async () => await this.beforeCommit(),
21280
+ commitFailed: (error) => this.commitFailed(error),
21281
+ committed: (job) => {
21282
+ applied = true;
21283
+ this.committed(job);
21284
+ },
21285
+ replayed: (job) => this.replayed(job)
21286
+ });
21287
+ return applied ? appliedStep(stepped.job, stepped.result) : replayedStep(stepped.job, stepped.result);
21288
+ }
21289
+ /** @internal */
21290
+ get hasAppliedMutation() {
21291
+ return this.appliedMutation;
21292
+ }
21293
+ /** @internal */
21294
+ get uncertainMutationFailure() {
21295
+ return this.mutationPhase === "uncertain" ? this.mutationFailure : void 0;
21296
+ }
21297
+ /** @internal */
21298
+ get hasUncertainMutation() {
21299
+ return this.mutationPhase === "uncertain";
21300
+ }
21301
+ /** @internal */
21302
+ assertAppliedOutcome(outcome) {
21303
+ if (!this.appliedMutation || !outcome.job.leaseToken.equals(this.leaseJob.leaseToken) || outcome.job.fencingToken !== this.leaseJob.fencingToken) {
21304
+ throw new Error("already-applied outcome does not match the workflow's refreshed claim");
21305
+ }
21306
+ }
20784
21307
  async value(name, defaultValue, options = {}) {
20785
21308
  const useLocalCache = options.localCache ?? this.workflow.valueConfig.localCache;
20786
21309
  if (useLocalCache && this.valueCache.has(name)) {
@@ -20860,7 +21383,49 @@ var WorkflowContext = class {
20860
21383
  return result;
20861
21384
  }
20862
21385
  valueMaxBytes() {
20863
- return this.workflow.stateRegistration(this.stateName)?.valueMaxBytes;
21386
+ return this.workflow.stateRegistration(this.currentStateName)?.valueMaxBytes;
21387
+ }
21388
+ durableClaim() {
21389
+ const runState = this.leaseJob.runState ?? (this.leaseJob[CLAIMED_ITEM_WIRE] == null ? void 0 : this.currentStateName);
21390
+ if (typeof runState !== "string" || runState.length === 0) {
21391
+ throw new TypeError("job.runState must be a non-empty string");
21392
+ }
21393
+ return {
21394
+ ...this.leaseJob,
21395
+ leaseToken: Buffer.from(this.leaseJob.leaseToken),
21396
+ runState
21397
+ };
21398
+ }
21399
+ async beforeCommit() {
21400
+ if (this.mutationPhase !== "idle") {
21401
+ throw new Error("a durable workflow mutation is already in progress or has an uncertain result");
21402
+ }
21403
+ await this.mutationCoordinator?.pause();
21404
+ this.mutationPhase = "committing";
21405
+ }
21406
+ commitFailed(error) {
21407
+ if (!durableMutationMayHaveCommitted(error)) {
21408
+ this.mutationFailure = void 0;
21409
+ this.mutationPhase = "idle";
21410
+ this.mutationCoordinator?.resume(this.leaseJob);
21411
+ return;
21412
+ }
21413
+ this.mutationFailure = error;
21414
+ this.mutationPhase = "uncertain";
21415
+ }
21416
+ committed(job) {
21417
+ this.acceptJob(job);
21418
+ this.appliedMutation = true;
21419
+ this.mutationCoordinator?.resume(job);
21420
+ }
21421
+ replayed(job) {
21422
+ this.acceptJob(job);
21423
+ }
21424
+ acceptJob(job) {
21425
+ this.leaseJob = job;
21426
+ this.currentStateName = job.runState ?? this.currentStateName;
21427
+ this.mutationFailure = void 0;
21428
+ this.mutationPhase = "idle";
20864
21429
  }
20865
21430
  };
20866
21431
  var WorkflowFlowCommands = class {
@@ -20898,6 +21463,7 @@ var WorkflowFlowCommands = class {
20898
21463
  toState
20899
21464
  });
20900
21465
  }
21466
+ /** @deprecated Use `ctx.advance(toState)`. */
20901
21467
  async stepContinue(toState, options = {}) {
20902
21468
  return await this.ctx.client.stepContinue(this.ctx.id, {
20903
21469
  ...options,
@@ -21060,25 +21626,86 @@ async function applyFail(client, ctx, outcome, returnRecord) {
21060
21626
  });
21061
21627
  }
21062
21628
 
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 }
21629
+ // src/workflow-worker-context.ts
21630
+ var WorkerWorkflowContext = class extends WorkflowContext {
21631
+ constructor(workflow, job, stateName, leaseJob, mutationCoordinator) {
21632
+ super(workflow, job, stateName);
21633
+ this.configureWorkerMutation(leaseJob, mutationCoordinator);
21634
+ }
21635
+ };
21636
+
21637
+ // src/workflow-job-execution.ts
21638
+ async function executeWorkflowJob(workflow, workerOptions, job, registration, guard) {
21639
+ guard.assertActive();
21640
+ const ctx = new WorkerWorkflowContext(workflow, job, registration.name, guard.job, {
21641
+ pause: async () => await guard.pauseForLeaseMutation(),
21642
+ resume: (refreshed) => guard.resumeWith(refreshed)
21643
+ });
21644
+ let value;
21645
+ try {
21646
+ value = await registration.handler(ctx);
21647
+ } catch (error) {
21648
+ await guard.stop();
21649
+ if (ctx.hasUncertainMutation) throw asError(error, "durable workflow mutation result is uncertain");
21650
+ await applyHandlerError(workflow, workerOptions, ctx, registration, error);
21651
+ return;
21652
+ }
21653
+ await guard.stop();
21654
+ if (ctx.hasUncertainMutation) {
21655
+ throw asError(ctx.uncertainMutationFailure, "durable workflow mutation result is uncertain");
21656
+ }
21657
+ if (isAlreadyAppliedOutcome(value)) {
21658
+ ctx.assertAppliedOutcome(value);
21659
+ await releaseAppliedMutation(workflow, ctx);
21660
+ return;
21661
+ }
21662
+ if (isReplayedStepResult(value)) {
21663
+ await applyWorkflowOutcome(
21664
+ workflow.client,
21665
+ ctx,
21666
+ complete({ result: value.result }),
21667
+ registration.returnRecord
21668
+ );
21669
+ return;
21670
+ }
21671
+ if (isOutcome(value)) {
21672
+ await applyWorkflowOutcome(workflow.client, ctx, value, registration.returnRecord);
21673
+ return;
21674
+ }
21675
+ if (ctx.hasAppliedMutation) {
21676
+ await releaseAppliedMutation(workflow, ctx);
21677
+ return;
21678
+ }
21679
+ await applyWorkflowOutcome(
21680
+ workflow.client,
21681
+ ctx,
21682
+ complete({ result: value }),
21683
+ registration.returnRecord
21684
+ );
21685
+ }
21686
+ function asError(value, message) {
21687
+ return value instanceof Error ? value : new Error(message, { cause: value });
21688
+ }
21689
+ async function releaseAppliedMutation(workflow, ctx) {
21690
+ await workflow.client.transition(ctx.id, {
21691
+ fencingToken: ctx.fencingToken,
21692
+ fromState: ctx.state,
21693
+ leaseToken: ctx.leaseToken,
21694
+ partitionKey: ctx.partitionKey,
21695
+ toState: ctx.logicalState
21080
21696
  });
21081
21697
  }
21698
+ async function applyHandlerError(workflow, workerOptions, ctx, registration, error) {
21699
+ const policy = workerOptions.exceptionPolicy ?? registration.exceptionPolicy;
21700
+ if (policy === "raise") throw error;
21701
+ const encodedError = workerErrorPayload(error, workerOptions, workflow.client.codec);
21702
+ await applyWorkflowOutcome(
21703
+ workflow.client,
21704
+ ctx,
21705
+ policy === "fail" ? fail2({ error: encodedError }) : retry({ error: encodedError }),
21706
+ registration.returnRecord
21707
+ );
21708
+ }
21082
21709
 
21083
21710
  // src/workflow.ts
21084
21711
  var WorkflowClient = class {
@@ -21106,7 +21733,7 @@ var Workflow = class {
21106
21733
  localCache: captured.valueConfig?.localCache ?? false,
21107
21734
  valueMaxBytes: captured.valueConfig?.valueMaxBytes
21108
21735
  };
21109
- this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto3.randomUUID)()}`;
21736
+ this.defaultWorker = captured.worker ?? `${this.type}-${(0, import_node_crypto4.randomUUID)()}`;
21110
21737
  }
21111
21738
  state(name, handler, options = {}) {
21112
21739
  this.states.set(name, createWorkflowStateRegistration(
@@ -21207,6 +21834,10 @@ var WorkflowWorker = class {
21207
21834
  claimed += jobs.length;
21208
21835
  const guards = jobs.map((job) => new LeaseRenewalGuard(this.workflow.client, job, leaseMs, this.options));
21209
21836
  let cursor = 0;
21837
+ let batchFailure;
21838
+ const recordFailure = (error) => {
21839
+ batchFailure ??= { error };
21840
+ };
21210
21841
  const runNext = async () => {
21211
21842
  while (cursor < jobs.length) {
21212
21843
  const index = cursor;
@@ -21217,19 +21848,22 @@ var WorkflowWorker = class {
21217
21848
  try {
21218
21849
  await this.applyJob(job, registration, guard);
21219
21850
  applied += 1;
21851
+ } catch (error) {
21852
+ recordFailure(error);
21220
21853
  } finally {
21221
- await guard.stop();
21854
+ try {
21855
+ await guard.stop();
21856
+ } catch (error) {
21857
+ recordFailure(error);
21858
+ }
21222
21859
  }
21223
21860
  }
21224
21861
  };
21225
21862
  try {
21226
- const handlers = await Promise.allSettled(
21863
+ await Promise.all(
21227
21864
  Array.from({ length: Math.min(concurrency, jobs.length) }, () => runNext())
21228
21865
  );
21229
- const failed = handlers.find((handler) => handler.status === "rejected");
21230
- if (failed != null) {
21231
- throw failed.reason;
21232
- }
21866
+ if (batchFailure != null) throwError(batchFailure.error);
21233
21867
  } finally {
21234
21868
  await Promise.allSettled(
21235
21869
  guards.slice(cursor).map(async (guard) => await guard.stop())
@@ -21340,45 +21974,15 @@ var WorkflowWorker = class {
21340
21974
  });
21341
21975
  }
21342
21976
  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
- );
21977
+ await executeWorkflowJob(this.workflow, this.options, job, registration, guard);
21377
21978
  }
21378
21979
  };
21980
+ function throwError(value) {
21981
+ throw value instanceof Error ? value : new Error("workflow worker failed", { cause: value });
21982
+ }
21379
21983
 
21380
21984
  // src/version.ts
21381
- var FERRICSTORE_SDK_VERSION = "0.12.0";
21985
+ var FERRICSTORE_SDK_VERSION = "0.13.1";
21382
21986
  var FERRICSTORE_MINIMUM_SERVER_VERSION = "0.11.4";
21383
21987
  var FERRICSTORE_NATIVE_PROTOCOL_VERSION = 1;
21384
21988
  // Annotate the CommonJS export names for ESM import in node:
@@ -21422,6 +22026,7 @@ var FERRICSTORE_NATIVE_PROTOCOL_VERSION = 1;
21422
22026
  QueueWorker,
21423
22027
  RawCodec,
21424
22028
  ReconnectingExecutor,
22029
+ RequestNotSentError,
21425
22030
  RequestTimeoutError,
21426
22031
  RerouteError,
21427
22032
  RoutingTopology,
@@ -21442,6 +22047,7 @@ var FERRICSTORE_NATIVE_PROTOCOL_VERSION = 1;
21442
22047
  complete,
21443
22048
  fail,
21444
22049
  httpCommandDisposition,
22050
+ isAlreadyAppliedOutcome,
21445
22051
  isOutcome,
21446
22052
  isReconnectableClosedConnectionError,
21447
22053
  mapException,