@ferricstore/ferricstore 0.1.3 → 0.1.5

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 (74) hide show
  1. package/README.md +37 -1
  2. package/dist/index.cjs +1264 -36
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +277 -6
  5. package/dist/index.d.ts +277 -6
  6. package/dist/index.js +1259 -36
  7. package/dist/index.js.map +1 -1
  8. package/docs/api/assets/hierarchy.js +1 -1
  9. package/docs/api/assets/highlight.css +6 -6
  10. package/docs/api/assets/navigation.js +1 -1
  11. package/docs/api/assets/search.js +1 -1
  12. package/docs/api/classes/FerricStoreClient.html +19 -2
  13. package/docs/api/classes/FerricStoreError.html +3 -3
  14. package/docs/api/classes/FlowAlreadyExistsError.html +3 -3
  15. package/docs/api/classes/FlowNotFoundError.html +3 -3
  16. package/docs/api/classes/FlowWrongStateError.html +3 -3
  17. package/docs/api/classes/InvalidCommandError.html +3 -3
  18. package/docs/api/classes/LockHeldError.html +3 -3
  19. package/docs/api/classes/LockNotOwnedError.html +3 -3
  20. package/docs/api/classes/NativeAdapter.html +3 -2
  21. package/docs/api/classes/OverloadedError.html +3 -3
  22. package/docs/api/classes/ReconnectingExecutor.html +7 -0
  23. package/docs/api/classes/RoutingTopology.html +9 -0
  24. package/docs/api/classes/StaleLeaseError.html +3 -3
  25. package/docs/api/classes/TopologyNativeAdapterPool.html +8 -0
  26. package/docs/api/functions/isReconnectableClosedConnectionError.html +1 -0
  27. package/docs/api/hierarchy.html +1 -1
  28. package/docs/api/index.html +16 -7
  29. package/docs/api/interfaces/AutoBatchOptions.html +2 -2
  30. package/docs/api/interfaces/CancelOptions.html +2 -2
  31. package/docs/api/interfaces/ClaimDueOptions.html +2 -2
  32. package/docs/api/interfaces/ClaimedItem.html +2 -3
  33. package/docs/api/interfaces/CommandExecutor.html +4 -2
  34. package/docs/api/interfaces/CompleteOptions.html +2 -2
  35. package/docs/api/interfaces/CompleteOutcome.html +1 -1
  36. package/docs/api/interfaces/CreateManyOptions.html +2 -2
  37. package/docs/api/interfaces/CreateOptions.html +2 -2
  38. package/docs/api/interfaces/ExecutePipelineOptions.html +2 -2
  39. package/docs/api/interfaces/FailOptions.html +2 -2
  40. package/docs/api/interfaces/FailOutcome.html +1 -1
  41. package/docs/api/interfaces/FencedItem.html +2 -2
  42. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +6 -0
  43. package/docs/api/interfaces/FerricStoreClientOptions.html +2 -2
  44. package/docs/api/interfaces/FetchOrComputeResult.html +2 -2
  45. package/docs/api/interfaces/FlowRecord.html +2 -2
  46. package/docs/api/interfaces/KeyInfo.html +2 -2
  47. package/docs/api/interfaces/MutateOptions.html +2 -2
  48. package/docs/api/interfaces/NamedValueMutation.html +1 -1
  49. package/docs/api/interfaces/NativeAdapterOptions.html +12 -2
  50. package/docs/api/interfaces/ProtocolCommand.html +5 -0
  51. package/docs/api/interfaces/RateLimitResult.html +2 -2
  52. package/docs/api/interfaces/ReadOptions.html +2 -2
  53. package/docs/api/interfaces/ReclaimOptions.html +2 -2
  54. package/docs/api/interfaces/ReconnectOptions.html +2 -0
  55. package/docs/api/interfaces/RetryOptions.html +2 -2
  56. package/docs/api/interfaces/RetryOutcome.html +1 -1
  57. package/docs/api/interfaces/RoutingEndpoint.html +5 -0
  58. package/docs/api/interfaces/RoutingRoute.html +7 -0
  59. package/docs/api/interfaces/SearchOptions.html +12 -0
  60. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  61. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  62. package/docs/api/interfaces/TransitionOptions.html +2 -2
  63. package/docs/api/interfaces/TransitionOutcome.html +1 -1
  64. package/docs/api/modules.html +1 -1
  65. package/docs/api/types/CommandArgument.html +1 -1
  66. package/docs/api/types/EndpointPolicy.html +1 -0
  67. package/docs/api/types/ManagementPairs.html +1 -0
  68. package/docs/api/types/QueueBatchHandler.html +1 -1
  69. package/docs/api/types/QueueHandler.html +1 -1
  70. package/docs/api/types/QueueJob.html +1 -0
  71. package/docs/api/types/SearchStateMeta.html +1 -0
  72. package/docs/api/variables/COMMAND_OPCODES.html +1 -0
  73. package/docs/python-parity.md +4 -1
  74. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -110,24 +110,171 @@ var FLAG_CUSTOM_PAYLOAD = 2;
110
110
  var FLAG_COMPRESSED = 8;
111
111
  var FLAG_MORE_CHUNKS = 32;
112
112
  var STATUS_OK = 0;
113
+ var COMMAND_OPCODES = {
114
+ "HELLO": 1,
115
+ "AUTH": 2,
116
+ "PING": 3,
117
+ "CLIENT.SETNAME": 4,
118
+ "CLIENT.INFO": 5,
119
+ "ROUTE": 6,
120
+ "SHARDS": 7,
121
+ "BACKPRESSURE": 8,
122
+ "QUIT": 9,
123
+ "GOAWAY": 10,
124
+ "OPTIONS": 11,
125
+ "STARTUP": 12,
126
+ "WINDOW_UPDATE": 13,
127
+ "PIPELINE": 14,
128
+ "ROUTE_BATCH": 15,
129
+ "EVENT": 16,
130
+ "SUBSCRIBE_EVENTS": 17,
131
+ "UNSUBSCRIBE_EVENTS": 18,
132
+ "COMMAND_EXEC": 256,
133
+ "GET": 257,
134
+ "SET": 258,
135
+ "DEL": 259,
136
+ "MGET": 260,
137
+ "MSET": 261,
138
+ "CAS": 262,
139
+ "LOCK": 263,
140
+ "UNLOCK": 264,
141
+ "EXTEND": 265,
142
+ "RATELIMIT.ADD": 266,
143
+ "FETCH_OR_COMPUTE": 267,
144
+ "FETCH_OR_COMPUTE_RESULT": 268,
145
+ "FETCH_OR_COMPUTE_ERROR": 269,
146
+ "HSET": 272,
147
+ "HGET": 273,
148
+ "HMGET": 274,
149
+ "HGETALL": 275,
150
+ "LPUSH": 288,
151
+ "RPUSH": 289,
152
+ "LPOP": 290,
153
+ "RPOP": 291,
154
+ "LRANGE": 292,
155
+ "SADD": 304,
156
+ "SREM": 305,
157
+ "SMEMBERS": 306,
158
+ "SISMEMBER": 307,
159
+ "ZADD": 320,
160
+ "ZREM": 321,
161
+ "ZRANGE": 322,
162
+ "ZSCORE": 323,
163
+ "FLOW.CREATE": 513,
164
+ "FLOW.GET": 514,
165
+ "FLOW.CLAIM_DUE": 515,
166
+ "FLOW.COMPLETE": 516,
167
+ "FLOW.TRANSITION": 517,
168
+ "FLOW.RETRY": 518,
169
+ "FLOW.FAIL": 519,
170
+ "FLOW.CANCEL": 520,
171
+ "FLOW.EXTEND_LEASE": 521,
172
+ "FLOW.HISTORY": 522,
173
+ "FLOW.VALUE.PUT": 523,
174
+ "FLOW.VALUE.MGET": 524,
175
+ "FLOW.SIGNAL": 525,
176
+ "FLOW.LIST": 526,
177
+ "FLOW.CREATE_MANY": 527,
178
+ "FLOW.COMPLETE_MANY": 528,
179
+ "FLOW.TRANSITION_MANY": 529,
180
+ "FLOW.RETRY_MANY": 530,
181
+ "FLOW.FAIL_MANY": 531,
182
+ "FLOW.CANCEL_MANY": 532,
183
+ "FLOW.RECLAIM": 533,
184
+ "FLOW.REWIND": 534,
185
+ "FLOW.TERMINALS": 535,
186
+ "FLOW.FAILURES": 536,
187
+ "FLOW.BY_PARENT": 537,
188
+ "FLOW.BY_ROOT": 538,
189
+ "FLOW.BY_CORRELATION": 539,
190
+ "FLOW.INFO": 540,
191
+ "FLOW.STUCK": 541,
192
+ "FLOW.POLICY.SET": 542,
193
+ "FLOW.POLICY.GET": 543,
194
+ "FLOW.SPAWN_CHILDREN": 544,
195
+ "FLOW.RETENTION_CLEANUP": 545,
196
+ "FLOW.STEP_CONTINUE": 546,
197
+ "FLOW.START_AND_CLAIM": 547,
198
+ "FLOW.RUN_STEPS_MANY": 548,
199
+ "FLOW.SCHEDULE.CREATE": 549,
200
+ "FLOW.SCHEDULE.GET": 550,
201
+ "FLOW.SCHEDULE.DELETE": 551,
202
+ "FLOW.SCHEDULE.FIRE_DUE": 552,
203
+ "FLOW.SCHEDULE.LIST": 553,
204
+ "FLOW.SCHEDULE.FIRE": 554,
205
+ "FLOW.SCHEDULE.PAUSE": 555,
206
+ "FLOW.SCHEDULE.RESUME": 556,
207
+ "FLOW.STATS": 557,
208
+ "FLOW.ATTRIBUTES": 558,
209
+ "FLOW.ATTRIBUTE_VALUES": 559,
210
+ "FLOW.SEARCH": 560,
211
+ "FLOW.EFFECT.RESERVE": 576,
212
+ "FLOW.EFFECT.CONFIRM": 577,
213
+ "FLOW.EFFECT.FAIL": 578,
214
+ "FLOW.EFFECT.COMPENSATE": 579,
215
+ "FLOW.EFFECT.GET": 580,
216
+ "FLOW.GOVERNANCE.LEDGER": 581,
217
+ "FLOW.APPROVAL.REQUEST": 582,
218
+ "FLOW.APPROVAL.APPROVE": 583,
219
+ "FLOW.APPROVAL.REJECT": 584,
220
+ "FLOW.APPROVAL.GET": 585,
221
+ "FLOW.CIRCUIT.OPEN": 586,
222
+ "FLOW.CIRCUIT.CLOSE": 587,
223
+ "FLOW.CIRCUIT.GET": 588,
224
+ "FLOW.BUDGET.RESERVE": 589,
225
+ "FLOW.BUDGET.GET": 590,
226
+ "FLOW.LIMIT.LEASE": 591,
227
+ "FLOW.LIMIT.SPEND": 592,
228
+ "FLOW.LIMIT.RELEASE": 593,
229
+ "FLOW.LIMIT.GET": 594,
230
+ "FLOW.APPROVAL.LIST": 595,
231
+ "FLOW.GOVERNANCE.OVERVIEW": 596,
232
+ "FLOW.BUDGET.LIST": 597,
233
+ "FLOW.LIMIT.LIST": 598,
234
+ "FLOW.BUDGET.COMMIT": 599,
235
+ "FLOW.BUDGET.RELEASE": 600,
236
+ "CLUSTER.HEALTH": 769,
237
+ "CLUSTER.STATS": 770,
238
+ "CLUSTER.KEYSLOT": 771,
239
+ "CLUSTER.SLOTS": 772,
240
+ "CLUSTER.STATUS": 773,
241
+ "CLUSTER.JOIN": 774,
242
+ "CLUSTER.LEAVE": 775,
243
+ "CLUSTER.FAILOVER": 776,
244
+ "CLUSTER.PROMOTE": 777,
245
+ "CLUSTER.DEMOTE": 778,
246
+ "CLUSTER.ROLE": 779,
247
+ "FERRICSTORE.KEY_INFO": 780,
248
+ "FERRICSTORE.CONFIG": 781,
249
+ "FERRICSTORE.HOTNESS": 782,
250
+ "FERRICSTORE.METRICS": 783,
251
+ "FERRICSTORE.BLOBGC": 784
252
+ };
113
253
  var OPCODES = {
114
- startup: 12,
115
- auth: 2,
116
- ping: 3,
117
- clientSetName: 4,
118
- clientInfo: 5,
119
- commandExec: 256,
120
- get: 257,
121
- set: 258,
122
- del: 259,
123
- mget: 260,
124
- mset: 261,
125
- pipeline: 14,
126
- flowCreate: 513,
127
- flowClaimDue: 515,
128
- flowComplete: 516,
129
- flowCreateMany: 527,
130
- flowCompleteMany: 528
254
+ startup: COMMAND_OPCODES.STARTUP,
255
+ auth: COMMAND_OPCODES.AUTH,
256
+ ping: COMMAND_OPCODES.PING,
257
+ clientSetName: COMMAND_OPCODES["CLIENT.SETNAME"],
258
+ clientInfo: COMMAND_OPCODES["CLIENT.INFO"],
259
+ route: COMMAND_OPCODES.ROUTE,
260
+ routeBatch: COMMAND_OPCODES.ROUTE_BATCH,
261
+ shards: COMMAND_OPCODES.SHARDS,
262
+ backpressure: COMMAND_OPCODES.BACKPRESSURE,
263
+ options: COMMAND_OPCODES.OPTIONS,
264
+ quit: COMMAND_OPCODES.QUIT,
265
+ commandExec: COMMAND_OPCODES.COMMAND_EXEC,
266
+ get: COMMAND_OPCODES.GET,
267
+ set: COMMAND_OPCODES.SET,
268
+ del: COMMAND_OPCODES.DEL,
269
+ mget: COMMAND_OPCODES.MGET,
270
+ mset: COMMAND_OPCODES.MSET,
271
+ pipeline: COMMAND_OPCODES.PIPELINE,
272
+ flowCreate: COMMAND_OPCODES["FLOW.CREATE"],
273
+ flowClaimDue: COMMAND_OPCODES["FLOW.CLAIM_DUE"],
274
+ flowComplete: COMMAND_OPCODES["FLOW.COMPLETE"],
275
+ flowCreateMany: COMMAND_OPCODES["FLOW.CREATE_MANY"],
276
+ flowCompleteMany: COMMAND_OPCODES["FLOW.COMPLETE_MANY"],
277
+ flowSearch: COMMAND_OPCODES["FLOW.SEARCH"]
131
278
  };
