@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.js CHANGED
@@ -30,9 +30,16 @@ var FerricStoreError = class extends Error {
30
30
  };
31
31
  var HTTPTransportError = class extends FerricStoreError {
32
32
  code = "http_transport";
33
+ requestDisposition;
33
34
  statusCode;
34
35
  constructor(message, options = {}) {
35
- super(message, options);
36
+ const disposition = options.requestDisposition;
37
+ super(message, {
38
+ ...options,
39
+ retryable: options.retryable ?? (disposition === "unsent" ? false : void 0),
40
+ safeToRetry: options.safeToRetry ?? (disposition === "unsent" ? true : disposition === "possibly_sent" ? false : void 0)
41
+ });
42
+ this.requestDisposition = disposition;
36
43
  this.statusCode = options.statusCode;
37
44
  }
38
45
  };
@@ -57,6 +64,20 @@ var RequestTimeoutError = class extends FerricStoreError {
57
64
  this.timeoutMs = timeoutMs;
58
65
  }
59
66
  };
67
+ var RequestNotSentError = class extends FerricStoreError {
68
+ code = "request_not_sent";
69
+ requestDisposition = "unsent";
70
+ constructor(message, options = {}) {
71
+ super(message, { ...options, retryable: false, safeToRetry: true });
72
+ }
73
+ };
74
+ function requestNotSentError(error, message) {
75
+ if (error instanceof RequestNotSentError) return error;
76
+ return new RequestNotSentError(
77
+ message ?? (error instanceof Error ? error.message : String(error)),
78
+ { cause: error, raw: error }
79
+ );
80
+ }
60
81
  var FlowNotFoundError = class extends FerricStoreError {
61
82
  code = "flow_not_found";
62
83
  };
@@ -6315,7 +6336,10 @@ function decodeCompactPipeline(data, budget, claimModes) {
6315
6336
  );
6316
6337
  offset = read.offset;
6317
6338
  } else {
6318
- throw new FerricStoreError("unknown compact pipeline status");
6339
+ throw new FerricStoreError(`unknown compact pipeline status ${status}`, {
6340
+ retryable: false,
6341
+ safeToRetry: false
6342
+ });
6319
6343
  }
6320
6344
  }
6321
6345
  if (offset !== data.byteLength) throw new FerricStoreError("trailing compact pipeline bytes");
@@ -6485,6 +6509,7 @@ var COUNTED_COMPACT_TAGS = /* @__PURE__ */ new Set([
6485
6509
  ]);
6486
6510
 
6487
6511
  // src/protocol-response.ts
6512
+ var KNOWN_NATIVE_RESPONSE_STATUSES = /* @__PURE__ */ new Set([0, 1, 2, 3, 4, 5, 6]);
6488
6513
  function decodeResponse(frame, expectedOpcode, hints = {}) {
6489
6514
  if (frame.opcode !== expectedOpcode) {
6490
6515
  throw new FerricStoreError(
@@ -6509,6 +6534,13 @@ function decodeResponse(frame, expectedOpcode, hints = {}) {
6509
6534
  customPayload
6510
6535
  );
6511
6536
  if (status === STATUS_OK) return value;
6537
+ if (!KNOWN_NATIVE_RESPONSE_STATUSES.has(status)) {
6538
+ throw new FerricStoreError(`unknown native response status ${status}`, {
6539
+ raw: value,
6540
+ retryable: false,
6541
+ safeToRetry: false
6542
+ });
6543
+ }
6512
6544
  const message = protocolErrorMessage(status, value);
6513
6545
  throw classifyServerError(message, value, void 0, status);
6514
6546
  }
@@ -6548,7 +6580,7 @@ function unwrapPipelineResponse(value, options = {}, expectedItems) {
6548
6580
  continue;
6549
6581
  }
6550
6582
  const error = classifyServerError(
6551
- protocolErrorMessage(status === "busy" ? 4 : 1, payload),
6583
+ protocolErrorMessage(pipelineStatusCode(status), payload),
6552
6584
  payload,
6553
6585
  void 0,
6554
6586
  status
@@ -6557,15 +6589,46 @@ function unwrapPipelineResponse(value, options = {}, expectedItems) {
6557
6589
  out[index] = error;
6558
6590
  continue;
6559
6591
  }
6592
+ if (pipelineStatusToken(item[0]) != null) {
6593
+ const unknown = pipelineStatusToken(item[0]) ?? "unknown";
6594
+ const error = new FerricStoreError(`unknown native pipeline status ${JSON.stringify(unknown)}`, {
6595
+ raw: item,
6596
+ retryable: false,
6597
+ safeToRetry: false
6598
+ });
6599
+ if (options.throwOnItemError !== false) throw error;
6600
+ out[index] = error;
6601
+ continue;
6602
+ }
6560
6603
  }
6561
6604
  out[index] = item;
6562
6605
  }
6563
6606
  return out;
6564
6607
  }
6565
6608
  function pipelineStatus(value) {
6609
+ const status = pipelineStatusToken(value);
6610
+ if (status == null) return null;
6611
+ return status === "ok" || status === "error" || status === "auth" || status === "noperm" || status === "busy" || status === "reroute" || status === "bad_request" ? status : null;
6612
+ }
6613
+ function pipelineStatusToken(value) {
6566
6614
  if (typeof value !== "string" && !Buffer26.isBuffer(value) && !(value instanceof Uint8Array)) return null;
6567
- const status = asText(value).toLowerCase();
6568
- return status === "ok" || status === "busy" || status === "error" ? status : null;
6615
+ return asText(value).toLowerCase();
6616
+ }
6617
+ function pipelineStatusCode(status) {
6618
+ switch (status) {
6619
+ case "error":
6620
+ return 1;
6621
+ case "auth":
6622
+ return 2;
6623
+ case "noperm":
6624
+ return 3;
6625
+ case "busy":
6626
+ return 4;
6627
+ case "reroute":
6628
+ return 5;
6629
+ case "bad_request":
6630
+ return 6;
6631
+ }
6569
6632
  }
