@antzsoft/chat-core 1.4.5 → 1.4.7

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.d.cts CHANGED
@@ -339,6 +339,7 @@ declare function getSocket(): Socket;
339
339
  declare function tryGetSocket(): Socket | null;
340
340
  declare function getSocketStatus(): SocketStatus;
341
341
  declare function onSocketStatus(listener: StatusListener): () => void;
342
+ declare function registerTeardownHook(hook: () => void): () => void;
342
343
  /** Drop all tracked rooms. Call on a genuine identity change (user/tenant
343
344
  * switch) so the reconnect re-flush can't re-join the previous user's rooms.
344
345
  * NOT called from disconnectSocket() — a token refresh is not an intent change. */
@@ -353,6 +354,9 @@ declare function reconnectSocket(token: string, userId?: string, tenantId?: stri
353
354
  */
354
355
  declare function refreshSocketAuth(): boolean;
355
356
 
357
+ /** Clears the typing throttle state. Call on teardown/identity change so a new
358
+ * session cannot inherit the previous one's suppression window. */
359
+ declare function resetTypingThrottle(): void;
356
360
  declare const socketEmit: {
357
361
  joinRoom(conversationId: string): void;
358
362
  leaveRoom(conversationId: string): void;
@@ -395,17 +399,66 @@ declare function encryptPayload(data: unknown, sessionKey: AnySessionKey): Promi
395
399
  declare function decryptPayload(envelope: TransitEnvelope, sessionKey: AnySessionKey): Promise<unknown>;
396
400
  declare function isTransitEnvelope(v: unknown): v is TransitEnvelope;
397
401
 
402
+ /**
403
+ * Caller identity attached to the pre-auth transit handshake requests.
404
+ *
405
+ * GET /crypto/pubkey and POST /crypto/session run BEFORE a transit session (and
406
+ * therefore before the authenticated axios client) exists, so they bypass the
407
+ * request interceptor that normally adds these headers. Without them the server
408
+ * can only rate-limit these two routes by client IP — which means every user
409
+ * behind one NAT, office proxy or ALB shares a single bucket, and one client's
410
+ * reload loop 429s everyone else.
411
+ *
412
+ * These values are NOT used for authentication: the endpoints are unauthenticated
413
+ * by design and the server treats the headers as a fairness hint only, with a
414
+ * per-IP ceiling underneath as the real abuse limit. Sending them is therefore
415
+ * safe, optional, and backward compatible — an older SDK that omits them simply
416
+ * falls back to the shared per-IP bucket.
417
+ */
418
+ interface TransitIdentity {
419
+ /** External user id — same value sent as x-user-id on authenticated requests. */
420
+ userId?: string;
421
+ /** Tenant id — same value sent as X-Tenant-ID on authenticated requests. */
422
+ tenantId?: string;
423
+ }
424
+ /**
425
+ * How long the server asked us to wait, in ms, from a 429's Retry-After.
426
+ *
427
+ * The chat server runs TWO named rate-limit layers, and @nestjs/throttler
428
+ * suffixes its headers with the throttler name unless that name is literally
429
+ * "default". So a 429 carries `Retry-After-identity` or `Retry-After-ip`, not a
430
+ * bare `Retry-After`. Older servers and intermediary proxies may still send the
431
+ * bare name, so all three are read; when more than one is present the LONGER
432
+ * wait wins, since retrying before the slower bucket drains just earns another
433
+ * 429.
434
+ *
435
+ * Returns undefined when no variant is readable — in a browser that also
436
+ * happens when the server omits these from Access-Control-Expose-Headers, in
437
+ * which case callers must fall back to their own backoff.
438
+ */
439
+ declare function readRetryAfterMs(headers: Headers): number | undefined;
440
+ /**
441
+ * Thrown when the transit handshake is rate-limited (HTTP 429).
442
+ *
443
+ * Distinct from a generic failure because the correct response differs: a 429
444
+ * means "wait", not "this is broken", so callers must not burn their retry
445
+ * budget on it and should honour `retryAfterMs` when the server supplied it.
446
+ */
447
+ declare class TransitRateLimitedError extends Error {
448
+ readonly retryAfterMs?: number;
449
+ constructor(retryAfterMs?: number);
450
+ }
398
451
  interface ServerPublicKeys {
399
452
  x25519: string;
400
453
  p256: string;
401
454
  enabled: boolean;
402
455
  }
403
- declare function fetchServerKeys(apiUrl: string): Promise<ServerPublicKeys>;
456
+ declare function fetchServerKeys(apiUrl: string, identity?: TransitIdentity): Promise<ServerPublicKeys>;
404
457
  declare function generateEphemeralKey(algo: TransitAlgo, serverKeys: ServerPublicKeys): Promise<{
405
458
  ephemeralPubB64: string;
406
459
  deriveSessionKey: (sessionId: string) => Promise<unknown>;
407
460
  }>;
408
- declare function createRestTransitSession(apiUrl: string): Promise<{
461
+ declare function createRestTransitSession(apiUrl: string, identity?: TransitIdentity): Promise<{
409
462
  sessionId: string;
410
463
  sessionKey: CryptoKey | Uint8Array;
411
464
  } | null>;
@@ -535,6 +588,10 @@ interface ChatState {
535
588
  } | null) => void;
536
589
  addTypingUser: (conversationId: string, user: TypingUser) => void;
537
590
  removeTypingUser: (conversationId: string, userId: string) => void;
591
+ /** Drops every typing indicator and cancels their expiry timers. For teardown
592
+ * and identity change — a socket drop no longer needs it, since indicators
593
+ * expire on their own. */
594
+ clearTypingUsers: () => void;
538
595
  setUserOnline: (userId: string) => void;
539
596
  setUserOffline: (userId: string) => void;
540
597
  setOnlineUsers: (userIds: string[]) => void;
@@ -799,4 +856,4 @@ declare class AntzChatClient {
799
856
  uploadIcon(conversationId: string, file: UploadableFile): Promise<Conversation>;
800
857
  }
801
858
 
802
- export { AntzChatAuthError, AntzChatClient, AntzChatConfig, AntzChatError, AntzChatNetworkError, AntzChatPermissionError, AntzChatServerError, AntzChatValidationError, AppConfig, AuthResponse, AuthTokens, BatchUploadResult, CompletedPart, Conversation, ConversationListParams, ConversationSyncResponse, ConversationUnreadCount, type CreateDirectData, type CreateGroupData, CrossConversationSyncResponse, CursorPaginatedResponse, FileResponse, FileType, ForwardAckPayload, ForwardMessagePayload, type ForwardResult, HIGHLY_FORWARDED_DEPTH_THRESHOLD, type LastReadEntry, type ListMessagesParams, LoginCredentials, MAX_FORWARD_TARGETS, type MentionPart, type MentionSegment, Message, MessageReactionsResponse, MessageReceiptsResponse, type MobileDeviceToken, PaginatedResponse, type ParsedMention, Participant, PersistStorage, PresignedUrlRequest, PresignedUrlResponse, RegisterData, type RegisterDeviceTokenPayload, ResolvedCompressionConfig, ResolvedConfig, type SearchParams, type SendData, SendMessagePayload, type SocketStatus, type StatusListener, type TokenStore, UnreadSummary, type UpdateConversationData, type UpdateProfilePayload, UploadableFile, User, UserPreferences, type WebPushDeviceToken, appConfigApi, authApi, buildMentionText, connectSocket, conversationsApi, createAuthStore, createRestTransitSession, decryptPayload, devicesApi, disconnectSocket, encryptPayload, extractMentionIds, fetchServerKeys, generateEphemeralKey, getApiClient, getAuthStore, getCompressionStrategy, getSessionId, getSessionKey, getSocket, getSocketStatus, initApiClient, initAuthStore, isApiClientConfigured, isMentionAll, isTransitEnvelope, messagesApi, normalizeAxiosError, normalizeConversation, onSocketStatus, parseMentions, performHandshake, reconnectSocket, refreshSocketAuth, renderMentionParts, resetAuthStore, resetTrackedRooms, resolveSystemMessageText, setApiClientInstance, setAuthReadyPromise, setTransitSession, socketEmit, syncApi, tryGetSocket, useChatStore, usersApi };
859
+ export { AntzChatAuthError, AntzChatClient, AntzChatConfig, AntzChatError, AntzChatNetworkError, AntzChatPermissionError, AntzChatServerError, AntzChatValidationError, AppConfig, AuthResponse, AuthTokens, BatchUploadResult, CompletedPart, Conversation, ConversationListParams, ConversationSyncResponse, ConversationUnreadCount, type CreateDirectData, type CreateGroupData, CrossConversationSyncResponse, CursorPaginatedResponse, FileResponse, FileType, ForwardAckPayload, ForwardMessagePayload, type ForwardResult, HIGHLY_FORWARDED_DEPTH_THRESHOLD, type LastReadEntry, type ListMessagesParams, LoginCredentials, MAX_FORWARD_TARGETS, type MentionPart, type MentionSegment, Message, MessageReactionsResponse, MessageReceiptsResponse, type MobileDeviceToken, PaginatedResponse, type ParsedMention, Participant, PersistStorage, PresignedUrlRequest, PresignedUrlResponse, RegisterData, type RegisterDeviceTokenPayload, ResolvedCompressionConfig, ResolvedConfig, type SearchParams, type SendData, SendMessagePayload, type SocketStatus, type StatusListener, type TokenStore, type TransitIdentity, TransitRateLimitedError, UnreadSummary, type UpdateConversationData, type UpdateProfilePayload, UploadableFile, User, UserPreferences, type WebPushDeviceToken, appConfigApi, authApi, buildMentionText, connectSocket, conversationsApi, createAuthStore, createRestTransitSession, decryptPayload, devicesApi, disconnectSocket, encryptPayload, extractMentionIds, fetchServerKeys, generateEphemeralKey, getApiClient, getAuthStore, getCompressionStrategy, getSessionId, getSessionKey, getSocket, getSocketStatus, initApiClient, initAuthStore, isApiClientConfigured, isMentionAll, isTransitEnvelope, messagesApi, normalizeAxiosError, normalizeConversation, onSocketStatus, parseMentions, performHandshake, readRetryAfterMs, reconnectSocket, refreshSocketAuth, registerTeardownHook, renderMentionParts, resetAuthStore, resetTrackedRooms, resetTypingThrottle, resolveSystemMessageText, setApiClientInstance, setAuthReadyPromise, setTransitSession, socketEmit, syncApi, tryGetSocket, useChatStore, usersApi };
package/dist/index.d.ts CHANGED
@@ -339,6 +339,7 @@ declare function getSocket(): Socket;
339
339
  declare function tryGetSocket(): Socket | null;
340
340
  declare function getSocketStatus(): SocketStatus;
341
341
  declare function onSocketStatus(listener: StatusListener): () => void;
342
+ declare function registerTeardownHook(hook: () => void): () => void;
342
343
  /** Drop all tracked rooms. Call on a genuine identity change (user/tenant
343
344
  * switch) so the reconnect re-flush can't re-join the previous user's rooms.
344
345
  * NOT called from disconnectSocket() — a token refresh is not an intent change. */
@@ -353,6 +354,9 @@ declare function reconnectSocket(token: string, userId?: string, tenantId?: stri
353
354
  */
354
355
  declare function refreshSocketAuth(): boolean;
355
356
 
357
+ /** Clears the typing throttle state. Call on teardown/identity change so a new
358
+ * session cannot inherit the previous one's suppression window. */
359
+ declare function resetTypingThrottle(): void;
356
360
  declare const socketEmit: {
357
361
  joinRoom(conversationId: string): void;
358
362
  leaveRoom(conversationId: string): void;
@@ -395,17 +399,66 @@ declare function encryptPayload(data: unknown, sessionKey: AnySessionKey): Promi
395
399
  declare function decryptPayload(envelope: TransitEnvelope, sessionKey: AnySessionKey): Promise<unknown>;
396
400
  declare function isTransitEnvelope(v: unknown): v is TransitEnvelope;
397
401
 
402
+ /**
403
+ * Caller identity attached to the pre-auth transit handshake requests.
404
+ *
405
+ * GET /crypto/pubkey and POST /crypto/session run BEFORE a transit session (and
406
+ * therefore before the authenticated axios client) exists, so they bypass the
407
+ * request interceptor that normally adds these headers. Without them the server
408
+ * can only rate-limit these two routes by client IP — which means every user
409
+ * behind one NAT, office proxy or ALB shares a single bucket, and one client's
410
+ * reload loop 429s everyone else.
411
+ *
412
+ * These values are NOT used for authentication: the endpoints are unauthenticated
413
+ * by design and the server treats the headers as a fairness hint only, with a
414
+ * per-IP ceiling underneath as the real abuse limit. Sending them is therefore
415
+ * safe, optional, and backward compatible — an older SDK that omits them simply
416
+ * falls back to the shared per-IP bucket.
417
+ */
418
+ interface TransitIdentity {
419
+ /** External user id — same value sent as x-user-id on authenticated requests. */
420
+ userId?: string;
421
+ /** Tenant id — same value sent as X-Tenant-ID on authenticated requests. */
422
+ tenantId?: string;
423
+ }
424
+ /**
425
+ * How long the server asked us to wait, in ms, from a 429's Retry-After.
426
+ *
427
+ * The chat server runs TWO named rate-limit layers, and @nestjs/throttler
428
+ * suffixes its headers with the throttler name unless that name is literally
429
+ * "default". So a 429 carries `Retry-After-identity` or `Retry-After-ip`, not a
430
+ * bare `Retry-After`. Older servers and intermediary proxies may still send the
431
+ * bare name, so all three are read; when more than one is present the LONGER
432
+ * wait wins, since retrying before the slower bucket drains just earns another
433
+ * 429.
434
+ *
435
+ * Returns undefined when no variant is readable — in a browser that also
436
+ * happens when the server omits these from Access-Control-Expose-Headers, in
437
+ * which case callers must fall back to their own backoff.
438
+ */
439
+ declare function readRetryAfterMs(headers: Headers): number | undefined;
440
+ /**
441
+ * Thrown when the transit handshake is rate-limited (HTTP 429).
442
+ *
443
+ * Distinct from a generic failure because the correct response differs: a 429
444
+ * means "wait", not "this is broken", so callers must not burn their retry
445
+ * budget on it and should honour `retryAfterMs` when the server supplied it.
446
+ */
447
+ declare class TransitRateLimitedError extends Error {
448
+ readonly retryAfterMs?: number;
449
+ constructor(retryAfterMs?: number);
450
+ }
398
451
  interface ServerPublicKeys {
399
452
  x25519: string;
400
453
  p256: string;
401
454
  enabled: boolean;
402
455
  }
403
- declare function fetchServerKeys(apiUrl: string): Promise<ServerPublicKeys>;
456
+ declare function fetchServerKeys(apiUrl: string, identity?: TransitIdentity): Promise<ServerPublicKeys>;
404
457
  declare function generateEphemeralKey(algo: TransitAlgo, serverKeys: ServerPublicKeys): Promise<{
405
458
  ephemeralPubB64: string;
406
459
  deriveSessionKey: (sessionId: string) => Promise<unknown>;
407
460
  }>;
408
- declare function createRestTransitSession(apiUrl: string): Promise<{
461
+ declare function createRestTransitSession(apiUrl: string, identity?: TransitIdentity): Promise<{
409
462
  sessionId: string;
410
463
  sessionKey: CryptoKey | Uint8Array;
411
464
  } | null>;
@@ -535,6 +588,10 @@ interface ChatState {
535
588
  } | null) => void;
536
589
  addTypingUser: (conversationId: string, user: TypingUser) => void;
537
590
  removeTypingUser: (conversationId: string, userId: string) => void;
591
+ /** Drops every typing indicator and cancels their expiry timers. For teardown
592
+ * and identity change — a socket drop no longer needs it, since indicators
593
+ * expire on their own. */
594
+ clearTypingUsers: () => void;
538
595
  setUserOnline: (userId: string) => void;
539
596
  setUserOffline: (userId: string) => void;
540
597
  setOnlineUsers: (userIds: string[]) => void;
@@ -799,4 +856,4 @@ declare class AntzChatClient {
799
856
  uploadIcon(conversationId: string, file: UploadableFile): Promise<Conversation>;
800
857
  }
801
858
 
802
- export { AntzChatAuthError, AntzChatClient, AntzChatConfig, AntzChatError, AntzChatNetworkError, AntzChatPermissionError, AntzChatServerError, AntzChatValidationError, AppConfig, AuthResponse, AuthTokens, BatchUploadResult, CompletedPart, Conversation, ConversationListParams, ConversationSyncResponse, ConversationUnreadCount, type CreateDirectData, type CreateGroupData, CrossConversationSyncResponse, CursorPaginatedResponse, FileResponse, FileType, ForwardAckPayload, ForwardMessagePayload, type ForwardResult, HIGHLY_FORWARDED_DEPTH_THRESHOLD, type LastReadEntry, type ListMessagesParams, LoginCredentials, MAX_FORWARD_TARGETS, type MentionPart, type MentionSegment, Message, MessageReactionsResponse, MessageReceiptsResponse, type MobileDeviceToken, PaginatedResponse, type ParsedMention, Participant, PersistStorage, PresignedUrlRequest, PresignedUrlResponse, RegisterData, type RegisterDeviceTokenPayload, ResolvedCompressionConfig, ResolvedConfig, type SearchParams, type SendData, SendMessagePayload, type SocketStatus, type StatusListener, type TokenStore, UnreadSummary, type UpdateConversationData, type UpdateProfilePayload, UploadableFile, User, UserPreferences, type WebPushDeviceToken, appConfigApi, authApi, buildMentionText, connectSocket, conversationsApi, createAuthStore, createRestTransitSession, decryptPayload, devicesApi, disconnectSocket, encryptPayload, extractMentionIds, fetchServerKeys, generateEphemeralKey, getApiClient, getAuthStore, getCompressionStrategy, getSessionId, getSessionKey, getSocket, getSocketStatus, initApiClient, initAuthStore, isApiClientConfigured, isMentionAll, isTransitEnvelope, messagesApi, normalizeAxiosError, normalizeConversation, onSocketStatus, parseMentions, performHandshake, reconnectSocket, refreshSocketAuth, renderMentionParts, resetAuthStore, resetTrackedRooms, resolveSystemMessageText, setApiClientInstance, setAuthReadyPromise, setTransitSession, socketEmit, syncApi, tryGetSocket, useChatStore, usersApi };
859
+ export { AntzChatAuthError, AntzChatClient, AntzChatConfig, AntzChatError, AntzChatNetworkError, AntzChatPermissionError, AntzChatServerError, AntzChatValidationError, AppConfig, AuthResponse, AuthTokens, BatchUploadResult, CompletedPart, Conversation, ConversationListParams, ConversationSyncResponse, ConversationUnreadCount, type CreateDirectData, type CreateGroupData, CrossConversationSyncResponse, CursorPaginatedResponse, FileResponse, FileType, ForwardAckPayload, ForwardMessagePayload, type ForwardResult, HIGHLY_FORWARDED_DEPTH_THRESHOLD, type LastReadEntry, type ListMessagesParams, LoginCredentials, MAX_FORWARD_TARGETS, type MentionPart, type MentionSegment, Message, MessageReactionsResponse, MessageReceiptsResponse, type MobileDeviceToken, PaginatedResponse, type ParsedMention, Participant, PersistStorage, PresignedUrlRequest, PresignedUrlResponse, RegisterData, type RegisterDeviceTokenPayload, ResolvedCompressionConfig, ResolvedConfig, type SearchParams, type SendData, SendMessagePayload, type SocketStatus, type StatusListener, type TokenStore, type TransitIdentity, TransitRateLimitedError, UnreadSummary, type UpdateConversationData, type UpdateProfilePayload, UploadableFile, User, UserPreferences, type WebPushDeviceToken, appConfigApi, authApi, buildMentionText, connectSocket, conversationsApi, createAuthStore, createRestTransitSession, decryptPayload, devicesApi, disconnectSocket, encryptPayload, extractMentionIds, fetchServerKeys, generateEphemeralKey, getApiClient, getAuthStore, getCompressionStrategy, getSessionId, getSessionKey, getSocket, getSocketStatus, initApiClient, initAuthStore, isApiClientConfigured, isMentionAll, isTransitEnvelope, messagesApi, normalizeAxiosError, normalizeConversation, onSocketStatus, parseMentions, performHandshake, readRetryAfterMs, reconnectSocket, refreshSocketAuth, registerTeardownHook, renderMentionParts, resetAuthStore, resetTrackedRooms, resetTypingThrottle, resolveSystemMessageText, setApiClientInstance, setAuthReadyPromise, setTransitSession, socketEmit, syncApi, tryGetSocket, useChatStore, usersApi };
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AntzChatPermissionError,
6
6
  AntzChatServerError,
7
7
  AntzChatValidationError,
8
+ TransitRateLimitedError,
8
9
  awaitTransitReadyOr,
9
10
  clearTransitSession,
10
11
  configureTransitIfUnset,
@@ -30,16 +31,17 @@ import {
30
31
  normalizeAxiosError,
31
32
  onTransitReady,
32
33
  performHandshake,
34
+ readRetryAfterMs,
33
35
  resetAlgoCache,
34
36
  setApiClientInstance,
35
37
  setAuthReadyPromise,
36
38
  setTransitSession,
37
39
  storageApi,
38
40
  uploadBatch
39
- } from "./chunk-WUNH3UTE.js";
41
+ } from "./chunk-U637W5MD.js";
40
42
  import {
41
43
  useChatStore
42
- } from "./chunk-UIYJAOGL.js";
44
+ } from "./chunk-QHELYVNT.js";
43
45
 
44
46
  // src/config/types.ts
45
47
  function resolveConfig(config) {
@@ -403,6 +405,19 @@ function secureOn(socket, event, handler) {
403
405
  handler(raw);
404
406
  });
405
407
  }
408
+ var _teardownHooks = /* @__PURE__ */ new Set();
409
+ function registerTeardownHook(hook) {
410
+ _teardownHooks.add(hook);
411
+ return () => _teardownHooks.delete(hook);
412
+ }
413
+ function runTeardownHooks() {
414
+ _teardownHooks.forEach((hook) => {
415
+ try {
416
+ hook();
417
+ } catch {
418
+ }
419
+ });
420
+ }
406
421
  var _joinedRooms = /* @__PURE__ */ new Set();
407
422
  function trackRoomJoin(conversationId) {
408
423
  _joinedRooms.add(conversationId);
@@ -456,7 +471,7 @@ async function _doConnect(config, getToken) {
456
471
  if (existingSession?.enabled && existingSession.sessionId) {
457
472
  httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
458
473
  } else {
459
- httpsSession = await createRestTransitSession(config.apiUrl);
474
+ httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
460
475
  if (httpsSession) {
461
476
  const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
462
477
  setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
@@ -467,7 +482,7 @@ async function _doConnect(config, getToken) {
467
482
  boundDeriveSessionKey = null;
468
483
  } else {
469
484
  try {
470
- const serverKeys = await fetchServerKeys(config.apiUrl);
485
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
471
486
  if (!serverKeys.enabled) {
472
487
  throw new AntzChatError(
473
488
  "TRANSIT_MISMATCH",
@@ -484,7 +499,7 @@ async function _doConnect(config, getToken) {
484
499
  }
485
500
  } else {
486
501
  try {
487
- const serverKeys = await fetchServerKeys(config.apiUrl);
502
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
488
503
  if (serverKeys.enabled) {
489
504
  throw new AntzChatError(
490
505
  "TRANSIT_MISMATCH",
@@ -546,13 +561,13 @@ async function _doConnect(config, getToken) {
546
561
  });
547
562
  }
548
563
  secureOn(_socket, "read_receipt", (event) => {
549
- import("./chat.store-UVTDBPEC.js").then(({ useChatStore: useChatStore2 }) => {
564
+ import("./chat.store-TA6G7PD6.js").then(({ useChatStore: useChatStore2 }) => {
550
565
  const e = event;
551
566
  useChatStore2.getState().setLastRead(e.conversationId, e.messageId, e.readAt);
552
567
  });
553
568
  });
554
569
  secureOn(_socket, "user_online", (event) => {
555
- import("./chat.store-UVTDBPEC.js").then(({ useChatStore: useChatStore2 }) => {
570
+ import("./chat.store-TA6G7PD6.js").then(({ useChatStore: useChatStore2 }) => {
556
571
  const store = useChatStore2.getState();
557
572
  const e = event;
558
573
  store.setUserOnline(e.userId);
@@ -560,7 +575,7 @@ async function _doConnect(config, getToken) {
560
575
  });
561
576
  });
562
577
  secureOn(_socket, "user_offline", (event) => {
563
- import("./chat.store-UVTDBPEC.js").then(({ useChatStore: useChatStore2 }) => {
578
+ import("./chat.store-TA6G7PD6.js").then(({ useChatStore: useChatStore2 }) => {
564
579
  const e = event;
565
580
  const store = useChatStore2.getState();
566
581
  store.setUserOffline(e.userId);
@@ -598,6 +613,10 @@ function disconnectSocket() {
598
613
  }
599
614
  clearTransitSession();
600
615
  resetAlgoCache();
616
+ runTeardownHooks();
617
+ import("./chat.store-TA6G7PD6.js").then(({ useChatStore: useChatStore2 }) => {
618
+ useChatStore2.getState().clearTypingUsers();
619
+ });
601
620
  _getToken = null;
602
621
  _userId = void 0;
603
622
  _tenantId = void 0;
@@ -649,6 +668,12 @@ var RECONNECT_WAIT_TIMEOUT = 15e3;
649
668
  var QUEUE_MAX_SIZE = 100;
650
669
  var QUEUE_ENTRY_TTL = 3e4;
651
670
  var sendQueues = /* @__PURE__ */ new Map();
671
+ var TYPING_THROTTLE_MS = 3e3;
672
+ var _lastTypingSentAt = /* @__PURE__ */ new Map();
673
+ function resetTypingThrottle() {
674
+ _lastTypingSentAt.clear();
675
+ }
676
+ registerTeardownHook(resetTypingThrottle);
652
677
  var sendQueueRunning = /* @__PURE__ */ new Map();
653
678
  async function drainSendQueue(conversationId) {
654
679
  if (sendQueueRunning.get(conversationId)) return;
@@ -779,8 +804,40 @@ var socketEmit = {
779
804
  return withAck("unpin_message", { messageId });
780
805
  },
781
806
  // markRead and typing are best-effort — silently dropped if socket not ready
807
+ //
808
+ // Typing is additionally LEADING-THROTTLED here rather than in the UI layer,
809
+ // so every consumer (both UI SDKs and any host calling socketEmit directly)
810
+ // gets the same emit budget and none can bypass it.
811
+ //
812
+ // The composer calls this on EVERY keystroke. A 40-word message is ~200 calls
813
+ // of which exactly one carries information: "this person started typing".
814
+ // Each one previously cost a round trip plus real server work, making typing
815
+ // the single most expensive event in the system per unit of user value.
816
+ //
817
+ // Shape of the throttle:
818
+ // isTyping:true — passed through at most once per TYPING_THROTTLE_MS per
819
+ // conversation. A continuous typist emits ~20/min instead
820
+ // of ~200/min.
821
+ // isTyping:false — ALWAYS passed through, and resets the window. It is the
822
+ // edge that clears the indicator on every peer, it is
823
+ // already debounced by the composer, and dropping it is
824
+ // exactly the failure that leaves "is typing…" stuck.
825
+ //
826
+ // The server refreshes its typing key on each true edge (10s TTL) and
827
+ // receivers expire their own indicators after TYPING_EXPIRY_MS, both of which
828
+ // are comfortably longer than TYPING_THROTTLE_MS — so a throttled-away event
829
+ // never lets an indicator lapse mid-typing.
782
830
  typing(conversationId, isTyping) {
783
- fireAndForget("typing", { conversationId, isTyping });
831
+ if (!isTyping) {
832
+ _lastTypingSentAt.delete(conversationId);
833
+ fireAndForget("typing", { conversationId, isTyping: false });
834
+ return;
835
+ }
836
+ const now = Date.now();
837
+ const last = _lastTypingSentAt.get(conversationId) ?? 0;
838
+ if (now - last < TYPING_THROTTLE_MS) return;
839
+ _lastTypingSentAt.set(conversationId, now);
840
+ fireAndForget("typing", { conversationId, isTyping: true });
784
841
  },
785
842
  markRead(conversationId, messageId) {
786
843
  fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
@@ -1331,6 +1388,7 @@ export {
1331
1388
  HIGHLY_FORWARDED_DEPTH_THRESHOLD,
1332
1389
  MAX_FORWARD_TARGETS,
1333
1390
  MENTION_ALL_ID,
1391
+ TransitRateLimitedError,
1334
1392
  appConfigApi,
1335
1393
  authApi,
1336
1394
  buildMentionText,
@@ -1363,11 +1421,14 @@ export {
1363
1421
  onSocketStatus,
1364
1422
  parseMentions,
1365
1423
  performHandshake,
1424
+ readRetryAfterMs,
1366
1425
  reconnectSocket,
1367
1426
  refreshSocketAuth,
1427
+ registerTeardownHook,
1368
1428
  renderMentionParts,
1369
1429
  resetAuthStore,
1370
1430
  resetTrackedRooms,
1431
+ resetTypingThrottle,
1371
1432
  resolveConfig,
1372
1433
  resolveSystemMessageText,
1373
1434
  setApiClientInstance,