132
279
  var COMPACT_FLOW_CLAIM_JOBS = 128;
133
280
  var COMPACT_OK_LIST = 129;
@@ -260,12 +407,46 @@ function buildProtocolCommand(args) {
260
407
  }
261
408
  const command = asText(args[0]).toUpperCase();
262
409
  const commandArgs = args.slice(1);
410
+ if (command === "COMMAND_EXEC" && commandArgs.length >= 1) {
411
+ return commandExec(commandArgs);
412
+ }
413
+ if (command === "HELLO" || command === "STARTUP" || command === "WINDOW_UPDATE") {
414
+ return { laneId: 0, opcode: COMMAND_OPCODES[command], payload: optionMap(commandArgs) };
415
+ }
416
+ if (command === "AUTH") {
417
+ const payload = commandArgs.length === 1 ? { password: commandArgs[0], username: "default" } : { password: commandArgs[1], username: commandArgs[0] };
418
+ return { laneId: 0, opcode: OPCODES.auth, payload };
419
+ }
263
420
  if (command === "PING") {
264
421
  return { laneId: 0, opcode: OPCODES.ping, payload: commandArgs.length === 0 ? {} : { message: commandArgs[0] } };
265
422
  }
423
+ if (command === "OPTIONS") {
424
+ return { laneId: 0, opcode: OPCODES.options, payload: {} };
425
+ }
426
+ if (command === "BACKPRESSURE") {
427
+ return { laneId: 0, opcode: OPCODES.backpressure, payload: {} };
428
+ }
429
+ if (command === "QUIT") {
430
+ return { laneId: 0, opcode: OPCODES.quit, payload: {} };
431
+ }
432
+ if (command === "ROUTE" && commandArgs.length >= 1) {
433
+ return { laneId: 0, opcode: OPCODES.route, payload: { key: commandArgs[0] } };
434
+ }
435
+ if (command === "ROUTE_BATCH") {
436
+ return { laneId: 0, opcode: OPCODES.routeBatch, payload: { keys: commandArgs } };
437
+ }
438
+ if (command === "SHARDS") {
439
+ return { laneId: 0, opcode: OPCODES.shards, payload: {} };
440
+ }
266
441
  if (command === "CLIENT" && asText(commandArgs[0]).toUpperCase() === "SETNAME" && commandArgs.length === 2) {
267
442
  return { laneId: 0, opcode: OPCODES.clientSetName, payload: { name: commandArgs[1] } };
268
443
  }
444
+ if (command === "CLIENT.SETNAME" && commandArgs.length === 1) {
445
+ return { laneId: 0, opcode: OPCODES.clientSetName, payload: { name: commandArgs[0] } };
446
+ }
447
+ if (command === "CLIENT" && asText(commandArgs[0]).toUpperCase() === "INFO" && commandArgs.length === 1 || command === "CLIENT.INFO" && commandArgs.length === 0) {
448
+ return { laneId: 0, opcode: OPCODES.clientInfo, payload: {} };
449
+ }
269
450
  if (command === "GET" && commandArgs.length >= 1) {
270
451
  return { opcode: OPCODES.get, payload: { key: commandArgs[0] } };
271
452
  }