6570
6633
  function decodeResponseValue(opcode, body, hints, allowCompact, customPayload) {
6571
6634
  if (customPayload && allowCompact) {
@@ -8225,6 +8288,22 @@ function plainRecord(value) {
8225
8288
  return value;
8226
8289
  }
8227
8290
 
8291
+ // src/native-command-preparation.ts
8292
+ function prepareNativeCommand(args, maxFrameBytes) {
8293
+ try {
8294
+ return buildProtocolCommand(args, maxFrameBytes);
8295
+ } catch (error) {
8296
+ throw requestNotSentError(error);
8297
+ }
8298
+ }
8299
+ function encodeNativeRequest(command, requestId, maxFrameBytes) {
8300
+ try {
8301
+ return encodeRequest(command, requestId, maxFrameBytes);
8302
+ } catch (error) {
8303
+ throw requestNotSentError(error);
8304
+ }
8305
+ }
8306
+
8228
8307
  // src/native-adapter.ts
8229
8308
  var NativeAdapter = class _NativeAdapter {
8230
8309
  socket;
@@ -8332,15 +8411,13 @@ var NativeAdapter = class _NativeAdapter {
8332
8411
  return await this.executeCommandArgs(args);
8333
8412
  }
8334
8413
  async executeCommandArgs(args) {
8335
- const command = buildProtocolCommand(args, this.capabilities.requestFrameBytes);
8414
+ const command = prepareNativeCommand(args, this.capabilities.requestFrameBytes);
8336
8415
  return await this.request(command);
8337
8416
  }
8338
8417
  /** @internal Execute raw command arguments on a topology-selected lane. */
8339
8418
  async executeCommandOnLane(args, laneId) {
8340
- return await this.executeProtocolCommand(
8341
- buildProtocolCommand(args, this.capabilities.requestFrameBytes),
8342
- laneId
8343
- );
8419
+ const command = prepareNativeCommand(args, this.capabilities.requestFrameBytes);
8420
+ return await this.executeProtocolCommand(command, laneId);
8344
8421
  }
8345
8422
  async executeProtocolCommand(command, laneId) {
8346
8423
  return await this.request(laneId == null ? command : { ...command, laneId });
@@ -8434,14 +8511,14 @@ var NativeAdapter = class _NativeAdapter {
8434
8511
  let frame;
8435
8512
  try {
8436
8513
  requestId = this.requestScheduler.nextRequestId();
8437
- frame = encodeRequest(
8514
+ frame = encodeNativeRequest(
8438
8515
  withFlowQueryDeadline(command, timeoutMs),
8439
8516
  requestId,
8440
8517
  this.capabilities.requestFrameBytes
8441
8518
  );
8442
8519
  } catch (error) {
8443
8520
  if (hasFlowControlCredit) this.flowControl.release(laneId);
8444
- return Promise.reject(error instanceof Error ? error : new FerricStoreError(String(error), { raw: error }));
8521
+ return Promise.reject(error instanceof Error ? error : requestNotSentError(error));
8445
8522
  }
8446
8523
  return new Promise((resolve, reject) => {
8447
8524
  const timer = timeoutMs == null ? void 0 : setLongTimeout(() => {
@@ -9016,12 +9093,16 @@ var HTTPTransport = class {
9016
9093
  #requests = /* @__PURE__ */ new Set();
9017
9094
  #closed = false;
9018
9095
  async post(body, timeoutMs) {
9019
- if (this.#closed) throw new HTTPTransportError("HTTP transport is closed");
9096
+ if (this.#closed) {
9097
+ throw new HTTPTransportError("HTTP transport is closed", { requestDisposition: "unsent" });
9098
+ }
9020
9099
  const controller = new AbortController();
9021
9100
  this.#requests.add(controller);
9022
9101
  const timer = timeoutMs == null ? void 0 : setLongTimeout(() => controller.abort(requestTimeoutReason), timeoutMs);
9023
9102
  timer?.unref();
9024
9103
  try {
9104
+ const response = await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9105
+ if (response.status !== 401 || this.config.headers.authorization == null) return response;
9025
9106
  return await this.request(this.config.commandUrl, "POST", body, 0, controller.signal);
9026
9107
  } catch (error) {
9027
9108
  if (controller.signal.aborted) {
@@ -9031,9 +9112,22 @@ var HTTPTransport = class {
9031
9112
  raw: { retryable: true, safe_to_retry: false }
9032
9113
  });
9033
9114
  }
9034
- if (error instanceof HTTPTransportError || error instanceof RequestTimeoutError) throw error;
9115
+ if (error instanceof RequestTimeoutError) throw error;
9116
+ if (error instanceof HTTPTransportError) {
9117
+ if (error.requestDisposition != null) throw error;
9118
+ throw new HTTPTransportError(error.message, {
9119
+ cause: error.cause ?? error,
9120
+ raw: error.raw,
9121
+ requestDisposition: "possibly_sent",
9122
+ retryAfterMs: error.retryAfterMs,
9123
+ retryable: error.retryable,
9124
+ safeToRetry: false,
9125
+ statusCode: error.statusCode
9126
+ });
9127
+ }
9035
9128
  throw new HTTPTransportError("HTTP command request failed", {
9036
9129
  cause: error,
9130
+ requestDisposition: "possibly_sent",
9037
9131
  retryable: true,
9038
9132
  safeToRetry: false
9039
9133
  });
@@ -9045,7 +9139,9 @@ var HTTPTransport = class {
9045
9139
  async close() {
9046
9140
  if (this.#closed) return;
9047
9141
  this.#closed = true;
9048
- const error = new HTTPTransportError("HTTP transport is closed");
9142
+ const error = new HTTPTransportError("HTTP transport is closed", {
9143
+ requestDisposition: "possibly_sent"
9144
+ });
9049
9145
  for (const controller of this.#requests) controller.abort(error);
9050
9146
  this.#httpAgent.destroy();
9051
9147
  this.#httpsAgent.destroy();
@@ -9288,16 +9384,22 @@ var HTTPAdapter = class _HTTPAdapter {
9288
9384
  await this.#transport.close();
9289
9385
  }
9290
9386
  async executeBatch(commands) {
9291
- if (commands.length > this.#config.maxBatchItems) {
9292
- throw new HTTPTransportError("HTTP command batch exceeds maxBatchItems");
9293
- }
9294
- for (const command of commands) assertHTTPCommandSupported(command[0]);
9295
- const prepared = commands.map((command) => prepareHTTPCommand(command, this.#config.maxRequestBytes));
9296
- const body = encodeHTTPCommands(prepared.map((command) => command.encoded), this.#config.maxRequestBytes);
9297
- if (body.byteLength > this.#config.maxRequestBytes) {
9298
- throw new HTTPTransportError("HTTP command request exceeds maxRequestBytes");
9387
+ let body;
9388
+ let serverBlockMs;
9389
+ try {
9390
+ if (commands.length > this.#config.maxBatchItems) {
9391
+ throw new HTTPTransportError("HTTP command batch exceeds maxBatchItems");
9392
+ }
9393
+ for (const command of commands) assertHTTPCommandSupported(command[0]);
9394
+ const prepared = commands.map((command) => prepareHTTPCommand(command, this.#config.maxRequestBytes));
9395
+ body = encodeHTTPCommands(prepared.map((command) => command.encoded), this.#config.maxRequestBytes);
9396
+ if (body.byteLength > this.#config.maxRequestBytes) {
9397
+ throw new HTTPTransportError("HTTP command request exceeds maxRequestBytes");
9398
+ }
9399
+ serverBlockMs = combinedServerBlockMs(prepared.map((command) => command.serverBlockMs));
9400
+ } catch (error) {
9401
+ throw unsentHTTPError(error);
9299
9402
  }
9300
- const serverBlockMs = combinedServerBlockMs(prepared.map((command) => command.serverBlockMs));
9301
9403
  const response = await this.#transport.post(
9302
9404
  body,
9303
9405
  serverResponseTimeoutMs(this.#config.timeoutMs, serverBlockMs)
@@ -9354,16 +9456,32 @@ function commandError(value) {
9354
9456
  }
9355
9457
  function topLevelError(status, envelope, retryAfter) {
9356
9458
  const details = isRecord2(envelope.error) ? envelope.error : {};
9459
+ const code = typeof details.code === "string" ? details.code.toLowerCase() : void 0;
9357
9460
  const message = typeof details.message === "string" ? details.message : `HTTP command request failed with status ${status}`;
9358
9461
  const retryAfterMs2 = retryAfterMilliseconds(retryAfter);
9462
+ const ambiguousTimeout = status === 408 || code === "request_timeout";
9463
+ const safeToRetry = !ambiguousTimeout && (details.safe_to_retry === true || definitelyRejectedHTTPStatus(status));
9359
9464
  return new HTTPTransportError(message, {
9360
9465
  raw: details,
9361
9466
  retryable: status === 408 || status === 425 || status === 429 || status >= 500,
9362
9467
  retryAfterMs: retryAfterMs2,
9363
- safeToRetry: false,
9468
+ requestDisposition: "possibly_sent",
9469
+ safeToRetry,
9364
9470
  statusCode: status
9365
9471
  });
9366
9472
  }
9473
+ function definitelyRejectedHTTPStatus(status) {
9474
+ 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;
9475
+ }
9476
+ function unsentHTTPError(error) {
9477
+ if (error instanceof HTTPTransportError && error.requestDisposition === "unsent") return error;
9478
+ const message = error instanceof Error ? error.message : String(error);
9479
+ return new HTTPTransportError(message, {
9480
+ cause: error,
9481
+ raw: error instanceof FerricStoreError ? error.raw ?? error : error,
9482
+ requestDisposition: "unsent"
9483
+ });
9484
+ }
9367
9485
  function retryAfterMilliseconds(value) {
9368
9486
  if (value == null) return void 0;
9369
9487
  if (/^\d+$/u.test(value)) {
@@ -9456,9 +9574,9 @@ var ReconnectingExecutor = class {
9456
9574
  const snapshot = snapshotPipelineCommands(commands);
9457
9575
  const snapshotOptions = snapshotPipelineOptions(options) ?? {};
9458
9576
  for (const command of snapshot) assertCommandHasStableConnectionState(command);
9459
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9577
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9460
9578
  const initialExecutor = await this.executorPromise;
9461
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9579
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9462
9580
  if (initialExecutor.executePipeline == null) {
9463
9581
  return await executeCommandArraysIndividually(
9464
9582
  async (command) => await this.executeCommandArgs(command),
@@ -9479,9 +9597,9 @@ var ReconnectingExecutor = class {
9479
9597
  const snapshot = snapshotPipelineCommands(commands);
9480
9598
  const snapshotOptions = snapshotPipelineOptions(options) ?? {};
9481
9599
  for (const command of snapshot) assertCommandHasStableConnectionState(command);
9482
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9600
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9483
9601
  const initialExecutor = await this.executorPromise;
9484
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9602
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9485
9603
  if (initialExecutor.executeFusedPipeline == null) return void 0;
9486
9604
  try {
9487
9605
  return await initialExecutor.executeFusedPipeline(snapshot, snapshotOptions);
@@ -9514,7 +9632,7 @@ var ReconnectingExecutor = class {
9514
9632
  return;
9515
9633
  }
9516
9634
  this.closed = true;
9517
- this.retryAbortController.abort(new FerricStoreError("FerricStore client is closed"));
9635
+ this.retryAbortController.abort(new RequestNotSentError("FerricStore client is closed"));
9518
9636
  const executorPromise = this.executorPromise;
9519
9637
  const reconnectPromise = this.reconnectPromise;
9520
9638
  this.closePromise = (async () => {
@@ -9536,7 +9654,7 @@ var ReconnectingExecutor = class {
9536
9654
  let executor = await this.executorPromise;
9537
9655
  let retries = 0;
9538
9656
  for (; ; ) {
9539
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9657
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9540
9658
  try {
9541
9659
  return await operation(executor);
9542
9660
  } catch (error) {
@@ -9565,17 +9683,17 @@ var ReconnectingExecutor = class {
9565
9683
  try {
9566
9684
  await sleep(Math.min(this.maxDelayMs, base + jitter), this.retryAbortController.signal);
9567
9685
  } catch (error) {
9568
- if (this.closed) throw new FerricStoreError("FerricStore client is closed", { cause: error });
9686
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed", { cause: error });
9569
9687
  throw error;
9570
9688
  }
9571
9689
  }
9572
9690
  async reconnect(staleExecutor) {
9573
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9691
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9574
9692
  const competingReconnect = this.reconnectPromise;
9575
9693
  if (competingReconnect != null) return await competingReconnect;
9576
9694
  const observedExecutorPromise = this.executorPromise;
9577
9695
  const currentExecutor = await observedExecutorPromise.catch(() => void 0);
9578
- if (this.closed) throw new FerricStoreError("FerricStore client is closed");
9696
+ if (this.closed) throw new RequestNotSentError("FerricStore client is closed");
9579
9697
  if (this.reconnectPromise != null) return await this.reconnectPromise;
9580
9698
  if (this.executorPromise !== observedExecutorPromise) return await this.executorPromise;
9581
9699
  if (currentExecutor != null && currentExecutor !== staleExecutor) return currentExecutor;
@@ -9584,7 +9702,7 @@ var ReconnectingExecutor = class {
9584
9702
  const nextExecutor = await this.createExecutor(this.retryAbortController.signal);
9585
9703
  if (this.closed) {
9586
9704
  await Promise.resolve(nextExecutor.close?.()).catch(() => void 0);
9587
- throw new FerricStoreError("FerricStore client is closed");
9705
+ throw new RequestNotSentError("FerricStore client is closed");
9588
9706
  }
9589
9707
  this.executorPromise = Promise.resolve(nextExecutor);
9590
9708
  return nextExecutor;
@@ -10994,7 +11112,7 @@ var TopologyNativeAdapterPool = class _TopologyNativeAdapterPool {
10994
11112
  );
10995
11113
  }
10996
11114
  assertOpen() {
10997
- if (this.closed) throw new FerricStoreError("FerricStore topology adapter pool is closed");
11115
+ if (this.closed) throw new RequestNotSentError("FerricStore topology adapter pool is closed");
10998
11116
  }
10999
11117
  };
11000
11118
 
@@ -12120,7 +12238,7 @@ var AutoBatchExecutor = class {
12120
12238
  }
12121
12239
  async executeCommandArgs(args) {
12122
12240
  if (this.closed) {
12123
- throw new FerricStoreError("FerricStore client is closed");
12241
+ throw new RequestNotSentError("FerricStore client is closed");
12124
12242
  }
12125
12243
  const command = snapshotCommandArguments(args);
12126
12244
  if (this.executor.executePipeline == null) {
@@ -12140,7 +12258,7 @@ var AutoBatchExecutor = class {
12140
12258
  if (orderingBarrier != null) {
12141
12259
  await orderingBarrier;
12142
12260
  if (this.closed) {
12143
- throw new FerricStoreError("FerricStore client is closed");
12261
+ throw new RequestNotSentError("FerricStore client is closed");
12144
12262
  }
12145
12263
  }
12146
12264
  return await new Promise((resolve, reject) => {
@@ -12154,7 +12272,7 @@ var AutoBatchExecutor = class {
12154
12272
  }
12155
12273
  async executePipeline(commands, options) {
12156
12274
  if (this.closed) {
12157
- throw new FerricStoreError("FerricStore client is closed");
12275
+ throw new RequestNotSentError("FerricStore client is closed");
12158
12276
  }
12159
12277
  const snapshot = snapshotPipelineCommands(commands);
12160
12278
  const snapshotOptions = snapshotPipelineOptions(options);
@@ -12171,7 +12289,7 @@ var AutoBatchExecutor = class {
12171
12289
  }
12172
12290
  async executeFusedPipeline(commands, options) {
12173
12291
  if (this.closed) {
12174
- throw new FerricStoreError("FerricStore client is closed");
12292
+ throw new RequestNotSentError("FerricStore client is closed");
12175
12293
  }
12176
12294
  if (this.executor.executeFusedPipeline == null) return void 0;
12177
12295
  const snapshot = snapshotPipelineCommands(commands);
@@ -12182,7 +12300,7 @@ var AutoBatchExecutor = class {
12182
12300
  }
12183
12301
  async refreshTopology() {
12184
12302
  if (this.closed) {
12185
- throw new FerricStoreError("FerricStore client is closed");
12303
+ throw new RequestNotSentError("FerricStore client is closed");
12186
12304
  }
12187
12305
  const refreshTopology = this.executor.refreshTopology?.bind(this.executor);
12188
12306
  if (refreshTopology == null) {
@@ -12192,7 +12310,7 @@ var AutoBatchExecutor = class {
12192
12310
  }
12193
12311
  async route(key) {
12194
12312
  if (this.closed) {
12195
- throw new FerricStoreError("FerricStore client is closed");
12313
+ throw new RequestNotSentError("FerricStore client is closed");
12196
12314
  }
12197
12315
  const route = this.executor.route?.bind(this.executor);
12198
12316
  if (route == null) {
@@ -12212,7 +12330,7 @@ var AutoBatchExecutor = class {
12212
12330
  this.timer.cancel();
12213
12331
  this.timer = void 0;
12214
12332
  }
12215
- this.failPending(new Error("FerricStore client closed before auto-batch flush"));
12333
+ this.failPending(new RequestNotSentError("FerricStore client closed before auto-batch flush"));
12216
12334
  await this.orderingBarrier;
12217
12335
  await this.waitForInFlightBatches();
12218
12336
  await Promise.allSettled([...this.inFlightHelpers]);
@@ -12222,7 +12340,7 @@ var AutoBatchExecutor = class {
12222
12340
  }
12223
12341
  async executeAfterPendingBatches(operation) {
12224
12342
  if (this.closed) {
12225
- throw new FerricStoreError("FerricStore client is closed");
12343
+ throw new RequestNotSentError("FerricStore client is closed");
12226
12344
  }
12227
12345
  const previousBarrier = this.orderingBarrier;
12228
12346
  let releaseBarrier;
@@ -12243,7 +12361,7 @@ var AutoBatchExecutor = class {
12243
12361
  }
12244
12362
  executeHelper(operation) {
12245
12363
  if (this.closed) {
12246
- return Promise.reject(new FerricStoreError("FerricStore client is closed"));
12364
+ return Promise.reject(new RequestNotSentError("FerricStore client is closed"));
12247
12365
  }
12248
12366
  const task = Promise.resolve().then(operation);
12249
12367
  this.inFlightHelpers.add(task);
@@ -12265,7 +12383,7 @@ var AutoBatchExecutor = class {
12265
12383
  await previousBarrier;
12266
12384
  await this.flushNow();
12267
12385
  if (this.closed) {
12268
- throw new FerricStoreError("FerricStore client is closed");
12386
+ throw new RequestNotSentError("FerricStore client is closed");
12269
12387
  }
12270
12388
  dispatched = operation();
12271
12389
  } finally {
@@ -14604,7 +14722,7 @@ var FerricStoreClientBase = class {
14604
14722
  return validatePipelineResponse(results, snapshot.length);
14605
14723
  }
14606
14724
  async close() {
14607
- this.closeAbortController.abort(new FerricStoreError("FerricStore client is closed"));
14725
+ this.closeAbortController.abort(new RequestNotSentError("FerricStore client is closed"));
14608
14726
  this.closePromise ??= (async () => await this.executor.close?.())();
14609
14727
  await this.closePromise;
14610
14728
  }
@@ -17680,7 +17798,7 @@ async function executeProducerWriteWithBackpressure(execute, policy, signal) {
17680
17798
  function throwIfClosed(signal) {
17681
17799
  if (!signal.aborted) return;
17682
17800
  if (signal.reason instanceof Error) throw signal.reason;
17683
- throw new FerricStoreError("FerricStore client is closed", { raw: signal.reason });
17801
+ throw new RequestNotSentError("FerricStore client is closed", { raw: signal.reason });
17684
17802
  }
17685
17803
 
17686
17804
  // src/flow-policy.ts
@@ -18498,12 +18616,20 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18498
18616
  return this.records(items);
18499
18617
  }
18500
18618
  async claimJobs(type, options) {
18501
- return await this.claimDue(type, {
18619
+ const requestedState = options.state;
18620
+ const includeState = options.includeState ?? requestedState == null;
18621
+ const jobs = await this.claimDue(type, {
18502
18622
  ...options,
18503
- includeState: options.includeState ?? false,
18623
+ includeState,
18504
18624
  jobOnly: true,
18505
18625
  limit: options.limit ?? 100
18506
18626
  });
18627
+ if (!includeState && requestedState != null) {
18628
+ for (const job of jobs) {
18629
+ if (job[CLAIMED_ITEM_WIRE] != null) job.runState ??= requestedState;
18630
+ }
18631
+ }
18632
+ return jobs;
18507
18633
  }
18508
18634
  async reclaim(type, options) {
18509
18635
  const captured = snapshotClaimOptions(options);
@@ -18526,7 +18652,7 @@ var FerricStoreClaimClient = class extends FerricStoreMutationClient {
18526
18652
  this.appendPartitionOptions(args, captured);
18527
18653
  append(args, "PRIORITY", captured.priority);
18528
18654
  if (jobOnly) {
18529
- append(args, "RETURN", compactClaimReturnMode2(false, captured.includeAttributes === true));
18655
+ append(args, "RETURN", compactClaimReturnMode2(true, captured.includeAttributes === true));
18530
18656
  }
18531
18657
  appendPayloadRead(args, captured.payload, captured.payloadMaxBytes);
18532
18658
  appendValueReturn(args, { values: captured.values, valueMaxBytes: captured.valueMaxBytes });
@@ -18907,6 +19033,291 @@ var FerricStoreProducerClient = class extends FerricStoreClaimClient {
18907
19033
  }
18908
19034
  };
18909
19035
 
19036
+ // src/client-durable-step.ts
19037
+ import { createHash as createHash2 } from "crypto";
19038
+
19039
+ // src/workflow-utilities.ts
19040
+ function resolveWorkflowStates(configured, registrationFor) {
19041
+ const stateNames = [...new Set(configured)];
19042
+ if (stateNames.length === 0) throw new Error("Workflow worker requires at least one state");
19043
+ return stateNames.map((stateName) => {
19044
+ const registration = registrationFor(stateName);
19045
+ if (registration == null) {
19046
+ throw new Error(`No handler registered for workflow state '${stateName}'`);
19047
+ }
19048
+ return { registration, stateName };
19049
+ });
19050
+ }
19051
+ function normalizeStateMode(mode) {
19052
+ if (mode === "fifo" || mode === "parallel") return mode;
19053
+ throw new Error("state mode must be 'fifo' or 'parallel'");
19054
+ }
19055
+ function valueRefToString(value) {
19056
+ if (typeof value === "string") return value;
19057
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
19058
+ if (typeof value === "object" && value != null && Object.hasOwn(value, "ref")) {
19059
+ const ref = value.ref;
19060
+ return typeof ref === "string" ? ref : Buffer.isBuffer(ref) ? ref.toString("utf8") : void 0;
19061
+ }
19062
+ return void 0;
19063
+ }
19064
+
19065
+ // src/client-durable-step.ts
19066
+ var DURABLE_STEP_VALUE_PREFIX = "__ferricstore_step__:sha256:";
19067
+ function durableMutationMayHaveCommitted(error) {
19068
+ if (error instanceof ConnectionClosedError || error instanceof RequestTimeoutError) {
19069
+ return error.requestDisposition === "possibly_sent";
19070
+ }
19071
+ if (error instanceof HTTPTransportError) return error.safeToRetry !== true;
19072
+ if (error instanceof FlowNotFoundError || error instanceof FlowWrongStateError || error instanceof InvalidCommandError || error instanceof StaleLeaseError) {
19073
+ return false;
19074
+ }
19075
+ if (error instanceof FerricStoreError && error.safeToRetry === true) return false;
19076
+ return true;
19077
+ }
19078
+ async function advanceClaim(client, job, options) {
19079
+ assertDurableClaim(job);
19080
+ assertNonEmptyString(options.toState, "toState");
19081
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19082
+ const response = await client.stepContinue(job.id, {
19083
+ attributesDelete: options.attributesDelete,
19084
+ attributesMerge: options.attributesMerge,
19085
+ dropValues: options.dropValues,
19086
+ fencingToken: job.fencingToken,
19087
+ fromState: job.runState,
19088
+ leaseMs: options.leaseMs,
19089
+ leaseToken: job.leaseToken,
19090
+ nowMs: options.nowMs,
19091
+ overrideValues: options.overrideValues,
19092
+ partitionKey,
19093
+ payload: options.payload,
19094
+ returnJob: true,
19095
+ stateMeta: options.stateMeta,
19096
+ type: job.type,
19097
+ toState: options.toState,
19098
+ valueRefs: options.valueRefs,
19099
+ values: options.values
19100
+ });
19101
+ const refreshed = response;
19102
+ if (refreshed[CLAIMED_ITEM_WIRE] != null) {
19103
+ if (refreshed.runState != null && refreshed.runState !== options.toState) {
19104
+ throw new FerricStoreError("compact continuation response returned the wrong runState");
19105
+ }
19106
+ refreshed.runState ??= options.toState;
19107
+ if (refreshed.type == null && job.type != null) refreshed.type = job.type;
19108
+ }
19109
+ assertContinuationClaim(job, refreshed, options.toState);
19110
+ return refreshed;
19111
+ }
19112
+ async function runDurableStep(client, job, options, hooks = {}) {
19113
+ assertDurableClaim(job);
19114
+ const name = options.name;
19115
+ const run2 = options.run;
19116
+ const continuation = snapshotAdvanceOptions(options);
19117
+ assertNonEmptyString(name, "name");
19118
+ if (name.trim().length === 0) throw new TypeError("name must not be blank");
19119
+ if (!name.isWellFormed()) throw new TypeError("name must contain valid Unicode");
19120
+ assertNonEmptyString(continuation.toState, "toState");
19121
+ if (typeof run2 !== "function") throw new TypeError("run must be a function");
19122
+ const resultName = durableStepValueName(name);
19123
+ assertStepValueIsReserved(continuation, resultName);
19124
+ const leaseMs = continuation.leaseMs ?? 3e4;
19125
+ const partitionKey = job.partitionKey === "" ? void 0 : job.partitionKey;
19126
+ const validated = await client.extendLease(job.id, {
19127
+ fencingToken: job.fencingToken,
19128
+ leaseMs,
19129
+ leaseToken: job.leaseToken,
19130
+ nowMs: continuation.nowMs,
19131
+ partitionKey
19132
+ });
19133
+ assertValidatedLease(job, validated);
19134
+ if (validated.valueRefs != null && Object.hasOwn(validated.valueRefs, resultName)) {
19135
+ if (validated.runState !== continuation.toState) {
19136
+ throw new FerricStoreError("committed durable step result does not match the requested target state");
19137
+ }
19138
+ const ref = valueRefToString(validated.valueRefs[resultName]);
19139
+ if (ref == null) {
19140
+ throw new FerricStoreError("committed durable step result has invalid reference metadata");
19141
+ }
19142
+ const entry = (await valueMGetEntries(client, [ref]))[0];
19143
+ if (entry?.found !== true) {
19144
+ throw new FerricStoreError("committed durable step result is missing");
19145
+ }
19146
+ const replayedJob = claimedItemFromRecord(validated);
19147
+ notifyWithoutReplacingOutcome(hooks.replayed, replayedJob);
19148
+ return { job: replayedJob, result: entry.value };
19149
+ }
19150
+ const result = normalizeStepResult(client, await run2());
19151
+ await hooks.beforeCommit?.();
19152
+ let refreshed;
19153
+ try {
19154
+ refreshed = await advanceClaim(client, validated, {
19155
+ ...continuation,
19156
+ leaseMs,
19157
+ values: { ...continuation.values, [resultName]: result }
19158
+ });
19159
+ } catch (error) {
19160
+ notifyWithoutReplacingOutcome(hooks.commitFailed, error);
19161
+ throw error;
19162
+ }
19163
+ notifyWithoutReplacingOutcome(hooks.committed, refreshed);
19164
+ return { job: refreshed, result };
19165
+ }
19166
+ function notifyWithoutReplacingOutcome(callback, value) {
19167
+ try {
19168
+ callback?.(value);
19169
+ } catch {
19170
+ }
19171
+ }
19172
+ function normalizeStepResult(client, value) {
19173
+ return client.codec.decode(client.codec.encode(value));
19174
+ }
19175
+ function durableStepValueName(name) {
19176
+ const digest = createHash2("sha256").update(name, "utf8").digest("hex");
19177
+ return `${DURABLE_STEP_VALUE_PREFIX}${digest}`;
19178
+ }
19179
+ function snapshotAdvanceOptions(options) {
19180
+ return {
19181
+ ...options.attributesDelete == null ? {} : { attributesDelete: [...options.attributesDelete] },
19182
+ ...options.attributesMerge == null ? {} : { attributesMerge: { ...options.attributesMerge } },
19183
+ ...options.dropValues == null ? {} : { dropValues: [...options.dropValues] },
19184
+ ...options.leaseMs == null ? {} : { leaseMs: options.leaseMs },
19185
+ ...options.nowMs == null ? {} : { nowMs: options.nowMs },
19186
+ ...options.overrideValues == null ? {} : { overrideValues: [...options.overrideValues] },
19187
+ ...options.payload === void 0 ? {} : { payload: options.payload },
19188
+ ...options.stateMeta == null ? {} : { stateMeta: { ...options.stateMeta } },
19189
+ toState: options.toState,
19190
+ ...options.valueRefs == null ? {} : { valueRefs: { ...options.valueRefs } },
19191
+ ...options.values == null ? {} : { values: { ...options.values } }
19192
+ };
19193
+ }
19194
+ function assertDurableClaim(job) {
19195
+ if (typeof job !== "object" || job == null) throw new TypeError("job must be a claimed Flow");
19196
+ assertNonEmptyString(job.id, "job.id");
19197
+ assertNonEmptyString(job.runState, "job.runState");
19198
+ if (!Buffer.isBuffer(job.leaseToken) || job.leaseToken.byteLength === 0) {
19199
+ throw new TypeError("job.leaseToken must be a non-empty Buffer");
19200
+ }
19201
+ if ((typeof job.fencingToken !== "number" || !Number.isSafeInteger(job.fencingToken)) && typeof job.fencingToken !== "bigint") {
19202
+ throw new TypeError("job.fencingToken must be an integer");
19203
+ }
19204
+ if (fencingValue(job.fencingToken) <= 0n) {
19205
+ throw new TypeError("job.fencingToken must be a positive integer");
19206
+ }
19207
+ if (job.state !== "running") {
19208
+ throw new TypeError("job.state must be running");
19209
+ }
19210
+ }
19211
+ function assertValidatedLease(expected, actual) {
19212
+ try {
19213
+ assertDurableClaim(actual);
19214
+ } catch (error) {
19215
+ throw new FerricStoreError("lease validation returned an invalid claim", { cause: error });
19216
+ }
19217
+ if (actual.id !== expected.id) throw leaseValidationMismatch("id");
19218
+ if (normalizedPartition(actual.partitionKey) !== normalizedPartition(expected.partitionKey)) {
19219
+ throw leaseValidationMismatch("partition");
19220
+ }
19221
+ if (!actual.leaseToken.equals(expected.leaseToken)) throw leaseValidationMismatch("lease token");
19222
+ if (fencingValue(actual.fencingToken) !== fencingValue(expected.fencingToken)) {
19223
+ throw leaseValidationMismatch("fencing token");
19224
+ }
19225
+ if (actual.state !== expected.state) throw leaseValidationMismatch("physical state");
19226
+ if (actual.runState !== expected.runState) throw leaseValidationMismatch("runState");
19227
+ }
19228
+ function assertContinuationClaim(previous, refreshed, toState) {
19229
+ try {
19230
+ assertDurableClaim(refreshed);
19231
+ } catch (error) {
19232
+ throw new FerricStoreError("continuation response returned an invalid claim", { cause: error });
19233
+ }
19234
+ if (refreshed.id !== previous.id) throw continuationMismatch("id");
19235
+ if (normalizedPartition(refreshed.partitionKey) !== normalizedPartition(previous.partitionKey)) {
19236
+ throw continuationMismatch("partition");
19237
+ }
19238
+ if (refreshed.leaseToken.equals(previous.leaseToken)) {
19239
+ throw continuationMismatch("lease token did not change");
19240
+ }
19241
+ if (fencingValue(refreshed.fencingToken) <= fencingValue(previous.fencingToken)) {
19242
+ throw continuationMismatch("fencing token did not increase");
19243
+ }
19244
+ if (refreshed.state !== "running") throw continuationMismatch("physical state");
19245
+ if (refreshed.runState !== toState) throw continuationMismatch("runState");
19246
+ }
19247
+ function leaseValidationMismatch(field3) {
19248
+ return new FerricStoreError(`lease validation returned a mismatched ${field3}`);
19249
+ }
19250
+ function continuationMismatch(field3) {
19251
+ return new FerricStoreError(`continuation response returned a mismatched ${field3}`);
19252
+ }
19253
+ function normalizedPartition(value) {
19254
+ return value == null || value === "" ? void 0 : value;
19255
+ }
19256
+ function fencingValue(value) {
19257
+ return typeof value === "bigint" ? value : BigInt(value);
19258
+ }
19259
+ function assertNonEmptyString(value, name) {
19260
+ if (typeof value !== "string" || value.length === 0) {
19261
+ throw new TypeError(`${name} must be a non-empty string`);
19262
+ }
19263
+ }
19264
+ function assertStepValueIsReserved(options, resultName) {
19265
+ if (options.values != null && Object.hasOwn(options.values, resultName)) {
19266
+ throw new TypeError("values cannot mutate the reserved durable step result");
19267
+ }
19268
+ if (options.valueRefs != null && Object.hasOwn(options.valueRefs, resultName)) {
19269
+ throw new TypeError("valueRefs cannot mutate the reserved durable step result");
19270
+ }
19271
+ if (options.dropValues?.includes(resultName) === true) {
19272
+ throw new TypeError("dropValues cannot mutate the reserved durable step result");
19273
+ }
19274
+ if (options.overrideValues?.includes(resultName) === true) {
19275
+ throw new TypeError("overrideValues cannot mutate the reserved durable step result");
19276
+ }
19277
+ }
19278
+ function claimedItemFromRecord(record) {
19279
+ return {
19280
+ ...record.attributes == null ? {} : { attributes: record.attributes },
19281
+ fencingToken: record.fencingToken,
19282
+ id: record.id,
19283
+ leaseToken: Buffer.from(record.leaseToken),
19284
+ ...record.partitionKey === "" ? {} : { partitionKey: record.partitionKey },
19285
+ ...record.payload === void 0 ? {} : { payload: record.payload },
19286
+ ...record.runState == null ? {} : { runState: record.runState },
19287
+ state: record.state,
19288
+ type: record.type
19289
+ };
19290
+ }
19291
+
19292
+ // src/client-step-continue.ts
19293
+ function stepContinueArguments(id, options, codec) {
19294
+ try {
19295
+ const args = [
19296
+ "FLOW.STEP_CONTINUE",
19297
+ id,
19298
+ options.leaseToken,
19299
+ options.fromState,
19300
+ options.toState,
19301
+ "FENCING",
19302
+ options.fencingToken,
19303
+ "LEASE_MS",
19304
+ options.leaseMs ?? 3e4,
19305
+ "NOW",
19306
+ options.nowMs ?? nowMs()
19307
+ ];
19308
+ append(args, "PARTITION", options.partitionKey);
19309
+ append(args, "WORKER", options.worker);
19310
+ appendEncoded(args, "PAYLOAD", codec, options.payload);
19311
+ if (options.returnJob === true) args.push("RETURN", "JOBS_COMPACT");
19312
+ appendStateMeta(args, options.stateMeta);
19313
+ appendNamedValues(args, codec, options);
19314
+ appendAttributeMutations(args, options);
19315
+ return args;
19316
+ } catch (error) {
19317
+ throw requestNotSentError(error);
19318
+ }
19319
+ }
19320
+
18910
19321
  // src/client.ts
18911
19322
  var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClient {
18912
19323
  static async fromUrl(url, options = {}) {
@@ -19021,33 +19432,30 @@ var FerricStoreClient = class _FerricStoreClient extends FerricStoreProducerClie
19021
19432
  appendNamedValues(args, this.codec, options);
19022
19433
  return await this.recordOrGet(await this.commandArgs(args), id, partitionKey);
19023
19434
  }
19435
+ /**
19436
+ * @deprecated Low-level compatibility API. Use {@link advance} for state-only
19437
+ * continuation or {@link step} for a durable closure.
19438
+ */
19024
19439
  async stepContinue(id, options) {
19025
19440
  const partitionKey = options.partitionKey;
19026
19441
  const returnJob = options.returnJob === true;
19027
19442
  const type = options.type;
19028
- const args = [
19029
- "FLOW.STEP_CONTINUE",
19030
- id,
19031
- options.leaseToken,
19032
- options.fromState,
19033
- options.toState,
19034
- "FENCING",
19035
- options.fencingToken,
19036
- "LEASE_MS",
19037
- options.leaseMs ?? 3e4,
19038
- "NOW",
19039
- options.nowMs ?? nowMs()
19040
- ];
19041
- append(args, "PARTITION", partitionKey);
19042
- append(args, "WORKER", options.worker);
19043
- appendEncoded(args, "PAYLOAD", this.codec, options.payload);
19044
- if (returnJob) args.push("RETURN", "JOBS_COMPACT");
19045
- appendStateMeta(args, options.stateMeta);
19046
- appendNamedValues(args, this.codec, options);
19047
- appendAttributeMutations(args, options);
19443
+ const args = stepContinueArguments(id, options, this.codec);
19048
19444
  const response = await this.commandArgs(args);
19049
19445
  return returnJob ? claimedItemFromResp(response, this.codec, { type }) : await this.recordOrGet(response, id, partitionKey);
19050
19446
  }
19447
+ /** Advance a claimed Flow and return its renewed claim. */
19448
+ async advance(job, options) {
19449
+ return await advanceClaim(this, job, options);
19450
+ }
19451
+ /**
19452
+ * Run a closure after validating the claim, then atomically journal its
19453
+ * result and advance the Flow. A committed result is replayed without
19454
+ * invoking the closure again.
19455
+ */
19456
+ async step(job, options) {
19457
+ return await runDurableStep(this, job, options);
19458
+ }
19051
19459
  async runStepsMany(items, options) {
19052
19460
  if (items.length === 0) return Buffer.from("OK");
19053
19461
  const hasStates = options.states != null;
@@ -19297,6 +19705,8 @@ function validateUnicodeScalarText2(value) {
19297
19705
 
19298
19706
  // src/outcomes.ts
19299
19707
  var OUTCOME_BRAND = /* @__PURE__ */ Symbol("ferricstore.outcome");
19708
+ var ALREADY_APPLIED_BRAND = /* @__PURE__ */ Symbol("ferricstore.alreadyApplied");
19709
+ var REPLAYED_STEP_BRAND = /* @__PURE__ */ Symbol("ferricstore.replayedStep");
19300
19710
  function transition(toState, options = {}) {
19301
19711
  return brandOutcome({ ...options, kind: "transition", toState });
19302
19712
  }
@@ -19309,6 +19719,22 @@ function retry(options = {}) {
19309
19719
  function fail2(options = {}) {
19310
19720
  return brandOutcome({ ...options, kind: "fail" });
19311
19721
  }
19722
+ function alreadyApplied(job) {
19723
+ return brandAlreadyApplied({ job, kind: "already_applied" });
19724
+ }
19725
+ function appliedStep(job, result) {
19726
+ return brandAlreadyApplied({ applied: true, job, kind: "already_applied", result });
19727
+ }
19728
+ function replayedStep(job, result) {
19729
+ const replayed = { applied: false, job, result };
19730
+ Object.defineProperty(replayed, REPLAYED_STEP_BRAND, {
19731
+ configurable: false,
19732
+ enumerable: false,
19733
+ value: true,
19734
+ writable: false
19735
+ });
19736
+ return replayed;
19737
+ }
19312
19738
  function isOutcome(value) {
19313
19739
  if (typeof value !== "object" || value == null || !Object.hasOwn(value, OUTCOME_BRAND) || value[OUTCOME_BRAND] !== true) {
19314
19740
  return false;
@@ -19319,6 +19745,12 @@ function isOutcome(value) {
19319
19745
  }
19320
19746
  return outcome.kind === "complete" || outcome.kind === "retry" || outcome.kind === "fail";
19321
19747
  }
19748
+ function isAlreadyAppliedOutcome(value) {
19749
+ return typeof value === "object" && value != null && Object.hasOwn(value, ALREADY_APPLIED_BRAND) && value[ALREADY_APPLIED_BRAND] === true && value.kind === "already_applied";
19750
+ }
19751
+ function isReplayedStepResult(value) {
19752
+ return typeof value === "object" && value != null && Object.hasOwn(value, REPLAYED_STEP_BRAND) && value[REPLAYED_STEP_BRAND] === true && value.applied === false;
19753
+ }
19322
19754
  function brandOutcome(outcome) {
19323
19755
  Object.defineProperty(outcome, "kind", {
19324
19756
  configurable: false,
@@ -19342,6 +19774,21 @@ function brandOutcome(outcome) {
19342
19774
  });
19343
19775
  return outcome;
19344
19776
  }
19777
+ function brandAlreadyApplied(outcome) {
19778
+ Object.defineProperty(outcome, "kind", {
19779
+ configurable: false,
19780
+ enumerable: true,
19781
+ value: "already_applied",
19782
+ writable: false
19783
+ });
19784
+ Object.defineProperty(outcome, ALREADY_APPLIED_BRAND, {
19785
+ configurable: false,
19786
+ enumerable: false,
19787
+ value: true,
19788
+ writable: false
19789
+ });
19790
+ return outcome;
19791
+ }
19345
19792
 
19346
19793
  // src/worker-config.ts
19347
19794
  function snapshotWorkerConfig(options) {
@@ -19358,8 +19805,7 @@ function snapshotWorkerConfig(options) {
19358
19805
  return Object.freeze(snapshot);
19359
19806
  }
19360
19807
 
19361
- // src/worker-internal.ts
19362
- var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19808
+ // src/worker-lease-guard.ts
19363
19809
  var LeaseRenewalError = class extends Error {
19364
19810
  constructor(cause) {
19365
19811
  super("FerricStore lease renewal failed", { cause });
@@ -19370,37 +19816,58 @@ var LeaseRenewalGuard = class {
19370
19816
  constructor(client, leasedJob, leaseMs, options) {
19371
19817
  this.client = client;
19372
19818
  this.leaseMs = leaseMs;
19373
- this.job = snapshotLeasedJob(leasedJob);
19374
- if (options.leaseRenewal === false) {
19375
- return;
19376
- }
19377
- const intervalMs = positiveInteger4(options.leaseRenewIntervalMs, Math.max(1, Math.trunc(leaseMs / 2)));
19378
- this.timer = setLongInterval(() => this.renew(), intervalMs);
19379
- this.timer.unref();
19819
+ this.currentJob = snapshotLeasedJob(leasedJob);
19820
+ this.renewalEnabled = options.leaseRenewal !== false;
19821
+ this.intervalMs = positiveInteger4(options.leaseRenewIntervalMs, Math.max(1, Math.trunc(leaseMs / 2)));
19822
+ this.startRenewal();
19380
19823
  }
19381
19824
  client;
19382
19825
  leaseMs;
19383
19826
  error;
19384
19827
  inFlight;
19385
- job;
19828
+ currentJob;
19829
+ intervalMs;
19830
+ renewalEnabled;
19831
+ state = "active";
19386
19832
  timer;
19833
+ get job() {
19834
+ return this.currentJob;
19835
+ }
19387
19836
  assertActive() {
19388
- if (this.error != null) {
19389
- throw this.error;
19390
- }
19837
+ if (this.error != null) throw this.error;
19391
19838
  }
19392
19839
  async stop() {
19393
- if (this.timer != null) {
19394
- this.timer.cancel();
19395
- this.timer = void 0;
19396
- }
19840
+ this.state = "stopped";
19841
+ this.cancelRenewal();
19842
+ await this.inFlight;
19843
+ this.assertActive();
19844
+ }
19845
+ async pauseForLeaseMutation() {
19846
+ if (this.state === "stopped") throw new Error("lease renewal guard is stopped");
19847
+ if (this.state === "paused") return;
19848
+ this.state = "paused";
19849
+ this.cancelRenewal();
19397
19850
  await this.inFlight;
19398
19851
  this.assertActive();
19399
19852
  }
19853
+ resumeWith(job) {
19854
+ if (this.state !== "paused") throw new Error("lease renewal guard is not paused");
19855
+ this.currentJob = snapshotLeasedJob(job);
19856
+ this.state = "active";
19857
+ this.startRenewal();
19858
+ }
19859
+ cancelRenewal() {
19860
+ if (this.timer == null) return;
19861
+ this.timer.cancel();
19862
+ this.timer = void 0;
19863
+ }
19864
+ startRenewal() {
19865
+ if (!this.renewalEnabled || this.state !== "active") return;
19866
+ this.timer = setLongInterval(() => this.renew(), this.intervalMs);
19867
+ this.timer.unref();
19868
+ }
19400
19869
  renew() {
19401
- if (this.error != null || this.inFlight != null) {
19402
- return;
19403
- }
19870
+ if (this.state !== "active" || this.error != null || this.inFlight != null) return;
19404
19871
  const renewal = this.client.extendLease(this.job.id, {
19405
19872
  fencingToken: this.job.fencingToken,
19406
19873
  leaseMs: this.leaseMs,
@@ -19411,17 +19878,12 @@ var LeaseRenewalGuard = class {
19411
19878
  () => void 0,
19412
19879
  (error) => {
19413
19880
  this.error = new LeaseRenewalError(error);
19414
- if (this.timer != null) {
19415
- this.timer.cancel();
19416
- this.timer = void 0;
19417
- }
19881
+ this.cancelRenewal();
19418
19882
  }
19419
19883
  );
19420
19884
  this.inFlight = renewal;
19421
19885
  void renewal.finally(() => {
19422
- if (this.inFlight === renewal) {
19423
- this.inFlight = void 0;
19424
- }
19886
+ if (this.inFlight === renewal) this.inFlight = void 0;
19425
19887
  });
19426
19888
  }
19427
19889
  };
@@ -19445,8 +19907,14 @@ function snapshotLeasedJob(job) {
19445
19907
  }
19446
19908
  return snapshot;
19447
19909
  }
19910
+ function positiveInteger4(value, fallback) {
19911
+ return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
19912
+ }
19913
+
19914
+ // src/worker-internal.ts
19915
+ var DEFAULT_FLOW_MANY_BATCH_LIMIT2 = 1e3;
19448
19916
  function workerConcurrency(options) {
19449
- return positiveInteger4(options.concurrency ?? options.workers, 1);
19917
+ return positiveInteger5(options.concurrency ?? options.workers, 1);
19450
19918
  }
19451
19919
  function workerRefillStrategy(options) {
19452
19920
  return options.refillStrategy === "wave" ? "wave" : "continuous";
@@ -19489,18 +19957,18 @@ function workerErrorPayload(error, options, codec) {
19489
19957
  }
19490
19958
  function workerBatchSize(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19491
19959
  return Math.min(
19492
- positiveInteger4(options.batchSize, 10),
19493
- positiveInteger4(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
19960
+ positiveInteger5(options.batchSize, 10),
19961
+ positiveInteger5(flowManyBatchLimit, DEFAULT_FLOW_MANY_BATCH_LIMIT2)
19494
19962
  );
19495
19963
  }
19496
19964
  function workerClaimLimit(options, flowManyBatchLimit = DEFAULT_FLOW_MANY_BATCH_LIMIT2) {
19497
19965
  return Math.min(workerBatchSize(options, flowManyBatchLimit), workerConcurrency(options));
19498
19966
  }
19499
19967
  function workerDrainBatches(options) {
19500
- return positiveInteger4(options.claimDrainBatches, 1);
19968
+ return positiveInteger5(options.claimDrainBatches, 1);
19501
19969
  }
19502
19970
  function workerLeaseMs(options, fallback = 3e4) {
19503
- return positiveInteger4(options.leaseMs, positiveInteger4(fallback, 3e4));
19971
+ return positiveInteger5(options.leaseMs, positiveInteger5(fallback, 3e4));
19504
19972
  }
19505
19973
  function workerIdleSleepMs(options) {
19506
19974
  return nonNegativeDuration(options.idleSleepMs, 250);
@@ -19512,7 +19980,7 @@ function workerClaimBlockMs(options, useBlocking) {
19512
19980
  if (options.signal == null || !Number.isFinite(options.blockMs) || options.blockMs < 0) {
19513
19981
  return options.blockMs;
19514
19982
  }
19515
- const abortPollMs = positiveInteger4(options.abortPollMs, 1e3);
19983
+ const abortPollMs = positiveInteger5(options.abortPollMs, 1e3);
19516
19984
  return options.blockMs === 0 ? abortPollMs : Math.min(Math.trunc(options.blockMs), abortPollMs);
19517
19985
  }
19518
19986
  function workerMaxIdleSleepMs(options, idleSleepMs = workerIdleSleepMs(options)) {
@@ -19523,8 +19991,8 @@ function nextWorkerIdleSleepMs(currentIdleSleepMs, maxIdleSleepMs) {
19523
19991
  return Math.min(maxIdleSleepMs, Math.max(1, currentIdleSleepMs * 2));
19524
19992
  }
19525
19993
  async function runContinuousWorkerPool(options) {
19526
- const concurrency = positiveInteger4(options.concurrency, 1);
19527
- const maxClaimSize = positiveInteger4(options.maxClaimSize, concurrency);
19994
+ const concurrency = positiveInteger5(options.concurrency, 1);
19995
+ const maxClaimSize = positiveInteger5(options.maxClaimSize, concurrency);
19528
19996
  const initialIdleSleepMs = nonNegativeDuration(options.idleSleepMs, 250);
19529
19997
  const maxIdleSleepMs = Math.max(
19530
19998
  initialIdleSleepMs,
@@ -19646,7 +20114,7 @@ function normalizeWorkflowWorkerConfig(options) {
19646
20114
  ...captured
19647
20115
  });
19648
20116
  }
19649
- function positiveInteger4(value, fallback) {
20117
+ function positiveInteger5(value, fallback) {
19650
20118
  return value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
19651
20119
  }
19652
20120
  function nonNegativeInteger5(value, fallback) {
@@ -20588,51 +21056,57 @@ var Queue = class {
20588
21056
  // src/workflow.ts
20589
21057
  import { randomUUID as randomUUID2 } from "crypto";
20590
21058
 
20591
- // src/workflow-utilities.ts
20592
- function resolveWorkflowStates(configured, registrationFor) {
20593
- const stateNames = [...new Set(configured)];
20594
- if (stateNames.length === 0) throw new Error("Workflow worker requires at least one state");
20595
- return stateNames.map((stateName) => {
20596
- const registration = registrationFor(stateName);
20597
- if (registration == null) {
20598
- throw new Error(`No handler registered for workflow state '${stateName}'`);
20599
- }
20600
- return { registration, stateName };
21059
+ // src/workflow-registration.ts
21060
+ function createWorkflowStateRegistration(name, handler, options, defaultValueMaxBytes) {
21061
+ const own2 = { ...options };
21062
+ const claimValues = own2.claimValues == null ? void 0 : snapshotOwnStringArray(own2.claimValues, "claimValues");
21063
+ const retryPolicy = own2.retryPolicy == null ? void 0 : Object.freeze({ ...own2.retryPolicy });
21064
+ return Object.freeze({
21065
+ claimPayload: own2.claimPayload ?? true,
21066
+ claimRecord: own2.claimRecord ?? true,
21067
+ exceptionPolicy: normalizeExceptionPolicy(own2.exceptionPolicy),
21068
+ handler,
21069
+ leaseMs: own2.leaseMs ?? 3e4,
21070
+ ...own2.mode == null ? {} : { mode: normalizeStateMode(own2.mode) },
21071
+ name,
21072
+ returnRecord: own2.returnRecord ?? false,
21073
+ valueMaxBytes: own2.valueMaxBytes ?? defaultValueMaxBytes,
21074
+ ...claimValues == null ? {} : { claimValues },
21075
+ ...retryPolicy == null ? {} : { retryPolicy }
20601
21076
  });
20602
21077
  }
20603
- function normalizeStateMode(mode) {
20604
- if (mode === "fifo" || mode === "parallel") return mode;
20605
- throw new Error("state mode must be 'fifo' or 'parallel'");
20606
- }
20607
- function valueRefToString(value) {
20608
- if (typeof value === "string") return value;
20609
- if (Buffer.isBuffer(value)) return value.toString("utf8");
20610
- if (typeof value === "object" && value != null && Object.hasOwn(value, "ref")) {
20611
- const ref = value.ref;
20612
- return typeof ref === "string" ? ref : Buffer.isBuffer(ref) ? ref.toString("utf8") : void 0;
20613
- }
20614
- return void 0;
20615
- }
20616
21078
 
20617
21079
  // src/workflow-context.ts
20618
21080
  var MISSING_VALUE = /* @__PURE__ */ Symbol("ferricstore.missingValue");
20619
21081
  var WorkflowContext = class {
20620
21082
  workflow;
20621
21083
  job;
20622
- stateName;
20623
21084
  flow;
21085
+ appliedMutation = false;
21086
+ currentStateName;
20624
21087
  leaseJob;
21088
+ mutationFailure;
21089
+ mutationPhase = "idle";
21090
+ mutationCoordinator;
20625
21091
  valueCache = /* @__PURE__ */ new Map();
20626
- constructor(workflow, job, stateName, leaseJob = job) {
21092
+ constructor(workflow, job, stateName) {
20627
21093
  this.workflow = workflow;
20628
21094
  this.job = job;
20629
- this.leaseJob = leaseJob;
20630
- this.stateName = stateName;
21095
+ this.leaseJob = job;
21096
+ this.currentStateName = stateName;
20631
21097
  this.flow = new WorkflowFlowCommands(this);
20632
21098
  }
21099
+ /** @internal */
21100
+ configureWorkerMutation(leaseJob, coordinator) {
21101
+ this.leaseJob = leaseJob;
21102
+ this.mutationCoordinator = coordinator;
21103
+ }
20633
21104
  get client() {
20634
21105
  return this.workflow.client;
20635
21106
  }
21107
+ get stateName() {
21108
+ return this.currentStateName;
21109
+ }
20636
21110
  get id() {
20637
21111
  return this.leaseJob.id;
20638
21112
  }
@@ -20643,7 +21117,7 @@ var WorkflowContext = class {
20643
21117
  return this.leaseJob.state;
20644
21118
  }
20645
21119
  get logicalState() {
20646
- return this.stateName;
21120
+ return this.currentStateName;
20647
21121
  }
20648
21122
  get partitionKey() {
20649
21123
  return this.leaseJob.partitionKey;
@@ -20681,6 +21155,53 @@ var WorkflowContext = class {
20681
21155
  get correlationId() {
20682
21156
  return "correlationId" in this.job ? this.job.correlationId : void 0;
20683
21157
  }
21158
+ async advance(toState, options = {}) {
21159
+ if (typeof toState !== "string" || toState.length === 0) {
21160
+ throw new TypeError("toState must be a non-empty string");
21161
+ }
21162
+ const current = this.durableClaim();
21163
+ await this.beforeCommit();
21164
+ let refreshed;
21165
+ try {
21166
+ refreshed = await advanceClaim(this.client, current, { ...options, toState });
21167
+ } catch (error) {
21168
+ this.commitFailed(error);
21169
+ throw error;
21170
+ }
21171
+ this.committed(refreshed);
21172
+ return alreadyApplied(refreshed);
21173
+ }
21174
+ async step(options) {
21175
+ let applied = false;
21176
+ const stepped = await runDurableStep(this.client, this.durableClaim(), options, {
21177
+ beforeCommit: async () => await this.beforeCommit(),
21178
+ commitFailed: (error) => this.commitFailed(error),
21179
+ committed: (job) => {
21180
+ applied = true;
21181
+ this.committed(job);
21182
+ },
21183
+ replayed: (job) => this.replayed(job)
21184
+ });
21185
+ return applied ? appliedStep(stepped.job, stepped.result) : replayedStep(stepped.job, stepped.result);
21186
+ }
21187
+ /** @internal */
21188
+ get hasAppliedMutation() {
21189
+ return this.appliedMutation;
21190
+ }
21191
+ /** @internal */
21192
+ get uncertainMutationFailure() {
21193
+ return this.mutationPhase === "uncertain" ? this.mutationFailure : void 0;
21194
+ }
21195
+ /** @internal */
21196
+ get hasUncertainMutation() {
21197
+ return this.mutationPhase === "uncertain";
21198
+ }
21199
+ /** @internal */
21200
+ assertAppliedOutcome(outcome) {
21201
+ if (!this.appliedMutation || !outcome.job.leaseToken.equals(this.leaseJob.leaseToken) || outcome.job.fencingToken !== this.leaseJob.fencingToken) {
21202
+ throw new Error("already-applied outcome does not match the workflow's refreshed claim");
21203
+ }
21204
+ }
20684
21205
  async value(name, defaultValue, options = {}) {
20685
21206
  const useLocalCache = options.localCache ?? this.workflow.valueConfig.localCache;
20686
21207
  if (useLocalCache && this.valueCache.has(name)) {
@@ -20760,7 +21281,49 @@ var WorkflowContext = class {
20760
21281
  return result;
20761
21282
  }
20762
21283
  valueMaxBytes() {
20763
- return this.workflow.stateRegistration(this.stateName)?.valueMaxBytes;
21284
+ return this.workflow.stateRegistration(this.currentStateName)?.valueMaxBytes;
21285
+ }
21286
+ durableClaim() {
21287
+ const runState = this.leaseJob.runState ?? (this.leaseJob[CLAIMED_ITEM_WIRE] == null ? void 0 : this.currentStateName);
21288
+ if (typeof runState !== "string" || runState.length === 0) {
21289
+ throw new TypeError("job.runState must be a non-empty string");
21290
+ }
21291
+ return {
21292
+ ...this.leaseJob,
21293
+ leaseToken: Buffer.from(this.leaseJob.leaseToken),
21294
+ runState
21295
+ };
21296
+ }
21297
+ async beforeCommit() {
21298
+ if (this.mutationPhase !== "idle") {
21299
+ throw new Error("a durable workflow mutation is already in progress or has an uncertain result");
21300
+ }
21301
+ await this.mutationCoordinator?.pause();
21302
+ this.mutationPhase = "committing";
21303
+ }
21304
+ commitFailed(error) {
21305
+ if (!durableMutationMayHaveCommitted(error)) {
21306
+ this.mutationFailure = void 0;
21307
+ this.mutationPhase = "idle";
21308
+ this.mutationCoordinator?.resume(this.leaseJob);
21309
+ return;
21310
+ }
21311
+ this.mutationFailure = error;
21312
+ this.mutationPhase = "uncertain";
21313
+ }
21314
+ committed(job) {
21315
+ this.acceptJob(job);
21316
+ this.appliedMutation = true;
21317
+ this.mutationCoordinator?.resume(job);
21318
+ }
21319
+ replayed(job) {
21320
+ this.acceptJob(job);
21321
+ }
21322
+ acceptJob(job) {
21323
+ this.leaseJob = job;
21324
+ this.currentStateName = job.runState ?? this.currentStateName;
21325
+ this.mutationFailure = void 0;
21326
+ this.mutationPhase = "idle";
20764
21327
  }
20765
21328
  };
20766
21329
  var WorkflowFlowCommands = class {
@@ -20798,6 +21361,7 @@ var WorkflowFlowCommands = class {
20798
21361
  toState
20799
21362
  });
20800
21363
  }
21364
+ /** @deprecated Use `ctx.advance(toState)`. */
20801
21365
  async stepContinue(toState, options = {}) {
20802
21366
  return await this.ctx.client.stepContinue(this.ctx.id, {
20803
21367
  ...options,
@@ -20960,25 +21524,86 @@ async function applyFail(client, ctx, outcome, returnRecord) {
20960
21524
  });
20961
21525
  }
20962
21526
 
20963
- // src/workflow-registration.ts
20964
- function createWorkflowStateRegistration(name, handler, options, defaultValueMaxBytes) {
20965
- const own2 = { ...options };
20966
- const claimValues = own2.claimValues == null ? void 0 : snapshotOwnStringArray(own2.claimValues, "claimValues");
20967
- const retryPolicy = own2.retryPolicy == null ? void 0 : Object.freeze({ ...own2.retryPolicy });
20968
- return Object.freeze({
20969
- claimPayload: own2.claimPayload ?? true,
20970
- claimRecord: own2.claimRecord ?? true,
20971
- exceptionPolicy: normalizeExceptionPolicy(own2.exceptionPolicy),
20972
- handler,
20973
- leaseMs: own2.leaseMs ?? 3e4,
20974
- ...own2.mode == null ? {} : { mode: normalizeStateMode(own2.mode) },
20975
- name,
20976
- returnRecord: own2.returnRecord ?? false,
20977
- valueMaxBytes: own2.valueMaxBytes ?? defaultValueMaxBytes,
20978
- ...claimValues == null ? {} : { claimValues },
20979
- ...retryPolicy == null ? {} : { retryPolicy }
21527
+ // src/workflow-worker-context.ts
21528
+ var WorkerWorkflowContext = class extends WorkflowContext {
21529
+ constructor(workflow, job, stateName, leaseJob, mutationCoordinator) {
21530
+ super(workflow, job, stateName);
21531
+ this.configureWorkerMutation(leaseJob, mutationCoordinator);
21532
+ }
21533
+ };
21534
+
21535
+ // src/workflow-job-execution.ts
21536
+ async function executeWorkflowJob(workflow, workerOptions, job, registration, guard) {
21537
+ guard.assertActive();
21538
+ const ctx = new WorkerWorkflowContext(workflow, job, registration.name, guard.job, {
21539
+ pause: async () => await guard.pauseForLeaseMutation(),
21540
+ resume: (refreshed) => guard.resumeWith(refreshed)
21541
+ });
21542
+ let value;
21543
+ try {
21544
+ value = await registration.handler(ctx);
21545
+ } catch (error) {
21546
+ await guard.stop();
21547
+ if (ctx.hasUncertainMutation) throw asError(error, "durable workflow mutation result is uncertain");
21548
+ await applyHandlerError(workflow, workerOptions, ctx, registration, error);
21549
+ return;
21550
+ }
21551
+ await guard.stop();
21552
+ if (ctx.hasUncertainMutation) {
21553
+ throw asError(ctx.uncertainMutationFailure, "durable workflow mutation result is uncertain");
21554
+ }
21555
+ if (isAlreadyAppliedOutcome(value)) {
21556
+ ctx.assertAppliedOutcome(value);
21557
+ await releaseAppliedMutation(workflow, ctx);
21558
+ return;
21559
+ }
21560
+ if (isReplayedStepResult(value)) {
21561
+ await applyWorkflowOutcome(
21562
+ workflow.client,
21563
+ ctx,
21564
+ complete({ result: value.result }),
21565
+ registration.returnRecord
21566
+ );
21567
+ return;
21568
+ }
21569
+ if (isOutcome(value)) {
21570
+ await applyWorkflowOutcome(workflow.client, ctx, value, registration.returnRecord);
21571
+ return;
21572
+ }
21573
+ if (ctx.hasAppliedMutation) {
21574
+ await releaseAppliedMutation(workflow, ctx);
21575
+ return;
21576
+ }
21577
+ await applyWorkflowOutcome(
21578
+ workflow.client,
21579
+ ctx,
21580
+ complete({ result: value }),
21581
+ registration.returnRecord
21582
+ );
21583
+ }
21584
+ function asError(value, message) {
21585
+ return value instanceof Error ? value : new Error(message, { cause: value });
21586
+ }
21587
+ async function releaseAppliedMutation(workflow, ctx) {
21588
+ await workflow.client.transition(ctx.id, {
21589
+ fencingToken: ctx.fencingToken,
21590
+ fromState: ctx.state,
21591
+ leaseToken: ctx.leaseToken,
21592
+ partitionKey: ctx.partitionKey,
21593
+ toState: ctx.logicalState
20980
21594
  });
20981
21595
  }
21596
+ async function applyHandlerError(workflow, workerOptions, ctx, registration, error) {
21597
+ const policy = workerOptions.exceptionPolicy ?? registration.exceptionPolicy;
21598
+ if (policy === "raise") throw error;
21599
+ const encodedError = workerErrorPayload(error, workerOptions, workflow.client.codec);
21600
+ await applyWorkflowOutcome(
21601
+ workflow.client,
21602
+ ctx,
21603
+ policy === "fail" ? fail2({ error: encodedError }) : retry({ error: encodedError }),
21604
+ registration.returnRecord
21605
+ );
21606
+ }
20982
21607
 
20983
21608
  // src/workflow.ts
20984
21609
  var WorkflowClient = class {
@@ -21107,6 +21732,10 @@ var WorkflowWorker = class {
21107
21732
  claimed += jobs.length;
21108
21733
  const guards = jobs.map((job) => new LeaseRenewalGuard(this.workflow.client, job, leaseMs, this.options));
21109
21734
  let cursor = 0;
21735
+ let batchFailure;
21736
+ const recordFailure = (error) => {
21737
+ batchFailure ??= { error };
21738
+ };
21110
21739
  const runNext = async () => {
21111
21740
  while (cursor < jobs.length) {
21112
21741
  const index = cursor;
@@ -21117,19 +21746,22 @@ var WorkflowWorker = class {
21117
21746
  try {
21118
21747
  await this.applyJob(job, registration, guard);
21119
21748
  applied += 1;
21749
+ } catch (error) {
21750
+ recordFailure(error);
21120
21751
  } finally {
21121
- await guard.stop();
21752
+ try {
21753
+ await guard.stop();
21754
+ } catch (error) {
21755
+ recordFailure(error);
21756
+ }
21122
21757
  }
21123
21758
  }
21124
21759
  };
21125
21760
  try {
21126
- const handlers = await Promise.allSettled(
21761
+ await Promise.all(
21127
21762
  Array.from({ length: Math.min(concurrency, jobs.length) }, () => runNext())
21128
21763
  );
21129
- const failed = handlers.find((handler) => handler.status === "rejected");
21130
- if (failed != null) {
21131
- throw failed.reason;
21132
- }
21764
+ if (batchFailure != null) throwError(batchFailure.error);
21133
21765
  } finally {
21134
21766
  await Promise.allSettled(
21135
21767
  guards.slice(cursor).map(async (guard) => await guard.stop())
@@ -21240,45 +21872,15 @@ var WorkflowWorker = class {
21240
21872
  });
21241
21873
  }
21242
21874
  async applyJob(job, registration, guard) {
21243
- guard.assertActive();
21244
- const ctx = new WorkflowContext(this.workflow, job, registration.name, guard.job);
21245
- let outcome;
21246
- try {
21247
- const value = await registration.handler(ctx);
21248
- outcome = isOutcome(value) ? value : complete({ result: value });
21249
- } catch (error) {
21250
- await guard.stop();
21251
- await this.applyHandlerError(ctx, registration, error);
21252
- return;
21253
- }
21254
- await guard.stop();
21255
- await applyWorkflowOutcome(this.workflow.client, ctx, outcome, registration.returnRecord);
21256
- }
21257
- async applyHandlerError(ctx, registration, error) {
21258
- const policy = this.options.exceptionPolicy ?? registration.exceptionPolicy;
21259
- if (policy === "raise") {
21260
- throw error;
21261
- }
21262
- if (policy === "fail") {
21263
- await applyWorkflowOutcome(
21264
- this.workflow.client,
21265
- ctx,
21266
- fail2({ error: workerErrorPayload(error, this.options, this.workflow.client.codec) }),
21267
- registration.returnRecord
21268
- );
21269
- return;
21270
- }
21271
- await applyWorkflowOutcome(
21272
- this.workflow.client,
21273
- ctx,
21274
- retry({ error: workerErrorPayload(error, this.options, this.workflow.client.codec) }),
21275
- registration.returnRecord
21276
- );
21875
+ await executeWorkflowJob(this.workflow, this.options, job, registration, guard);
21277
21876
  }
21278
21877
  };
21878
+ function throwError(value) {
21879
+ throw value instanceof Error ? value : new Error("workflow worker failed", { cause: value });
21880
+ }
21279
21881
 
21280
21882
  // src/version.ts
21281
- var FERRICSTORE_SDK_VERSION = "0.12.0";
21883
+ var FERRICSTORE_SDK_VERSION = "0.13.1";
21282
21884
  var FERRICSTORE_MINIMUM_SERVER_VERSION = "0.11.4";
21283
21885
  var FERRICSTORE_NATIVE_PROTOCOL_VERSION = 1;
21284
21886
  export {
@@ -21321,6 +21923,7 @@ export {
21321
21923
  QueueWorker,
21322
21924
  RawCodec,
21323
21925
  ReconnectingExecutor,
21926
+ RequestNotSentError,
21324
21927
  RequestTimeoutError,
21325
21928
  RerouteError,
21326
21929
  RoutingTopology,
@@ -21341,6 +21944,7 @@ export {
21341
21944
  complete,
21342
21945
  fail2 as fail,
21343
21946
  httpCommandDisposition,
21947
+ isAlreadyAppliedOutcome,
21344
21948
  isOutcome,
21345
21949
  isReconnectableClosedConnectionError,
21346
21950
  mapException,