@antzsoft/chat-core 1.4.4 → 1.4.6

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.cjs CHANGED
@@ -111,6 +111,7 @@ __export(src_exports, {
111
111
  HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
112
112
  MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
113
113
  MENTION_ALL_ID: () => MENTION_ALL_ID,
114
+ TransitRateLimitedError: () => TransitRateLimitedError,
114
115
  appConfigApi: () => appConfigApi,
115
116
  authApi: () => authApi,
116
117
  buildMentionText: () => buildMentionText,
@@ -134,6 +135,7 @@ __export(src_exports, {
134
135
  getSocketStatus: () => getSocketStatus,
135
136
  initApiClient: () => initApiClient,
136
137
  initAuthStore: () => initAuthStore,
138
+ isApiClientConfigured: () => isApiClientConfigured,
137
139
  isMentionAll: () => isMentionAll,
138
140
  isTransitEnvelope: () => isTransitEnvelope,
139
141
  messagesApi: () => messagesApi,
@@ -142,13 +144,16 @@ __export(src_exports, {
142
144
  onSocketStatus: () => onSocketStatus,
143
145
  parseMentions: () => parseMentions,
144
146
  performHandshake: () => performHandshake,
147
+ readRetryAfterMs: () => readRetryAfterMs,
145
148
  reconnectSocket: () => reconnectSocket,
146
149
  refreshSocketAuth: () => refreshSocketAuth,
147
150
  renderMentionParts: () => renderMentionParts,
148
151
  resetAuthStore: () => resetAuthStore,
152
+ resetTrackedRooms: () => resetTrackedRooms,
149
153
  resolveConfig: () => resolveConfig,
150
154
  resolveSystemMessageText: () => resolveSystemMessageText,
151
155
  setApiClientInstance: () => setApiClientInstance,
156
+ setAuthReadyPromise: () => setAuthReadyPromise,
152
157
  setTransitSession: () => setTransitSession,
153
158
  socketEmit: () => socketEmit,
154
159
  storageApi: () => storageApi,
@@ -214,6 +219,7 @@ function resolveConfig(config) {
214
219
  compressDocuments: config.compression?.compressDocuments ?? true
215
220
  },
216
221
  persistStorage: config.persistStorage,
222
+ onSendError: config.onSendError,
217
223
  messagePageSize: config.messagePageSize ?? 40,
218
224
  starredMessagePageSize: config.starredMessagePageSize ?? 30,
219
225
  searchPageSize: config.searchPageSize ?? 50
@@ -386,7 +392,8 @@ function getState() {
386
392
  sessionEverEstablished: false,
387
393
  readyResolve: null,
388
394
  readyPromise: null,
389
- transitConfigured: null
395
+ transitConfigured: null,
396
+ readyListeners: /* @__PURE__ */ new Set()
390
397
  };
391
398
  }
392
399
  return g[_KEY];
@@ -399,11 +406,13 @@ function configureTransit(enabled) {
399
406
  s.readyResolve = null;
400
407
  }
401
408
  }
409
+ function configureTransitIfUnset(enabled) {
410
+ if (getState().transitConfigured === null) configureTransit(enabled);
411
+ }
402
412
  function waitForTransitReady() {
403
413
  const s = getState();
404
414
  if (!s.transitConfigured) return Promise.resolve();
405
415
  if (s.session) return Promise.resolve();
406
- if (s.sessionEverEstablished) return Promise.resolve();
407
416
  if (!s.readyPromise) {
408
417
  s.readyPromise = new Promise((resolve) => {
409
418
  s.readyResolve = resolve;
@@ -417,6 +426,29 @@ function setTransitSession(session) {
417
426
  s.sessionEverEstablished = true;
418
427
  s.readyResolve?.();
419
428
  s.readyResolve = null;
429
+ s.readyListeners.forEach((fn) => {
430
+ try {
431
+ fn();
432
+ } catch {
433
+ }
434
+ });
435
+ }
436
+ function onTransitReady(listener) {
437
+ const s = getState();
438
+ s.readyListeners.add(listener);
439
+ return () => {
440
+ s.readyListeners.delete(listener);
441
+ };
442
+ }
443
+ async function awaitTransitReadyOr(timeoutMs) {
444
+ const s = getState();
445
+ if (!s.transitConfigured || s.session) return true;
446
+ await Promise.race([
447
+ waitForTransitReady(),
448
+ new Promise((r) => setTimeout(r, timeoutMs))
449
+ ]);
450
+ const now = getState();
451
+ return Boolean(now.session) || now.transitConfigured !== true;
420
452
  }
421
453
  function getTransitSession() {
422
454
  return getState().session;
@@ -424,12 +456,16 @@ function getTransitSession() {
424
456
  function clearTransitSession() {
425
457
  const s = getState();
426
458
  s.session = null;
459
+ s.sessionEverEstablished = false;
427
460
  s.readyPromise = null;
428
461
  s.readyResolve = null;
429
462
  }
430
463
  function isTransitEnabled() {
431
464
  return getState().session?.enabled === true;
432
465
  }
466
+ function isTransitRequired() {
467
+ return getState().transitConfigured === true;
468
+ }
433
469
  function getSessionKey() {
434
470
  return getState().session?.sessionKey ?? null;
435
471
  }
@@ -458,11 +494,36 @@ function resetAlgoCache() {
458
494
  }
459
495
 
460
496
  // src/crypto/handshake.ts
497
+ function readRetryAfterMs(headers) {
498
+ const parse = (raw) => {
499
+ if (!raw) return void 0;
500
+ const secs = Number(raw);
501
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
502
+ const when = Date.parse(raw);
503
+ return Number.isNaN(when) ? void 0 : Math.max(0, when - Date.now());
504
+ };
505
+ const found = ["Retry-After-identity", "Retry-After-ip", "Retry-After"].map((name) => parse(headers.get(name))).filter((ms) => ms != null);
506
+ return found.length > 0 ? Math.max(...found) : void 0;
507
+ }
508
+ var TransitRateLimitedError = class extends Error {
509
+ constructor(retryAfterMs) {
510
+ super("[AntzChat] transit handshake rate-limited (429)");
511
+ this.name = "TransitRateLimitedError";
512
+ this.retryAfterMs = retryAfterMs;
513
+ }
514
+ };
515
+ function identityHeaders(identity) {
516
+ const headers = {};
517
+ if (identity?.userId) headers["x-user-id"] = identity.userId;
518
+ if (identity?.tenantId) headers["X-Tenant-ID"] = identity.tenantId;
519
+ return headers;
520
+ }
461
521
  function hasWebCrypto2() {
462
522
  return typeof globalThis.crypto?.subtle !== "undefined";
463
523
  }
464
- async function fetchServerKeys(apiUrl) {
465
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
524
+ async function fetchServerKeys(apiUrl, identity) {
525
+ const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });
526
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
466
527
  if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
467
528
  const body = await res.json();
468
529
  return body?.data ?? body;
@@ -473,24 +534,26 @@ async function generateEphemeralKey(algo, serverKeys) {
473
534
  }
474
535
  return generateNobleEphemeralKey(serverKeys);
475
536
  }
476
- async function createRestTransitSession(apiUrl) {
537
+ async function createRestTransitSession(apiUrl, identity) {
477
538
  try {
478
- const serverKeys = await fetchServerKeys(apiUrl);
539
+ const serverKeys = await fetchServerKeys(apiUrl, identity);
479
540
  if (!serverKeys.enabled) return null;
480
541
  const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
481
542
  const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
482
543
  const res = await fetch(`${apiUrl}/crypto/session`, {
483
544
  method: "POST",
484
- headers: { "Content-Type": "application/json" },
545
+ headers: { "Content-Type": "application/json", ...identityHeaders(identity) },
485
546
  body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
486
547
  });
548
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
487
549
  if (!res.ok) return null;
488
550
  const body = await res.json();
489
551
  const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
490
552
  if (!sessionId) return null;
491
553
  const sessionKey = await deriveSessionKey(sessionId);
492
554
  return { sessionId, sessionKey };
493
- } catch {
555
+ } catch (err) {
556
+ if (err instanceof TransitRateLimitedError) throw err;
494
557
  return null;
495
558
  }
496
559
  }
@@ -668,13 +731,72 @@ function normalizeAxiosError(error) {
668
731
  }
669
732
 
670
733
  // src/api/client.ts
734
+ var TRANSIT_GATE_MAX_WAIT_MS = 3e4;
671
735
  var _tokenStore = null;
672
736
  var _config = null;
673
737
  var _avatarSent = false;
674
738
  var _transitHandshakePromise = null;
739
+ var _authReadyPromise = null;
675
740
  function getTransitHandshakePromise() {
676
741
  return _transitHandshakePromise;
677
742
  }
743
+ function setAuthReadyPromise(promise) {
744
+ _authReadyPromise = promise;
745
+ }
746
+ function isApiClientConfigured() {
747
+ return _config !== null;
748
+ }
749
+ function ensureRestTransitHandshake() {
750
+ if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;
751
+ const apiUrl = _config.apiUrl;
752
+ _transitHandshakePromise = (async () => {
753
+ try {
754
+ const MAX_FAILURES = 5;
755
+ const BACKSTOP_MS = 2 * 6e4;
756
+ const deadline = Date.now() + BACKSTOP_MS;
757
+ let failures = 0;
758
+ let rateLimitHits = 0;
759
+ while (failures < MAX_FAILURES && Date.now() < deadline) {
760
+ if (getTransitSession()) return;
761
+ let waitMs;
762
+ try {
763
+ const identity = { userId: _config?.userId, tenantId: _config?.tenantId };
764
+ const keys = await fetchServerKeys(apiUrl, identity);
765
+ if (!keys?.enabled) {
766
+ configureTransit(false);
767
+ return;
768
+ }
769
+ const session = await createRestTransitSession(apiUrl, identity);
770
+ if (session && !getTransitSession()) {
771
+ const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
772
+ setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
773
+ return;
774
+ }
775
+ failures++;
776
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
777
+ } catch (err) {
778
+ if (err instanceof TransitRateLimitedError) {
779
+ const blind = Math.min(15e3 * 2 ** rateLimitHits, 6e4);
780
+ waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), 6e4) : blind;
781
+ rateLimitHits++;
782
+ console.warn(
783
+ `[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
784
+ );
785
+ } else {
786
+ failures++;
787
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
788
+ }
789
+ }
790
+ await new Promise((r) => setTimeout(r, waitMs));
791
+ }
792
+ console.error(
793
+ "[AntzChat] transit handshake could not establish a session \u2014 chat requests stay gated until one succeeds (server requires transit)."
794
+ );
795
+ } finally {
796
+ _transitHandshakePromise = null;
797
+ }
798
+ })();
799
+ }
678
800
  function initApiClient(config, tokenStore) {
679
801
  _config = config;
680
802
  _tokenStore = tokenStore;
@@ -684,24 +806,9 @@ function initApiClient(config, tokenStore) {
684
806
  headers: { "Content-Type": "application/json" }
685
807
  });
686
808
  configureTransit(config.transitEncryption);
687
- if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
688
- _transitHandshakePromise = (async () => {
689
- try {
690
- const session = await createRestTransitSession(config.apiUrl);
691
- if (session && !getTransitSession()) {
692
- const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
693
- setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
694
- } else if (!session) {
695
- configureTransit(false);
696
- }
697
- } catch {
698
- configureTransit(false);
699
- } finally {
700
- _transitHandshakePromise = null;
701
- }
702
- })();
703
- }
809
+ ensureRestTransitHandshake();
704
810
  client.interceptors.request.use(async (req) => {
811
+ if (_authReadyPromise) await _authReadyPromise;
705
812
  const token = _tokenStore?.getAccessToken();
706
813
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
707
814
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
@@ -711,7 +818,17 @@ function initApiClient(config, tokenStore) {
711
818
  else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
712
819
  _avatarSent = true;
713
820
  }
714
- if (token) await waitForTransitReady();
821
+ if (_config?.transitEncryption) {
822
+ if (!getTransitSession()) ensureRestTransitHandshake();
823
+ const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);
824
+ if (!ready) {
825
+ throw new AntzChatNetworkError(
826
+ "Secure channel to chat server not established \u2014 request not sent. It will retry automatically.",
827
+ "TRANSIT_NOT_READY",
828
+ { url: req.url }
829
+ );
830
+ }
831
+ }
715
832
  if (isTransitEnabled()) {
716
833
  const sessionId = getSessionId();
717
834
  const key = getSessionKey();
@@ -1035,7 +1152,6 @@ var _getToken = null;
1035
1152
  var _userId;
1036
1153
  var _tenantId;
1037
1154
  var _config2 = null;
1038
- var _preservingSession = false;
1039
1155
  function setStatus(s) {
1040
1156
  _status = s;
1041
1157
  _statusListeners.forEach((l) => l(s));
@@ -1054,29 +1170,48 @@ function onSocketStatus(listener) {
1054
1170
  _statusListeners.add(listener);
1055
1171
  return () => _statusListeners.delete(listener);
1056
1172
  }
1057
- async function secureEmit(socket, event, payload, ack) {
1058
- if (isTransitEnabled()) {
1059
- const key = getSessionKey();
1060
- if (key) {
1061
- const envelope = await encryptPayload(payload, key);
1062
- if (ack) {
1063
- socket.emit(event, envelope, async (encryptedResponse) => {
1064
- if (isTransitEnvelope(encryptedResponse)) {
1065
- try {
1066
- const decrypted = await decryptPayload(encryptedResponse, key);
1067
- ack(decrypted);
1068
- } catch {
1069
- ack(encryptedResponse);
1070
- }
1071
- } else {
1072
- ack(encryptedResponse);
1073
- }
1074
- });
1173
+ var SECURE_EMIT_WAIT_MS = 4e3;
1174
+ function emitEncrypted(socket, event, envelope, key, ack) {
1175
+ if (ack) {
1176
+ socket.emit(event, envelope, async (encryptedResponse) => {
1177
+ if (isTransitEnvelope(encryptedResponse)) {
1178
+ try {
1179
+ ack(await decryptPayload(encryptedResponse, key));
1180
+ } catch {
1181
+ ack(encryptedResponse);
1182
+ }
1075
1183
  } else {
1076
- socket.emit(event, envelope);
1184
+ ack(encryptedResponse);
1077
1185
  }
1186
+ });
1187
+ } else {
1188
+ socket.emit(event, envelope);
1189
+ }
1190
+ }
1191
+ async function secureEmit(socket, event, payload, ack) {
1192
+ let key = getSessionKey();
1193
+ if (key && isTransitEnabled()) {
1194
+ emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
1195
+ return;
1196
+ }
1197
+ if (isTransitRequired()) {
1198
+ try {
1199
+ ensureRestTransitHandshake();
1200
+ } catch {
1201
+ }
1202
+ const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);
1203
+ key = getSessionKey();
1204
+ if (ok && key && isTransitEnabled()) {
1205
+ emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
1078
1206
  return;
1079
1207
  }
1208
+ if (isTransitRequired()) {
1209
+ throw new AntzChatNetworkError(
1210
+ `Transit encryption required but no session established \u2014 "${event}" not sent`,
1211
+ "TRANSIT_NOT_READY",
1212
+ { event }
1213
+ );
1214
+ }
1080
1215
  }
1081
1216
  if (ack) {
1082
1217
  socket.emit(event, payload, ack);
@@ -1102,6 +1237,28 @@ function secureOn(socket, event, handler) {
1102
1237
  handler(raw);
1103
1238
  });
1104
1239
  }
1240
+ var _joinedRooms = /* @__PURE__ */ new Set();
1241
+ function trackRoomJoin(conversationId) {
1242
+ _joinedRooms.add(conversationId);
1243
+ }
1244
+ function trackRoomLeave(conversationId) {
1245
+ _joinedRooms.delete(conversationId);
1246
+ }
1247
+ function resetTrackedRooms() {
1248
+ _joinedRooms.clear();
1249
+ }
1250
+ function flushJoinedRooms() {
1251
+ const socket = tryGetSocket();
1252
+ if (!socket || _joinedRooms.size === 0) return;
1253
+ for (const conversationId of _joinedRooms) {
1254
+ void secureEmit(socket, "join_room", { conversationId }).catch(() => {
1255
+ });
1256
+ }
1257
+ }
1258
+ onTransitReady(flushJoinedRooms);
1259
+ onSocketStatus((status) => {
1260
+ if (status === "connected") flushJoinedRooms();
1261
+ });
1105
1262
  async function connectSocket(config, getToken) {
1106
1263
  if (_socket && !_socket.disconnected) return _socket;
1107
1264
  if (_connectingPromise) return _connectingPromise;
@@ -1123,6 +1280,7 @@ async function _doConnect(config, getToken) {
1123
1280
  ...config.avatar?.url && { avatarUrl: config.avatar.url },
1124
1281
  ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
1125
1282
  };
1283
+ configureTransitIfUnset(config.transitEncryption);
1126
1284
  let boundDeriveSessionKey = null;
1127
1285
  if (config.transitEncryption) {
1128
1286
  const inFlight = getTransitHandshakePromise();
@@ -1132,7 +1290,7 @@ async function _doConnect(config, getToken) {
1132
1290
  if (existingSession?.enabled && existingSession.sessionId) {
1133
1291
  httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
1134
1292
  } else {
1135
- httpsSession = await createRestTransitSession(config.apiUrl);
1293
+ httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1136
1294
  if (httpsSession) {
1137
1295
  const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1138
1296
  setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
@@ -1143,7 +1301,7 @@ async function _doConnect(config, getToken) {
1143
1301
  boundDeriveSessionKey = null;
1144
1302
  } else {
1145
1303
  try {
1146
- const serverKeys = await fetchServerKeys(config.apiUrl);
1304
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1147
1305
  if (!serverKeys.enabled) {
1148
1306
  throw new AntzChatError(
1149
1307
  "TRANSIT_MISMATCH",
@@ -1160,7 +1318,7 @@ async function _doConnect(config, getToken) {
1160
1318
  }
1161
1319
  } else {
1162
1320
  try {
1163
- const serverKeys = await fetchServerKeys(config.apiUrl);
1321
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1164
1322
  if (serverKeys.enabled) {
1165
1323
  throw new AntzChatError(
1166
1324
  "TRANSIT_MISMATCH",
@@ -1187,7 +1345,6 @@ async function _doConnect(config, getToken) {
1187
1345
  _socket.on("connect", () => setStatus("connected"));
1188
1346
  _socket.on("disconnect", () => {
1189
1347
  setStatus("disconnected");
1190
- if (!_preservingSession) clearTransitSession();
1191
1348
  });
1192
1349
  _socket.on("connect_error", (err) => {
1193
1350
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
@@ -1202,8 +1359,9 @@ async function _doConnect(config, getToken) {
1202
1359
  resolve();
1203
1360
  };
1204
1361
  const timeout = setTimeout(() => {
1205
- console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1206
- configureTransit(false);
1362
+ console.error(
1363
+ "[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED \u2014 chat requests are gated (not downgraded to plaintext) until the handshake succeeds via REST retry or socket reconnect. Persistent failure = check POST /crypto/session (rate limit / 5xx) and the socket transit middleware."
1364
+ );
1207
1365
  done();
1208
1366
  }, 5e3);
1209
1367
  _socket.on("transit_session", async ({ sessionId }) => {
@@ -1290,13 +1448,8 @@ function reconnectSocket(token, userId, tenantId) {
1290
1448
  ...tenantId && { tenantId },
1291
1449
  transitSessionId: existing.sessionId
1292
1450
  };
1293
- _preservingSession = true;
1294
- try {
1295
- if (_socket.connected) _socket.disconnect();
1296
- _socket.connect();
1297
- } finally {
1298
- _preservingSession = false;
1299
- }
1451
+ if (_socket.connected) _socket.disconnect();
1452
+ _socket.connect();
1300
1453
  return;
1301
1454
  }
1302
1455
  if (_getToken) {
@@ -1341,7 +1494,15 @@ async function drainSendQueue(conversationId) {
1341
1494
  entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1342
1495
  continue;
1343
1496
  }
1344
- entry.run().then(entry.resolve).catch(entry.reject);
1497
+ let acked;
1498
+ try {
1499
+ ({ acked } = await emitWithAck("send_message", entry.payload));
1500
+ } catch (err) {
1501
+ entry.reject(err);
1502
+ while (queue.length > 0) queue.shift().reject(err);
1503
+ break;
1504
+ }
1505
+ acked.then(entry.resolve).catch(entry.reject);
1345
1506
  }
1346
1507
  sendQueues.delete(conversationId);
1347
1508
  sendQueueRunning.delete(conversationId);
@@ -1354,7 +1515,7 @@ function queueSendMessage(payload) {
1354
1515
  return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1355
1516
  }
1356
1517
  return new Promise((resolve, reject) => {
1357
- queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1518
+ queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });
1358
1519
  drainSendQueue(conversationId);
1359
1520
  });
1360
1521
  }
@@ -1377,33 +1538,44 @@ function waitForReconnect() {
1377
1538
  });
1378
1539
  });
1379
1540
  }
1380
- async function withAck(event, payload) {
1541
+ async function emitWithAck(event, payload) {
1381
1542
  let socket = tryGetSocket();
1382
1543
  if (!socket) {
1383
1544
  await waitForReconnect();
1384
1545
  socket = tryGetSocket();
1385
1546
  }
1386
- if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1387
- return new Promise((resolve, reject) => {
1388
- let timer;
1389
- secureEmit(socket, event, payload, (response) => {
1390
- clearTimeout(timer);
1391
- resolve(response);
1392
- }).then(() => {
1393
- timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1394
- }).catch(reject);
1547
+ if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event });
1548
+ let resolveAck;
1549
+ let rejectAck;
1550
+ const acked = new Promise((res, rej) => {
1551
+ resolveAck = res;
1552
+ rejectAck = rej;
1395
1553
  });
1554
+ await secureEmit(socket, event, payload, (response) => resolveAck(response));
1555
+ const timer = setTimeout(
1556
+ () => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })),
1557
+ ACK_TIMEOUT
1558
+ );
1559
+ void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));
1560
+ return { acked };
1561
+ }
1562
+ async function withAck(event, payload) {
1563
+ const { acked } = await emitWithAck(event, payload);
1564
+ return acked;
1396
1565
  }
1397
1566
  function fireAndForget(event, payload) {
1398
1567
  const socket = tryGetSocket();
1399
1568
  if (!socket) return;
1400
- secureEmit(socket, event, payload);
1569
+ secureEmit(socket, event, payload).catch(() => {
1570
+ });
1401
1571
  }
1402
1572
  var socketEmit = {
1403
1573
  joinRoom(conversationId) {
1574
+ trackRoomJoin(conversationId);
1404
1575
  fireAndForget("join_room", { conversationId });
1405
1576
  },
1406
1577
  leaveRoom(conversationId) {
1578
+ trackRoomLeave(conversationId);
1407
1579
  fireAndForget("leave_room", { conversationId });
1408
1580
  },
1409
1581
  sendMessage(payload) {
@@ -2160,6 +2332,7 @@ var AntzChatClient = class {
2160
2332
  HIGHLY_FORWARDED_DEPTH_THRESHOLD,
2161
2333
  MAX_FORWARD_TARGETS,
2162
2334
  MENTION_ALL_ID,
2335
+ TransitRateLimitedError,
2163
2336
  appConfigApi,
2164
2337
  authApi,
2165
2338
  buildMentionText,
@@ -2183,6 +2356,7 @@ var AntzChatClient = class {
2183
2356
  getSocketStatus,
2184
2357
  initApiClient,
2185
2358
  initAuthStore,
2359
+ isApiClientConfigured,
2186
2360
  isMentionAll,
2187
2361
  isTransitEnvelope,
2188
2362
  messagesApi,
@@ -2191,13 +2365,16 @@ var AntzChatClient = class {
2191
2365
  onSocketStatus,
2192
2366
  parseMentions,
2193
2367
  performHandshake,
2368
+ readRetryAfterMs,
2194
2369
  reconnectSocket,
2195
2370
  refreshSocketAuth,
2196
2371
  renderMentionParts,
2197
2372
  resetAuthStore,
2373
+ resetTrackedRooms,
2198
2374
  resolveConfig,
2199
2375
  resolveSystemMessageText,
2200
2376
  setApiClientInstance,
2377
+ setAuthReadyPromise,
2201
2378
  setTransitSession,
2202
2379
  socketEmit,
2203
2380
  storageApi,