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