@ferricstore/ferricstore 0.1.2 → 0.1.4

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 (52) hide show
  1. package/README.md +13 -1
  2. package/dist/index.cjs +5348 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +1322 -0
  5. package/dist/index.d.ts +34 -4
  6. package/dist/index.js +130 -3
  7. package/dist/index.js.map +1 -1
  8. package/docs/api/assets/hierarchy.js +1 -1
  9. package/docs/api/assets/highlight.css +10 -10
  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 +2 -2
  13. package/docs/api/classes/FerricStoreError.html +6 -6
  14. package/docs/api/classes/FlowAlreadyExistsError.html +6 -6
  15. package/docs/api/classes/FlowNotFoundError.html +6 -6
  16. package/docs/api/classes/FlowWrongStateError.html +6 -6
  17. package/docs/api/classes/InvalidCommandError.html +6 -6
  18. package/docs/api/classes/LockHeldError.html +6 -6
  19. package/docs/api/classes/LockNotOwnedError.html +6 -6
  20. package/docs/api/classes/NativeAdapter.html +2 -2
  21. package/docs/api/classes/OverloadedError.html +6 -6
  22. package/docs/api/classes/ReconnectingExecutor.html +5 -0
  23. package/docs/api/classes/StaleLeaseError.html +6 -6
  24. package/docs/api/functions/isReconnectableClosedConnectionError.html +1 -0
  25. package/docs/api/hierarchy.html +1 -1
  26. package/docs/api/index.html +18 -10
  27. package/docs/api/interfaces/AutoBatchOptions.html +2 -2
  28. package/docs/api/interfaces/CancelOptions.html +2 -2
  29. package/docs/api/interfaces/ClaimDueOptions.html +2 -2
  30. package/docs/api/interfaces/CommandExecutor.html +1 -1
  31. package/docs/api/interfaces/CompleteOptions.html +2 -2
  32. package/docs/api/interfaces/CompleteOutcome.html +1 -1
  33. package/docs/api/interfaces/CreateManyOptions.html +2 -2
  34. package/docs/api/interfaces/CreateOptions.html +2 -2
  35. package/docs/api/interfaces/FailOptions.html +2 -2
  36. package/docs/api/interfaces/FailOutcome.html +1 -1
  37. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +6 -0
  38. package/docs/api/interfaces/FerricStoreClientOptions.html +2 -2
  39. package/docs/api/interfaces/MutateOptions.html +2 -2
  40. package/docs/api/interfaces/NamedValueMutation.html +1 -1
  41. package/docs/api/interfaces/NativeAdapterOptions.html +6 -2
  42. package/docs/api/interfaces/ReadOptions.html +2 -2
  43. package/docs/api/interfaces/ReclaimOptions.html +2 -2
  44. package/docs/api/interfaces/ReconnectOptions.html +2 -0
  45. package/docs/api/interfaces/RetryOptions.html +2 -2
  46. package/docs/api/interfaces/RetryOutcome.html +1 -1
  47. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  48. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  49. package/docs/api/interfaces/TransitionOptions.html +2 -2
  50. package/docs/api/interfaces/TransitionOutcome.html +1 -1
  51. package/docs/api/modules.html +1 -1
  52. package/package.json +17 -7
package/dist/index.d.ts CHANGED
@@ -25,8 +25,12 @@ interface ExecutePipelineOptions {
25
25
  throwOnItemError?: boolean;
26
26
  }
