@antzsoft/chat-core 1.4.6 → 1.4.8
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/README.md +86 -7
- package/dist/chat.store-TA6G7PD6.js +7 -0
- package/dist/{chunk-U637W5MD.js → chunk-L537XWRD.js} +12 -4
- package/dist/chunk-L537XWRD.js.map +1 -0
- package/dist/chunk-QHELYVNT.js +109 -0
- package/dist/chunk-QHELYVNT.js.map +1 -0
- package/dist/index.cjs +145 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +82 -9
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +1 -1
- package/docs/integration-guide.html +19 -5
- package/package.json +1 -1
- package/dist/chat.store-UVTDBPEC.js +0 -7
- package/dist/chunk-U637W5MD.js.map +0 -1
- package/dist/chunk-UIYJAOGL.js +0 -62
- package/dist/chunk-UIYJAOGL.js.map +0 -1
- /package/dist/{chat.store-UVTDBPEC.js.map → chat.store-TA6G7PD6.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -35,11 +35,37 @@ var chat_store_exports = {};
|
|
|
35
35
|
__export(chat_store_exports, {
|
|
36
36
|
useChatStore: () => useChatStore
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
function clearTypingExpiry(conversationId, userId) {
|
|
39
|
+
const key = typingKey(conversationId, userId);
|
|
40
|
+
const timer = _typingExpiryTimers.get(key);
|
|
41
|
+
if (timer) {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
_typingExpiryTimers.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function clearAllTypingExpiry() {
|
|
47
|
+
_typingExpiryTimers.forEach((timer) => clearTimeout(timer));
|
|
48
|
+
_typingExpiryTimers.clear();
|
|
49
|
+
}
|
|
50
|
+
function scheduleTypingExpiry(conversationId, userId) {
|
|
51
|
+
const key = typingKey(conversationId, userId);
|
|
52
|
+
const existing = _typingExpiryTimers.get(key);
|
|
53
|
+
if (existing) clearTimeout(existing);
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
_typingExpiryTimers.delete(key);
|
|
56
|
+
useChatStore.getState().removeTypingUser(conversationId, userId);
|
|
57
|
+
}, TYPING_EXPIRY_MS);
|
|
58
|
+
timer.unref?.();
|
|
59
|
+
_typingExpiryTimers.set(key, timer);
|
|
60
|
+
}
|
|
61
|
+
var import_zustand2, TYPING_EXPIRY_MS, _typingExpiryTimers, typingKey, useChatStore;
|
|
39
62
|
var init_chat_store = __esm({
|
|
40
63
|
"src/stores/chat.store.ts"() {
|
|
41
64
|
"use strict";
|
|
42
65
|
import_zustand2 = require("zustand");
|
|
66
|
+
TYPING_EXPIRY_MS = 6e3;
|
|
67
|
+
_typingExpiryTimers = /* @__PURE__ */ new Map();
|
|
68
|
+
typingKey = (conversationId, userId) => `${conversationId}\0${userId}`;
|
|
43
69
|
useChatStore = (0, import_zustand2.create)((set) => ({
|
|
44
70
|
activeConversationId: null,
|
|
45
71
|
pendingTarget: null,
|
|
@@ -56,19 +82,40 @@ var init_chat_store = __esm({
|
|
|
56
82
|
messageInfoId: null,
|
|
57
83
|
setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
|
|
58
84
|
setPendingTarget: (target) => set({ pendingTarget: target }),
|
|
85
|
+
// Each typing user carries a self-expiring timer, so an indicator can never
|
|
86
|
+
// outlive the evidence for it. Previously the ONLY things that cleared an
|
|
87
|
+
// indicator were an explicit isTyping:false from the sender and a local
|
|
88
|
+
// socket disconnect — so any lost false edge left "is typing…" on screen
|
|
89
|
+
// indefinitely. That is reachable in normal use: a sender whose tab is killed
|
|
90
|
+
// while their other devices stay connected never triggers the server's
|
|
91
|
+
// disconnect cleanup (it only fires when the user's LAST socket goes), and
|
|
92
|
+
// the false edge is fire-and-forget so it is never retried.
|
|
93
|
+
//
|
|
94
|
+
// The timer is the authority on liveness; the sender's false edge is now just
|
|
95
|
+
// a fast path. TYPING_EXPIRY_MS must exceed chat-core's outbound throttle
|
|
96
|
+
// window (3s) by enough that a still-typing peer always re-asserts before it
|
|
97
|
+
// fires, otherwise indicators would visibly flicker mid-typing.
|
|
59
98
|
addTypingUser: (conversationId, user) => set((state) => {
|
|
99
|
+
scheduleTypingExpiry(conversationId, user.userId);
|
|
60
100
|
const existing = state.typingUsers[conversationId] ?? [];
|
|
61
101
|
const deduped = existing.filter((u) => u.userId !== user.userId);
|
|
62
102
|
return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
|
|
63
103
|
}),
|
|
64
|
-
removeTypingUser: (conversationId, userId) => set((state) =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
removeTypingUser: (conversationId, userId) => set((state) => {
|
|
105
|
+
clearTypingExpiry(conversationId, userId);
|
|
106
|
+
const existing = state.typingUsers[conversationId];
|
|
107
|
+
if (!existing || !existing.some((u) => u.userId === userId)) return state;
|
|
108
|
+
return {
|
|
109
|
+
typingUsers: {
|
|
110
|
+
...state.typingUsers,
|
|
111
|
+
[conversationId]: existing.filter((u) => u.userId !== userId)
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
clearTypingUsers: () => {
|
|
116
|
+
clearAllTypingExpiry();
|
|
117
|
+
set({ typingUsers: {} });
|
|
118
|
+
},
|
|
72
119
|
setUserOnline: (userId) => set((state) => ({
|
|
73
120
|
onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
|
|
74
121
|
})),
|
|
@@ -111,6 +158,7 @@ __export(src_exports, {
|
|
|
111
158
|
HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
|
|
112
159
|
MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
|
|
113
160
|
MENTION_ALL_ID: () => MENTION_ALL_ID,
|
|
161
|
+
TRANSIT_OPTIONAL_HEADER: () => TRANSIT_OPTIONAL_HEADER,
|
|
114
162
|
TransitRateLimitedError: () => TransitRateLimitedError,
|
|
115
163
|
appConfigApi: () => appConfigApi,
|
|
116
164
|
authApi: () => authApi,
|
|
@@ -147,9 +195,11 @@ __export(src_exports, {
|
|
|
147
195
|
readRetryAfterMs: () => readRetryAfterMs,
|
|
148
196
|
reconnectSocket: () => reconnectSocket,
|
|
149
197
|
refreshSocketAuth: () => refreshSocketAuth,
|
|
198
|
+
registerTeardownHook: () => registerTeardownHook,
|
|
150
199
|
renderMentionParts: () => renderMentionParts,
|
|
151
200
|
resetAuthStore: () => resetAuthStore,
|
|
152
201
|
resetTrackedRooms: () => resetTrackedRooms,
|
|
202
|
+
resetTypingThrottle: () => resetTypingThrottle,
|
|
153
203
|
resolveConfig: () => resolveConfig,
|
|
154
204
|
resolveSystemMessageText: () => resolveSystemMessageText,
|
|
155
205
|
setApiClientInstance: () => setApiClientInstance,
|
|
@@ -776,8 +826,9 @@ function ensureRestTransitHandshake() {
|
|
|
776
826
|
waitMs = Math.min(500 * 2 ** failures, 8e3);
|
|
777
827
|
} catch (err) {
|
|
778
828
|
if (err instanceof TransitRateLimitedError) {
|
|
779
|
-
const
|
|
780
|
-
|
|
829
|
+
const ceiling = Math.min(6e4, TRANSIT_GATE_MAX_WAIT_MS);
|
|
830
|
+
const blind = Math.min(15e3 * 2 ** rateLimitHits, ceiling);
|
|
831
|
+
waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), ceiling) : blind;
|
|
781
832
|
rateLimitHits++;
|
|
782
833
|
console.warn(
|
|
783
834
|
`[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
|
|
@@ -797,6 +848,12 @@ function ensureRestTransitHandshake() {
|
|
|
797
848
|
}
|
|
798
849
|
})();
|
|
799
850
|
}
|
|
851
|
+
var TRANSIT_OPTIONAL_HEADER = "x-antz-transit-optional";
|
|
852
|
+
function isTransitOptional(req) {
|
|
853
|
+
const present = req.headers?.[TRANSIT_OPTIONAL_HEADER] != null;
|
|
854
|
+
if (present) delete req.headers[TRANSIT_OPTIONAL_HEADER];
|
|
855
|
+
return present;
|
|
856
|
+
}
|
|
800
857
|
function initApiClient(config, tokenStore) {
|
|
801
858
|
_config = config;
|
|
802
859
|
_tokenStore = tokenStore;
|
|
@@ -818,7 +875,7 @@ function initApiClient(config, tokenStore) {
|
|
|
818
875
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
819
876
|
_avatarSent = true;
|
|
820
877
|
}
|
|
821
|
-
if (_config?.transitEncryption) {
|
|
878
|
+
if (_config?.transitEncryption && !isTransitOptional(req)) {
|
|
822
879
|
if (!getTransitSession()) ensureRestTransitHandshake();
|
|
823
880
|
const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);
|
|
824
881
|
if (!ready) {
|
|
@@ -926,6 +983,7 @@ function getApiClient() {
|
|
|
926
983
|
}
|
|
927
984
|
|
|
928
985
|
// src/api/auth.ts
|
|
986
|
+
var teardownRequest = { headers: { [TRANSIT_OPTIONAL_HEADER]: "1" } };
|
|
929
987
|
var authApi = {
|
|
930
988
|
async login(credentials) {
|
|
931
989
|
const { data } = await getApiClient().post("/auth/login", credentials);
|
|
@@ -939,11 +997,22 @@ var authApi = {
|
|
|
939
997
|
const { data } = await getApiClient().post("/auth/refresh", { refreshToken });
|
|
940
998
|
return data;
|
|
941
999
|
},
|
|
1000
|
+
/**
|
|
1001
|
+
* Ends the session. Never blocks on the transit handshake: logout has to work
|
|
1002
|
+
* on a degraded channel, which is exactly when it matters most.
|
|
1003
|
+
*
|
|
1004
|
+
* The refreshToken is dropped when no transit session exists, so it is never
|
|
1005
|
+
* sent in the clear. Losing it costs nothing meaningful — the server revokes
|
|
1006
|
+
* against the JWT's own user id either way; passing it only lets the server
|
|
1007
|
+
* target that one refresh token instead of the caller's session set.
|
|
1008
|
+
*/
|
|
942
1009
|
async logout(refreshToken) {
|
|
943
|
-
|
|
1010
|
+
const encrypted = getTransitSession() != null;
|
|
1011
|
+
const body = refreshToken && encrypted ? { refreshToken } : {};
|
|
1012
|
+
await getApiClient().post("/auth/logout", body, teardownRequest);
|
|
944
1013
|
},
|
|
945
1014
|
async logoutAll() {
|
|
946
|
-
await getApiClient().post("/auth/logout-all");
|
|
1015
|
+
await getApiClient().post("/auth/logout-all", {}, teardownRequest);
|
|
947
1016
|
},
|
|
948
1017
|
async getMe() {
|
|
949
1018
|
const { data } = await getApiClient().get("/users/me");
|
|
@@ -1237,6 +1306,19 @@ function secureOn(socket, event, handler) {
|
|
|
1237
1306
|
handler(raw);
|
|
1238
1307
|
});
|
|
1239
1308
|
}
|
|
1309
|
+
var _teardownHooks = /* @__PURE__ */ new Set();
|
|
1310
|
+
function registerTeardownHook(hook) {
|
|
1311
|
+
_teardownHooks.add(hook);
|
|
1312
|
+
return () => _teardownHooks.delete(hook);
|
|
1313
|
+
}
|
|
1314
|
+
function runTeardownHooks() {
|
|
1315
|
+
_teardownHooks.forEach((hook) => {
|
|
1316
|
+
try {
|
|
1317
|
+
hook();
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1240
1322
|
var _joinedRooms = /* @__PURE__ */ new Set();
|
|
1241
1323
|
function trackRoomJoin(conversationId) {
|
|
1242
1324
|
_joinedRooms.add(conversationId);
|
|
@@ -1432,6 +1514,10 @@ function disconnectSocket() {
|
|
|
1432
1514
|
}
|
|
1433
1515
|
clearTransitSession();
|
|
1434
1516
|
resetAlgoCache();
|
|
1517
|
+
runTeardownHooks();
|
|
1518
|
+
Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
|
|
1519
|
+
useChatStore2.getState().clearTypingUsers();
|
|
1520
|
+
});
|
|
1435
1521
|
_getToken = null;
|
|
1436
1522
|
_userId = void 0;
|
|
1437
1523
|
_tenantId = void 0;
|
|
@@ -1483,6 +1569,12 @@ var RECONNECT_WAIT_TIMEOUT = 15e3;
|
|
|
1483
1569
|
var QUEUE_MAX_SIZE = 100;
|
|
1484
1570
|
var QUEUE_ENTRY_TTL = 3e4;
|
|
1485
1571
|
var sendQueues = /* @__PURE__ */ new Map();
|
|
1572
|
+
var TYPING_THROTTLE_MS = 3e3;
|
|
1573
|
+
var _lastTypingSentAt = /* @__PURE__ */ new Map();
|
|
1574
|
+
function resetTypingThrottle() {
|
|
1575
|
+
_lastTypingSentAt.clear();
|
|
1576
|
+
}
|
|
1577
|
+
registerTeardownHook(resetTypingThrottle);
|
|
1486
1578
|
var sendQueueRunning = /* @__PURE__ */ new Map();
|
|
1487
1579
|
async function drainSendQueue(conversationId) {
|
|
1488
1580
|
if (sendQueueRunning.get(conversationId)) return;
|
|
@@ -1613,8 +1705,40 @@ var socketEmit = {
|
|
|
1613
1705
|
return withAck("unpin_message", { messageId });
|
|
1614
1706
|
},
|
|
1615
1707
|
// markRead and typing are best-effort — silently dropped if socket not ready
|
|
1708
|
+
//
|
|
1709
|
+
// Typing is additionally LEADING-THROTTLED here rather than in the UI layer,
|
|
1710
|
+
// so every consumer (both UI SDKs and any host calling socketEmit directly)
|
|
1711
|
+
// gets the same emit budget and none can bypass it.
|
|
1712
|
+
//
|
|
1713
|
+
// The composer calls this on EVERY keystroke. A 40-word message is ~200 calls
|
|
1714
|
+
// of which exactly one carries information: "this person started typing".
|
|
1715
|
+
// Each one previously cost a round trip plus real server work, making typing
|
|
1716
|
+
// the single most expensive event in the system per unit of user value.
|
|
1717
|
+
//
|
|
1718
|
+
// Shape of the throttle:
|
|
1719
|
+
// isTyping:true — passed through at most once per TYPING_THROTTLE_MS per
|
|
1720
|
+
// conversation. A continuous typist emits ~20/min instead
|
|
1721
|
+
// of ~200/min.
|
|
1722
|
+
// isTyping:false — ALWAYS passed through, and resets the window. It is the
|
|
1723
|
+
// edge that clears the indicator on every peer, it is
|
|
1724
|
+
// already debounced by the composer, and dropping it is
|
|
1725
|
+
// exactly the failure that leaves "is typing…" stuck.
|
|
1726
|
+
//
|
|
1727
|
+
// The server refreshes its typing key on each true edge (10s TTL) and
|
|
1728
|
+
// receivers expire their own indicators after TYPING_EXPIRY_MS, both of which
|
|
1729
|
+
// are comfortably longer than TYPING_THROTTLE_MS — so a throttled-away event
|
|
1730
|
+
// never lets an indicator lapse mid-typing.
|
|
1616
1731
|
typing(conversationId, isTyping) {
|
|
1617
|
-
|
|
1732
|
+
if (!isTyping) {
|
|
1733
|
+
_lastTypingSentAt.delete(conversationId);
|
|
1734
|
+
fireAndForget("typing", { conversationId, isTyping: false });
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const now = Date.now();
|
|
1738
|
+
const last = _lastTypingSentAt.get(conversationId) ?? 0;
|
|
1739
|
+
if (now - last < TYPING_THROTTLE_MS) return;
|
|
1740
|
+
_lastTypingSentAt.set(conversationId, now);
|
|
1741
|
+
fireAndForget("typing", { conversationId, isTyping: true });
|
|
1618
1742
|
},
|
|
1619
1743
|
markRead(conversationId, messageId) {
|
|
1620
1744
|
fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
|
|
@@ -2142,7 +2266,9 @@ var devicesApi = {
|
|
|
2142
2266
|
* Call this on logout so the user stops receiving push notifications on this device.
|
|
2143
2267
|
*/
|
|
2144
2268
|
async remove(deviceId) {
|
|
2145
|
-
await getApiClient().post(`/users/me/devices/${deviceId}/remove
|
|
2269
|
+
await getApiClient().post(`/users/me/devices/${deviceId}/remove`, {}, {
|
|
2270
|
+
headers: { [TRANSIT_OPTIONAL_HEADER]: "1" }
|
|
2271
|
+
});
|
|
2146
2272
|
}
|
|
2147
2273
|
};
|
|
2148
2274
|
|
|
@@ -2332,6 +2458,7 @@ var AntzChatClient = class {
|
|
|
2332
2458
|
HIGHLY_FORWARDED_DEPTH_THRESHOLD,
|
|
2333
2459
|
MAX_FORWARD_TARGETS,
|
|
2334
2460
|
MENTION_ALL_ID,
|
|
2461
|
+
TRANSIT_OPTIONAL_HEADER,
|
|
2335
2462
|
TransitRateLimitedError,
|
|
2336
2463
|
appConfigApi,
|
|
2337
2464
|
authApi,
|
|
@@ -2368,9 +2495,11 @@ var AntzChatClient = class {
|
|
|
2368
2495
|
readRetryAfterMs,
|
|
2369
2496
|
reconnectSocket,
|
|
2370
2497
|
refreshSocketAuth,
|
|
2498
|
+
registerTeardownHook,
|
|
2371
2499
|
renderMentionParts,
|
|
2372
2500
|
resetAuthStore,
|
|
2373
2501
|
resetTrackedRooms,
|
|
2502
|
+
resetTypingThrottle,
|
|
2374
2503
|
resolveConfig,
|
|
2375
2504
|
resolveSystemMessageText,
|
|
2376
2505
|
setApiClientInstance,
|