@@ -284,7 +465,7 @@ function buildProtocolCommand(args) {
284
465
  if (command === "DEL") {
285
466
  return { opcode: OPCODES.del, payload: { keys: commandArgs } };
286
467
  }
287
- if (command.startsWith("FLOW.") && hasFlowCommandOnlyOption(commandArgs)) {
468
+ if (command.startsWith("FLOW.") && hasFlowCommandOnlyOption(command, commandArgs)) {
288
469
  return commandExec(args);
289
470
  }
290
471
  if (command === "FLOW.CREATE") {
@@ -302,6 +483,9 @@ function buildProtocolCommand(args) {
302
483
  if (command === "FLOW.COMPLETE_MANY") {
303
484
  return flowCompleteManyPayload(commandArgs) ?? commandExec(args);
304
485
  }
486
+ if (command === "FLOW.SEARCH") {
487
+ return flowSearchPayload(commandArgs) ?? commandExec(args);
488
+ }
305
489
  return commandExec(args);
306
490
  }
307
491
  function commandExec(args) {
@@ -646,13 +830,13 @@ function writeOptionalBinary(out, offset, value) {
646
830
  }
647
831
  return writeBinary(out, offset, value);
648
832
  }
649
- function hasFlowCommandOnlyOption(args) {
833
+ function hasFlowCommandOnlyOption(command, args) {
650
834
  return args.some((arg) => {
651
835
  if (typeof arg !== "string" && !Buffer2.isBuffer(arg)) {
652
836
  return false;
653
837
  }
654
838
  const token = asText(arg).toUpperCase();
655
- return token === "STATE_META" || token === "INDEXED_STATE_META";
839
+ return token === "INDEXED_STATE_META" || command !== "FLOW.SEARCH" && token === "STATE_META";
656
840
  });
657
841
  }
658
842
  function flowCreatePayload(args) {
@@ -770,6 +954,12 @@ function flowCompleteManyPayload(args) {
770
954
  if (!auto && !mixed) payload.partition_key = partition;
771
955
  return { opcode: OPCODES.flowCompleteMany, payload };
772
956
  }
957
+ function flowSearchPayload(args) {
958
+ if (args.length < 1) return void 0;
959
+ const payload = optionMap(args.slice(1));
960
+ normalizeFlowSearchStateMeta(payload);
961
+ return { opcode: OPCODES.flowSearch, payload: { type: args[0], ...payload } };
962
+ }
773
963
  function compactFlowCreateManyPayload(partition, rawItems, mixed, auto, options) {
774
964
  if ("priority" in options || "idempotent" in options || "retention_ttl_ms" in options || typeof options.type === "undefined" || typeof options.state === "undefined" || typeof options.now_ms !== "number") {
775
965
  return void 0;
@@ -872,6 +1062,78 @@ function compactFlowCompleteManyPayload(partition, rawItems, mixed, auto, option
872
1062
  }
873
1063
  return { flags: FLAG_CUSTOM_PAYLOAD, opcode: OPCODES.flowCompleteMany, payload: out };
874
1064
  }
1065
+ var OPTION_FIELDS = {
1066
+ CONSISTENT_PROJECTION: "consistent_projection",
1067
+ COUNT: "count",
1068
+ FROM_MS: "from_ms",
1069
+ INCLUDE_COLD: "include_cold",
1070
+ PARTITION: "partition_key",
1071
+ REV: "rev",
1072
+ STATE: "state",
1073
+ TERMINAL_ONLY: "terminal_only",
1074
+ TO_MS: "to_ms"
1075
+ };
1076
+ var BOOL_OPTION_FIELDS = /* @__PURE__ */ new Set(["consistent_projection", "include_cold", "rev", "terminal_only"]);
1077
+ function optionMap(args) {
1078
+ const payload = {};
1079
+ for (let index = 0; index < args.length; ) {
1080
+ const token = asText(args[index]).toUpperCase();
1081
+ if (token === "ATTRIBUTE") {
1082
+ if (index + 2 >= args.length) {
1083
+ throw new FerricStoreError("ATTRIBUTE requires name and value");
1084
+ }
1085
+ const name = asText(args[index + 1]);
1086
+ const attributes = ensureObjectField(payload, "attributes");
1087
+ attributes[name] = args[index + 2];
1088
+ index += 3;
1089
+ continue;
1090
+ }
1091
+ if (token === "STATE_META") {
1092
+ if (index + 2 >= args.length) {
1093
+ throw new FerricStoreError("STATE_META requires name and value");
1094
+ }
1095
+ const name = asText(args[index + 1]);
1096
+ const stateMeta = ensureObjectField(payload, "state_meta");
1097
+ stateMeta[name] = args[index + 2];
1098
+ index += 3;
1099
+ continue;
1100
+ }
1101
+ const field2 = OPTION_FIELDS[token] ?? token.toLowerCase();
1102
+ if (index + 1 >= args.length) {
1103
+ throw new FerricStoreError(`${token} requires a value`);
1104
+ }
1105
+ const value = args[index + 1];
1106
+ payload[field2] = BOOL_OPTION_FIELDS.has(field2) ? boolArg(value) : value;
1107
+ index += 2;
1108
+ }
1109
+ return payload;
1110
+ }
1111
+ function ensureObjectField(payload, field2) {
1112
+ const value = payload[field2];
1113
+ if (isPlainObject(value)) {
1114
+ return value;
1115
+ }
1116
+ const next = {};
1117
+ payload[field2] = next;
1118
+ return next;
1119
+ }
1120
+ function normalizeFlowSearchStateMeta(payload) {
1121
+ const stateMeta = payload.state_meta;
1122
+ if (!isPlainObject(stateMeta) || Object.keys(stateMeta).length === 0) {
1123
+ return;
1124
+ }
1125
+ if (Object.values(stateMeta).every(isPlainObject)) {
1126
+ return;
1127
+ }
1128
+ const state = payload.state;
1129
+ if (state == null) {
1130
+ throw new FerricStoreError("FLOW.SEARCH STATE_META filters require STATE or nested state metadata");
1131
+ }
1132
+ payload.state_meta = { [asText(state)]: stateMeta };
1133
+ }
1134
+ function isPlainObject(value) {
1135
+ return typeof value === "object" && value != null && !Array.isArray(value) && !Buffer2.isBuffer(value) && !(value instanceof Uint8Array);
1136
+ }
875
1137
  function parseFlowOptions(args, start, end, config) {
876
1138
  const payload = {};
877
1139
  const seen = /* @__PURE__ */ new Set();
@@ -1393,15 +1655,21 @@ var NativeAdapter = class _NativeAdapter {
1393
1655
  maxChunkBytes;
1394
1656
  requestId = 0n;
1395
1657
  protocolLanes;
1658
+ heartbeatIntervalMs;
1659
+ heartbeatInFlight = false;
1660
+ heartbeatTimer;
1661
+ lastActivityMs = Date.now();
1396
1662
  timeoutMs;
1397
- constructor(socket, timeoutMs, protocolLanes, maxChunkBytes) {
1663
+ constructor(socket, timeoutMs, protocolLanes, maxChunkBytes, heartbeatIntervalMs) {
1398
1664
  this.socket = socket;
1399
1665
  this.maxChunkBytes = Math.max(1, Math.trunc(maxChunkBytes));
1400
1666
  this.timeoutMs = timeoutMs;
1401
1667
  this.protocolLanes = Math.max(1, Math.trunc(protocolLanes));
1668
+ this.heartbeatIntervalMs = normalizeHeartbeatInterval(heartbeatIntervalMs);
1402
1669
  this.socket.on("data", (chunk) => this.onData(chunk));
1403
1670
  this.socket.on("error", (error) => this.failAll(error));
1404
1671
  this.socket.on("close", () => this.failAll(new FerricStoreError("FerricStore connection closed")));
1672
+ this.startHeartbeat();
1405
1673
  }
1406
1674
  static async fromUrl(url, options = {}) {
1407
1675
  const parsed = parseFerricUrl(url);
@@ -1410,7 +1678,8 @@ var NativeAdapter = class _NativeAdapter {
1410
1678
  socket,
1411
1679
  options.timeoutMs ?? 3e4,
1412
1680
  options.protocolLanes ?? 8,
1413
- options.maxChunkBytes ?? 64 * 1024 * 1024
1681
+ options.maxChunkBytes ?? 64 * 1024 * 1024,
1682
+ options.heartbeatIntervalMs
1414
1683
  );
1415
1684
  await adapter.startup(options.clientName);
1416
1685
  const password = options.password ?? parsed.password;
@@ -1423,6 +1692,9 @@ var NativeAdapter = class _NativeAdapter {
1423
1692
  const command = buildProtocolCommand(args);
1424
1693
  return await this.request(command);
1425
1694
  }
1695
+ async executeProtocolCommand(command, laneId) {
1696
+ return await this.request(laneId == null ? command : { ...command, laneId });
1697
+ }
1426
1698
  async executePipeline(commands, options = {}) {
1427
1699
  if (commands.length === 0) {
1428
1700
  return [];
@@ -1434,6 +1706,7 @@ var NativeAdapter = class _NativeAdapter {
1434
1706
  return;
1435
1707
  }
1436
1708
  this.closed = true;
1709
+ this.stopHeartbeat();
1437
1710
  this.socket.end();
1438
1711
  }
1439
1712
  async startup(clientName) {
@@ -1455,6 +1728,7 @@ var NativeAdapter = class _NativeAdapter {
1455
1728
  if (this.closed) {
1456
1729
  throw new FerricStoreError("FerricStore connection is closed");
1457
1730
  }
1731
+ this.lastActivityMs = Date.now();
1458
1732
  const requestId = this.nextRequestId();
1459
1733
  const frame = encodeRequest(this.assignLane(command), requestId);
1460
1734
  return await new Promise((resolve, reject) => {
@@ -1468,10 +1742,12 @@ var NativeAdapter = class _NativeAdapter {
1468
1742
  opcode: command.opcode,
1469
1743
  reject: (reason) => {
1470
1744
  clearTimeout(timer);
1745
+ this.lastActivityMs = Date.now();
1471
1746
  reject(reason instanceof Error ? reason : classifyServerError(String(reason), reason));
1472
1747
  },
1473
1748
  resolve: (value) => {
1474
1749
  clearTimeout(timer);
1750
+ this.lastActivityMs = Date.now();
1475
1751
  resolve(value);
1476
1752
  }
1477
1753
  });
@@ -1480,11 +1756,43 @@ var NativeAdapter = class _NativeAdapter {
1480
1756
  this.pending.delete(requestId);
1481
1757
  this.cleanupChunksForRequest(requestId);
1482
1758
  clearTimeout(timer);
1759
+ this.lastActivityMs = Date.now();
1483
1760
  reject(error);
1484
1761
  }
1485
1762
  });
1486
1763
  });
1487
1764
  }
1765
+ startHeartbeat() {
1766
+ if (this.heartbeatIntervalMs == null) {
1767
+ return;
1768
+ }
1769
+ this.heartbeatTimer = setInterval(() => {
1770
+ void this.sendHeartbeat();
1771
+ }, this.heartbeatIntervalMs);
1772
+ this.heartbeatTimer.unref?.();
1773
+ }
1774
+ stopHeartbeat() {
1775
+ if (this.heartbeatTimer == null) {
1776
+ return;
1777
+ }
1778
+ clearInterval(this.heartbeatTimer);
1779
+ this.heartbeatTimer = void 0;
1780
+ }
1781
+ async sendHeartbeat() {
1782
+ if (this.closed || this.heartbeatInFlight || this.pending.size > 0 || this.heartbeatIntervalMs == null) {
1783
+ return;
1784
+ }
1785
+ if (Date.now() - this.lastActivityMs < this.heartbeatIntervalMs) {
1786
+ return;
1787
+ }
1788
+ this.heartbeatInFlight = true;
1789
+ try {
1790
+ await this.request(buildProtocolCommand(["PING"]));
1791
+ } catch {
1792
+ } finally {
1793
+ this.heartbeatInFlight = false;
1794
+ }
1795
+ }
1488
1796
  onData(chunk) {
1489
1797
  try {
1490
1798
  this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer3.concat([this.buffer, chunk]);
@@ -1543,6 +1851,7 @@ var NativeAdapter = class _NativeAdapter {
1543
1851
  return;
1544
1852
  }
1545
1853
  this.closed = true;
1854
+ this.stopHeartbeat();
1546
1855
  const error = reason instanceof Error ? reason : classifyServerError(String(reason), reason);
1547
1856
  for (const pending of this.pending.values()) {
1548
1857
  pending.reject(error);
@@ -1575,6 +1884,88 @@ var NativeAdapter = class _NativeAdapter {
1575
1884
  return { ...command, laneId: this.dataLane };
1576
1885
  }
1577
1886
  };
1887
+ var ReconnectingExecutor = class {
1888
+ constructor(createExecutor, options = {}) {
1889
+ this.createExecutor = createExecutor;
1890
+ this.maxRetries = Math.max(0, Math.trunc(options.maxRetries ?? 1));
1891
+ this.executorPromise = createExecutor();
1892
+ }
1893
+ createExecutor;
1894
+ closed = false;
1895
+ maxRetries;
1896
+ executorPromise;
1897
+ reconnectPromise;
1898
+ async executeCommand(...args) {
1899
+ return await this.withReconnect((executor) => executor.executeCommand(...args));
1900
+ }
1901
+ async executePipeline(commands, options = {}) {
1902
+ return await this.withReconnect(async (executor) => {
1903
+ if (executor.executePipeline != null) {
1904
+ return await executor.executePipeline(commands, options);
1905
+ }
1906
+ return await Promise.all(commands.map((command) => executor.executeCommand(...command)));
1907
+ });
1908
+ }
1909
+ async refreshTopology() {
1910
+ const executor = await this.executorPromise;
1911
+ if (executor.refreshTopology == null) {
1912
+ throw new FerricStoreError("topology refresh requires a topology-aware native executor");
1913
+ }
1914
+ return await executor.refreshTopology();
1915
+ }
1916
+ async route(key) {
1917
+ const executor = await this.executorPromise;
1918
+ if (executor.route == null) {
1919
+ throw new FerricStoreError("route lookup requires a topology-aware native executor");
1920
+ }
1921
+ return await executor.route(key);
1922
+ }
1923
+ async close() {
1924
+ if (this.closed) {
1925
+ return;
1926
+ }
1927
+ this.closed = true;
1928
+ const executor = await this.executorPromise.catch(() => void 0);
1929
+ await Promise.resolve(executor?.close?.());
1930
+ }
1931
+ async withReconnect(operation) {
1932
+ let executor = await this.executorPromise;
1933
+ for (let attempt = 0; ; attempt++) {
1934
+ try {
1935
+ return await operation(executor);
1936
+ } catch (error) {
1937
+ if (this.closed || attempt >= this.maxRetries || !isReconnectableClosedConnectionError(error)) {
1938
+ throw error;
1939
+ }
1940
+ executor = await this.reconnect(executor);
1941
+ }
1942
+ }
1943
+ }
1944
+ async reconnect(staleExecutor) {
1945
+ if (this.closed) {
1946
+ throw new FerricStoreError("FerricStore client is closed");
1947
+ }
1948
+ if (this.reconnectPromise != null) {
1949
+ return await this.reconnectPromise;
1950
+ }
1951
+ const currentExecutor = await this.executorPromise.catch(() => void 0);
1952
+ if (currentExecutor != null && currentExecutor !== staleExecutor) {
1953
+ return currentExecutor;
1954
+ }
1955
+ this.reconnectPromise = (async () => {
1956
+ await Promise.resolve(staleExecutor.close?.()).catch(() => void 0);
1957
+ const nextExecutor = await this.createExecutor();
1958
+ this.executorPromise = Promise.resolve(nextExecutor);
1959
+ return nextExecutor;
1960
+ })().finally(() => {
1961
+ this.reconnectPromise = void 0;
1962
+ });
1963
+ return await this.reconnectPromise;
1964
+ }
1965
+ };
1966
+ function isReconnectableClosedConnectionError(error) {
1967
+ return error instanceof Error && error.message === "FerricStore connection is closed";
1968
+ }
1578
1969
  function parseFerricUrl(value) {
1579
1970
  const url = new URL(value);
1580
1971
  if (url.protocol !== "ferric:" && url.protocol !== "ferrics:") {
@@ -1598,6 +1989,7 @@ async function connect(parsed, options) {
1598
1989
  }, timeoutMs);
1599
1990
  timer.unref?.();
1600
1991
  socket.setNoDelay(true);
1992
+ socket.setKeepAlive(options.keepAlive ?? true, normalizeKeepAliveInitialDelay(options.keepAliveInitialDelayMs));
1601
1993
  socket.once("connect", () => {
1602
1994
  clearTimeout(timer);
1603
1995
  resolve(socket);
@@ -1608,6 +2000,662 @@ async function connect(parsed, options) {
1608
2000
  });
1609
2001
  });
1610
2002
  }
2003
+ function normalizeHeartbeatInterval(value) {
2004
+ if (value != null && value <= 0) {
2005
+ return void 0;
2006
+ }
2007
+ return Math.trunc(value ?? 6e4);
2008
+ }
2009
+ function normalizeKeepAliveInitialDelay(value) {
2010
+ return Math.max(0, Math.trunc(value ?? 3e4));
2011
+ }
2012
+
2013
+ // src/topology.ts
2014
+ import { Buffer as Buffer4 } from "buffer";
2015
+ var ROUTE_SLOT_COUNT = 1024;
2016
+ var ROUTE_SLOT_MASK = ROUTE_SLOT_COUNT - 1;
2017
+ var RoutingTopology = class _RoutingTopology {
2018
+ routeEpoch;
2019
+ shardCount;
2020
+ slots;
2021
+ endpoints;
2022
+ constructor(routeEpoch, shardCount, slots, endpoints) {
2023
+ this.routeEpoch = routeEpoch;
2024
+ this.shardCount = shardCount;
2025
+ this.slots = slots;
2026
+ this.endpoints = endpoints;
2027
+ }
2028
+ static empty() {
2029
+ return new _RoutingTopology(0, 0, Array.from({ length: ROUTE_SLOT_COUNT }), /* @__PURE__ */ new Map());
2030
+ }
2031
+ static build(payload) {
2032
+ const ranges = getField(payload, "ranges");
2033
+ if (!Array.isArray(ranges)) {
2034
+ throw new FerricStoreError("invalid SHARDS topology payload", { raw: payload });
2035
+ }
2036
+ const slots = Array.from({ length: ROUTE_SLOT_COUNT });
2037
+ const endpoints = /* @__PURE__ */ new Map();
2038
+ for (const item of ranges) {
2039
+ if (textOrUndefined(getField(item, "hint")) === "leader_unknown") {
2040
+ throw new FerricStoreError("SHARDS range has no leader", { raw: item });
2041
+ }
2042
+ const first = numberOrUndefined(getField(item, "first_slot"));
2043
+ const last = numberOrUndefined(getField(item, "last_slot"));
2044
+ const shard = numberOrUndefined(getField(item, "shard"));
2045
+ const laneId = numberOrUndefined(getField(item, "lane_id"));
2046
+ const endpoint = endpointFromRange(item);
2047
+ if (first == null || last == null || shard == null || laneId == null || first < 0 || last < first || last >= ROUTE_SLOT_COUNT) {
2048
+ throw new FerricStoreError("invalid SHARDS range", { raw: item });
2049
+ }
2050
+ const endpointKey = endpointKeyFor(endpoint);
2051
+ const route = {
2052
+ endpoint,
2053
+ endpointKey,
2054
+ laneId,
2055
+ leaderNode: endpoint.node,
2056
+ shard
2057
+ };
2058
+ for (let slot = first; slot <= last; slot += 1) {
2059
+ slots[slot] = route;
2060
+ }
2061
+ endpoints.set(endpointKey, endpoint);
2062
+ }
2063
+ return new _RoutingTopology(
2064
+ numberOrUndefined(getField(payload, "route_epoch")) ?? 0,
2065
+ numberOrUndefined(getField(payload, "shard_count")) ?? 0,
2066
+ slots,
2067
+ endpoints
2068
+ );
2069
+ }
2070
+ static slotForKey(key) {
2071
+ const textKey = Buffer4.isBuffer(key) ? key.toString("utf8") : key;
2072
+ let hashInput;
2073
+ if (textKey.startsWith("f:{")) {
2074
+ hashInput = flowHashTag(textKey.slice(3), textKey);
2075
+ } else if (textKey.startsWith("X:f:{")) {
2076
+ hashInput = flowHashTag(textKey.slice(5), textKey);
2077
+ } else {
2078
+ hashInput = hashTagOrKey(textKey);
2079
+ }
2080
+ return crc32(Buffer4.from(hashInput)) & ROUTE_SLOT_MASK;
2081
+ }
2082
+ routeKey(key) {
2083
+ const slot = _RoutingTopology.slotForKey(key);
2084
+ const route = this.slots[slot];
2085
+ if (route == null) {
2086
+ throw new FerricStoreError(`no route for slot ${slot}`);
2087
+ }
2088
+ return { ...route, slot };
2089
+ }
2090
+ };
2091
+ var TopologyNativeAdapterPool = class _TopologyNativeAdapterPool {
2092
+ adapterOptions;
2093
+ adapters = /* @__PURE__ */ new Map();
2094
+ endpointPolicy;
2095
+ endpointValidator;
2096
+ seedEndpointKeys;
2097
+ seedUrls;
2098
+ tls;
2099
+ trustedHosts;
2100
+ warmConnections;
2101
+ topologyValue = RoutingTopology.empty();
2102
+ constructor(urls, options = {}) {
2103
+ if (urls.length === 0) {
2104
+ throw new FerricStoreError("TopologyNativeAdapterPool requires at least one seed URL");
2105
+ }
2106
+ this.seedUrls = [...urls];
2107
+ this.endpointPolicy = options.endpointPolicy ?? "seed_hosts";
2108
+ this.endpointValidator = options.endpointValidator;
2109
+ this.warmConnections = options.warmConnections ?? false;
2110
+ this.tls = options.tlsOptions != null || urls.some((url) => parseFerricUrl2(url).tls);
2111
+ this.adapterOptions = nativeOnlyOptions(withSeedAuthDefaults(urls, options));
2112
+ this.seedEndpointKeys = new Set(urls.map((url) => endpointKeyFor(endpointFromUrl(url))));
2113
+ this.trustedHosts = normalizedHostSet(options.trustedHosts ?? []);
2114
+ }
2115
+ static async fromUrls(urls, options = {}) {
2116
+ const pool = new _TopologyNativeAdapterPool(urls, options);
2117
+ await pool.refreshTopology();
2118
+ return pool;
2119
+ }
2120
+ get topology() {
2121
+ return this.topologyValue;
2122
+ }
2123
+ async refreshTopology() {
2124
+ let lastError;
2125
+ for (const url of this.refreshCandidateUrls()) {
2126
+ try {
2127
+ const adapter = await this.adapterForUrl(url);
2128
+ const topology = RoutingTopology.build(await adapter.executeCommand("SHARDS"));
2129
+ this.topologyValue = topology;
2130
+ if (this.warmConnections) {
2131
+ await Promise.allSettled([...topology.endpoints.values()].map(async (endpoint) => await this.adapterForEndpoint(endpoint)));
2132
+ }
2133
+ return topology;
2134
+ } catch (error) {
2135
+ lastError = error;
2136
+ }
2137
+ }
2138
+ throw new FerricStoreError("no FerricStore topology endpoint reachable", { raw: lastError });
2139
+ }
2140
+ route(key) {
2141
+ const route = this.topologyValue.routeKey(key);
2142
+ this.validateEndpoint(route.endpoint);
2143
+ return route;
2144
+ }
2145
+ async executeCommand(...args) {
2146
+ const routed = this.routeData(args);
2147
+ if (routed == null) {
2148
+ return await (await this.controlAdapter()).executeCommand(...args);
2149
+ }
2150
+ const adapter = await this.adapterForEndpoint(routed.route.endpoint);
2151
+ try {
2152
+ return await adapter.executeProtocolCommand(routed.command, routed.route.laneId);
2153
+ } catch (error) {
2154
+ if (isRetryableRouteError(error)) {
2155
+ await this.refreshTopology().catch(() => void 0);
2156
+ }
2157
+ throw error;
2158
+ }
2159
+ }
2160
+ async executePipeline(commands, options = {}) {
2161
+ if (commands.length === 0) {
2162
+ return [];
2163
+ }
2164
+ const route = this.singleRouteForCommands(commands);
2165
+ if (route == null) {
2166
+ return await (await this.controlAdapter()).executePipeline(commands, options);
2167
+ }
2168
+ const adapter = await this.adapterForEndpoint(route.endpoint);
2169
+ try {
2170
+ const response = await adapter.executeProtocolCommand(pipelineCommand(commands), route.laneId);
2171
+ return unwrapPipelineResponse(response, options);
2172
+ } catch (error) {
2173
+ if (isRetryableRouteError(error)) {
2174
+ await this.refreshTopology().catch(() => void 0);
2175
+ }
2176
+ throw error;
2177
+ }
2178
+ }
2179
+ async close() {
2180
+ await Promise.allSettled([...this.adapters.values()].map(async (adapter) => await adapter.close()));
2181
+ this.adapters.clear();
2182
+ }
2183
+ routeData(args) {
2184
+ if (args.length === 0) {
2185
+ return void 0;
2186
+ }
2187
+ let command;
2188
+ try {
2189
+ command = buildProtocolCommand(args);
2190
+ } catch {
2191
+ return void 0;
2192
+ }
2193
+ const key = routingKey(args, command);
2194
+ if (key == null) {
2195
+ return void 0;
2196
+ }
2197
+ return { command, route: this.route(key) };
2198
+ }
2199
+ singleRouteForCommands(commands) {
2200
+ let route;
2201
+ for (const command of commands) {
2202
+ const routed = this.routeData(command);
2203
+ if (routed == null) {
2204
+ return void 0;
2205
+ }
2206
+ const current = routed.route;
2207
+ if (route == null) {
2208
+ route = current;
2209
+ } else if (route.endpointKey !== current.endpointKey || route.laneId !== current.laneId) {
2210
+ return void 0;
2211
+ }
2212
+ }
2213
+ return route;
2214
+ }
2215
+ async controlAdapter() {
2216
+ for (const url of this.refreshCandidateUrls()) {
2217
+ try {
2218
+ return await this.adapterForUrl(url);
2219
+ } catch {
2220
+ }
2221
+ }
2222
+ return await this.adapterForUrl(this.seedUrls[0] ?? "ferric://127.0.0.1:6388");
2223
+ }
2224
+ async adapterForUrl(url) {
2225
+ const endpoint = endpointFromUrl(url);
2226
+ const key = endpointKeyFor(endpoint);
2227
+ const existing = this.adapters.get(key);
2228
+ if (existing != null) {
2229
+ return existing;
2230
+ }
2231
+ const adapter = await NativeAdapter.fromUrl(url, this.adapterOptions);
2232
+ this.adapters.set(key, adapter);
2233
+ return adapter;
2234
+ }
2235
+ async adapterForEndpoint(endpoint) {
2236
+ this.validateEndpoint(endpoint);
2237
+ const key = endpointKeyFor(endpoint);
2238
+ const existing = this.adapters.get(key);
2239
+ if (existing != null) {
2240
+ return existing;
2241
+ }
2242
+ const adapter = await NativeAdapter.fromUrl(urlFromEndpoint(endpoint, this.tls), this.adapterOptions);
2243
+ this.adapters.set(key, adapter);
2244
+ return adapter;
2245
+ }
2246
+ validateEndpoint(endpoint) {
2247
+ let allowed;
2248
+ if (this.endpointPolicy === "any" || this.endpointPolicy === "none") {
2249
+ allowed = true;
2250
+ } else if (this.endpointPolicy === "seed_hosts") {
2251
+ allowed = this.seedEndpointKeys.has(endpointKeyFor(endpoint)) || this.trustedHosts.has(endpoint.host.toLowerCase());
2252
+ } else if ("allowHosts" in this.endpointPolicy) {
2253
+ allowed = normalizedHostSet(this.endpointPolicy.allowHosts).has(endpoint.host.toLowerCase());
2254
+ } else {
2255
+ throw new FerricStoreError("invalid endpoint policy", { raw: this.endpointPolicy });
2256
+ }
2257
+ if (!allowed) {
2258
+ throw new FerricStoreError("unsafe learned endpoint", { raw: endpoint });
2259
+ }
2260
+ if (this.endpointValidator != null && !this.endpointValidator(endpoint)) {
2261
+ throw new FerricStoreError("unsafe learned endpoint", { raw: endpoint });
2262
+ }
2263
+ }
2264
+ refreshCandidateUrls() {
2265
+ const urls = [
2266
+ ...this.seedUrls,
2267
+ ...[...this.topologyValue.endpoints.values()].map((endpoint) => urlFromEndpoint(endpoint, this.tls))
2268
+ ];
2269
+ return [...new Set(urls)];
2270
+ }
2271
+ };
2272
+ function routingKey(args, command) {
2273
+ const name = commandName(args);
2274
+ if (command.opcode < 256 || name === "CLUSTER.KEYSLOT" || name === "SHARDS" || name === "ROUTE") {
2275
+ return void 0;
2276
+ }
2277
+ const keyFromArgs = routingKeyFromArgs(name, args);
2278
+ if (keyFromArgs != null) {
2279
+ return keyFromArgs;
2280
+ }
2281
+ if (!isPlainObject2(command.payload)) {
2282
+ return void 0;
2283
+ }
2284
+ for (const field2 of ["key", "partition_key", "id", "owner_flow_id", "parent_id", "root_id", "correlation_id", "scope"]) {
2285
+ const value = getField(command.payload, field2);
2286
+ if (typeof value === "string" || Buffer4.isBuffer(value)) {
2287
+ return value;
2288
+ }
2289
+ }
2290
+ const keys = getField(command.payload, "keys");
2291
+ if (Array.isArray(keys)) {
2292
+ return singleShardKey(keys);
2293
+ }
2294
+ const pairsValue = getField(command.payload, "pairs");
2295
+ if (Array.isArray(pairsValue)) {
2296
+ return singleShardKey(
2297
+ pairsValue.filter((pair) => Array.isArray(pair) && pair.length > 0).map((pair) => pair[0])
2298
+ );
2299
+ }
2300
+ return void 0;
2301
+ }
2302
+ function routingKeyFromArgs(name, args) {
2303
+ if (name == null) {
2304
+ return void 0;
2305
+ }
2306
+ if (name === "MGET" || name === "DEL") {
2307
+ return singleShardKey(args.slice(1));
2308
+ }
2309
+ if (name === "MSET") {
2310
+ const keys = [];
2311
+ for (let index = 1; index < args.length; index += 2) {
2312
+ keys.push(args[index]);
2313
+ }
2314
+ return singleShardKey(keys);
2315
+ }
2316
+ if (name === "BITOP") {
2317
+ return bitopRoutingKey(args);
2318
+ }
2319
+ if (name === "RENAME" || name === "RENAMENX") {
2320
+ return singleShardKey(args.slice(1, 3));
2321
+ }
2322
+ if (name === "XREAD" || name === "XREADGROUP") {
2323
+ return streamReadRoutingKey(args);
2324
+ }
2325
+ if (name.startsWith("FLOW.")) {
2326
+ return flowRoutingKey(name, args);
2327
+ }
2328
+ if (firstKeyCommands.has(name)) {
2329
+ const key = args[1];
2330
+ return typeof key === "string" || Buffer4.isBuffer(key) ? key : void 0;
2331
+ }
2332
+ return void 0;
2333
+ }
2334
+ function flowRoutingKey(name, args) {
2335
+ const flowArgs = args.slice(1);
2336
+ if (flowArgs.length === 0) {
2337
+ return void 0;
2338
+ }
2339
+ if (name === "FLOW.CLAIM_DUE" || name === "FLOW.RECLAIM") {
2340
+ return flowPartitionRoutingKey(flowArgs, 1);
2341
+ }
2342
+ if (name === "FLOW.CREATE_MANY" || name === "FLOW.COMPLETE_MANY" || name === "FLOW.TRANSITION_MANY" || name === "FLOW.RETRY_MANY" || name === "FLOW.FAIL_MANY" || name === "FLOW.CANCEL_MANY") {
2343
+ const partition2 = flowArgs[0];
2344
+ if (typeof partition2 === "string" && partition2.toUpperCase() !== "AUTO" && partition2.toUpperCase() !== "MIXED") {
2345
+ return partition2;
2346
+ }
2347
+ if (Buffer4.isBuffer(partition2)) {
2348
+ const text2 = partition2.toString("utf8").toUpperCase();
2349
+ if (text2 !== "AUTO" && text2 !== "MIXED") {
2350
+ return partition2;
2351
+ }
2352
+ }
2353
+ return void 0;
2354
+ }
2355
+ const partition = flowPartitionRoutingKey(flowArgs, 1);
2356
+ if (partition != null) {
2357
+ return partition;
2358
+ }
2359
+ if (typeScopedFlowCommands.has(name)) {
2360
+ return void 0;
2361
+ }
2362
+ const id = flowArgs[0];
2363
+ return typeof id === "string" || Buffer4.isBuffer(id) ? id : void 0;
2364
+ }
2365
+ function bitopRoutingKey(args) {
2366
+ if (args.length < 4) {
2367
+ return void 0;
2368
+ }
2369
+ return singleShardKey(args.slice(2));
2370
+ }
2371
+ function streamReadRoutingKey(args) {
2372
+ const streamsIndex = args.findIndex((arg) => commandPart(arg) === "STREAMS");
2373
+ if (streamsIndex < 0) {
2374
+ return void 0;
2375
+ }
2376
+ const streamArgs = args.slice(streamsIndex + 1);
2377
+ if (streamArgs.length === 0 || streamArgs.length % 2 !== 0) {
2378
+ return void 0;
2379
+ }
2380
+ return singleShardKey(streamArgs.slice(0, streamArgs.length / 2));
2381
+ }
2382
+ function flowPartitionRoutingKey(args, start) {
2383
+ for (let index = start; index < args.length; index += 1) {
2384
+ const token = commandPart(args[index]);
2385
+ if (token === "PARTITION") {
2386
+ const key = args[index + 1];
2387
+ return typeof key === "string" || Buffer4.isBuffer(key) ? key : void 0;
2388
+ }
2389
+ if (token === "PARTITIONS") {
2390
+ const count = Number(args[index + 1]);
2391
+ if (!Number.isInteger(count) || count < 0) {
2392
+ return void 0;
2393
+ }
2394
+ return singleShardKey(args.slice(index + 2, index + 2 + count));
2395
+ }
2396
+ }
2397
+ return void 0;
2398
+ }
2399
+ function singleShardKey(keys) {
2400
+ const usable = keys.filter((key) => typeof key === "string" || Buffer4.isBuffer(key));
2401
+ if (usable.length === 0) {
2402
+ return void 0;
2403
+ }
2404
+ const first = usable[0];
2405
+ if (first == null) {
2406
+ return void 0;
2407
+ }
2408
+ const firstSlot = RoutingTopology.slotForKey(first);
2409
+ return usable.every((key) => RoutingTopology.slotForKey(key) === firstSlot) ? first : void 0;
2410
+ }
2411
+ function commandName(args) {
2412
+ const first = commandPart(args[0]);
2413
+ if (first == null) {
2414
+ return void 0;
2415
+ }
2416
+ if (first === "FLOW" && args.length > 1) {
2417
+ const second = commandPart(args[1]);
2418
+ return second == null ? first : `${first}.${second}`;
2419
+ }
2420
+ if (first === "CLIENT" && args.length > 1) {
2421
+ const second = commandPart(args[1]);
2422
+ return second == null ? first : `${first}.${second}`;
2423
+ }
2424
+ if (first === "CLUSTER" && args.length > 1) {
2425
+ const second = commandPart(args[1]);
2426
+ return second == null ? first : `${first}.${second}`;
2427
+ }
2428
+ return first;
2429
+ }
2430
+ function commandPart(value) {
2431
+ if (typeof value === "string") {
2432
+ return value.toUpperCase();
2433
+ }
2434
+ if (Buffer4.isBuffer(value)) {
2435
+ return value.toString("utf8").toUpperCase();
2436
+ }
2437
+ return void 0;
2438
+ }
2439
+ function isPlainObject2(value) {
2440
+ return typeof value === "object" && value != null && !Array.isArray(value) && !Buffer4.isBuffer(value);
2441
+ }
2442
+ function getField(source, ...keys) {
2443
+ if (source instanceof Map) {
2444
+ for (const key of keys) {
2445
+ if (source.has(key)) return source.get(key);
2446
+ const bufferKey = Buffer4.from(key);
2447
+ for (const [itemKey, value] of source.entries()) {
2448
+ if (Buffer4.isBuffer(itemKey) && itemKey.equals(bufferKey)) {
2449
+ return value;
2450
+ }
2451
+ }
2452
+ }
2453
+ return void 0;
2454
+ }
2455
+ if (isPlainObject2(source)) {
2456
+ for (const key of keys) {
2457
+ if (key in source) return source[key];
2458
+ }
2459
+ }
2460
+ return void 0;
2461
+ }
2462
+ function textOrUndefined(value) {
2463
+ if (typeof value === "string") {
2464
+ return value;
2465
+ }
2466
+ if (Buffer4.isBuffer(value)) {
2467
+ return value.toString("utf8");
2468
+ }
2469
+ return void 0;
2470
+ }
2471
+ function numberOrUndefined(value) {
2472
+ return typeof value === "number" && Number.isInteger(value) ? value : void 0;
2473
+ }
2474
+ function endpointFromRange(item) {
2475
+ const endpointValue = getField(item, "endpoint");
2476
+ const raw = isPlainObject2(endpointValue) || endpointValue instanceof Map ? endpointValue : item;
2477
+ const host = textOrUndefined(getField(raw, "host", "native_host"));
2478
+ const nativePort = numberOrUndefined(getField(raw, "native_port"));
2479
+ if (host == null || nativePort == null) {
2480
+ throw new FerricStoreError("invalid SHARDS endpoint", { raw: item });
2481
+ }
2482
+ const nativeTlsPort = numberOrUndefined(getField(raw, "native_tls_port"));
2483
+ return {
2484
+ host,
2485
+ nativePort,
2486
+ node: textOrUndefined(getField(raw, "node", "leader_node", "owner_node")) ?? host,
2487
+ ...nativeTlsPort == null ? {} : { nativeTlsPort }
2488
+ };
2489
+ }
2490
+ function endpointFromUrl(url) {
2491
+ const parsed = parseFerricUrl2(url);
2492
+ return {
2493
+ host: parsed.host,
2494
+ nativePort: parsed.port,
2495
+ node: parsed.host,
2496
+ ...parsed.tls ? { nativeTlsPort: parsed.port } : {}
2497
+ };
2498
+ }
2499
+ function parseFerricUrl2(value) {
2500
+ const url = new URL(value);
2501
+ if (url.protocol !== "ferric:" && url.protocol !== "ferrics:") {
2502
+ throw new FerricStoreError(`unsupported FerricStore URL scheme: ${url.protocol}`);
2503
+ }
2504
+ return {
2505
+ host: url.hostname || "127.0.0.1",
2506
+ ...url.password === "" ? {} : { password: decodeURIComponent(url.password) },
2507
+ port: Number(url.port || (url.protocol === "ferrics:" ? 6389 : 6388)),
2508
+ tls: url.protocol === "ferrics:",
2509
+ ...url.username === "" ? {} : { username: decodeURIComponent(url.username) }
2510
+ };
2511
+ }
2512
+ function endpointKeyFor(endpoint) {
2513
+ return `${endpoint.host.toLowerCase()}:${endpoint.nativePort}`;
2514
+ }
2515
+ function urlFromEndpoint(endpoint, useTls) {
2516
+ const port = useTls && endpoint.nativeTlsPort != null ? endpoint.nativeTlsPort : endpoint.nativePort;
2517
+ const host = endpoint.host.includes(":") && !endpoint.host.startsWith("[") ? `[${endpoint.host}]` : endpoint.host;
2518
+ return `${useTls ? "ferrics" : "ferric"}://${host}:${port}`;
2519
+ }
2520
+ function nativeOnlyOptions(options) {
2521
+ const nativeOptions = { ...options };
2522
+ delete nativeOptions.endpointPolicy;
2523
+ delete nativeOptions.endpointValidator;
2524
+ delete nativeOptions.haRouting;
2525
+ delete nativeOptions.seeds;
2526
+ delete nativeOptions.trustedHosts;
2527
+ delete nativeOptions.warmConnections;
2528
+ return nativeOptions;
2529
+ }
2530
+ function withSeedAuthDefaults(urls, options) {
2531
+ const next = { ...options };
2532
+ for (const url of urls) {
2533
+ const parsed = parseFerricUrl2(url);
2534
+ if (next.username == null && parsed.username != null) {
2535
+ next.username = parsed.username;
2536
+ }
2537
+ if (next.password == null && parsed.password != null) {
2538
+ next.password = parsed.password;
2539
+ }
2540
+ if (next.username != null && next.password != null) {
2541
+ break;
2542
+ }
2543
+ }
2544
+ return next;
2545
+ }
2546
+ function isRetryableRouteError(error) {
2547
+ if (!(error instanceof Error)) {
2548
+ return false;
2549
+ }
2550
+ const message = error.message.toLowerCase();
2551
+ return message.includes("connection closed") || message.includes("connection is closed") || message.includes("shard not available") || message.includes("leader") || message.includes("route");
2552
+ }
2553
+ function normalizedHostSet(hosts) {
2554
+ return new Set(hosts.filter((host) => host != null && host !== "").map((host) => host.toLowerCase()));
2555
+ }
2556
+ function hashTagOrKey(key) {
2557
+ const start = key.indexOf("{");
2558
+ if (start < 0) {
2559
+ return key;
2560
+ }
2561
+ const end = key.indexOf("}", start + 1);
2562
+ return end > start + 1 ? key.slice(start + 1, end) : key;
2563
+ }
2564
+ function flowHashTag(rest, fallbackKey) {
2565
+ const end = rest.indexOf("}");
2566
+ return end > 0 ? rest.slice(0, end) : hashTagOrKey(fallbackKey);
2567
+ }
2568
+ var crc32Table = new Uint32Array(256);
2569
+ for (let index = 0; index < 256; index += 1) {
2570
+ let value = index;
2571
+ for (let bit = 0; bit < 8; bit += 1) {
2572
+ value = (value & 1) !== 0 ? 3988292384 ^ value >>> 1 : value >>> 1;
2573
+ }
2574
+ crc32Table[index] = value >>> 0;
2575
+ }
2576
+ function crc32(buffer) {
2577
+ let value = 4294967295;
2578
+ for (const byte of buffer) {
2579
+ value = value >>> 8 ^ (crc32Table[(value ^ byte) & 255] ?? 0);
2580
+ }
2581
+ return (value ^ 4294967295) >>> 0;
2582
+ }
2583
+ var firstKeyCommands = /* @__PURE__ */ new Set([
2584
+ "BITCOUNT",
2585
+ "BITFIELD",
2586
+ "BITPOS",
2587
+ "CAS",
2588
+ "EXISTS",
2589
+ "EXPIRE",
2590
+ "EXPIREAT",
2591
+ "FETCH_OR_COMPUTE",
2592
+ "FETCH_OR_COMPUTE_ERROR",
2593
+ "FETCH_OR_COMPUTE_RESULT",
2594
+ "FERRICSTORE.KEY_INFO",
2595
+ "GET",
2596
+ "GETBIT",
2597
+ "GETDEL",
2598
+ "GETEX",
2599
+ "HDEL",
2600
+ "HEXISTS",
2601
+ "HGET",
2602
+ "HGETALL",
2603
+ "HINCRBY",
2604
+ "HKEYS",
2605
+ "HLEN",
2606
+ "HMGET",
2607
+ "HMSET",
2608
+ "HSET",
2609
+ "HVALS",
2610
+ "LOCK",
2611
+ "LPOP",
2612
+ "LPUSH",
2613
+ "LRANGE",
2614
+ "LREM",
2615
+ "RATELIMIT.ADD",
2616
+ "RPOP",
2617
+ "RPUSH",
2618
+ "SADD",
2619
+ "SCARD",
2620
+ "SISMEMBER",
2621
+ "SMEMBERS",
2622
+ "SREM",
2623
+ "SET",
2624
+ "SETBIT",
2625
+ "STRLEN",
2626
+ "TTL",
2627
+ "TYPE",
2628
+ "UNLINK",
2629
+ "UNLOCK",
2630
+ "XADD",
2631
+ "XLEN",
2632
+ "XRANGE",
2633
+ "ZADD",
2634
+ "ZCARD",
2635
+ "ZRANGE",
2636
+ "ZREM",
2637
+ "ZSCORE"
2638
+ ]);
2639
+ var typeScopedFlowCommands = /* @__PURE__ */ new Set([
2640
+ "FLOW.APPROVAL.LIST",
2641
+ "FLOW.ATTRIBUTE_VALUES",
2642
+ "FLOW.ATTRIBUTES",
2643
+ "FLOW.BUDGET.LIST",
2644
+ "FLOW.FAILURES",
2645
+ "FLOW.GOVERNANCE.OVERVIEW",
2646
+ "FLOW.INFO",
2647
+ "FLOW.LIMIT.LIST",
2648
+ "FLOW.LIST",
2649
+ "FLOW.POLICY.GET",
2650
+ "FLOW.POLICY.SET",
2651
+ "FLOW.RETENTION_CLEANUP",
2652
+ "FLOW.SCHEDULE.FIRE_DUE",
2653
+ "FLOW.SCHEDULE.LIST",
2654
+ "FLOW.SEARCH",
2655
+ "FLOW.STATS",
2656
+ "FLOW.STUCK",
2657
+ "FLOW.TERMINALS"
2658
+ ]);
1611
2659
 
1612
2660
  // src/codecs.ts
1613
2661
  var RawCodec = class {
@@ -1822,7 +2870,7 @@ function parseKvResponse(value) {
1822
2870
  return { value };
1823
2871
  }
1824
2872
  function autoPartitionKeyForId(id) {
1825
- return `${AUTO_PARTITION_PREFIX}${crc32(Buffer.from(id)) % AUTO_PARTITION_BUCKETS}`;
2873
+ return `${AUTO_PARTITION_PREFIX}${crc322(Buffer.from(id)) % AUTO_PARTITION_BUCKETS}`;
1826
2874
  }
1827
2875
  function expandManyResponse(value, count) {
1828
2876
  if (Array.isArray(value) && value.length === count) {
@@ -1892,7 +2940,7 @@ function coerceDiagnosticValue(value) {
1892
2940
  }
1893
2941
  return value;
1894
2942
  }
1895
- function crc32(buffer) {
2943
+ function crc322(buffer) {
1896
2944
  let crc = 4294967295;
1897
2945
  for (const byte of buffer) {
1898
2946
  crc ^= byte;
@@ -2818,14 +3866,14 @@ function claimedItemFromResp(value, codec) {
2818
3866
  const partition = tuple[1] == null ? null : bytes(tuple[1]);
2819
3867
  const leaseToken = bytes(tuple[2]);
2820
3868
  const fencingToken = integer(tuple[3]);
2821
- const attrs = tuple[5] ?? (isPlainObject(tuple[4]) ? tuple[4] : void 0);
3869
+ const attrs = tuple[5] ?? (isPlainObject3(tuple[4]) ? tuple[4] : void 0);
2822
3870
  const item = {
2823
3871
  id: text(id),
2824
3872
  partitionKey: optionalString(partition),
2825
3873
  leaseToken,
2826
3874
  fencingToken,
2827
3875
  runState: optionalString(tuple[4]),
2828
- attributes: isPlainObject(attrs) ? attrs : void 0,
3876
+ attributes: isPlainObject3(attrs) ? attrs : void 0,
2829
3877
  type: "",
2830
3878
  state: "running"
2831
3879
  };
@@ -2945,9 +3993,9 @@ function decodeValues(value, codec) {
2945
3993
  }
2946
3994
  function plainObjectField(value, key) {
2947
3995
  const item = field(value, key);
2948
- return isPlainObject(item) ? item : void 0;
3996
+ return isPlainObject3(item) ? item : void 0;
2949
3997
  }
2950
- function isPlainObject(value) {
3998
+ function isPlainObject3(value) {
2951
3999
  return typeof value === "object" && value != null && !Buffer.isBuffer(value) && !(value instanceof Uint8Array) && !Array.isArray(value);
2952
4000
  }
2953
4001
 
@@ -2998,7 +4046,21 @@ var FerricStoreClient = class _FerricStoreClient {
2998
4046
  this.zset = new SortedSetStore(this);
2999
4047
  }
3000
4048
  static async fromUrl(url, options = {}) {
3001
- return new _FerricStoreClient(await NativeAdapter.fromUrl(url, options.nativeOptions), options);
4049
+ const reconnectOptions = options.reconnect ?? options.nativeOptions?.autoReconnect ?? true;
4050
+ const nativeOptions = options.nativeOptions ?? {};
4051
+ const seeds = nativeOptions.seeds ?? [];
4052
+ const createExecutor = async () => nativeOptions.haRouting === true || seeds.length > 0 ? await TopologyNativeAdapterPool.fromUrls([url, ...seeds], nativeOptions) : await NativeAdapter.fromUrl(url, nativeOptions);
4053
+ const executor = reconnectOptions === false ? await createExecutor() : new ReconnectingExecutor(createExecutor, reconnectOptions === true ? {} : reconnectOptions);
4054
+ return new _FerricStoreClient(executor, options);
4055
+ }
4056
+ static async fromUrls(urls, options = {}) {
4057
+ if (urls.length === 0) {
4058
+ throw new Error("FerricStoreClient.fromUrls requires at least one URL");
4059
+ }
4060
+ const reconnectOptions = options.reconnect ?? options.nativeOptions?.autoReconnect ?? true;
4061
+ const createExecutor = async () => await TopologyNativeAdapterPool.fromUrls(urls, options.nativeOptions);
4062
+ const executor = reconnectOptions === false ? await createExecutor() : new ReconnectingExecutor(createExecutor, reconnectOptions === true ? {} : reconnectOptions);
4063
+ return new _FerricStoreClient(executor, options);
3002
4064
  }
3003
4065
  async command(...args) {
3004
4066
  return await this.executor.executeCommand(...args);
@@ -3012,6 +4074,18 @@ var FerricStoreClient = class _FerricStoreClient {
3012
4074
  async close() {
3013
4075
  await this.executor.close?.();
3014
4076
  }
4077
+ async refreshTopology() {
4078
+ if (this.executor.refreshTopology == null) {
4079
+ throw new Error("topology refresh requires a topology-aware native executor");
4080
+ }
4081
+ return await this.executor.refreshTopology();
4082
+ }
4083
+ async route(key) {
4084
+ if (this.executor.route == null) {
4085
+ throw new Error("route lookup requires a topology-aware native executor");
4086
+ }
4087
+ return await this.executor.route(key);
4088
+ }
3015
4089
  async ping(message) {
3016
4090
  return await this.command("PING", ...message == null ? [] : [message]);
3017
4091
  }
@@ -3074,7 +4148,7 @@ var FerricStoreClient = class _FerricStoreClient {
3074
4148
  return response == null ? null : text(response);
3075
4149
  }
3076
4150
  async clientInfo() {
3077
- return text(await this.command("CLIENT", "INFO"));
4151
+ return infoText(await this.command("CLIENT", "INFO"));
3078
4152
  }
3079
4153
  async clientList(options = {}) {
3080
4154
  return text(await this.command("CLIENT", "LIST", ...options.type == null ? [] : ["TYPE", options.type]));
@@ -3129,7 +4203,7 @@ var FerricStoreClient = class _FerricStoreClient {
3129
4203
  return Number(await this.command("PUBSUB", "NUMPAT"));
3130
4204
  }
3131
4205
  async aclSetUser(username, rules) {
3132
- return okResponse(await this.command("ACL", "SETUSER", username, ...rules));
4206
+ return okResponse(await this.command("ACL", "SETUSER", username, ...ruleArgs(rules)));
3133
4207
  }
3134
4208
  async aclDelUser(...usernames) {
3135
4209
  return Number(await this.command("ACL", "DELUSER", ...usernames));
@@ -3140,6 +4214,9 @@ var FerricStoreClient = class _FerricStoreClient {
3140
4214
  async aclList() {
3141
4215
  return arrayResponse(await this.command("ACL", "LIST"));
3142
4216
  }
4217
+ async aclListUsers() {
4218
+ return await this.aclList();
4219
+ }
3143
4220
  async aclWhoami() {
3144
4221
  return text(await this.command("ACL", "WHOAMI"));
3145
4222
  }
@@ -3235,6 +4312,54 @@ var FerricStoreClient = class _FerricStoreClient {
3235
4312
  async ferricstoreDoctor(...args) {
3236
4313
  return await this.command("FERRICSTORE.DOCTOR", ...args);
3237
4314
  }
4315
+ async capabilities() {
4316
+ return normalizeAdminResponse(await this.command("FERRICSTORE.CAPABILITIES"));
4317
+ }
4318
+ async ensureNamespace(prefix, attrs = {}) {
4319
+ return normalizeAdminResponse(
4320
+ await this.command("FERRICSTORE.NAMESPACE", "ENSURE", prefix, ...managementPairArgs(attrs))
4321
+ );
4322
+ }
4323
+ async getNamespace(prefix) {
4324
+ return normalizeAdminResponse(await this.command("FERRICSTORE.NAMESPACE", "GET", prefix));
4325
+ }
4326
+ async listNamespaces() {
4327
+ return normalizeAdminResponse(await this.command("FERRICSTORE.NAMESPACE", "LIST"));
4328
+ }
4329
+ async deleteNamespace(prefix) {
4330
+ return normalizeAdminResponse(await this.command("FERRICSTORE.NAMESPACE", "DELETE", prefix));
4331
+ }
4332
+ async setQuota(namespace, quotaSpec = {}) {
4333
+ return normalizeAdminResponse(
4334
+ await this.command("FERRICSTORE.QUOTA", "SET", namespace, ...managementPairArgs(quotaSpec))
4335
+ );
4336
+ }
4337
+ async getQuota(namespace) {
4338
+ return normalizeAdminResponse(await this.command("FERRICSTORE.QUOTA", "GET", namespace));
4339
+ }
4340
+ async quotaUsage(namespace) {
4341
+ return normalizeAdminResponse(await this.command("FERRICSTORE.QUOTA", "USAGE", namespace));
4342
+ }
4343
+ async clusterInfo() {
4344
+ return normalizeAdminResponse(await this.command("FERRICSTORE.TELEMETRY", "CLUSTER_INFO"));
4345
+ }
4346
+ async namespaceUsage(prefix) {
4347
+ return normalizeAdminResponse(
4348
+ await this.command("FERRICSTORE.TELEMETRY", "NAMESPACE_USAGE", prefix)
4349
+ );
4350
+ }
4351
+ async flowQuery(attrs = {}) {
4352
+ const response = normalizeAdminResponse(
4353
+ await this.command("FERRICSTORE.TELEMETRY", "FLOW_QUERY", ...managementPairArgs(attrs))
4354
+ );
4355
+ return Array.isArray(response) ? [...response] : [];
4356
+ }
4357
+ async flowHistory(id, attrs = {}) {
4358
+ const response = normalizeAdminResponse(
4359
+ await this.command("FERRICSTORE.TELEMETRY", "FLOW_HISTORY", id, ...managementPairArgs(attrs))
4360
+ );
4361
+ return Array.isArray(response) ? [...response] : [];
4362
+ }
3238
4363
  async create(id, options) {
3239
4364
  const currentNowMs = options.nowMs ?? nowMs();
3240
4365
  const args = [
@@ -3715,6 +4840,14 @@ var FerricStoreClient = class _FerricStoreClient {
3715
4840
  async list(type, options = {}) {
3716
4841
  return await this.indexQuery("FLOW.LIST", type, options);
3717
4842
  }
4843
+ async search(type, options = {}) {
4844
+ const args = ["FLOW.SEARCH", type];
4845
+ appendReadOptions(args, options);
4846
+ appendAttributes(args, options.attributes);
4847
+ appendSearchStateMeta(args, options.state, options.stateMeta);
4848
+ const response = await this.command(...args);
4849
+ return Array.isArray(response) ? this.records(response) : [];
4850
+ }
3718
4851
  async terminals(type, options = {}) {
3719
4852
  return await this.indexQuery("FLOW.TERMINALS", type, options);
3720
4853
  }
@@ -3884,6 +5017,18 @@ var ErrorMappingExecutor = class {
3884
5017
  throwMapped(error);
3885
5018
  }
3886
5019
  }
5020
+ async refreshTopology() {
5021
+ if (this.executor.refreshTopology == null) {
5022
+ throw new Error("topology refresh requires a topology-aware native executor");
5023
+ }
5024
+ return await this.executor.refreshTopology();
5025
+ }
5026
+ async route(key) {
5027
+ if (this.executor.route == null) {
5028
+ throw new Error("route lookup requires a topology-aware native executor");
5029
+ }
5030
+ return await this.executor.route(key);
5031
+ }
3887
5032
  async close() {
3888
5033
  await this.executor.close?.();
3889
5034
  }
@@ -3919,6 +5064,18 @@ var AutoBatchExecutor = class {
3919
5064
  }
3920
5065
  return await Promise.all(commands.map((command) => this.executor.executeCommand(...command)));
3921
5066
  }
5067
+ async refreshTopology() {
5068
+ if (this.executor.refreshTopology == null) {
5069
+ throw new Error("topology refresh requires a topology-aware native executor");
5070
+ }
5071
+ return await this.executor.refreshTopology();
5072
+ }
5073
+ async route(key) {
5074
+ if (this.executor.route == null) {
5075
+ throw new Error("route lookup requires a topology-aware native executor");
5076
+ }
5077
+ return await this.executor.route(key);
5078
+ }
3922
5079
  async close() {
3923
5080
  this.scheduled = false;
3924
5081
  if (this.timer != null) {
@@ -4079,7 +5236,7 @@ var neverAutoBatchCommandPrefixes = /* @__PURE__ */ new Set([
4079
5236
  ]);
4080
5237
  var neverAutoBatchCommands = /* @__PURE__ */ new Set(["FLOW.CLAIM_DUE", "FLOW.RECLAIM"]);
4081
5238
  function autoBatchCommandAllowed(command, mode) {
4082
- const name = commandName(command);
5239
+ const name = commandName2(command);
4083
5240
  if (name == null || neverAutoBatchCommands.has(name)) {
4084
5241
  return false;
4085
5242
  }
@@ -4089,18 +5246,18 @@ function autoBatchCommandAllowed(command, mode) {
4089
5246
  }
4090
5247
  return mode === "all" || safeAutoBatchCommands.has(name);
4091
5248
  }
4092
- function commandName(command) {
4093
- const first = commandPart(command[0]);
5249
+ function commandName2(command) {
5250
+ const first = commandPart2(command[0]);
4094
5251
  if (first == null) {
4095
5252
  return null;
4096
5253
  }
4097
5254
  if (first === "FLOW" && command.length > 1) {
4098
- const second = commandPart(command[1]);
5255
+ const second = commandPart2(command[1]);
4099
5256
  return second == null ? first : `${first}.${second}`;
4100
5257
  }
4101
5258
  return first;
4102
5259
  }
4103
- function commandPart(value) {
5260
+ function commandPart2(value) {
4104
5261
  if (typeof value === "string") {
4105
5262
  return value.toUpperCase();
4106
5263
  }
@@ -4125,6 +5282,67 @@ function appendStateMeta(args, stateMeta) {
4125
5282
  args.push("STATE_META", name, value);
4126
5283
  }
4127
5284
  }
5285
+ function appendAttributes(args, attributes) {
5286
+ for (const [name, value] of Object.entries(attributes ?? {})) {
5287
+ args.push("ATTRIBUTE", name, value);
5288
+ }
5289
+ }
5290
+ function appendSearchStateMeta(args, state, stateMeta) {
5291
+ const entries = Object.entries(stateMeta ?? {});
5292
+ if (entries.length === 0) {
5293
+ return;
5294
+ }
5295
+ if (entries.every(([, value]) => isPlainObject4(value))) {
5296
+ for (const [metaState, values] of entries) {
5297
+ args.push("STATE_META", metaState, values);
5298
+ }
5299
+ return;
5300
+ }
5301
+ if (state == null) {
5302
+ throw new Error("search stateMeta filters require state or nested state metadata");
5303
+ }
5304
+ args.push("STATE_META", state, stateMeta);
5305
+ }
5306
+ function normalizeAdminResponse(value) {
5307
+ return normalizeRefMeta(value);
5308
+ }
5309
+ function infoText(value) {
5310
+ if (typeof value === "string" || Buffer.isBuffer(value)) {
5311
+ return text(value);
5312
+ }
5313
+ const normalized = normalizeRefMeta(value);
5314
+ if (isPlainObject4(normalized)) {
5315
+ return Object.entries(normalized).map(([key, item]) => `${key}=${diagnosticValueText(item)}`).join(" ");
5316
+ }
5317
+ return diagnosticValueText(normalized);
5318
+ }
5319
+ function diagnosticValueText(value) {
5320
+ if (value == null) {
5321
+ return "";
5322
+ }
5323
+ if (Buffer.isBuffer(value)) {
5324
+ return value.toString("utf8");
5325
+ }
5326
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
5327
+ return String(value);
5328
+ }
5329
+ return JSON.stringify(value);
5330
+ }
5331
+ function managementPairArgs(pairs2) {
5332
+ const args = [];
5333
+ for (const [key, value] of Object.entries(pairs2)) {
5334
+ if (value != null) {
5335
+ args.push(key.toUpperCase(), value);
5336
+ }
5337
+ }
5338
+ return args;
5339
+ }
5340
+ function ruleArgs(rules) {
5341
+ return typeof rules === "string" ? [rules] : [...rules];
5342
+ }
5343
+ function isPlainObject4(value) {
5344
+ return typeof value === "object" && value != null && !Array.isArray(value) && !Buffer.isBuffer(value);
5345
+ }
4128
5346
  function sharedCreateManyStateMeta(items, stateMeta) {
4129
5347
  const itemMetas = items.map((item) => item.stateMeta).filter((meta) => meta != null);
4130
5348
  if (itemMetas.length === 0) {
@@ -5097,6 +6315,7 @@ function valueRefToString(value) {
5097
6315
  export {
5098
6316
  BitmapStore,
5099
6317
  BloomFilterStore,
6318
+ COMMAND_OPCODES,
5100
6319
  CountMinSketchStore,
5101
6320
  CuckooFilterStore,
5102
6321
  FerricStoreClient,
@@ -5120,12 +6339,15 @@ export {
5120
6339
  QueueClient,
5121
6340
  QueueWorker,
5122
6341
  RawCodec,
6342
+ ReconnectingExecutor,
6343
+ RoutingTopology,
5123
6344
  SetStore,
5124
6345
  SortedSetStore,
5125
6346
  StaleLeaseError,
5126
6347
  StreamStore,
5127
6348
  TDigestStore,
5128
6349
  TopKStore,
6350
+ TopologyNativeAdapterPool,
5129
6351
  Workflow,
5130
6352
  WorkflowClient,
5131
6353
  WorkflowContext,
@@ -5134,6 +6356,7 @@ export {
5134
6356
  classifyServerError,
5135
6357
  complete,
5136
6358
  fail,
6359
+ isReconnectableClosedConnectionError,
5137
6360
  mapException,
5138
6361
  retry,
5139
6362
  transition