@dotwire/client 0.0.0-stage → 0.2.0
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/LICENSE +176 -0
- package/README.md +311 -2
- package/dist/index.cjs +785 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +759 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DotwireClient: () => DotwireClient,
|
|
24
|
+
DotwireClientError: () => DotwireClientError,
|
|
25
|
+
DotwireRoomHandle: () => DotwireRoomHandle,
|
|
26
|
+
isSystemSender: () => isSystemSender,
|
|
27
|
+
toDotwireClientError: () => toDotwireClientError
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/types.ts
|
|
32
|
+
function isSystemSender(senderId) {
|
|
33
|
+
if (!senderId) return false;
|
|
34
|
+
const lower = senderId.toLowerCase();
|
|
35
|
+
return lower === "system" || lower.startsWith("system:") || lower.startsWith("bot:");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
var DotwireClientError = class extends Error {
|
|
40
|
+
status;
|
|
41
|
+
kind;
|
|
42
|
+
code;
|
|
43
|
+
reason;
|
|
44
|
+
retryAfterMs;
|
|
45
|
+
constructor(init) {
|
|
46
|
+
super(init.reason ?? init.code ?? `dotwire request failed with status ${init.status}`);
|
|
47
|
+
this.name = "DotwireClientError";
|
|
48
|
+
this.status = init.status;
|
|
49
|
+
this.kind = init.kind;
|
|
50
|
+
this.code = init.code;
|
|
51
|
+
this.reason = init.reason;
|
|
52
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
async function toDotwireClientError(response) {
|
|
56
|
+
const status = response.status;
|
|
57
|
+
let code;
|
|
58
|
+
let reason;
|
|
59
|
+
try {
|
|
60
|
+
const body = await response.json();
|
|
61
|
+
if (body && typeof body === "object") {
|
|
62
|
+
const record = body;
|
|
63
|
+
if (typeof record.error === "string") code = record.error;
|
|
64
|
+
if (typeof record.reason === "string") reason = record.reason;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
let retryAfterMs;
|
|
69
|
+
const retryAfterHeader = response.headers?.get?.("Retry-After");
|
|
70
|
+
if (retryAfterHeader) {
|
|
71
|
+
const seconds = Number(retryAfterHeader);
|
|
72
|
+
if (!Number.isNaN(seconds)) {
|
|
73
|
+
retryAfterMs = seconds * 1e3;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return new DotwireClientError({ status, kind: classifyKind(status, code), code, reason, retryAfterMs });
|
|
77
|
+
}
|
|
78
|
+
function classifyKind(status, code) {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case 400:
|
|
81
|
+
return "BadRequest";
|
|
82
|
+
case 401:
|
|
83
|
+
return "Unauthorized";
|
|
84
|
+
case 403:
|
|
85
|
+
return "Forbidden";
|
|
86
|
+
case 404:
|
|
87
|
+
return "NotFound";
|
|
88
|
+
case 422:
|
|
89
|
+
return code === "presend_rejected" ? "PresendRejected" : "BadRequest";
|
|
90
|
+
case 429:
|
|
91
|
+
return "RateLimited";
|
|
92
|
+
case 503:
|
|
93
|
+
return "Unavailable";
|
|
94
|
+
default:
|
|
95
|
+
return "Unknown";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/client.ts
|
|
100
|
+
var import_signalr = require("@microsoft/signalr");
|
|
101
|
+
|
|
102
|
+
// src/seq-set.ts
|
|
103
|
+
var BoundedSeqSet = class {
|
|
104
|
+
constructor(capacity = 1e3) {
|
|
105
|
+
this.capacity = capacity;
|
|
106
|
+
}
|
|
107
|
+
capacity;
|
|
108
|
+
seen = /* @__PURE__ */ new Set();
|
|
109
|
+
has(seq) {
|
|
110
|
+
return this.seen.has(seq);
|
|
111
|
+
}
|
|
112
|
+
add(seq) {
|
|
113
|
+
if (this.seen.has(seq)) return;
|
|
114
|
+
this.seen.add(seq);
|
|
115
|
+
if (this.seen.size > this.capacity) {
|
|
116
|
+
const oldest = this.seen.values().next().value;
|
|
117
|
+
if (oldest !== void 0) {
|
|
118
|
+
this.seen.delete(oldest);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/room.ts
|
|
125
|
+
var DotwireRoomHandle = class {
|
|
126
|
+
constructor(roomId, client) {
|
|
127
|
+
this.roomId = roomId;
|
|
128
|
+
this.client = client;
|
|
129
|
+
}
|
|
130
|
+
roomId;
|
|
131
|
+
client;
|
|
132
|
+
autoStopTypingTimer = null;
|
|
133
|
+
lastTypingSentAt = 0;
|
|
134
|
+
isCurrentlyTyping = false;
|
|
135
|
+
listeners = [];
|
|
136
|
+
/**
|
|
137
|
+
* Subscribes on the hub and, when `history` is given, fetches and delivers that many of the
|
|
138
|
+
* latest messages (spec §6) before returning them.
|
|
139
|
+
*/
|
|
140
|
+
async subscribe(options = {}) {
|
|
141
|
+
await this.client.subscribe(this.roomId);
|
|
142
|
+
if (options.history === void 0) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
const page = await this.client.getHistory(this.roomId, { limit: options.history });
|
|
146
|
+
for (const message of page.messages) {
|
|
147
|
+
this.client.deliverMessage(message);
|
|
148
|
+
}
|
|
149
|
+
return page;
|
|
150
|
+
}
|
|
151
|
+
async unsubscribe() {
|
|
152
|
+
this.clearTypingTimer();
|
|
153
|
+
return this.client.unsubscribe(this.roomId);
|
|
154
|
+
}
|
|
155
|
+
async sendMessage(content, timeoutMs) {
|
|
156
|
+
return this.client.sendMessage(this.roomId, content, timeoutMs);
|
|
157
|
+
}
|
|
158
|
+
async getHistory(options = {}) {
|
|
159
|
+
return this.client.getHistory(this.roomId, options);
|
|
160
|
+
}
|
|
161
|
+
async getParticipants(timeoutMs) {
|
|
162
|
+
return this.client.getParticipants(this.roomId, timeoutMs);
|
|
163
|
+
}
|
|
164
|
+
async sendTyping(isTyping) {
|
|
165
|
+
this.clearTypingTimer();
|
|
166
|
+
this.isCurrentlyTyping = isTyping;
|
|
167
|
+
if (isTyping) {
|
|
168
|
+
this.lastTypingSentAt = Date.now();
|
|
169
|
+
const autoStopMs = this.client.options.autoStopTypingMs ?? 3e3;
|
|
170
|
+
this.autoStopTypingTimer = setTimeout(() => {
|
|
171
|
+
this.stopTyping().catch(() => {
|
|
172
|
+
});
|
|
173
|
+
}, autoStopMs);
|
|
174
|
+
}
|
|
175
|
+
return this.client.sendTyping(this.roomId, isTyping);
|
|
176
|
+
}
|
|
177
|
+
async startTyping() {
|
|
178
|
+
return this.sendTyping(true);
|
|
179
|
+
}
|
|
180
|
+
async stopTyping() {
|
|
181
|
+
this.clearTypingTimer();
|
|
182
|
+
if (this.isCurrentlyTyping) {
|
|
183
|
+
this.isCurrentlyTyping = false;
|
|
184
|
+
return this.client.sendTyping(this.roomId, false);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async notifyTyping() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const throttleMs = this.client.options.typingThrottleMs ?? 1e3;
|
|
190
|
+
const autoStopMs = this.client.options.autoStopTypingMs ?? 3e3;
|
|
191
|
+
this.clearTypingTimer();
|
|
192
|
+
this.autoStopTypingTimer = setTimeout(() => {
|
|
193
|
+
this.stopTyping().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
}, autoStopMs);
|
|
196
|
+
if (!this.isCurrentlyTyping || now - this.lastTypingSentAt >= throttleMs) {
|
|
197
|
+
this.isCurrentlyTyping = true;
|
|
198
|
+
this.lastTypingSentAt = now;
|
|
199
|
+
await this.client.sendTyping(this.roomId, true);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
clearTypingTimer() {
|
|
203
|
+
if (this.autoStopTypingTimer) {
|
|
204
|
+
clearTimeout(this.autoStopTypingTimer);
|
|
205
|
+
this.autoStopTypingTimer = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
onMessage(handler) {
|
|
209
|
+
const unsub = this.client.onMessage((message) => {
|
|
210
|
+
if (message.roomId === this.roomId) {
|
|
211
|
+
handler(message);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
this.listeners.push(unsub);
|
|
215
|
+
return unsub;
|
|
216
|
+
}
|
|
217
|
+
onSystemMessage(handler) {
|
|
218
|
+
const unsub = this.client.onMessage((message) => {
|
|
219
|
+
if (message.roomId === this.roomId && isSystemSender(message.senderId)) {
|
|
220
|
+
handler(message);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
this.listeners.push(unsub);
|
|
224
|
+
return unsub;
|
|
225
|
+
}
|
|
226
|
+
onUserMessage(handler) {
|
|
227
|
+
const unsub = this.client.onMessage((message) => {
|
|
228
|
+
if (message.roomId === this.roomId && !isSystemSender(message.senderId)) {
|
|
229
|
+
handler(message);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.listeners.push(unsub);
|
|
233
|
+
return unsub;
|
|
234
|
+
}
|
|
235
|
+
onMessageRetracted(handler) {
|
|
236
|
+
const unsub = this.client.onMessageRetracted((payload) => {
|
|
237
|
+
if (payload.roomId === this.roomId) {
|
|
238
|
+
handler(payload);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
this.listeners.push(unsub);
|
|
242
|
+
return unsub;
|
|
243
|
+
}
|
|
244
|
+
onMembershipRevoked(handler) {
|
|
245
|
+
const unsub = this.client.onMembershipRevoked((payload) => {
|
|
246
|
+
if (payload.roomId === this.roomId) {
|
|
247
|
+
handler(payload);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
this.listeners.push(unsub);
|
|
251
|
+
return unsub;
|
|
252
|
+
}
|
|
253
|
+
onTyping(handler) {
|
|
254
|
+
const unsub = this.client.onTyping((notification) => {
|
|
255
|
+
if (notification.roomId === this.roomId) {
|
|
256
|
+
handler(notification);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
this.listeners.push(unsub);
|
|
260
|
+
return unsub;
|
|
261
|
+
}
|
|
262
|
+
onTypingWithExpiry(handler, autoExpireMs = 3500) {
|
|
263
|
+
const expiryTimers = /* @__PURE__ */ new Map();
|
|
264
|
+
const unsub = this.client.onTyping((notification) => {
|
|
265
|
+
if (notification.roomId !== this.roomId) return;
|
|
266
|
+
const existing = expiryTimers.get(notification.userId);
|
|
267
|
+
if (existing) {
|
|
268
|
+
clearTimeout(existing);
|
|
269
|
+
expiryTimers.delete(notification.userId);
|
|
270
|
+
}
|
|
271
|
+
handler(notification);
|
|
272
|
+
if (notification.isTyping) {
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
expiryTimers.delete(notification.userId);
|
|
275
|
+
handler({
|
|
276
|
+
roomId: this.roomId,
|
|
277
|
+
userId: notification.userId,
|
|
278
|
+
isTyping: false
|
|
279
|
+
});
|
|
280
|
+
}, autoExpireMs);
|
|
281
|
+
expiryTimers.set(notification.userId, timer);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const cleanup = () => {
|
|
285
|
+
unsub();
|
|
286
|
+
for (const timer of expiryTimers.values()) {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
}
|
|
289
|
+
expiryTimers.clear();
|
|
290
|
+
};
|
|
291
|
+
this.listeners.push(cleanup);
|
|
292
|
+
return cleanup;
|
|
293
|
+
}
|
|
294
|
+
onPresence(handler) {
|
|
295
|
+
const unsub = this.client.onPresence((delta) => {
|
|
296
|
+
if (delta.roomId === this.roomId) {
|
|
297
|
+
handler(delta);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
this.listeners.push(unsub);
|
|
301
|
+
return unsub;
|
|
302
|
+
}
|
|
303
|
+
release() {
|
|
304
|
+
this.clearTypingTimer();
|
|
305
|
+
for (const cleanup of this.listeners) {
|
|
306
|
+
cleanup();
|
|
307
|
+
}
|
|
308
|
+
this.listeners.length = 0;
|
|
309
|
+
this.client.releaseRoomHandle(this);
|
|
310
|
+
}
|
|
311
|
+
dispose() {
|
|
312
|
+
this.release();
|
|
313
|
+
this.unsubscribe().catch(() => {
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/token.ts
|
|
319
|
+
var TokenManager = class {
|
|
320
|
+
constructor(tokenUrl, marginMs, fetchImpl = fetch) {
|
|
321
|
+
this.tokenUrl = tokenUrl;
|
|
322
|
+
this.marginMs = marginMs;
|
|
323
|
+
this.fetchImpl = fetchImpl;
|
|
324
|
+
}
|
|
325
|
+
tokenUrl;
|
|
326
|
+
marginMs;
|
|
327
|
+
fetchImpl;
|
|
328
|
+
cachedToken = null;
|
|
329
|
+
cachedExpMs = null;
|
|
330
|
+
pending = null;
|
|
331
|
+
async getToken() {
|
|
332
|
+
if (this.cachedToken !== null && !this.isExpiringSoon()) {
|
|
333
|
+
return this.cachedToken;
|
|
334
|
+
}
|
|
335
|
+
if (!this.pending) {
|
|
336
|
+
this.pending = this.fetchAndCache().finally(() => {
|
|
337
|
+
this.pending = null;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return this.pending;
|
|
341
|
+
}
|
|
342
|
+
isExpiringSoon() {
|
|
343
|
+
if (this.cachedExpMs === null) return false;
|
|
344
|
+
return Date.now() >= this.cachedExpMs - this.marginMs;
|
|
345
|
+
}
|
|
346
|
+
async fetchAndCache() {
|
|
347
|
+
const response = await this.fetchImpl(this.tokenUrl, { credentials: "include" });
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw await toDotwireClientError(response);
|
|
350
|
+
}
|
|
351
|
+
const body = await response.json();
|
|
352
|
+
this.cachedToken = body.token;
|
|
353
|
+
this.cachedExpMs = decodeExpMs(body.token);
|
|
354
|
+
return this.cachedToken;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
function decodeExpMs(token) {
|
|
358
|
+
const parts = token.split(".");
|
|
359
|
+
if (parts.length < 2) return null;
|
|
360
|
+
try {
|
|
361
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
362
|
+
return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function base64UrlDecode(segment) {
|
|
368
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
369
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
370
|
+
if (typeof Buffer !== "undefined") {
|
|
371
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
372
|
+
}
|
|
373
|
+
const binary = atob(padded);
|
|
374
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
375
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/client.ts
|
|
379
|
+
var DotwireClient = class {
|
|
380
|
+
constructor(options) {
|
|
381
|
+
this.options = options;
|
|
382
|
+
if (!options.tokenUrl && !options.getAccessToken) {
|
|
383
|
+
throw new Error('DotwireClient requires either "tokenUrl" or "getAccessToken".');
|
|
384
|
+
}
|
|
385
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
386
|
+
this.hubPath = options.hubPath ?? "/hub/rooms";
|
|
387
|
+
if (options.getAccessToken) {
|
|
388
|
+
this.getToken = options.getAccessToken;
|
|
389
|
+
} else {
|
|
390
|
+
const tokenManager = new TokenManager(options.tokenUrl, options.tokenRefreshMarginMs ?? 3e4);
|
|
391
|
+
this.getToken = () => tokenManager.getToken();
|
|
392
|
+
}
|
|
393
|
+
this.connection = this.buildConnection();
|
|
394
|
+
this.registerHubHandlers();
|
|
395
|
+
this.registerUnloadHandler();
|
|
396
|
+
}
|
|
397
|
+
options;
|
|
398
|
+
baseUrl;
|
|
399
|
+
hubPath;
|
|
400
|
+
connection;
|
|
401
|
+
getToken;
|
|
402
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
403
|
+
retractedListeners = /* @__PURE__ */ new Set();
|
|
404
|
+
revokedListeners = /* @__PURE__ */ new Set();
|
|
405
|
+
typingListeners = /* @__PURE__ */ new Set();
|
|
406
|
+
presenceListeners = /* @__PURE__ */ new Set();
|
|
407
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
408
|
+
subscribedRooms = /* @__PURE__ */ new Set();
|
|
409
|
+
roomState = /* @__PURE__ */ new Map();
|
|
410
|
+
activeRoomHandles = /* @__PURE__ */ new Set();
|
|
411
|
+
unloadListener = null;
|
|
412
|
+
pendingConnect = null;
|
|
413
|
+
buildConnection() {
|
|
414
|
+
const url = `${this.baseUrl}${this.hubPath}`;
|
|
415
|
+
const httpOptions = {
|
|
416
|
+
accessTokenFactory: () => this.getToken()
|
|
417
|
+
};
|
|
418
|
+
if ((this.options.transport ?? "websockets") === "websockets") {
|
|
419
|
+
httpOptions.transport = import_signalr.HttpTransportType.WebSockets;
|
|
420
|
+
httpOptions.skipNegotiation = true;
|
|
421
|
+
}
|
|
422
|
+
if (this.options.connectionFactory) {
|
|
423
|
+
return this.options.connectionFactory(url, httpOptions);
|
|
424
|
+
}
|
|
425
|
+
const builder = new import_signalr.HubConnectionBuilder().withUrl(url, httpOptions).configureLogging(mapLogLevel(this.options.logLevel));
|
|
426
|
+
if (this.options.automaticReconnect !== false) {
|
|
427
|
+
builder.withAutomaticReconnect();
|
|
428
|
+
}
|
|
429
|
+
const connection = builder.build();
|
|
430
|
+
connection.serverTimeoutInMilliseconds = this.options.serverTimeoutMs ?? 9e4;
|
|
431
|
+
connection.keepAliveIntervalInMilliseconds = this.options.keepAliveIntervalMs ?? 15e3;
|
|
432
|
+
return connection;
|
|
433
|
+
}
|
|
434
|
+
registerHubHandlers() {
|
|
435
|
+
this.connection.on("ReceiveMessage", (message) => {
|
|
436
|
+
this.deliverMessage(message);
|
|
437
|
+
});
|
|
438
|
+
this.connection.on("UserTyping", (notification) => {
|
|
439
|
+
for (const listener of this.typingListeners) {
|
|
440
|
+
listener(notification);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
this.connection.on("PresenceUpdated", (delta) => {
|
|
444
|
+
for (const listener of this.presenceListeners) {
|
|
445
|
+
listener(delta);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
this.connection.on("MessageRetracted", (payload) => {
|
|
449
|
+
for (const listener of this.retractedListeners) {
|
|
450
|
+
listener(payload);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
this.connection.on("MembershipRevoked", (payload) => {
|
|
454
|
+
this.subscribedRooms.delete(payload.roomId);
|
|
455
|
+
const state = this.roomState.get(payload.roomId);
|
|
456
|
+
if (state) state.subscribed = false;
|
|
457
|
+
for (const listener of this.revokedListeners) {
|
|
458
|
+
listener(payload);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
this.connection.onclose(() => {
|
|
462
|
+
this.notifyStateChange("disconnected");
|
|
463
|
+
});
|
|
464
|
+
this.connection.onreconnecting(() => {
|
|
465
|
+
this.notifyStateChange("reconnecting");
|
|
466
|
+
});
|
|
467
|
+
this.connection.onreconnected(async () => {
|
|
468
|
+
this.notifyStateChange("connected");
|
|
469
|
+
for (const roomId of Array.from(this.subscribedRooms)) {
|
|
470
|
+
const resumeFrom = this.roomState.get(roomId)?.lastSeq ?? null;
|
|
471
|
+
try {
|
|
472
|
+
await this.connection.invoke("Subscribe", roomId);
|
|
473
|
+
} catch {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (this.options.gapFillOnReconnect === false || resumeFrom === null) continue;
|
|
477
|
+
try {
|
|
478
|
+
await this.gapFill(roomId, resumeFrom);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
registerUnloadHandler() {
|
|
485
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
486
|
+
const onUnload = () => {
|
|
487
|
+
this.disconnect().catch(() => {
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
window.addEventListener("beforeunload", onUnload);
|
|
491
|
+
window.addEventListener("pagehide", onUnload);
|
|
492
|
+
this.unloadListener = () => {
|
|
493
|
+
window.removeEventListener("beforeunload", onUnload);
|
|
494
|
+
window.removeEventListener("pagehide", onUnload);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
notifyStateChange(state) {
|
|
499
|
+
for (const listener of this.stateListeners) {
|
|
500
|
+
listener(state);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
getOrCreateRoomState(roomId) {
|
|
504
|
+
let state = this.roomState.get(roomId);
|
|
505
|
+
if (!state) {
|
|
506
|
+
state = { subscribed: false, lastSeq: null, seen: new BoundedSeqSet(1e3) };
|
|
507
|
+
this.roomState.set(roomId, state);
|
|
508
|
+
}
|
|
509
|
+
return state;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Applies seq dedupe/tracking (spec §6) and, if the message is new, notifies listeners.
|
|
513
|
+
* Used for both live `ReceiveMessage` delivery and history/gap-fill replay.
|
|
514
|
+
*/
|
|
515
|
+
deliverMessage(message) {
|
|
516
|
+
const state = this.getOrCreateRoomState(message.roomId);
|
|
517
|
+
if (state.seen.has(message.seq)) return;
|
|
518
|
+
state.seen.add(message.seq);
|
|
519
|
+
if (state.lastSeq === null || message.seq > state.lastSeq) {
|
|
520
|
+
state.lastSeq = message.seq;
|
|
521
|
+
}
|
|
522
|
+
for (const listener of this.messageListeners) {
|
|
523
|
+
listener(message);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async gapFill(roomId, fromSeq) {
|
|
527
|
+
const pageSize = this.options.gapFillPageSize ?? 100;
|
|
528
|
+
let afterSeq = fromSeq;
|
|
529
|
+
let hasMore = true;
|
|
530
|
+
while (hasMore) {
|
|
531
|
+
const page = await this.getHistory(roomId, { afterSeq, limit: pageSize });
|
|
532
|
+
if (page.messages.length === 0) break;
|
|
533
|
+
for (const message of page.messages) {
|
|
534
|
+
this.deliverMessage({ ...message, replayed: true });
|
|
535
|
+
afterSeq = Math.max(afterSeq, message.seq);
|
|
536
|
+
}
|
|
537
|
+
hasMore = page.hasMore;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
getConnectionState() {
|
|
541
|
+
switch (this.connection.state) {
|
|
542
|
+
case import_signalr.HubConnectionState.Connected:
|
|
543
|
+
return "connected";
|
|
544
|
+
case import_signalr.HubConnectionState.Connecting:
|
|
545
|
+
return "connecting";
|
|
546
|
+
case import_signalr.HubConnectionState.Reconnecting:
|
|
547
|
+
return "reconnecting";
|
|
548
|
+
case import_signalr.HubConnectionState.Disconnecting:
|
|
549
|
+
return "disconnecting";
|
|
550
|
+
default:
|
|
551
|
+
return "disconnected";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async connect() {
|
|
555
|
+
if (this.connection.state === import_signalr.HubConnectionState.Connected) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!this.pendingConnect) {
|
|
559
|
+
this.pendingConnect = (async () => {
|
|
560
|
+
this.notifyStateChange("connecting");
|
|
561
|
+
await this.connection.start();
|
|
562
|
+
this.notifyStateChange("connected");
|
|
563
|
+
})().finally(() => {
|
|
564
|
+
this.pendingConnect = null;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return this.pendingConnect;
|
|
568
|
+
}
|
|
569
|
+
async disconnect() {
|
|
570
|
+
if (this.connection.state === import_signalr.HubConnectionState.Disconnected) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
this.notifyStateChange("disconnecting");
|
|
574
|
+
await this.connection.stop();
|
|
575
|
+
this.subscribedRooms.clear();
|
|
576
|
+
this.notifyStateChange("disconnected");
|
|
577
|
+
}
|
|
578
|
+
room(roomId) {
|
|
579
|
+
const handle = new DotwireRoomHandle(roomId, this);
|
|
580
|
+
this.activeRoomHandles.add(handle);
|
|
581
|
+
return handle;
|
|
582
|
+
}
|
|
583
|
+
releaseRoomHandle(handle) {
|
|
584
|
+
this.activeRoomHandles.delete(handle);
|
|
585
|
+
}
|
|
586
|
+
async subscribe(roomId) {
|
|
587
|
+
if (this.connection.state !== import_signalr.HubConnectionState.Connected) {
|
|
588
|
+
await this.connect();
|
|
589
|
+
}
|
|
590
|
+
await this.connection.invoke("Subscribe", roomId);
|
|
591
|
+
this.subscribedRooms.add(roomId);
|
|
592
|
+
this.getOrCreateRoomState(roomId).subscribed = true;
|
|
593
|
+
}
|
|
594
|
+
async unsubscribe(roomId) {
|
|
595
|
+
if (this.connection.state === import_signalr.HubConnectionState.Connected) {
|
|
596
|
+
await this.connection.invoke("Unsubscribe", roomId);
|
|
597
|
+
}
|
|
598
|
+
this.subscribedRooms.delete(roomId);
|
|
599
|
+
const state = this.roomState.get(roomId);
|
|
600
|
+
if (state) state.subscribed = false;
|
|
601
|
+
}
|
|
602
|
+
async sendTyping(roomId, isTyping) {
|
|
603
|
+
if (this.connection.state !== import_signalr.HubConnectionState.Connected) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
await this.connection.invoke("Typing", roomId, isTyping);
|
|
607
|
+
}
|
|
608
|
+
async startTyping(roomId) {
|
|
609
|
+
return this.sendTyping(roomId, true);
|
|
610
|
+
}
|
|
611
|
+
async stopTyping(roomId) {
|
|
612
|
+
return this.sendTyping(roomId, false);
|
|
613
|
+
}
|
|
614
|
+
async sendMessage(roomId, content, timeoutMs) {
|
|
615
|
+
const timeout = timeoutMs ?? this.options.requestTimeoutMs ?? 15e3;
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
618
|
+
try {
|
|
619
|
+
let response;
|
|
620
|
+
if (this.options.sendUrl) {
|
|
621
|
+
response = await fetch(this.options.sendUrl, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "Content-Type": "application/json" },
|
|
624
|
+
credentials: "include",
|
|
625
|
+
body: JSON.stringify({ roomId, content }),
|
|
626
|
+
signal: controller.signal
|
|
627
|
+
});
|
|
628
|
+
} else {
|
|
629
|
+
const token = await this.getToken();
|
|
630
|
+
response = await fetch(`${this.baseUrl}/rooms/${roomId}/messages`, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: {
|
|
633
|
+
"Content-Type": "application/json",
|
|
634
|
+
Authorization: `Bearer ${token}`
|
|
635
|
+
},
|
|
636
|
+
body: JSON.stringify({ content }),
|
|
637
|
+
signal: controller.signal
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (!response.ok) {
|
|
641
|
+
throw await toDotwireClientError(response);
|
|
642
|
+
}
|
|
643
|
+
return await response.json();
|
|
644
|
+
} finally {
|
|
645
|
+
clearTimeout(timer);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async getHistory(roomId, options = {}) {
|
|
649
|
+
const token = await this.getToken();
|
|
650
|
+
const params = new URLSearchParams();
|
|
651
|
+
if (options.afterSeq !== void 0) params.set("afterSeq", options.afterSeq.toString());
|
|
652
|
+
if (options.beforeSeq !== void 0) params.set("beforeSeq", options.beforeSeq.toString());
|
|
653
|
+
if (options.limit !== void 0) params.set("limit", options.limit.toString());
|
|
654
|
+
const qs = params.toString();
|
|
655
|
+
const url = `${this.baseUrl}/rooms/${roomId}/messages${qs ? `?${qs}` : ""}`;
|
|
656
|
+
const timeout = options.timeoutMs ?? this.options.requestTimeoutMs ?? 15e3;
|
|
657
|
+
const controller = new AbortController();
|
|
658
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
659
|
+
try {
|
|
660
|
+
const response = await fetch(url, {
|
|
661
|
+
method: "GET",
|
|
662
|
+
headers: {
|
|
663
|
+
Authorization: `Bearer ${token}`
|
|
664
|
+
},
|
|
665
|
+
signal: controller.signal
|
|
666
|
+
});
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
throw await toDotwireClientError(response);
|
|
669
|
+
}
|
|
670
|
+
const body = await response.json();
|
|
671
|
+
const messages = body.messages.map((m) => ({
|
|
672
|
+
roomId: body.roomId,
|
|
673
|
+
seq: m.seq,
|
|
674
|
+
senderId: m.senderId,
|
|
675
|
+
time: m.time,
|
|
676
|
+
content: m.content
|
|
677
|
+
}));
|
|
678
|
+
return { messages, hasMore: body.hasMore };
|
|
679
|
+
} finally {
|
|
680
|
+
clearTimeout(timer);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async getParticipants(roomId, timeoutMs) {
|
|
684
|
+
const token = await this.getToken();
|
|
685
|
+
const controller = new AbortController();
|
|
686
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.options.requestTimeoutMs ?? 15e3);
|
|
687
|
+
try {
|
|
688
|
+
const response = await fetch(`${this.baseUrl}/rooms/${roomId}/participants`, {
|
|
689
|
+
method: "GET",
|
|
690
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
691
|
+
signal: controller.signal
|
|
692
|
+
});
|
|
693
|
+
if (!response.ok) throw await toDotwireClientError(response);
|
|
694
|
+
return await response.json();
|
|
695
|
+
} finally {
|
|
696
|
+
clearTimeout(timer);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
onMessage(handler) {
|
|
700
|
+
this.messageListeners.add(handler);
|
|
701
|
+
return () => {
|
|
702
|
+
this.messageListeners.delete(handler);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
onSystemMessage(handler) {
|
|
706
|
+
return this.onMessage((message) => {
|
|
707
|
+
if (isSystemSender(message.senderId)) {
|
|
708
|
+
handler(message);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
onUserMessage(handler) {
|
|
713
|
+
return this.onMessage((message) => {
|
|
714
|
+
if (!isSystemSender(message.senderId)) {
|
|
715
|
+
handler(message);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
onMessageRetracted(handler) {
|
|
720
|
+
this.retractedListeners.add(handler);
|
|
721
|
+
return () => {
|
|
722
|
+
this.retractedListeners.delete(handler);
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
onMembershipRevoked(handler) {
|
|
726
|
+
this.revokedListeners.add(handler);
|
|
727
|
+
return () => {
|
|
728
|
+
this.revokedListeners.delete(handler);
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
onTyping(handler) {
|
|
732
|
+
this.typingListeners.add(handler);
|
|
733
|
+
return () => {
|
|
734
|
+
this.typingListeners.delete(handler);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
onPresence(handler) {
|
|
738
|
+
this.presenceListeners.add(handler);
|
|
739
|
+
return () => {
|
|
740
|
+
this.presenceListeners.delete(handler);
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
onStateChange(handler) {
|
|
744
|
+
this.stateListeners.add(handler);
|
|
745
|
+
return () => {
|
|
746
|
+
this.stateListeners.delete(handler);
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
dispose() {
|
|
750
|
+
if (this.unloadListener) {
|
|
751
|
+
this.unloadListener();
|
|
752
|
+
this.unloadListener = null;
|
|
753
|
+
}
|
|
754
|
+
for (const room of this.activeRoomHandles) {
|
|
755
|
+
room.dispose();
|
|
756
|
+
}
|
|
757
|
+
this.activeRoomHandles.clear();
|
|
758
|
+
this.disconnect().catch(() => {
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
function mapLogLevel(level) {
|
|
763
|
+
switch (level) {
|
|
764
|
+
case "error":
|
|
765
|
+
return import_signalr.LogLevel.Error;
|
|
766
|
+
case "warning":
|
|
767
|
+
return import_signalr.LogLevel.Warning;
|
|
768
|
+
case "information":
|
|
769
|
+
return import_signalr.LogLevel.Information;
|
|
770
|
+
case "debug":
|
|
771
|
+
return import_signalr.LogLevel.Debug;
|
|
772
|
+
case "none":
|
|
773
|
+
default:
|
|
774
|
+
return import_signalr.LogLevel.None;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
778
|
+
0 && (module.exports = {
|
|
779
|
+
DotwireClient,
|
|
780
|
+
DotwireClientError,
|
|
781
|
+
DotwireRoomHandle,
|
|
782
|
+
isSystemSender,
|
|
783
|
+
toDotwireClientError
|
|
784
|
+
});
|
|
785
|
+
//# sourceMappingURL=index.cjs.map
|