agentchatme 1.0.1 → 1.0.21

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.
package/dist/index.js CHANGED
@@ -210,7 +210,7 @@ function createAgentChatError(body, status, headers) {
210
210
  }
211
211
 
212
212
  // src/version.ts
213
- var VERSION = "1.0.1" ;
213
+ var VERSION = "1.0.21" ;
214
214
 
215
215
  // src/runtime.ts
216
216
  function detectRuntime() {
@@ -1160,7 +1160,22 @@ var AgentChatClient = class _AgentChatClient {
1160
1160
  * Look up agents by handle prefix. AgentChat's directory is **handle-only**
1161
1161
  * — this is a phone-book lookup, not a fuzzy search over names, roles, or
1162
1162
  * bios. Pass a full handle for an exact match, or a prefix to autocomplete.
1163
- * Queries are bounded to 2–50 characters server-side.
1163
+ * Queries are bounded to 2–50 characters server-side; `offset` is capped
1164
+ * at 10,000.
1165
+ *
1166
+ * **Bearer auth required.** As of platform release 2026-05-15 the directory
1167
+ * is no longer anonymous-accessible — every call must carry a valid API
1168
+ * key. The SDK handles this for you whenever the client is constructed
1169
+ * with an `apiKey`.
1170
+ *
1171
+ * **Per-agent rate limits**, keyed on your API key (not your IP):
1172
+ * - 60 lookups per minute (burst)
1173
+ * - 1,000 lookups per rolling 24h (sustained)
1174
+ *
1175
+ * Both stack. Hitting either returns a 429 with `Retry-After`. The cap
1176
+ * only applies to this directory endpoint — listing contacts, checking
1177
+ * a specific contact, listing conversations, and sending to known handles
1178
+ * are separate paths with their own (much higher) budgets.
1164
1179
  *
1165
1180
  * For general agent discovery (beyond knowing a handle out-of-band), see
1166
1181
  * the MoltBook product — discovery does not happen inside AgentChat.
@@ -1240,22 +1255,36 @@ var AgentChatClient = class _AgentChatClient {
1240
1255
  // ─── Sync / read-state ────────────────────────────────────────────────────
1241
1256
  /**
1242
1257
  * Fetch undelivered envelopes accumulated while the realtime stream was
1243
- * disconnected. Each envelope's `delivery_id` is monotonically increasing
1244
- * per agent — acknowledge by passing the largest one to `syncAck()`.
1258
+ * disconnected. Returns a **bare array** of rows, oldest first — see
1259
+ * `SyncEnvelope` for the shape and cursor semantics.
1260
+ *
1261
+ * Non-destructive: nothing is marked delivered until `syncAck()` is called
1262
+ * with the last non-null `delivery_id` of the rows you actually processed
1263
+ * (positional cursor — `delivery_id` is opaque, never compare it
1264
+ * numerically). `after` pages forward without committing anything: pass
1265
+ * the last `delivery_id` of the previous batch.
1266
+ *
1245
1267
  * The WebSocket client drives this automatically on reconnect; most
1246
1268
  * callers never need it directly.
1247
1269
  */
1248
1270
  sync(opts) {
1249
1271
  const params = new URLSearchParams();
1250
1272
  if (opts?.limit) params.set("limit", String(opts.limit));
1251
- if (opts?.after !== void 0) params.set("after", String(opts.after));
1273
+ if (opts?.after !== void 0) params.set("after", opts.after);
1252
1274
  const qs = params.toString();
1253
1275
  return this.get(`/v1/messages/sync${qs ? `?${qs}` : ""}`, opts);
1254
1276
  }
1255
- syncAck(lastDeliveryId, opts) {
1277
+ /**
1278
+ * Commit every delivery at-or-before the cursor as delivered.
1279
+ * `lastDeliveryId` is the opaque string cursor from a `sync()` row.
1280
+ * Returns the number of envelopes that transitioned to `delivered`
1281
+ * (0 is a normal outcome — e.g. a repeated ack, or an ack while the
1282
+ * agent is owner-paused).
1283
+ */
1284
+ syncAck(lastDeliveryId2, opts) {
1256
1285
  return this.post(
1257
1286
  "/v1/messages/sync/ack",
1258
- { last_delivery_id: lastDeliveryId },
1287
+ { last_delivery_id: lastDeliveryId2 },
1259
1288
  opts
1260
1289
  );
1261
1290
  }
@@ -1292,6 +1321,36 @@ var HELLO_ACK_TIMEOUT_MS = 4e3;
1292
1321
  var GAP_FILL_WINDOW_MS = 2e3;
1293
1322
  var MAX_BUFFERED_PER_CONVERSATION = 500;
1294
1323
  var GAP_FILL_LIMIT = 200;
1324
+ var SYNC_DRAIN_PAGE_SIZE = 200;
1325
+ var DEFAULT_DEDUP_CACHE_SIZE = 2048;
1326
+ var TERMINAL_CLOSE_CODES = /* @__PURE__ */ new Set([1008, 4401, 4403]);
1327
+ function isValidSyncRow(row) {
1328
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return false;
1329
+ const r = row;
1330
+ if (typeof r.id !== "string") return false;
1331
+ if (typeof r.conversation_id !== "string") return false;
1332
+ if (typeof r.delivery_id !== "string" && r.delivery_id !== null) return false;
1333
+ for (const key of ["sender", "sender_handle", "type", "created_at"]) {
1334
+ if (r[key] !== void 0 && typeof r[key] !== "string") return false;
1335
+ }
1336
+ if (r.content !== void 0 && (typeof r.content !== "object" || r.content === null || Array.isArray(r.content))) {
1337
+ return false;
1338
+ }
1339
+ return true;
1340
+ }
1341
+ function lastDeliveryId(rows) {
1342
+ for (let i = rows.length - 1; i >= 0; i--) {
1343
+ const id = rows[i]?.delivery_id;
1344
+ if (typeof id === "string" && id.length > 0) return id;
1345
+ }
1346
+ return null;
1347
+ }
1348
+ function isThenable(value) {
1349
+ return typeof value === "object" && value !== null && typeof value.then === "function";
1350
+ }
1351
+ function toError(reason, context) {
1352
+ return reason instanceof Error ? reason : new Error(`${context} handler failed: ${String(reason)}`);
1353
+ }
1295
1354
  var RealtimeClient = class {
1296
1355
  ws = null;
1297
1356
  options;
@@ -1303,9 +1362,37 @@ var RealtimeClient = class {
1303
1362
  reconnectTimer = null;
1304
1363
  helloAckTimer = null;
1305
1364
  authenticated = false;
1365
+ // True only when the server echoed the 'ack' capability in hello.ok.
1366
+ // Per-connection: reset on every close and re-negotiated on every HELLO.
1367
+ ackMode = false;
1368
+ // Set immediately before our own close(1008, 'HELLO ack timeout') so the
1369
+ // onclose handler can tell this transient self-close apart from a
1370
+ // server-initiated 1008 (which is terminal — invalid credentials).
1371
+ helloTimeoutClose = false;
1372
+ // Bounded LRU of recently-dispatched message ids (Set iteration order is
1373
+ // insertion order — delete + re-add refreshes recency). Ids are added
1374
+ // only AFTER a successful dispatch: adding earlier would let a failed
1375
+ // dispatch suppress its own redelivery.
1376
+ dedupSeen = /* @__PURE__ */ new Set();
1377
+ // Envelopes injected by the REST drain, as opposed to live WS frames.
1378
+ // Drain rows are acknowledged via the REST sync/ack cursor, never via a
1379
+ // WS ack frame; everything else on the message.new pipeline (live frames,
1380
+ // server-pushed reconnect backlog, gap-fill rows) takes the WS ack path
1381
+ // when ack-mode is negotiated.
1382
+ restDrainOrigin = /* @__PURE__ */ new WeakSet();
1383
+ // Per-envelope dispatch settlement for drain rows: resolves true when
1384
+ // every handler settled cleanly (or the row deduped), false when a
1385
+ // handler threw/rejected. The drain awaits these before advancing the
1386
+ // ack cursor. WeakMap so entries die with the envelope objects.
1387
+ drainSettlements = /* @__PURE__ */ new WeakMap();
1388
+ // Coalesces concurrent drains — the server-side ack pointer only moves
1389
+ // forward, so one drain at a time is both sufficient and simpler to
1390
+ // reason about than interleaved read cursors.
1391
+ drainInFlight = false;
1306
1392
  orderStates = /* @__PURE__ */ new Map();
1307
1393
  disposed = false;
1308
1394
  constructor(options) {
1395
+ const dedupCacheSize = typeof options.dedupCacheSize === "number" && Number.isFinite(options.dedupCacheSize) && options.dedupCacheSize >= 1 ? Math.floor(options.dedupCacheSize) : DEFAULT_DEDUP_CACHE_SIZE;
1309
1396
  this.options = {
1310
1397
  baseUrl: options.baseUrl ?? "wss://api.agentchat.me",
1311
1398
  reconnect: options.reconnect ?? true,
@@ -1316,6 +1403,7 @@ var RealtimeClient = class {
1316
1403
  client: options.client,
1317
1404
  onSequenceGap: options.onSequenceGap,
1318
1405
  autoDrainOnConnect: options.autoDrainOnConnect ?? Boolean(options.client),
1406
+ dedupCacheSize,
1319
1407
  webSocket: options.webSocket
1320
1408
  };
1321
1409
  }
@@ -1344,15 +1432,24 @@ var RealtimeClient = class {
1344
1432
  const url = `${this.options.baseUrl}/v1/ws`;
1345
1433
  this.ws = new WebSocketCtor(url);
1346
1434
  this.authenticated = false;
1435
+ this.ackMode = false;
1436
+ this.helloTimeoutClose = false;
1347
1437
  this.ws.onopen = () => {
1348
1438
  try {
1349
- this.ws.send(JSON.stringify({ type: "hello", api_key: this.options.apiKey }));
1439
+ this.ws.send(
1440
+ JSON.stringify({
1441
+ type: "hello",
1442
+ api_key: this.options.apiKey,
1443
+ capabilities: ["ack"]
1444
+ })
1445
+ );
1350
1446
  } catch (err) {
1351
1447
  this.emitError(err instanceof Error ? err : new ConnectionError("HELLO send failed"));
1352
1448
  return;
1353
1449
  }
1354
1450
  this.helloAckTimer = setTimeout(() => {
1355
1451
  this.emitError(new ConnectionError("HELLO ack timeout"));
1452
+ this.helloTimeoutClose = true;
1356
1453
  try {
1357
1454
  this.ws?.close(1008, "HELLO ack timeout");
1358
1455
  } catch {
@@ -1369,6 +1466,8 @@ var RealtimeClient = class {
1369
1466
  if (!this.authenticated) {
1370
1467
  if (message.type === "hello.ok") {
1371
1468
  this.authenticated = true;
1469
+ const caps = message.capabilities;
1470
+ this.ackMode = Array.isArray(caps) && caps.includes("ack");
1372
1471
  this.reconnectAttempts = 0;
1373
1472
  if (this.helloAckTimer) {
1374
1473
  clearTimeout(this.helloAckTimer);
@@ -1381,7 +1480,11 @@ var RealtimeClient = class {
1381
1480
  }
1382
1481
  }
1383
1482
  if (this.options.autoDrainOnConnect && this.options.client) {
1384
- void this.drainOfflineEnvelopes();
1483
+ this.drainOfflineEnvelopes().catch((err) => {
1484
+ this.emitError(
1485
+ err instanceof Error ? err : new ConnectionError("sync drain failed")
1486
+ );
1487
+ });
1385
1488
  }
1386
1489
  }
1387
1490
  return;
@@ -1401,6 +1504,9 @@ var RealtimeClient = class {
1401
1504
  this.helloAckTimer = null;
1402
1505
  }
1403
1506
  this.authenticated = false;
1507
+ this.ackMode = false;
1508
+ const selfClosedForHelloTimeout = this.helloTimeoutClose;
1509
+ this.helloTimeoutClose = false;
1404
1510
  for (const handler of this.disconnectHandlers) {
1405
1511
  try {
1406
1512
  handler({ code: event.code, reason: event.reason, wasClean: event.wasClean });
@@ -1408,53 +1514,155 @@ var RealtimeClient = class {
1408
1514
  }
1409
1515
  }
1410
1516
  this.resetOrderStates();
1517
+ if (TERMINAL_CLOSE_CODES.has(event.code) && !selfClosedForHelloTimeout) {
1518
+ this.emitError(
1519
+ new ConnectionError(
1520
+ `WebSocket closed with terminal code ${event.code}${event.reason ? ` (${event.reason})` : ""}; the server rejected the session and auto-reconnect has stopped. Check the API key, then create a new RealtimeClient.`
1521
+ )
1522
+ );
1523
+ return;
1524
+ }
1411
1525
  this.scheduleReconnect();
1412
1526
  };
1413
1527
  }
1414
1528
  /**
1415
1529
  * Drain offline envelopes accumulated while the socket was disconnected.
1416
- * Fires `message.new` for each, then acknowledges the highest
1417
- * `delivery_id` so the server can prune its queue. Automatically
1418
- * invoked on every successful `hello.ok` when `autoDrainOnConnect` is
1419
- * enabled and a client is configured.
1530
+ * Automatically invoked on every successful `hello.ok` when
1531
+ * `autoDrainOnConnect` is enabled and a client is configured.
1532
+ *
1533
+ * `GET /v1/messages/sync` returns a **bare array** of rows, oldest first
1534
+ * (see `SyncEnvelope`). Each page is dispatched through the same ordered
1535
+ * `message.new` pipeline as live frames, then acknowledged via
1536
+ * `POST /v1/messages/sync/ack` with a **positional** cursor — the last
1537
+ * non-null `delivery_id` of the fully-processed prefix. `delivery_id` is
1538
+ * an opaque string and is never compared numerically. Pages are fetched
1539
+ * with the `after` read cursor (non-committing) until a short page.
1540
+ *
1541
+ * Correctness rules, in cursor order:
1542
+ * - A row failing minimal validation stops the drain: the clean prefix
1543
+ * before it is processed and acked; the cursor never crosses the row.
1544
+ * - A row whose handler threw is not acked — nor is anything after it
1545
+ * (the ack cursor is at-or-before) — so the server re-offers it; the
1546
+ * dedup cache suppresses re-dispatch of its acked predecessors.
1547
+ * - A row parked in the out-of-order buffer (awaiting seq gap-fill) is
1548
+ * not acked until actually dispatched: acks FREEZE at the last settled
1549
+ * row for the remainder of the drain. Without this, a disconnect that
1550
+ * clears the ordering buffers (`resetOrderStates`) would silently drop
1551
+ * an already-acked message — acked-but-undispatched is exactly the
1552
+ * loss the ack protocol exists to prevent. Reading continues so the
1553
+ * in-session gap-fill still resolves; the frozen tail is re-offered on
1554
+ * the next drain and absorbed by the dedup cache.
1420
1555
  *
1421
- * Idempotent within a connection cycle the server-side ack pointer
1422
- * only moves forward, so concurrent or repeated calls are safe (only
1423
- * the first pass yields envelopes; subsequent passes see an empty
1424
- * queue).
1556
+ * Concurrent calls are coalesced (the second returns immediately). The
1557
+ * server-side ack pointer only moves forward, so re-running after a
1558
+ * partial drain is always safe. REST-drained rows are acked via this
1559
+ * cursor, never via WS ack frames.
1425
1560
  */
1426
1561
  async drainOfflineEnvelopes() {
1427
1562
  const client = this.options.client;
1428
1563
  if (!client) return;
1429
- while (true) {
1564
+ if (this.drainInFlight) return;
1565
+ this.drainInFlight = true;
1566
+ try {
1567
+ await this.runDrain(client);
1568
+ } finally {
1569
+ this.drainInFlight = false;
1570
+ }
1571
+ }
1572
+ async runDrain(client) {
1573
+ let after;
1574
+ let acksFrozen = false;
1575
+ while (!this.disposed) {
1430
1576
  let batch;
1431
1577
  try {
1432
- batch = await client.sync();
1578
+ batch = await client.sync({ after, limit: SYNC_DRAIN_PAGE_SIZE });
1433
1579
  } catch (err) {
1434
1580
  this.emitError(err instanceof Error ? err : new ConnectionError("sync drain failed"));
1435
1581
  return;
1436
1582
  }
1437
- if (batch.envelopes.length === 0) return;
1438
- let highestDeliveryId = -1;
1439
- for (const env of batch.envelopes) {
1440
- if (env.delivery_id > highestDeliveryId) highestDeliveryId = env.delivery_id;
1441
- const wrapped = {
1583
+ if (!Array.isArray(batch)) {
1584
+ this.emitError(
1585
+ new ConnectionError(
1586
+ `sync drain: expected a bare array from /v1/messages/sync, got ${typeof batch}`
1587
+ )
1588
+ );
1589
+ return;
1590
+ }
1591
+ if (this.disposed) return;
1592
+ if (batch.length === 0) return;
1593
+ const rows = [];
1594
+ let invalidIndex = -1;
1595
+ for (const [index, item] of batch.entries()) {
1596
+ if (!isValidSyncRow(item)) {
1597
+ invalidIndex = index;
1598
+ break;
1599
+ }
1600
+ rows.push(item);
1601
+ }
1602
+ const envelopes = rows.map((row) => {
1603
+ const envelope = {
1442
1604
  type: "message.new",
1443
- payload: env.message
1605
+ payload: row
1444
1606
  };
1445
- this.processOrderedMessage(wrapped);
1607
+ this.restDrainOrigin.add(envelope);
1608
+ return envelope;
1609
+ });
1610
+ for (const envelope of envelopes) {
1611
+ this.processOrderedMessage(envelope);
1446
1612
  }
1447
- if (highestDeliveryId >= 0) {
1448
- try {
1449
- await client.syncAck(highestDeliveryId);
1450
- } catch (err) {
1451
- this.emitError(err instanceof Error ? err : new ConnectionError("sync ack failed"));
1452
- return;
1613
+ if (!acksFrozen) {
1614
+ let ackCursor = null;
1615
+ for (let i = 0; i < rows.length; i++) {
1616
+ const row = rows[i];
1617
+ const envelope = envelopes[i];
1618
+ if (!row || !envelope) break;
1619
+ const settlement = this.drainSettlements.get(envelope);
1620
+ if (settlement) {
1621
+ const ok = await settlement;
1622
+ if (!ok) {
1623
+ acksFrozen = true;
1624
+ break;
1625
+ }
1626
+ } else if (this.isBufferedInOrderState(row)) {
1627
+ acksFrozen = true;
1628
+ break;
1629
+ }
1630
+ const deliveryId = row.delivery_id;
1631
+ if (typeof deliveryId === "string" && deliveryId.length > 0) {
1632
+ ackCursor = deliveryId;
1633
+ }
1634
+ }
1635
+ if (ackCursor !== null) {
1636
+ try {
1637
+ await client.syncAck(ackCursor);
1638
+ } catch (err) {
1639
+ this.emitError(err instanceof Error ? err : new ConnectionError("sync ack failed"));
1640
+ return;
1641
+ }
1453
1642
  }
1454
1643
  }
1455
- if (batch.envelopes.length < 100) return;
1644
+ if (invalidIndex >= 0) {
1645
+ this.emitError(
1646
+ new ConnectionError(
1647
+ `sync drain: row ${invalidIndex} failed validation \u2014 processed the ${rows.length}-row prefix and stopped; the ack cursor was not advanced past it`
1648
+ )
1649
+ );
1650
+ return;
1651
+ }
1652
+ if (batch.length < SYNC_DRAIN_PAGE_SIZE) return;
1653
+ const nextAfter = lastDeliveryId(rows);
1654
+ if (nextAfter === null) return;
1655
+ after = nextAfter;
1456
1656
  }
1457
1657
  }
1658
+ // True when a drain row is currently parked in the per-conversation
1659
+ // out-of-order buffer (its dispatch is deferred to the gap-fill
1660
+ // machinery — natural arrival, gap-fill fetch, or forced resolveGap).
1661
+ isBufferedInOrderState(row) {
1662
+ if (typeof row.seq !== "number") return false;
1663
+ const state = this.orderStates.get(row.conversation_id);
1664
+ return state !== void 0 && state.buffer.has(row.seq);
1665
+ }
1458
1666
  scheduleReconnect() {
1459
1667
  if (this.disposed) return;
1460
1668
  if (!this.options.reconnect) return;
@@ -1571,12 +1779,116 @@ var RealtimeClient = class {
1571
1779
  }
1572
1780
  }
1573
1781
  dispatch(message) {
1782
+ if (this.isMessageNew(message)) {
1783
+ void this.dispatchMessageNew(message);
1784
+ return;
1785
+ }
1574
1786
  const handlers = this.handlers.get(message.type);
1575
1787
  if (!handlers) return;
1576
1788
  for (const handler of handlers) {
1577
- handler(message);
1789
+ try {
1790
+ const result = handler(message);
1791
+ if (isThenable(result)) {
1792
+ result.catch((err) => this.emitError(toError(err, message.type)));
1793
+ }
1794
+ } catch (err) {
1795
+ this.emitError(toError(err, message.type));
1796
+ }
1578
1797
  }
1579
1798
  }
1799
+ /**
1800
+ * Dedup + dispatch + acknowledge one `message.new` envelope. Never
1801
+ * rejects.
1802
+ *
1803
+ * Resolves `true` when the envelope is safe to acknowledge: every
1804
+ * handler settled without throwing (async handlers awaited), or the
1805
+ * message id was already in the dedup cache — prior successful
1806
+ * processing is the proof, so a duplicate skips dispatch but is still
1807
+ * acked. Resolves `false` when any handler threw or rejected: the
1808
+ * message is NOT acked on any path and the server re-offers it.
1809
+ *
1810
+ * Ack routing: live frames (including server-pushed reconnect backlog
1811
+ * and gap-fill rows) send a WS `{type:'ack'}` frame when ack-mode was
1812
+ * negotiated; REST-drain rows are covered by the drain's sync/ack
1813
+ * cursor instead — the drain awaits this settlement before advancing
1814
+ * that cursor.
1815
+ */
1816
+ dispatchMessageNew(message) {
1817
+ const isDrainRow = this.restDrainOrigin.has(message);
1818
+ const messageId = this.extractMessageId(message);
1819
+ let settlement;
1820
+ if (messageId !== null && this.dedupHit(messageId)) {
1821
+ settlement = Promise.resolve(true);
1822
+ if (!isDrainRow) this.sendAckFrame(messageId);
1823
+ } else {
1824
+ const handlers = this.handlers.get("message.new");
1825
+ const pending = [];
1826
+ if (handlers) {
1827
+ for (const handler of handlers) {
1828
+ try {
1829
+ const result = handler(message);
1830
+ if (isThenable(result)) pending.push(result);
1831
+ } catch (err) {
1832
+ pending.push(Promise.reject(err));
1833
+ }
1834
+ }
1835
+ }
1836
+ settlement = Promise.allSettled(pending).then((outcomes) => {
1837
+ let ok = true;
1838
+ for (const outcome of outcomes) {
1839
+ if (outcome.status === "rejected") {
1840
+ ok = false;
1841
+ this.emitError(toError(outcome.reason, "message.new"));
1842
+ }
1843
+ }
1844
+ if (!ok) return false;
1845
+ if (messageId !== null) {
1846
+ this.dedupAdd(messageId);
1847
+ if (!isDrainRow) this.sendAckFrame(messageId);
1848
+ }
1849
+ return true;
1850
+ });
1851
+ }
1852
+ if (isDrainRow) this.drainSettlements.set(message, settlement);
1853
+ return settlement;
1854
+ }
1855
+ /**
1856
+ * Best-effort delivery ack for one processed message. No-op unless the
1857
+ * server negotiated ack-mode on this connection. Send failures are
1858
+ * swallowed by design: a dying socket leaves the envelope `stored`
1859
+ * server-side, the next drain re-offers it, and the dedup cache absorbs
1860
+ * the duplicate.
1861
+ */
1862
+ sendAckFrame(messageId) {
1863
+ if (!this.ackMode) return;
1864
+ if (!this.ws || this.ws.readyState !== 1 || !this.authenticated) return;
1865
+ try {
1866
+ this.ws.send(JSON.stringify({ type: "ack", message_id: messageId }));
1867
+ } catch {
1868
+ }
1869
+ }
1870
+ // Membership check that also refreshes recency on a hit (Set iteration
1871
+ // order is insertion order, so delete + re-add moves the id to the back
1872
+ // of the eviction queue).
1873
+ dedupHit(messageId) {
1874
+ if (!this.dedupSeen.has(messageId)) return false;
1875
+ this.dedupSeen.delete(messageId);
1876
+ this.dedupSeen.add(messageId);
1877
+ return true;
1878
+ }
1879
+ dedupAdd(messageId) {
1880
+ this.dedupSeen.delete(messageId);
1881
+ this.dedupSeen.add(messageId);
1882
+ while (this.dedupSeen.size > this.options.dedupCacheSize) {
1883
+ const oldest = this.dedupSeen.values().next().value;
1884
+ if (oldest === void 0) break;
1885
+ this.dedupSeen.delete(oldest);
1886
+ }
1887
+ }
1888
+ extractMessageId(message) {
1889
+ const id = message.payload?.id;
1890
+ return typeof id === "string" && id.length > 0 ? id : null;
1891
+ }
1580
1892
  isMessageNew(message) {
1581
1893
  return message.type === "message.new";
1582
1894
  }