27
27
  interface NativeAdapterOptions {
28
+ autoReconnect?: boolean | ReconnectOptions;
28
29
  clientName?: string;
29
30
  connectTimeoutMs?: number;
31
+ heartbeatIntervalMs?: number;
32
+ keepAlive?: boolean;
33
+ keepAliveInitialDelayMs?: number;
30
34
  maxChunkBytes?: number;
31
35
  protocolLanes?: number;
32
36
  timeoutMs?: number;
@@ -34,6 +38,9 @@ interface NativeAdapterOptions {
34
38
  password?: string;
35
39
  tlsOptions?: tls.ConnectionOptions;
36
40
  }
41
+ interface ReconnectOptions {
42
+ maxRetries?: number;
43
+ }
37
44
  declare class NativeAdapter implements CommandExecutor {
38
45
  private readonly socket;
39
46
  private readonly pending;
@@ -45,6 +52,10 @@ declare class NativeAdapter implements CommandExecutor {
45
52
  private readonly maxChunkBytes;
46
53
  private requestId;
47
54
  private readonly protocolLanes;
55
+ private readonly heartbeatIntervalMs?;
56
+ private heartbeatInFlight;
57
+ private heartbeatTimer?;
58
+ private lastActivityMs;
48
59
  private readonly timeoutMs;
49
60
  private constructor();
50
61
  static fromUrl(url: string, options?: NativeAdapterOptions): Promise<NativeAdapter>;
@@ -54,6 +65,9 @@ declare class NativeAdapter implements CommandExecutor {
54
65
  private startup;
55
66
  private auth;
56
67
  private request;
68
+ private startHeartbeat;
69
+ private stopHeartbeat;
70
+ private sendHeartbeat;
57
71
  private onData;
58
72
  private handleFrame;
59
73
  private failAll;
@@ -61,6 +75,20 @@ declare class NativeAdapter implements CommandExecutor {
61
75
  private nextRequestId;
62
76
  private assignLane;
63
77
  }
78
+ declare class ReconnectingExecutor implements CommandExecutor {
79
+ private readonly createExecutor;
80
+ private closed;
81
+ private readonly maxRetries;
82
+ private executorPromise;
83
+ private reconnectPromise?;
84
+ constructor(createExecutor: () => Promise<CommandExecutor>, options?: ReconnectOptions);
85
+ executeCommand(...args: CommandArgument[]): Promise<unknown>;
86
+ executePipeline(commands: readonly Command[], options?: ExecutePipelineOptions): Promise<unknown[]>;
87
+ close(): Promise<void>;
88
+ private withReconnect;
89
+ private reconnect;
90
+ }
91
+ declare function isReconnectableClosedConnectionError(error: unknown): boolean;
64
92
 
65
93
  interface StoreCommandClient {
66
94
  readonly codec: Codec;
@@ -576,6 +604,10 @@ interface FerricStoreClientOptions {
576
604
  codec?: Codec;
577
605
  backpressure?: BackpressurePolicy;
578
606
  }
607
+ interface FerricStoreClientFromUrlOptions extends FerricStoreClientOptions {
608
+ nativeOptions?: NativeAdapterOptions;
609
+ reconnect?: boolean | ReconnectOptions;
610
+ }
579
611
  interface CreateOptions {
580
612
  type: string;
581
613
  state?: string;
@@ -695,9 +727,7 @@ declare class FerricStoreClient {
695
727
  readonly topk: TopKStore;
696
728
  readonly zset: SortedSetStore;
697
729
  constructor(executor: CommandExecutor, options?: FerricStoreClientOptions);
698
- static fromUrl(url: string, options?: FerricStoreClientOptions & {
699
- nativeOptions?: NativeAdapterOptions;
700
- }): Promise<FerricStoreClient>;
730
+ static fromUrl(url: string, options?: FerricStoreClientFromUrlOptions): Promise<FerricStoreClient>;
701
731
  command(...args: CommandArgument[]): Promise<unknown>;
702
732
  pipeline(commands: readonly Command[]): Promise<unknown[]>;
703
733
  close(): Promise<void>;
@@ -1289,4 +1319,4 @@ declare class WorkflowWorker {
1289
1319
  private applyHandlerError;
1290
1320
  }
1291
1321
 
1292
- export { type AutoBatchOptions, type BackoffKind, type BackpressurePolicy, BitmapStore, BloomFilterStore, type CancelOptions, type ChildSpec, type ClaimDueOptions, type ClaimedItem, type Codec, type Command, type CommandArgument, type CommandExecutor, type CompleteOptions, type CompleteOutcome, type CountMinMergeOptions, CountMinSketchStore, type CreateItem, type CreateManyOptions, type CreateOptions, CuckooFilterStore, type ExceptionPolicy, type ExecutePipelineOptions, type FailOptions, type FailOutcome, type FencedItem, FerricStoreClient, type FerricStoreClientOptions, FerricStoreError, type FetchOrComputeResult, FlowAlreadyExistsError, FlowNotFoundError, type FlowRecord, FlowWrongStateError, type GeoAddOptions, type GeoMember, GeoStore, type GetExOptions, HashStore, HyperLogLogStore, InvalidCommandError, JsonCodec, type JsonSetOptions, JsonStore, type KeyInfo, KeyValueStore, ListStore, LockHeldError, LockNotOwnedError, type MutateOptions, type NamedValueMutation, NativeAdapter, type NativeAdapterOptions, type Outcome, OverloadedError, Queue, type QueueBatchHandler, QueueClient, type QueueHandler, type QueueOptions, QueueWorker, type QueueWorkerResult, type RangeLimit, type RateLimitResult, RawCodec, type ReadOptions, type ReclaimOptions, type RetryOptions, type RetryOutcome, type RetryPolicy, type ScanOptions, type SetOptions, SetStore, SortedSetStore, StaleLeaseError, type StateMeta, type StateMetaValue, type StateOptions, type StateRegistration, type StoreCommandClient, StreamStore, type TDigestCreateOptions, type TDigestMergeOptions, TDigestStore, type TopKReserveOptions, TopKStore, type TransitionOptions, type TransitionOutcome, type ValueConfig, type WorkerConfig, type WorkerProfile, Workflow, WorkflowClient, WorkflowContext, WorkflowFlowCommands, type WorkflowHandler, type WorkflowOptions, WorkflowWorker, type WorkflowWorkerResult, type XReadStream, type ZAddMember, type ZAddOptions, classifyServerError, complete, fail, mapException, retry, transition };
1322
+ export { type AutoBatchOptions, type BackoffKind, type BackpressurePolicy, BitmapStore, BloomFilterStore, type CancelOptions, type ChildSpec, type ClaimDueOptions, type ClaimedItem, type Codec, type Command, type CommandArgument, type CommandExecutor, type CompleteOptions, type CompleteOutcome, type CountMinMergeOptions, CountMinSketchStore, type CreateItem, type CreateManyOptions, type CreateOptions, CuckooFilterStore, type ExceptionPolicy, type ExecutePipelineOptions, type FailOptions, type FailOutcome, type FencedItem, FerricStoreClient, type FerricStoreClientFromUrlOptions, type FerricStoreClientOptions, FerricStoreError, type FetchOrComputeResult, FlowAlreadyExistsError, FlowNotFoundError, type FlowRecord, FlowWrongStateError, type GeoAddOptions, type GeoMember, GeoStore, type GetExOptions, HashStore, HyperLogLogStore, InvalidCommandError, JsonCodec, type JsonSetOptions, JsonStore, type KeyInfo, KeyValueStore, ListStore, LockHeldError, LockNotOwnedError, type MutateOptions, type NamedValueMutation, NativeAdapter, type NativeAdapterOptions, type Outcome, OverloadedError, Queue, type QueueBatchHandler, QueueClient, type QueueHandler, type QueueOptions, QueueWorker, type QueueWorkerResult, type RangeLimit, type RateLimitResult, RawCodec, type ReadOptions, type ReclaimOptions, type ReconnectOptions, ReconnectingExecutor, type RetryOptions, type RetryOutcome, type RetryPolicy, type ScanOptions, type SetOptions, SetStore, SortedSetStore, StaleLeaseError, type StateMeta, type StateMetaValue, type StateOptions, type StateRegistration, type StoreCommandClient, StreamStore, type TDigestCreateOptions, type TDigestMergeOptions, TDigestStore, type TopKReserveOptions, TopKStore, type TransitionOptions, type TransitionOutcome, type ValueConfig, type WorkerConfig, type WorkerProfile, Workflow, WorkflowClient, WorkflowContext, WorkflowFlowCommands, type WorkflowHandler, type WorkflowOptions, WorkflowWorker, type WorkflowWorkerResult, type XReadStream, type ZAddMember, type ZAddOptions, classifyServerError, complete, fail, isReconnectableClosedConnectionError, mapException, retry, transition };
package/dist/index.js CHANGED
@@ -1393,15 +1393,21 @@ var NativeAdapter = class _NativeAdapter {
1393
1393
  maxChunkBytes;
1394
1394
  requestId = 0n;
1395
1395
  protocolLanes;
1396
+ heartbeatIntervalMs;
1397
+ heartbeatInFlight = false;
1398
+ heartbeatTimer;
1399
+ lastActivityMs = Date.now();
1396
1400
  timeoutMs;
1397
- constructor(socket, timeoutMs, protocolLanes, maxChunkBytes) {
1401
+ constructor(socket, timeoutMs, protocolLanes, maxChunkBytes, heartbeatIntervalMs) {
1398
1402
  this.socket = socket;
1399
1403
  this.maxChunkBytes = Math.max(1, Math.trunc(maxChunkBytes));
1400
1404
  this.timeoutMs = timeoutMs;
1401
1405
  this.protocolLanes = Math.max(1, Math.trunc(protocolLanes));
1406
+ this.heartbeatIntervalMs = normalizeHeartbeatInterval(heartbeatIntervalMs);
1402
1407
  this.socket.on("data", (chunk) => this.onData(chunk));
1403
1408
  this.socket.on("error", (error) => this.failAll(error));
1404
1409
  this.socket.on("close", () => this.failAll(new FerricStoreError("FerricStore connection closed")));
1410
+ this.startHeartbeat();
1405
1411
  }
1406
1412
  static async fromUrl(url, options = {}) {
1407
1413
  const parsed = parseFerricUrl(url);
@@ -1410,7 +1416,8 @@ var NativeAdapter = class _NativeAdapter {
1410
1416
  socket,
1411
1417
  options.timeoutMs ?? 3e4,
1412
1418
  options.protocolLanes ?? 8,
1413
- options.maxChunkBytes ?? 64 * 1024 * 1024
1419
+ options.maxChunkBytes ?? 64 * 1024 * 1024,
1420
+ options.heartbeatIntervalMs
1414
1421
  );
1415
1422
  await adapter.startup(options.clientName);
1416
1423
  const password = options.password ?? parsed.password;
@@ -1434,6 +1441,7 @@ var NativeAdapter = class _NativeAdapter {
1434
1441
  return;
1435
1442
  }
1436
1443
  this.closed = true;
1444
+ this.stopHeartbeat();
1437
1445
  this.socket.end();
1438
1446
  }
1439
1447
  async startup(clientName) {
@@ -1455,6 +1463,7 @@ var NativeAdapter = class _NativeAdapter {
1455
1463
  if (this.closed) {
1456
1464
  throw new FerricStoreError("FerricStore connection is closed");
1457
1465
  }
1466
+ this.lastActivityMs = Date.now();
1458
1467
  const requestId = this.nextRequestId();
1459
1468
  const frame = encodeRequest(this.assignLane(command), requestId);
1460
1469
  return await new Promise((resolve, reject) => {
@@ -1468,10 +1477,12 @@ var NativeAdapter = class _NativeAdapter {
1468
1477
  opcode: command.opcode,
1469
1478
  reject: (reason) => {
1470
1479
  clearTimeout(timer);
1480
+ this.lastActivityMs = Date.now();
1471
1481
  reject(reason instanceof Error ? reason : classifyServerError(String(reason), reason));
1472
1482
  },
1473
1483
  resolve: (value) => {
1474
1484
  clearTimeout(timer);
1485
+ this.lastActivityMs = Date.now();
1475
1486
  resolve(value);
1476
1487
  }
1477
1488
  });
@@ -1480,11 +1491,43 @@ var NativeAdapter = class _NativeAdapter {
1480
1491
  this.pending.delete(requestId);
1481
1492
  this.cleanupChunksForRequest(requestId);
1482
1493
  clearTimeout(timer);
1494
+ this.lastActivityMs = Date.now();
1483
1495
  reject(error);
1484
1496
  }
1485
1497
  });
1486
1498
  });
1487
1499
  }
1500
+ startHeartbeat() {
1501
+ if (this.heartbeatIntervalMs == null) {
1502
+ return;
1503
+ }
1504
+ this.heartbeatTimer = setInterval(() => {
1505
+ void this.sendHeartbeat();
1506
+ }, this.heartbeatIntervalMs);
1507
+ this.heartbeatTimer.unref?.();
1508
+ }
1509
+ stopHeartbeat() {
1510
+ if (this.heartbeatTimer == null) {
1511
+ return;
1512
+ }
1513
+ clearInterval(this.heartbeatTimer);
1514
+ this.heartbeatTimer = void 0;
1515
+ }
1516
+ async sendHeartbeat() {
1517
+ if (this.closed || this.heartbeatInFlight || this.pending.size > 0 || this.heartbeatIntervalMs == null) {
1518
+ return;
1519
+ }
1520
+ if (Date.now() - this.lastActivityMs < this.heartbeatIntervalMs) {
1521
+ return;
1522
+ }
1523
+ this.heartbeatInFlight = true;
1524
+ try {
1525
+ await this.request(buildProtocolCommand(["PING"]));
1526
+ } catch {
1527
+ } finally {
1528
+ this.heartbeatInFlight = false;
1529
+ }
1530
+ }
1488
1531
  onData(chunk) {
1489
1532
  try {
1490
1533
  this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer3.concat([this.buffer, chunk]);
@@ -1543,6 +1586,7 @@ var NativeAdapter = class _NativeAdapter {
1543
1586
  return;
1544
1587
  }
1545
1588
  this.closed = true;
1589
+ this.stopHeartbeat();
1546
1590
  const error = reason instanceof Error ? reason : classifyServerError(String(reason), reason);
1547
1591
  for (const pending of this.pending.values()) {
1548
1592
  pending.reject(error);
@@ -1575,6 +1619,74 @@ var NativeAdapter = class _NativeAdapter {
1575
1619
  return { ...command, laneId: this.dataLane };
1576
1620
  }
1577
1621
  };
1622
+ var ReconnectingExecutor = class {
1623
+ constructor(createExecutor, options = {}) {
1624
+ this.createExecutor = createExecutor;
1625
+ this.maxRetries = Math.max(0, Math.trunc(options.maxRetries ?? 1));
1626
+ this.executorPromise = createExecutor();
1627
+ }
1628
+ createExecutor;
1629
+ closed = false;
1630
+ maxRetries;
1631
+ executorPromise;
1632
+ reconnectPromise;
1633
+ async executeCommand(...args) {
1634
+ return await this.withReconnect((executor) => executor.executeCommand(...args));
1635
+ }
1636
+ async executePipeline(commands, options = {}) {
1637
+ return await this.withReconnect(async (executor) => {
1638
+ if (executor.executePipeline != null) {
1639
+ return await executor.executePipeline(commands, options);
1640
+ }
1641
+ return await Promise.all(commands.map((command) => executor.executeCommand(...command)));
1642
+ });
1643
+ }
1644
+ async close() {
1645
+ if (this.closed) {
1646
+ return;
1647
+ }
1648
+ this.closed = true;
1649
+ const executor = await this.executorPromise.catch(() => void 0);
1650
+ await Promise.resolve(executor?.close?.());
1651
+ }
1652
+ async withReconnect(operation) {
1653
+ let executor = await this.executorPromise;
1654
+ for (let attempt = 0; ; attempt++) {
1655
+ try {
1656
+ return await operation(executor);
1657
+ } catch (error) {
1658
+ if (this.closed || attempt >= this.maxRetries || !isReconnectableClosedConnectionError(error)) {
1659
+ throw error;
1660
+ }
1661
+ executor = await this.reconnect(executor);
1662
+ }
1663
+ }
1664
+ }
1665
+ async reconnect(staleExecutor) {
1666
+ if (this.closed) {
1667
+ throw new FerricStoreError("FerricStore client is closed");
1668
+ }
1669
+ if (this.reconnectPromise != null) {
1670
+ return await this.reconnectPromise;
1671
+ }
1672
+ const currentExecutor = await this.executorPromise.catch(() => void 0);
1673
+ if (currentExecutor != null && currentExecutor !== staleExecutor) {
1674
+ return currentExecutor;
1675
+ }
1676
+ this.reconnectPromise = (async () => {
1677
+ await Promise.resolve(staleExecutor.close?.()).catch(() => void 0);
1678
+ const nextExecutor = await this.createExecutor();
1679
+ this.executorPromise = Promise.resolve(nextExecutor);
1680
+ return nextExecutor;
1681
+ })().finally(() => {
1682
+ this.reconnectPromise = void 0;
1683
+ });
1684
+ return await this.reconnectPromise;
1685
+ }
1686
+ };
1687
+ function isReconnectableClosedConnectionError(error) {
1688
+ return error instanceof Error && error.message === "FerricStore connection is closed";
1689
+ }
1578
1690
  function parseFerricUrl(value) {
1579
1691
  const url = new URL(value);
1580
1692
  if (url.protocol !== "ferric:" && url.protocol !== "ferrics:") {
@@ -1598,6 +1710,7 @@ async function connect(parsed, options) {
1598
1710
  }, timeoutMs);
1599
1711
  timer.unref?.();
1600
1712
  socket.setNoDelay(true);
1713
+ socket.setKeepAlive(options.keepAlive ?? true, normalizeKeepAliveInitialDelay(options.keepAliveInitialDelayMs));
1601
1714
  socket.once("connect", () => {
1602
1715
  clearTimeout(timer);
1603
1716
  resolve(socket);
@@ -1608,6 +1721,15 @@ async function connect(parsed, options) {
1608
1721
  });
1609
1722
  });
1610
1723
  }
1724
+ function normalizeHeartbeatInterval(value) {
1725
+ if (value != null && value <= 0) {
1726
+ return void 0;
1727
+ }
1728
+ return Math.trunc(value ?? 6e4);
1729
+ }
1730
+ function normalizeKeepAliveInitialDelay(value) {
1731
+ return Math.max(0, Math.trunc(value ?? 3e4));
1732
+ }
1611
1733
 
1612
1734
  // src/codecs.ts
1613
1735
  var RawCodec = class {
@@ -2998,7 +3120,10 @@ var FerricStoreClient = class _FerricStoreClient {
2998
3120
  this.zset = new SortedSetStore(this);
2999
3121
  }
3000
3122
  static async fromUrl(url, options = {}) {
3001
- return new _FerricStoreClient(await NativeAdapter.fromUrl(url, options.nativeOptions), options);
3123
+ const reconnectOptions = options.reconnect ?? options.nativeOptions?.autoReconnect ?? true;
3124
+ const createExecutor = async () => await NativeAdapter.fromUrl(url, options.nativeOptions);
3125
+ const executor = reconnectOptions === false ? await createExecutor() : new ReconnectingExecutor(createExecutor, reconnectOptions === true ? {} : reconnectOptions);
3126
+ return new _FerricStoreClient(executor, options);
3002
3127
  }
3003
3128
  async command(...args) {
3004
3129
  return await this.executor.executeCommand(...args);
@@ -5120,6 +5245,7 @@ export {
5120
5245
  QueueClient,
5121
5246
  QueueWorker,
5122
5247
  RawCodec,
5248
+ ReconnectingExecutor,
5123
5249
  SetStore,
5124
5250
  SortedSetStore,
5125
5251
  StaleLeaseError,
@@ -5134,6 +5260,7 @@ export {
5134
5260
  classifyServerError,
5135
5261
  complete,
5136
5262
  fail,
5263
+ isReconnectableClosedConnectionError,
5137
5264
  mapException,
5138
5265
  retry,
5139
5266
  transition