@kispi/chat 0.2.1 → 0.2.3
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.en.md +68 -4
- package/README.md +65 -4
- package/dist/index.cjs +239 -17
- package/dist/index.d.cts +165 -4
- package/dist/index.d.ts +165 -4
- package/dist/index.js +239 -17
- package/dist/server/index.cjs +2 -0
- package/dist/server/index.d.cts +39 -1
- package/dist/server/index.d.ts +39 -1
- package/dist/server/index.js +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -147,6 +147,25 @@ var Timeline = class {
|
|
|
147
147
|
shared = false;
|
|
148
148
|
/** The `loadOlder` in flight, so a scroll handler firing twice asks once. */
|
|
149
149
|
older;
|
|
150
|
+
/**
|
|
151
|
+
* The lowest seq the room still keeps (`minSeq` from the subscribe ack;
|
|
152
|
+
* 1 until told). Below it there is nothing to read, whatever seq says.
|
|
153
|
+
*/
|
|
154
|
+
floor = 1;
|
|
155
|
+
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
156
|
+
exhausted = false;
|
|
157
|
+
/**
|
|
158
|
+
* Presses this client has made and the server has not answered yet, one
|
|
159
|
+
* entry per (message, emoji) pair, each remembering the last value the
|
|
160
|
+
* **server** gave for that pair.
|
|
161
|
+
*
|
|
162
|
+
* 되돌릴 자리로 "누르기 직전 화면값"을 쓰면 안 된다. 눌렀다 곧바로 취소하고
|
|
163
|
+
* 두 거절이 차례로 오면, 두 번째 거절이 첫 번째의 낙관값 — 서버가 한 번도
|
|
164
|
+
* 가진 적 없는 값 — 을 화면에 되살린다. 마지막으로 서버가 말한 값만이
|
|
165
|
+
* 되돌릴 자리다.
|
|
166
|
+
*/
|
|
167
|
+
presses = /* @__PURE__ */ new Map();
|
|
168
|
+
pressSeq = 0;
|
|
150
169
|
constructor(options) {
|
|
151
170
|
this.options = options;
|
|
152
171
|
}
|
|
@@ -155,6 +174,22 @@ var Timeline = class {
|
|
|
155
174
|
this.shared = true;
|
|
156
175
|
return this.items;
|
|
157
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether `loadOlder()` would find anything, answered without asking.
|
|
179
|
+
*
|
|
180
|
+
* `loadOlder()`의 `hasMore`와 같은 판정이다: 맨 위 행이 방의 보존 하한
|
|
181
|
+
* (`minSeq`, seq는 1부터 구멍 없이 붙는다)보다 위에 있고, 앞선 `loadOlder`가
|
|
182
|
+
* 덜 찬 페이지로 끝을 알린 적이 없으면 더 있다. 첫 `loadOlder()` 전에 "이전
|
|
183
|
+
* 메시지 더 보기"를 그릴지 정할 수 있게 한다. 목록이 비었으면 false다.
|
|
184
|
+
*/
|
|
185
|
+
get hasOlder() {
|
|
186
|
+
const first = this.items[0];
|
|
187
|
+
return !this.exhausted && first !== void 0 && first.seq > this.floor;
|
|
188
|
+
}
|
|
189
|
+
/** Records the room's retention floor (`minSeq`). */
|
|
190
|
+
setFloor(minSeq) {
|
|
191
|
+
this.floor = Math.max(1, minSeq);
|
|
192
|
+
}
|
|
158
193
|
/** The highest seq this timeline holds, hole or no hole. */
|
|
159
194
|
get highestSeq() {
|
|
160
195
|
return this.items.at(-1)?.seq ?? 0;
|
|
@@ -198,7 +233,9 @@ var Timeline = class {
|
|
|
198
233
|
this.epoch++;
|
|
199
234
|
this.fills.clear();
|
|
200
235
|
this.pendingDeletes.clear();
|
|
236
|
+
this.presses.clear();
|
|
201
237
|
this.items = [];
|
|
238
|
+
this.exhausted = false;
|
|
202
239
|
this.shared = false;
|
|
203
240
|
this.changed();
|
|
204
241
|
}
|
|
@@ -224,8 +261,10 @@ var Timeline = class {
|
|
|
224
261
|
this.epoch++;
|
|
225
262
|
this.fills.clear();
|
|
226
263
|
const page = [...messages].sort((a, b) => a.seq - b.seq);
|
|
264
|
+
for (const m of page) this.serverRendered(m.id);
|
|
227
265
|
const top = page.at(-1)?.seq ?? 0;
|
|
228
266
|
this.items = [...page, ...this.items.filter((m) => m.seq > top)];
|
|
267
|
+
this.exhausted = false;
|
|
229
268
|
this.shared = false;
|
|
230
269
|
for (const m of this.items) {
|
|
231
270
|
if (this.pendingDeletes.has(m.id)) this.applyDelete(m.id);
|
|
@@ -276,20 +315,84 @@ var Timeline = class {
|
|
|
276
315
|
*
|
|
277
316
|
* The server sends the new `count` with the event, so this is a
|
|
278
317
|
* replacement rather than an increment: two clients reacting at once
|
|
279
|
-
* cannot drift the way `+1`/`-1` would.
|
|
318
|
+
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
319
|
+
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
320
|
+
* `myReactions` alone (somebody else's event).
|
|
321
|
+
*
|
|
322
|
+
* This is the **authoritative** entry point: a value the server sent.
|
|
323
|
+
* So a press still waiting on an answer for the same pair is finished
|
|
324
|
+
* here -- a refusal landing afterwards must not push this value back
|
|
325
|
+
* to the older one it was going to restore.
|
|
326
|
+
*/
|
|
327
|
+
reaction(messageId, emoji, count, mine) {
|
|
328
|
+
if (mine !== void 0) this.presses.delete(pairKey(messageId, emoji));
|
|
329
|
+
this.applyReaction(messageId, emoji, count, mine);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Applies this client's own react/unreact to `myReactions` **before the
|
|
333
|
+
* frame goes out**, and returns the press so its answer can settle it.
|
|
334
|
+
*
|
|
335
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로, 미리
|
|
336
|
+
* 적용해도 ack가 새로 알려 줄 것이 없다. `count`는 일부러 건드리지 않는다 —
|
|
337
|
+
* 그 사이 남이 같은 이모지를 누르면 로컬 +1/-1은 서버가 보낼 수와 다르고,
|
|
338
|
+
* 그 차이를 고칠 이벤트는 이미 지나갔다.
|
|
339
|
+
*
|
|
340
|
+
* Undefined when the row is not held: there is nothing to show now and
|
|
341
|
+
* nothing to put back later.
|
|
342
|
+
*/
|
|
343
|
+
pressReaction(messageId, emoji, mine) {
|
|
344
|
+
const at = this.items.findIndex((m) => m.id === messageId);
|
|
345
|
+
if (at === -1) return void 0;
|
|
346
|
+
const key = pairKey(messageId, emoji);
|
|
347
|
+
const base = this.presses.get(key)?.base ?? (this.items[at].myReactions ?? []).includes(emoji);
|
|
348
|
+
const press = { messageId, emoji, mine, seq: ++this.pressSeq };
|
|
349
|
+
this.presses.set(key, { seq: press.seq, base });
|
|
350
|
+
this.applyReaction(messageId, emoji, void 0, mine);
|
|
351
|
+
return press;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Settles a press with what the server said.
|
|
355
|
+
*
|
|
356
|
+
* `accepted` re-asserts the value the press asked for; a refusal puts
|
|
357
|
+
* the last server-given value back. Either way it does nothing once
|
|
358
|
+
* this press no longer owns the pair -- a newer press took it over
|
|
359
|
+
* (and that one's answer decides), or something authoritative already
|
|
360
|
+
* landed on it (a `reaction.*` event carrying this user's id, a
|
|
361
|
+
* `reload()`, a reconnect's catch-up page). Reverting there would
|
|
362
|
+
* resurrect a value older than what is on screen.
|
|
280
363
|
*/
|
|
281
|
-
|
|
364
|
+
settleReaction(press, accepted) {
|
|
365
|
+
const key = pairKey(press.messageId, press.emoji);
|
|
366
|
+
const held = this.presses.get(key);
|
|
367
|
+
if (held === void 0 || held.seq !== press.seq) return;
|
|
368
|
+
this.presses.delete(key);
|
|
369
|
+
this.applyReaction(press.messageId, press.emoji, void 0, accepted ? press.mine : held.base);
|
|
370
|
+
}
|
|
371
|
+
applyReaction(messageId, emoji, count, mine) {
|
|
282
372
|
const at = this.items.findIndex((m) => m.id === messageId);
|
|
283
373
|
if (at === -1) return;
|
|
284
374
|
const current = this.items[at];
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
375
|
+
let reactions = current.reactions;
|
|
376
|
+
if (count !== void 0) {
|
|
377
|
+
const existing = current.reactions ?? [];
|
|
378
|
+
const slot = existing.findIndex((r) => r.emoji === emoji);
|
|
379
|
+
if (count <= 0) reactions = existing.filter((r) => r.emoji !== emoji);
|
|
380
|
+
else if (slot === -1) reactions = [...existing, { emoji, count }];
|
|
381
|
+
else reactions = existing.map((r, i) => i === slot ? { emoji, count } : r);
|
|
382
|
+
}
|
|
383
|
+
let myReactions = current.myReactions;
|
|
384
|
+
if (mine !== void 0) {
|
|
385
|
+
const held = current.myReactions ?? [];
|
|
386
|
+
const has = held.includes(emoji);
|
|
387
|
+
if (mine && !has) myReactions = [...held, emoji];
|
|
388
|
+
else if (!mine && has) myReactions = held.filter((e) => e !== emoji);
|
|
389
|
+
}
|
|
390
|
+
if (reactions === current.reactions && myReactions === current.myReactions) return;
|
|
391
|
+
const next = { ...current };
|
|
392
|
+
if (reactions !== void 0) next.reactions = reactions;
|
|
393
|
+
if (myReactions !== void 0) next.myReactions = myReactions;
|
|
291
394
|
this.own();
|
|
292
|
-
this.items[at] =
|
|
395
|
+
this.items[at] = next;
|
|
293
396
|
this.changed();
|
|
294
397
|
}
|
|
295
398
|
/** Applies a `thread.updated` to the root message's aggregate. */
|
|
@@ -326,14 +429,16 @@ var Timeline = class {
|
|
|
326
429
|
}
|
|
327
430
|
async readOlder(limit) {
|
|
328
431
|
const first = this.items[0];
|
|
329
|
-
if (first === void 0 || first.seq <=
|
|
432
|
+
if (first === void 0 || first.seq <= this.floor) return { messages: [], hasMore: false };
|
|
330
433
|
const epoch = this.epoch;
|
|
331
434
|
const page = await this.options.fetchRange({ after: 0, before: first.seq, limit });
|
|
332
435
|
if (this.epoch !== epoch) return { messages: [], hasMore: true };
|
|
333
436
|
for (const m of page) this.insert(m, false);
|
|
334
437
|
if (page.length > 0) this.changed();
|
|
335
438
|
const top = page[0];
|
|
336
|
-
|
|
439
|
+
const hasMore = page.length >= limit && top !== void 0 && top.seq > this.floor;
|
|
440
|
+
this.exhausted = !hasMore;
|
|
441
|
+
return { messages: page, hasMore };
|
|
337
442
|
}
|
|
338
443
|
/**
|
|
339
444
|
* Fetches everything between what we have and `upTo`.
|
|
@@ -380,6 +485,7 @@ var Timeline = class {
|
|
|
380
485
|
}
|
|
381
486
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
382
487
|
insert(message, notify = true) {
|
|
488
|
+
this.serverRendered(message.id);
|
|
383
489
|
const pending = this.pendingDeletes.has(message.id);
|
|
384
490
|
if (pending) this.pendingDeletes.delete(message.id);
|
|
385
491
|
const withDelete = pending ? emptied(message) : message;
|
|
@@ -404,6 +510,21 @@ var Timeline = class {
|
|
|
404
510
|
this.changed();
|
|
405
511
|
return true;
|
|
406
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Forgets the presses on a row the server has just re-rendered.
|
|
515
|
+
*
|
|
516
|
+
* 히스토리 응답의 행은 뷰어별 `myReactions`를 싣고 오므로 그 값이 서버의
|
|
517
|
+
* 값이다. 누름을 남겨 두면 그 뒤에 온 거절이 더 새로운 진실 위에 옛 값을
|
|
518
|
+
* 덮어쓴다. 행 전체 단위인 것은 페이지가 그 행의 이모지 전부를 다시 그려
|
|
519
|
+
* 주기 때문이다.
|
|
520
|
+
*/
|
|
521
|
+
serverRendered(messageId) {
|
|
522
|
+
if (this.presses.size === 0) return;
|
|
523
|
+
const prefix = `${messageId}\0`;
|
|
524
|
+
for (const key of this.presses.keys()) {
|
|
525
|
+
if (key.startsWith(prefix)) this.presses.delete(key);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
407
528
|
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
408
529
|
own() {
|
|
409
530
|
if (!this.shared) return;
|
|
@@ -418,6 +539,9 @@ var Timeline = class {
|
|
|
418
539
|
function emptied(message) {
|
|
419
540
|
return { ...message, body: {}, deletedAt: message.deletedAt ?? Date.now() };
|
|
420
541
|
}
|
|
542
|
+
function pairKey(messageId, emoji) {
|
|
543
|
+
return `${messageId}\0${emoji}`;
|
|
544
|
+
}
|
|
421
545
|
function createTimeline(options) {
|
|
422
546
|
return new Timeline(options);
|
|
423
547
|
}
|
|
@@ -481,6 +605,17 @@ var Room = class extends Emitter {
|
|
|
481
605
|
onChange: (messages) => this.emit("messages", messages)
|
|
482
606
|
});
|
|
483
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Whether older history exists above the first message held -- what
|
|
610
|
+
* `loadOlder()` would report as `hasMore`, known before calling it.
|
|
611
|
+
*
|
|
612
|
+
* Derived from the first row's seq against the room's retention floor
|
|
613
|
+
* (the subscribe ack's `minSeq`), and false once a `loadOlder()` came
|
|
614
|
+
* back short. False while nothing is loaded. Re-read it on `messages`.
|
|
615
|
+
*/
|
|
616
|
+
get hasOlder() {
|
|
617
|
+
return this.timeline.hasOlder;
|
|
618
|
+
}
|
|
484
619
|
/** Everything this client knows about the room, in seq order. A new array whenever it changes. */
|
|
485
620
|
get messages() {
|
|
486
621
|
return this.timeline.messages;
|
|
@@ -559,6 +694,7 @@ var Room = class extends Emitter {
|
|
|
559
694
|
this.id = ack.roomId;
|
|
560
695
|
this.chat.registerRoomId(ack.roomId, this);
|
|
561
696
|
this.lastSeq = ack.lastSeq;
|
|
697
|
+
if (typeof ack.minSeq === "number") this.timeline.setFloor(ack.minSeq);
|
|
562
698
|
if (ack.presence !== void 0) this.presence = ack.presence;
|
|
563
699
|
if (attempt.wasReset) {
|
|
564
700
|
await this.loadRecent(current);
|
|
@@ -602,12 +738,52 @@ var Room = class extends Emitter {
|
|
|
602
738
|
});
|
|
603
739
|
return page.messages;
|
|
604
740
|
}
|
|
605
|
-
/**
|
|
741
|
+
/**
|
|
742
|
+
* Adds a reaction. Idempotent, like the frame.
|
|
743
|
+
*
|
|
744
|
+
* The row's `myReactions` moves **before the frame goes out** and rolls
|
|
745
|
+
* back if the server refuses; its count stays the server's. See
|
|
746
|
+
* `toggleReaction`.
|
|
747
|
+
*/
|
|
606
748
|
async react(messageId, emoji) {
|
|
607
|
-
await this.
|
|
749
|
+
await this.toggleReaction(messageId, emoji, true);
|
|
608
750
|
}
|
|
751
|
+
/** Removes a reaction. Idempotent, and optimistic the same way `react` is. */
|
|
609
752
|
async unreact(messageId, emoji) {
|
|
610
|
-
await this.
|
|
753
|
+
await this.toggleReaction(messageId, emoji, false);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Moves `myReactions` now, sends the frame after.
|
|
757
|
+
*
|
|
758
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로 ack를
|
|
759
|
+
* 기다릴 이유가 없다 — 기다리면 누른 느낌이 왕복 시간만큼 늦는다. 소비자가
|
|
760
|
+
* 자기 낙관 계층을 덧대면 이 필드의 주인이 둘이 되므로, SDK가 한다.
|
|
761
|
+
* **발행은 다르다**: `before_publish` 훅이 본문을 바꿀 수 있어 미리 그릴 값이
|
|
762
|
+
* 로컬에 없다. 그래서 `send()`는 낙관적이지 않다.
|
|
763
|
+
*
|
|
764
|
+
* 개수는 옮기지 않는다. ack가 오기 전에 남이 같은 이모지를 누르면 로컬
|
|
765
|
+
* +1/-1은 서버가 보낸 수와 다른 값이 되고, 그것을 고칠 이벤트는 이미
|
|
766
|
+
* 지나갔다. 또 ack와 방 프레임은 다른 길로 와 순서가 없으므로, 늦게 온 내
|
|
767
|
+
* ack(count 1)가 앞서 온 남의 이벤트(count 2)를 되돌린다.
|
|
768
|
+
*
|
|
769
|
+
* 거절·타임아웃·끊김이면 되돌리지만, 그 사이 더 새로운 값이 자리를 차지했다면
|
|
770
|
+
* 두고 나온다(`Timeline.settleReaction`).
|
|
771
|
+
*/
|
|
772
|
+
async toggleReaction(messageId, emoji, mine) {
|
|
773
|
+
const press = this.timeline.pressReaction(messageId, emoji, mine);
|
|
774
|
+
try {
|
|
775
|
+
await this.chat.send("react", {
|
|
776
|
+
roomId: await this.resolveId("reacting"),
|
|
777
|
+
messageId,
|
|
778
|
+
emoji,
|
|
779
|
+
op: mine ? "add" : "remove"
|
|
780
|
+
});
|
|
781
|
+
} catch (err) {
|
|
782
|
+
if (press !== void 0) this.timeline.settleReaction(press, false);
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
if (press !== void 0) this.timeline.settleReaction(press, true);
|
|
786
|
+
else this.timeline.reaction(messageId, emoji, void 0, mine);
|
|
611
787
|
}
|
|
612
788
|
/**
|
|
613
789
|
* Moves this user's read cursor.
|
|
@@ -802,7 +978,9 @@ var Room = class extends Emitter {
|
|
|
802
978
|
case "reaction.added":
|
|
803
979
|
case "reaction.removed": {
|
|
804
980
|
const r = data;
|
|
805
|
-
this.
|
|
981
|
+
const me = this.chat.user?.id;
|
|
982
|
+
const mine = me !== void 0 && r.userId === me ? type === "reaction.added" : void 0;
|
|
983
|
+
this.timeline.reaction(r.messageId, r.emoji, r.count, mine);
|
|
806
984
|
break;
|
|
807
985
|
}
|
|
808
986
|
case "thread.updated": {
|
|
@@ -1051,6 +1229,48 @@ var ChatClient = class extends Emitter {
|
|
|
1051
1229
|
});
|
|
1052
1230
|
return this.opening;
|
|
1053
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Re-authenticates: calls `token()` again and replaces the socket, keeping
|
|
1234
|
+
* every room handle and every subscription.
|
|
1235
|
+
*
|
|
1236
|
+
* For when the identity the server holds has to change mid-session --
|
|
1237
|
+
* a nickname change, a new avatar, fresh claims. The server reads the
|
|
1238
|
+
* token once, at `auth`, so `sender.name` on the next message is the old
|
|
1239
|
+
* one until the connection is re-authenticated. `close()` + `connect()`
|
|
1240
|
+
* does that too, but passes through `closed` (a consumer's "you are
|
|
1241
|
+
* offline" screen) and tears down the page listeners.
|
|
1242
|
+
*
|
|
1243
|
+
* States: `open` → `reconnecting` → `open`, never `closed`. Rooms
|
|
1244
|
+
* resubscribe from what they hold and catch up the gap, as after any
|
|
1245
|
+
* drop; `messages` is kept. Frames awaiting an ack on the old socket are
|
|
1246
|
+
* rejected with `closed`. `chat.user` is the new identity once this
|
|
1247
|
+
* resolves. If `token()` fails the client keeps retrying in the
|
|
1248
|
+
* background like any reconnect (and this rejects); a `ChatError` from
|
|
1249
|
+
* `token()` stops it at `closed`, as on connect.
|
|
1250
|
+
*
|
|
1251
|
+
* On a client that is not connected (never connected, or `close()`d) it
|
|
1252
|
+
* is `connect()`.
|
|
1253
|
+
*/
|
|
1254
|
+
async reconnect() {
|
|
1255
|
+
if (this.opening !== void 0) await this.opening.catch(() => {
|
|
1256
|
+
});
|
|
1257
|
+
const socket = this.socket;
|
|
1258
|
+
if (socket === void 0 || this.state !== "open") return this.connect();
|
|
1259
|
+
this.clearTimers();
|
|
1260
|
+
this.retired.add(socket);
|
|
1261
|
+
this.socket = void 0;
|
|
1262
|
+
this.failPending(new ChatError("closed", "the connection was replaced by reconnect()"));
|
|
1263
|
+
try {
|
|
1264
|
+
socket.close(1e3, "client reconnecting");
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1267
|
+
this.opening = this.openOnce("reconnecting").finally(() => {
|
|
1268
|
+
this.opening = void 0;
|
|
1269
|
+
});
|
|
1270
|
+
return this.opening;
|
|
1271
|
+
}
|
|
1272
|
+
/** Sockets `reconnect()` replaced: their late events are not this client's any more. */
|
|
1273
|
+
retired = /* @__PURE__ */ new WeakSet();
|
|
1054
1274
|
/**
|
|
1055
1275
|
* Closes for good.
|
|
1056
1276
|
*
|
|
@@ -1164,8 +1384,8 @@ var ChatClient = class extends Emitter {
|
|
|
1164
1384
|
this.state = next;
|
|
1165
1385
|
this.emit("state", next);
|
|
1166
1386
|
}
|
|
1167
|
-
async openOnce() {
|
|
1168
|
-
this.setState(
|
|
1387
|
+
async openOnce(state = this.attempt === 0 ? "connecting" : "reconnecting") {
|
|
1388
|
+
this.setState(state);
|
|
1169
1389
|
let authData;
|
|
1170
1390
|
try {
|
|
1171
1391
|
authData = await this.authData();
|
|
@@ -1191,6 +1411,7 @@ var ChatClient = class extends Emitter {
|
|
|
1191
1411
|
else reject(err);
|
|
1192
1412
|
};
|
|
1193
1413
|
socket.addEventListener("message", (ev) => {
|
|
1414
|
+
if (this.retired.has(socket)) return;
|
|
1194
1415
|
const frame = JSON.parse(String(ev.data));
|
|
1195
1416
|
if (frame.type === "hello") {
|
|
1196
1417
|
this.onHello(frame.data);
|
|
@@ -1211,6 +1432,7 @@ var ChatClient = class extends Emitter {
|
|
|
1211
1432
|
this.onFrame(frame);
|
|
1212
1433
|
});
|
|
1213
1434
|
socket.addEventListener("close", () => {
|
|
1435
|
+
if (this.retired.has(socket)) return;
|
|
1214
1436
|
if (this.socket !== void 0 && this.socket !== socket) return;
|
|
1215
1437
|
this.clearTimers();
|
|
1216
1438
|
this.socket = void 0;
|
package/dist/server/index.cjs
CHANGED
|
@@ -274,6 +274,7 @@ function createChatServer(options) {
|
|
|
274
274
|
custom: async (roomId, payload) => {
|
|
275
275
|
await rest.call("POST", `/v1/rooms/${roomId}/custom`, { payload });
|
|
276
276
|
},
|
|
277
|
+
presence: (roomId, o) => rest.call("GET", `/v1/rooms/${roomId}/presence${rest.query({ full: o?.full ? "true" : void 0 })}`),
|
|
277
278
|
members: {
|
|
278
279
|
list: (roomId, o) => rest.call("GET", `/v1/rooms/${roomId}/members${rest.query({ cursor: o?.cursor, limit: o?.limit })}`),
|
|
279
280
|
add: async (roomId, userId, role) => {
|
|
@@ -320,6 +321,7 @@ function createChatServer(options) {
|
|
|
320
321
|
// the messages left standing. Nothing failed; the caller was told
|
|
321
322
|
// it had worked.
|
|
322
323
|
delete: (userId, o) => rest.call("DELETE", `/v1/users/${userId}${rest.query({ purge_messages: o?.purgeMessages ? "true" : void 0 })}`),
|
|
324
|
+
purgeMessages: (userId) => rest.call("DELETE", `/v1/users/${userId}/messages`),
|
|
323
325
|
revokeTokens: async (userId) => {
|
|
324
326
|
await rest.call("POST", `/v1/users/${userId}/revoke-tokens`);
|
|
325
327
|
},
|
package/dist/server/index.d.cts
CHANGED
|
@@ -109,6 +109,25 @@ type ServerSendInput = {
|
|
|
109
109
|
replyTo?: string;
|
|
110
110
|
threadId?: string;
|
|
111
111
|
};
|
|
112
|
+
type RoomPresence = {
|
|
113
|
+
count: number;
|
|
114
|
+
/** Only with `full: true`. */
|
|
115
|
+
users?: {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
avatar?: string;
|
|
119
|
+
meta?: Record<string, unknown>;
|
|
120
|
+
}[];
|
|
121
|
+
capped?: boolean;
|
|
122
|
+
};
|
|
123
|
+
/** What `users.purgeMessages` and the withdrawal purge report. */
|
|
124
|
+
type PurgeMessagesResult = {
|
|
125
|
+
userId: string;
|
|
126
|
+
/** Messages this call turned into tombstones. 0 on a repeat call after a finished one. */
|
|
127
|
+
purgedMessages: number;
|
|
128
|
+
/** The per-call cap was hit: **call again** until this is false. */
|
|
129
|
+
purgeCapped: boolean;
|
|
130
|
+
};
|
|
112
131
|
type GuestInput = Omit<TokenInput, 'userId'> & {
|
|
113
132
|
/** What `guest()` returned last time, as the browser kept it. Missing or invalid means a new guest. */
|
|
114
133
|
credential?: string | null;
|
|
@@ -169,6 +188,14 @@ type ChatServer = {
|
|
|
169
188
|
* in one frame). `sk_` only, like every other route on this object.
|
|
170
189
|
*/
|
|
171
190
|
custom: (roomId: string, payload: Record<string, unknown>) => Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Who is in the room right now. `{count}` by default; `full: true` adds
|
|
193
|
+
* `users` (at most 1000, `capped: true` when cut). An empty room is
|
|
194
|
+
* `{count: 0}`, not a 404.
|
|
195
|
+
*/
|
|
196
|
+
presence: (roomId: string, options?: {
|
|
197
|
+
full?: boolean;
|
|
198
|
+
}) => Promise<RoomPresence>;
|
|
172
199
|
members: {
|
|
173
200
|
list: (roomId: string, options?: {
|
|
174
201
|
cursor?: string;
|
|
@@ -216,6 +243,17 @@ type ChatServer = {
|
|
|
216
243
|
avatar?: string;
|
|
217
244
|
meta?: Record<string, unknown>;
|
|
218
245
|
}) => Promise<unknown>;
|
|
246
|
+
/**
|
|
247
|
+
* Moderation: deletes every live message this user sent in the app
|
|
248
|
+
* (tombstones -- seq stays gap-free, clients get `message.deleted`),
|
|
249
|
+
* **without** withdrawing them. The name stays on the tombstones.
|
|
250
|
+
*
|
|
251
|
+
* Ban first (`ban`), then call this: a message posted while it runs is
|
|
252
|
+
* not in its listing. Bounded per call -- loop while `purgeCapped`.
|
|
253
|
+
* A withdrawn user is 404; their messages go with
|
|
254
|
+
* `delete(userId, { purgeMessages: true })`.
|
|
255
|
+
*/
|
|
256
|
+
purgeMessages: (userId: string) => Promise<PurgeMessagesResult>;
|
|
219
257
|
/** Withdrawal: anonymises rather than deleting rows. */
|
|
220
258
|
delete: (userId: string, options?: {
|
|
221
259
|
purgeMessages?: boolean;
|
|
@@ -256,4 +294,4 @@ type ChatServer = {
|
|
|
256
294
|
};
|
|
257
295
|
declare function createChatServer(options: ChatServerOptions): ChatServer;
|
|
258
296
|
|
|
259
|
-
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
|
297
|
+
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type PurgeMessagesResult, type RoomPresence, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -109,6 +109,25 @@ type ServerSendInput = {
|
|
|
109
109
|
replyTo?: string;
|
|
110
110
|
threadId?: string;
|
|
111
111
|
};
|
|
112
|
+
type RoomPresence = {
|
|
113
|
+
count: number;
|
|
114
|
+
/** Only with `full: true`. */
|
|
115
|
+
users?: {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
avatar?: string;
|
|
119
|
+
meta?: Record<string, unknown>;
|
|
120
|
+
}[];
|
|
121
|
+
capped?: boolean;
|
|
122
|
+
};
|
|
123
|
+
/** What `users.purgeMessages` and the withdrawal purge report. */
|
|
124
|
+
type PurgeMessagesResult = {
|
|
125
|
+
userId: string;
|
|
126
|
+
/** Messages this call turned into tombstones. 0 on a repeat call after a finished one. */
|
|
127
|
+
purgedMessages: number;
|
|
128
|
+
/** The per-call cap was hit: **call again** until this is false. */
|
|
129
|
+
purgeCapped: boolean;
|
|
130
|
+
};
|
|
112
131
|
type GuestInput = Omit<TokenInput, 'userId'> & {
|
|
113
132
|
/** What `guest()` returned last time, as the browser kept it. Missing or invalid means a new guest. */
|
|
114
133
|
credential?: string | null;
|
|
@@ -169,6 +188,14 @@ type ChatServer = {
|
|
|
169
188
|
* in one frame). `sk_` only, like every other route on this object.
|
|
170
189
|
*/
|
|
171
190
|
custom: (roomId: string, payload: Record<string, unknown>) => Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Who is in the room right now. `{count}` by default; `full: true` adds
|
|
193
|
+
* `users` (at most 1000, `capped: true` when cut). An empty room is
|
|
194
|
+
* `{count: 0}`, not a 404.
|
|
195
|
+
*/
|
|
196
|
+
presence: (roomId: string, options?: {
|
|
197
|
+
full?: boolean;
|
|
198
|
+
}) => Promise<RoomPresence>;
|
|
172
199
|
members: {
|
|
173
200
|
list: (roomId: string, options?: {
|
|
174
201
|
cursor?: string;
|
|
@@ -216,6 +243,17 @@ type ChatServer = {
|
|
|
216
243
|
avatar?: string;
|
|
217
244
|
meta?: Record<string, unknown>;
|
|
218
245
|
}) => Promise<unknown>;
|
|
246
|
+
/**
|
|
247
|
+
* Moderation: deletes every live message this user sent in the app
|
|
248
|
+
* (tombstones -- seq stays gap-free, clients get `message.deleted`),
|
|
249
|
+
* **without** withdrawing them. The name stays on the tombstones.
|
|
250
|
+
*
|
|
251
|
+
* Ban first (`ban`), then call this: a message posted while it runs is
|
|
252
|
+
* not in its listing. Bounded per call -- loop while `purgeCapped`.
|
|
253
|
+
* A withdrawn user is 404; their messages go with
|
|
254
|
+
* `delete(userId, { purgeMessages: true })`.
|
|
255
|
+
*/
|
|
256
|
+
purgeMessages: (userId: string) => Promise<PurgeMessagesResult>;
|
|
219
257
|
/** Withdrawal: anonymises rather than deleting rows. */
|
|
220
258
|
delete: (userId: string, options?: {
|
|
221
259
|
purgeMessages?: boolean;
|
|
@@ -256,4 +294,4 @@ type ChatServer = {
|
|
|
256
294
|
};
|
|
257
295
|
declare function createChatServer(options: ChatServerOptions): ChatServer;
|
|
258
296
|
|
|
259
|
-
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
|
297
|
+
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type PurgeMessagesResult, type RoomPresence, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
package/dist/server/index.js
CHANGED
|
@@ -217,6 +217,7 @@ function createChatServer(options) {
|
|
|
217
217
|
custom: async (roomId, payload) => {
|
|
218
218
|
await rest.call("POST", `/v1/rooms/${roomId}/custom`, { payload });
|
|
219
219
|
},
|
|
220
|
+
presence: (roomId, o) => rest.call("GET", `/v1/rooms/${roomId}/presence${rest.query({ full: o?.full ? "true" : void 0 })}`),
|
|
220
221
|
members: {
|
|
221
222
|
list: (roomId, o) => rest.call("GET", `/v1/rooms/${roomId}/members${rest.query({ cursor: o?.cursor, limit: o?.limit })}`),
|
|
222
223
|
add: async (roomId, userId, role) => {
|
|
@@ -263,6 +264,7 @@ function createChatServer(options) {
|
|
|
263
264
|
// the messages left standing. Nothing failed; the caller was told
|
|
264
265
|
// it had worked.
|
|
265
266
|
delete: (userId, o) => rest.call("DELETE", `/v1/users/${userId}${rest.query({ purge_messages: o?.purgeMessages ? "true" : void 0 })}`),
|
|
267
|
+
purgeMessages: (userId) => rest.call("DELETE", `/v1/users/${userId}/messages`),
|
|
266
268
|
revokeTokens: async (userId) => {
|
|
267
269
|
await rest.call("POST", `/v1/users/${userId}/revoke-tokens`);
|
|
268
270
|
},
|