@ferricstore/ferricstore 0.1.3 → 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 (49) hide show
  1. package/dist/index.cjs +132 -3
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +34 -4
  4. package/dist/index.d.ts +34 -4
  5. package/dist/index.js +130 -3
  6. package/dist/index.js.map +1 -1
  7. package/docs/api/assets/hierarchy.js +1 -1
  8. package/docs/api/assets/navigation.js +1 -1
  9. package/docs/api/assets/search.js +1 -1
  10. package/docs/api/classes/FerricStoreClient.html +2 -2
  11. package/docs/api/classes/FerricStoreError.html +1 -1
  12. package/docs/api/classes/FlowAlreadyExistsError.html +1 -1
  13. package/docs/api/classes/FlowNotFoundError.html +1 -1
  14. package/docs/api/classes/FlowWrongStateError.html +1 -1
  15. package/docs/api/classes/InvalidCommandError.html +1 -1
  16. package/docs/api/classes/LockHeldError.html +1 -1
  17. package/docs/api/classes/LockNotOwnedError.html +1 -1
  18. package/docs/api/classes/NativeAdapter.html +2 -2
  19. package/docs/api/classes/OverloadedError.html +1 -1
  20. package/docs/api/classes/ReconnectingExecutor.html +5 -0
  21. package/docs/api/classes/StaleLeaseError.html +1 -1
  22. package/docs/api/functions/isReconnectableClosedConnectionError.html +1 -0
  23. package/docs/api/hierarchy.html +1 -1
  24. package/docs/api/interfaces/AutoBatchOptions.html +2 -2
  25. package/docs/api/interfaces/CancelOptions.html +2 -2
  26. package/docs/api/interfaces/ClaimDueOptions.html +2 -2
  27. package/docs/api/interfaces/CommandExecutor.html +1 -1
  28. package/docs/api/interfaces/CompleteOptions.html +2 -2
  29. package/docs/api/interfaces/CompleteOutcome.html +1 -1
  30. package/docs/api/interfaces/CreateManyOptions.html +2 -2
  31. package/docs/api/interfaces/CreateOptions.html +2 -2
  32. package/docs/api/interfaces/FailOptions.html +2 -2
  33. package/docs/api/interfaces/FailOutcome.html +1 -1
  34. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +6 -0
  35. package/docs/api/interfaces/FerricStoreClientOptions.html +2 -2
  36. package/docs/api/interfaces/MutateOptions.html +2 -2
  37. package/docs/api/interfaces/NamedValueMutation.html +1 -1
  38. package/docs/api/interfaces/NativeAdapterOptions.html +6 -2
  39. package/docs/api/interfaces/ReadOptions.html +2 -2
  40. package/docs/api/interfaces/ReclaimOptions.html +2 -2
  41. package/docs/api/interfaces/ReconnectOptions.html +2 -0
  42. package/docs/api/interfaces/RetryOptions.html +2 -2
  43. package/docs/api/interfaces/RetryOutcome.html +1 -1
  44. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  45. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  46. package/docs/api/interfaces/TransitionOptions.html +2 -2
  47. package/docs/api/interfaces/TransitionOutcome.html +1 -1
  48. package/docs/api/modules.html +1 -1
  49. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -55,6 +55,7 @@ __export(index_exports, {
55
55
  QueueClient: () => QueueClient,
56
56
  QueueWorker: () => QueueWorker,
57
57
  RawCodec: () => RawCodec,
58
+ ReconnectingExecutor: () => ReconnectingExecutor,
58
59
  SetStore: () => SetStore,
59
60
  SortedSetStore: () => SortedSetStore,
60
61
  StaleLeaseError: () => StaleLeaseError,
@@ -69,6 +70,7 @@ __export(index_exports, {
69
70
  classifyServerError: () => classifyServerError,
70
71
  complete: () => complete,
71
72
  fail: () => fail,
73
+ isReconnectableClosedConnectionError: () => isReconnectableClosedConnectionError,
72
74
  mapException: () => mapException,
73
75
  retry: () => retry,
74
76
  transition: () => transition
@@ -1470,15 +1472,21 @@ var NativeAdapter = class _NativeAdapter {
1470
1472
  maxChunkBytes;
1471
1473
  requestId = 0n;
1472
1474
  protocolLanes;
1475
+ heartbeatIntervalMs;
1476
+ heartbeatInFlight = false;
1477
+ heartbeatTimer;
1478
+ lastActivityMs = Date.now();
1473
1479
  timeoutMs;
1474
- constructor(socket, timeoutMs, protocolLanes, maxChunkBytes) {
1480
+ constructor(socket, timeoutMs, protocolLanes, maxChunkBytes, heartbeatIntervalMs) {
1475
1481
  this.socket = socket;
1476
1482
  this.maxChunkBytes = Math.max(1, Math.trunc(maxChunkBytes));
1477
1483
  this.timeoutMs = timeoutMs;
1478
1484
  this.protocolLanes = Math.max(1, Math.trunc(protocolLanes));
1485
+ this.heartbeatIntervalMs = normalizeHeartbeatInterval(heartbeatIntervalMs);
1479
1486
  this.socket.on("data", (chunk) => this.onData(chunk));
1480
1487
  this.socket.on("error", (error) => this.failAll(error));
1481
1488
  this.socket.on("close", () => this.failAll(new FerricStoreError("FerricStore connection closed")));
1489
+ this.startHeartbeat();
1482
1490
  }
1483
1491
  static async fromUrl(url, options = {}) {
1484
1492
  const parsed = parseFerricUrl(url);
@@ -1487,7 +1495,8 @@ var NativeAdapter = class _NativeAdapter {
1487
1495
  socket,
1488
1496
  options.timeoutMs ?? 3e4,
1489
1497
  options.protocolLanes ?? 8,
1490
- options.maxChunkBytes ?? 64 * 1024 * 1024
1498
+ options.maxChunkBytes ?? 64 * 1024 * 1024,
1499
+ options.heartbeatIntervalMs
1491
1500
  );
1492
1501
  await adapter.startup(options.clientName);
1493
1502
  const password = options.password ?? parsed.password;
@@ -1511,6 +1520,7 @@ var NativeAdapter = class _NativeAdapter {
1511
1520
  return;
1512
1521
  }
1513
1522
  this.closed = true;
1523
+ this.stopHeartbeat();
1514
1524
  this.socket.end();
1515
1525
  }
1516
1526
  async startup(clientName) {
@@ -1532,6 +1542,7 @@ var NativeAdapter = class _NativeAdapter {
1532
1542
  if (this.closed) {
1533
1543
  throw new FerricStoreError("FerricStore connection is closed");
1534
1544
  }
1545
+ this.lastActivityMs = Date.now();
1535
1546
  const requestId = this.nextRequestId();
1536
1547
  const frame = encodeRequest(this.assignLane(command), requestId);
1537
1548
  return await new Promise((resolve, reject) => {
@@ -1545,10 +1556,12 @@ var NativeAdapter = class _NativeAdapter {
1545
1556
  opcode: command.opcode,
1546
1557
  reject: (reason) => {
1547
1558
  clearTimeout(timer);
1559
+ this.lastActivityMs = Date.now();
1548
1560
  reject(reason instanceof Error ? reason : classifyServerError(String(reason), reason));
1549
1561
  },
1550
1562
  resolve: (value) => {
1551
1563
  clearTimeout(timer);
1564
+ this.lastActivityMs = Date.now();
1552
1565
  resolve(value);
1553
1566
  }
1554
1567
  });
@@ -1557,11 +1570,43 @@ var NativeAdapter = class _NativeAdapter {
1557
1570
  this.pending.delete(requestId);
1558
1571
  this.cleanupChunksForRequest(requestId);
1559
1572
  clearTimeout(timer);
1573
+ this.lastActivityMs = Date.now();
1560
1574
  reject(error);
1561
1575
  }
1562
1576
  });
1563
1577
  });
1564
1578
  }
1579
+ startHeartbeat() {
1580
+ if (this.heartbeatIntervalMs == null) {
1581
+ return;
1582
+ }
1583
+ this.heartbeatTimer = setInterval(() => {
1584
+ void this.sendHeartbeat();
1585
+ }, this.heartbeatIntervalMs);
1586
+ this.heartbeatTimer.unref?.();
1587
+ }
1588
+ stopHeartbeat() {
1589
+ if (this.heartbeatTimer == null) {
1590
+ return;
1591
+ }
1592
+ clearInterval(this.heartbeatTimer);
1593
+ this.heartbeatTimer = void 0;
1594
+ }
1595
+ async sendHeartbeat() {
1596
+ if (this.closed || this.heartbeatInFlight || this.pending.size > 0 || this.heartbeatIntervalMs == null) {
1597
+ return;
1598
+ }
1599
+ if (Date.now() - this.lastActivityMs < this.heartbeatIntervalMs) {
1600
+ return;
1601
+ }
1602
+ this.heartbeatInFlight = true;
1603
+ try {
1604
+ await this.request(buildProtocolCommand(["PING"]));
1605
+ } catch {
1606
+ } finally {
1607
+ this.heartbeatInFlight = false;
1608
+ }
1609
+ }
1565
1610
  onData(chunk) {
1566
1611
  try {
1567
1612
  this.buffer = this.buffer.byteLength === 0 ? chunk : import_node_buffer2.Buffer.concat([this.buffer, chunk]);
@@ -1620,6 +1665,7 @@ var NativeAdapter = class _NativeAdapter {
1620
1665
  return;
1621
1666
  }
1622
1667
  this.closed = true;
1668
+ this.stopHeartbeat();
1623
1669
  const error = reason instanceof Error ? reason : classifyServerError(String(reason), reason);
1624
1670
  for (const pending of this.pending.values()) {
1625
1671
  pending.reject(error);
@@ -1652,6 +1698,74 @@ var NativeAdapter = class _NativeAdapter {
1652
1698
  return { ...command, laneId: this.dataLane };
1653
1699
  }
1654
1700
  };
1701
+ var ReconnectingExecutor = class {
1702
+ constructor(createExecutor, options = {}) {
1703
+ this.createExecutor = createExecutor;
1704
+ this.maxRetries = Math.max(0, Math.trunc(options.maxRetries ?? 1));
1705
+ this.executorPromise = createExecutor();
1706
+ }
1707
+ createExecutor;
1708
+ closed = false;
1709
+ maxRetries;
1710
+ executorPromise;
1711
+ reconnectPromise;
1712
+ async executeCommand(...args) {
1713
+ return await this.withReconnect((executor) => executor.executeCommand(...args));
1714
+ }
1715
+ async executePipeline(commands, options = {}) {
1716
+ return await this.withReconnect(async (executor) => {
1717
+ if (executor.executePipeline != null) {
1718
+ return await executor.executePipeline(commands, options);
1719
+ }
1720
+ return await Promise.all(commands.map((command) => executor.executeCommand(...command)));
1721
+ });
1722
+ }
1723
+ async close() {
1724
+ if (this.closed) {
1725
+ return;
1726
+ }
1727
+ this.closed = true;
1728
+ const executor = await this.executorPromise.catch(() => void 0);
1729
+ await Promise.resolve(executor?.close?.());
1730
+ }
1731
+ async withReconnect(operation) {
1732
+ let executor = await this.executorPromise;
1733
+ for (let attempt = 0; ; attempt++) {
1734
+ try {
1735
+ return await operation(executor);
1736
+ } catch (error) {
1737
+ if (this.closed || attempt >= this.maxRetries || !isReconnectableClosedConnectionError(error)) {
1738
+ throw error;
1739
+ }
1740
+ executor = await this.reconnect(executor);
1741
+ }
1742
+ }
1743
+ }
1744
+ async reconnect(staleExecutor) {
1745
+ if (this.closed) {
1746
+ throw new FerricStoreError("FerricStore client is closed");
1747
+ }
1748
+ if (this.reconnectPromise != null) {
1749
+ return await this.reconnectPromise;
1750
+ }
1751
+ const currentExecutor = await this.executorPromise.catch(() => void 0);
1752
+ if (currentExecutor != null && currentExecutor !== staleExecutor) {
1753
+ return currentExecutor;
1754
+ }
1755
+ this.reconnectPromise = (async () => {
1756
+ await Promise.resolve(staleExecutor.close?.()).catch(() => void 0);
1757
+ const nextExecutor = await this.createExecutor();
1758
+ this.executorPromise = Promise.resolve(nextExecutor);
1759
+ return nextExecutor;
1760
+ })().finally(() => {
1761
+ this.reconnectPromise = void 0;
1762
+ });
1763
+ return await this.reconnectPromise;
1764
+ }
1765
+ };
1766
+ function isReconnectableClosedConnectionError(error) {
1767
+ return error instanceof Error && error.message === "FerricStore connection is closed";
1768
+ }
1655
1769
  function parseFerricUrl(value) {
1656
1770
  const url = new URL(value);
1657
1771
  if (url.protocol !== "ferric:" && url.protocol !== "ferrics:") {
@@ -1675,6 +1789,7 @@ async function connect(parsed, options) {
1675
1789
  }, timeoutMs);
1676
1790
  timer.unref?.();
1677
1791
  socket.setNoDelay(true);
1792
+ socket.setKeepAlive(options.keepAlive ?? true, normalizeKeepAliveInitialDelay(options.keepAliveInitialDelayMs));
1678
1793
  socket.once("connect", () => {
1679
1794
  clearTimeout(timer);
1680
1795
  resolve(socket);
@@ -1685,6 +1800,15 @@ async function connect(parsed, options) {
1685
1800
  });
1686
1801
  });
1687
1802
  }
1803
+ function normalizeHeartbeatInterval(value) {
1804
+ if (value != null && value <= 0) {
1805
+ return void 0;
1806
+ }
1807
+ return Math.trunc(value ?? 6e4);
1808
+ }
1809
+ function normalizeKeepAliveInitialDelay(value) {
1810
+ return Math.max(0, Math.trunc(value ?? 3e4));
1811
+ }
1688
1812
 
1689
1813
  // src/codecs.ts
1690
1814
  var RawCodec = class {
@@ -3075,7 +3199,10 @@ var FerricStoreClient = class _FerricStoreClient {
3075
3199
  this.zset = new SortedSetStore(this);
3076
3200
  }
3077
3201
  static async fromUrl(url, options = {}) {
3078
- return new _FerricStoreClient(await NativeAdapter.fromUrl(url, options.nativeOptions), options);
3202
+ const reconnectOptions = options.reconnect ?? options.nativeOptions?.autoReconnect ?? true;
3203
+ const createExecutor = async () => await NativeAdapter.fromUrl(url, options.nativeOptions);
3204
+ const executor = reconnectOptions === false ? await createExecutor() : new ReconnectingExecutor(createExecutor, reconnectOptions === true ? {} : reconnectOptions);
3205
+ return new _FerricStoreClient(executor, options);
3079
3206
  }
3080
3207
  async command(...args) {
3081
3208
  return await this.executor.executeCommand(...args);
@@ -5198,6 +5325,7 @@ function valueRefToString(value) {
5198
5325
  QueueClient,
5199
5326
  QueueWorker,
5200
5327
  RawCodec,
5328
+ ReconnectingExecutor,
5201
5329
  SetStore,
5202
5330
  SortedSetStore,
5203
5331
  StaleLeaseError,
@@ -5212,6 +5340,7 @@ function valueRefToString(value) {
5212
5340
  classifyServerError,
5213
5341
  complete,
5214
5342
  fail,
5343
+ isReconnectableClosedConnectionError,
5215
5344
  mapException,
5216
5345
  retry,
5217
5346
  transition