@kispi/chat 0.2.0 → 0.2.2
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 +442 -0
- package/README.md +86 -14
- package/dist/index.cjs +243 -39
- package/dist/index.d.cts +153 -36
- package/dist/index.d.ts +153 -36
- package/dist/index.js +243 -39
- 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 +3 -2
package/dist/index.cjs
CHANGED
|
@@ -204,6 +204,13 @@ var Timeline = class {
|
|
|
204
204
|
shared = false;
|
|
205
205
|
/** The `loadOlder` in flight, so a scroll handler firing twice asks once. */
|
|
206
206
|
older;
|
|
207
|
+
/**
|
|
208
|
+
* The lowest seq the room still keeps (`minSeq` from the subscribe ack;
|
|
209
|
+
* 1 until told). Below it there is nothing to read, whatever seq says.
|
|
210
|
+
*/
|
|
211
|
+
floor = 1;
|
|
212
|
+
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
213
|
+
exhausted = false;
|
|
207
214
|
constructor(options) {
|
|
208
215
|
this.options = options;
|
|
209
216
|
}
|
|
@@ -212,6 +219,22 @@ var Timeline = class {
|
|
|
212
219
|
this.shared = true;
|
|
213
220
|
return this.items;
|
|
214
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether `loadOlder()` would find anything, answered without asking.
|
|
224
|
+
*
|
|
225
|
+
* `loadOlder()`의 `hasMore`와 같은 판정이다: 맨 위 행이 방의 보존 하한
|
|
226
|
+
* (`minSeq`, seq는 1부터 구멍 없이 붙는다)보다 위에 있고, 앞선 `loadOlder`가
|
|
227
|
+
* 덜 찬 페이지로 끝을 알린 적이 없으면 더 있다. 첫 `loadOlder()` 전에 "이전
|
|
228
|
+
* 메시지 더 보기"를 그릴지 정할 수 있게 한다. 목록이 비었으면 false다.
|
|
229
|
+
*/
|
|
230
|
+
get hasOlder() {
|
|
231
|
+
const first = this.items[0];
|
|
232
|
+
return !this.exhausted && first !== void 0 && first.seq > this.floor;
|
|
233
|
+
}
|
|
234
|
+
/** Records the room's retention floor (`minSeq`). */
|
|
235
|
+
setFloor(minSeq) {
|
|
236
|
+
this.floor = Math.max(1, minSeq);
|
|
237
|
+
}
|
|
215
238
|
/** The highest seq this timeline holds, hole or no hole. */
|
|
216
239
|
get highestSeq() {
|
|
217
240
|
return this.items.at(-1)?.seq ?? 0;
|
|
@@ -256,6 +279,7 @@ var Timeline = class {
|
|
|
256
279
|
this.fills.clear();
|
|
257
280
|
this.pendingDeletes.clear();
|
|
258
281
|
this.items = [];
|
|
282
|
+
this.exhausted = false;
|
|
259
283
|
this.shared = false;
|
|
260
284
|
this.changed();
|
|
261
285
|
}
|
|
@@ -283,6 +307,7 @@ var Timeline = class {
|
|
|
283
307
|
const page = [...messages].sort((a, b) => a.seq - b.seq);
|
|
284
308
|
const top = page.at(-1)?.seq ?? 0;
|
|
285
309
|
this.items = [...page, ...this.items.filter((m) => m.seq > top)];
|
|
310
|
+
this.exhausted = false;
|
|
286
311
|
this.shared = false;
|
|
287
312
|
for (const m of this.items) {
|
|
288
313
|
if (this.pendingDeletes.has(m.id)) this.applyDelete(m.id);
|
|
@@ -333,20 +358,35 @@ var Timeline = class {
|
|
|
333
358
|
*
|
|
334
359
|
* The server sends the new `count` with the event, so this is a
|
|
335
360
|
* replacement rather than an increment: two clients reacting at once
|
|
336
|
-
* cannot drift the way `+1`/`-1` would.
|
|
361
|
+
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
362
|
+
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
363
|
+
* `myReactions` alone (somebody else's event).
|
|
337
364
|
*/
|
|
338
|
-
reaction(messageId, emoji, count) {
|
|
365
|
+
reaction(messageId, emoji, count, mine) {
|
|
339
366
|
const at = this.items.findIndex((m) => m.id === messageId);
|
|
340
367
|
if (at === -1) return;
|
|
341
368
|
const current = this.items[at];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
369
|
+
let reactions = current.reactions;
|
|
370
|
+
if (count !== void 0) {
|
|
371
|
+
const existing = current.reactions ?? [];
|
|
372
|
+
const slot = existing.findIndex((r) => r.emoji === emoji);
|
|
373
|
+
if (count <= 0) reactions = existing.filter((r) => r.emoji !== emoji);
|
|
374
|
+
else if (slot === -1) reactions = [...existing, { emoji, count }];
|
|
375
|
+
else reactions = existing.map((r, i) => i === slot ? { emoji, count } : r);
|
|
376
|
+
}
|
|
377
|
+
let myReactions = current.myReactions;
|
|
378
|
+
if (mine !== void 0) {
|
|
379
|
+
const held = current.myReactions ?? [];
|
|
380
|
+
const has = held.includes(emoji);
|
|
381
|
+
if (mine && !has) myReactions = [...held, emoji];
|
|
382
|
+
else if (!mine && has) myReactions = held.filter((e) => e !== emoji);
|
|
383
|
+
}
|
|
384
|
+
if (reactions === current.reactions && myReactions === current.myReactions) return;
|
|
385
|
+
const next = { ...current };
|
|
386
|
+
if (reactions !== void 0) next.reactions = reactions;
|
|
387
|
+
if (myReactions !== void 0) next.myReactions = myReactions;
|
|
348
388
|
this.own();
|
|
349
|
-
this.items[at] =
|
|
389
|
+
this.items[at] = next;
|
|
350
390
|
this.changed();
|
|
351
391
|
}
|
|
352
392
|
/** Applies a `thread.updated` to the root message's aggregate. */
|
|
@@ -383,14 +423,16 @@ var Timeline = class {
|
|
|
383
423
|
}
|
|
384
424
|
async readOlder(limit) {
|
|
385
425
|
const first = this.items[0];
|
|
386
|
-
if (first === void 0 || first.seq <=
|
|
426
|
+
if (first === void 0 || first.seq <= this.floor) return { messages: [], hasMore: false };
|
|
387
427
|
const epoch = this.epoch;
|
|
388
428
|
const page = await this.options.fetchRange({ after: 0, before: first.seq, limit });
|
|
389
429
|
if (this.epoch !== epoch) return { messages: [], hasMore: true };
|
|
390
430
|
for (const m of page) this.insert(m, false);
|
|
391
431
|
if (page.length > 0) this.changed();
|
|
392
432
|
const top = page[0];
|
|
393
|
-
|
|
433
|
+
const hasMore = page.length >= limit && top !== void 0 && top.seq > this.floor;
|
|
434
|
+
this.exhausted = !hasMore;
|
|
435
|
+
return { messages: page, hasMore };
|
|
394
436
|
}
|
|
395
437
|
/**
|
|
396
438
|
* Fetches everything between what we have and `upTo`.
|
|
@@ -481,6 +523,8 @@ function createTimeline(options) {
|
|
|
481
523
|
|
|
482
524
|
// src/room.ts
|
|
483
525
|
var HistoryPageSize = 100;
|
|
526
|
+
var MaxRepairs = 5;
|
|
527
|
+
var RepairBaseMs = 1e3;
|
|
484
528
|
var Room = class extends Emitter {
|
|
485
529
|
/** Resolved on subscribe when the room was addressed by key. */
|
|
486
530
|
id;
|
|
@@ -518,6 +562,9 @@ var Room = class extends Emitter {
|
|
|
518
562
|
*/
|
|
519
563
|
wanted = false;
|
|
520
564
|
subscribing;
|
|
565
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
566
|
+
repairTimer;
|
|
567
|
+
repairs = 0;
|
|
521
568
|
constructor(chat, rest, address) {
|
|
522
569
|
super();
|
|
523
570
|
this.chat = chat;
|
|
@@ -533,6 +580,17 @@ var Room = class extends Emitter {
|
|
|
533
580
|
onChange: (messages) => this.emit("messages", messages)
|
|
534
581
|
});
|
|
535
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Whether older history exists above the first message held -- what
|
|
585
|
+
* `loadOlder()` would report as `hasMore`, known before calling it.
|
|
586
|
+
*
|
|
587
|
+
* Derived from the first row's seq against the room's retention floor
|
|
588
|
+
* (the subscribe ack's `minSeq`), and false once a `loadOlder()` came
|
|
589
|
+
* back short. False while nothing is loaded. Re-read it on `messages`.
|
|
590
|
+
*/
|
|
591
|
+
get hasOlder() {
|
|
592
|
+
return this.timeline.hasOlder;
|
|
593
|
+
}
|
|
536
594
|
/** Everything this client knows about the room, in seq order. A new array whenever it changes. */
|
|
537
595
|
get messages() {
|
|
538
596
|
return this.timeline.messages;
|
|
@@ -584,7 +642,12 @@ var Room = class extends Emitter {
|
|
|
584
642
|
this.wanted = true;
|
|
585
643
|
if (this.subscribing !== void 0) return this.subscribing;
|
|
586
644
|
if (this.subscribed) return;
|
|
587
|
-
const attempt = this.doSubscribe().
|
|
645
|
+
const attempt = this.doSubscribe().then(() => {
|
|
646
|
+
const successor = this.subscribing;
|
|
647
|
+
if (successor !== void 0 && successor !== attempt) return successor;
|
|
648
|
+
if (this.subscribed) this.repairs = 0;
|
|
649
|
+
return void 0;
|
|
650
|
+
}).finally(() => {
|
|
588
651
|
if (this.subscribing === attempt) this.subscribing = void 0;
|
|
589
652
|
});
|
|
590
653
|
this.subscribing = attempt;
|
|
@@ -593,6 +656,10 @@ var Room = class extends Emitter {
|
|
|
593
656
|
async doSubscribe() {
|
|
594
657
|
const gen = this.generation;
|
|
595
658
|
const current = () => gen === this.generation;
|
|
659
|
+
if (this.chat.state !== "open") {
|
|
660
|
+
await this.chat.whenOpen();
|
|
661
|
+
if (!current()) return;
|
|
662
|
+
}
|
|
596
663
|
const data = this.id !== void 0 ? { roomId: this.id } : { key: this.key };
|
|
597
664
|
const since = this.timeline.contiguousSeq;
|
|
598
665
|
if (since > 0) data["since"] = since;
|
|
@@ -602,6 +669,7 @@ var Room = class extends Emitter {
|
|
|
602
669
|
this.id = ack.roomId;
|
|
603
670
|
this.chat.registerRoomId(ack.roomId, this);
|
|
604
671
|
this.lastSeq = ack.lastSeq;
|
|
672
|
+
if (typeof ack.minSeq === "number") this.timeline.setFloor(ack.minSeq);
|
|
605
673
|
if (ack.presence !== void 0) this.presence = ack.presence;
|
|
606
674
|
if (attempt.wasReset) {
|
|
607
675
|
await this.loadRecent(current);
|
|
@@ -645,12 +713,20 @@ var Room = class extends Emitter {
|
|
|
645
713
|
});
|
|
646
714
|
return page.messages;
|
|
647
715
|
}
|
|
648
|
-
/**
|
|
716
|
+
/**
|
|
717
|
+
* Adds a reaction. Idempotent, like the frame.
|
|
718
|
+
*
|
|
719
|
+
* The ack updates the row's `myReactions` (not its count -- see below),
|
|
720
|
+
* so the "I reacted" state is right even if the broadcast is missed.
|
|
721
|
+
*/
|
|
649
722
|
async react(messageId, emoji) {
|
|
650
723
|
await this.chat.send("react", { roomId: await this.resolveId("reacting"), messageId, emoji, op: "add" });
|
|
724
|
+
this.timeline.reaction(messageId, emoji, void 0, true);
|
|
651
725
|
}
|
|
726
|
+
/** Removes a reaction. Idempotent; the ack clears it from `myReactions`. */
|
|
652
727
|
async unreact(messageId, emoji) {
|
|
653
728
|
await this.chat.send("react", { roomId: await this.resolveId("reacting"), messageId, emoji, op: "remove" });
|
|
729
|
+
this.timeline.reaction(messageId, emoji, void 0, false);
|
|
654
730
|
}
|
|
655
731
|
/**
|
|
656
732
|
* Moves this user's read cursor.
|
|
@@ -722,13 +798,33 @@ var Room = class extends Emitter {
|
|
|
722
798
|
* transient 500 takes a room out of the live feed for the life of the
|
|
723
799
|
* page.
|
|
724
800
|
*/
|
|
725
|
-
async resume() {
|
|
801
|
+
async resume(restart = true) {
|
|
726
802
|
if (!this.wanted) return;
|
|
803
|
+
if (!restart) return this.subscribe();
|
|
727
804
|
this.generation++;
|
|
728
805
|
this.subscribing = void 0;
|
|
729
806
|
this.subscribed = false;
|
|
730
807
|
await this.subscribe();
|
|
731
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Reports a failure no caller was waiting for, and repairs.
|
|
811
|
+
*
|
|
812
|
+
* 백그라운드 실패(재접속 뒤 다시 구독, 구멍 메우기)에는 거절을 받을 호출자가
|
|
813
|
+
* 없다. 이벤트만 내고 두면 방은 다음 재접속이나 탭 복귀까지 구멍을 안고 —
|
|
814
|
+
* 구독 프레임부터 실패했다면 라이브 피드 밖에서 — 머문다. 그래서 스스로 다시
|
|
815
|
+
* 구독한다. 횟수를 묶는 것은 고칠 수 없는 실패(권한, 없어진 방)가 서버를
|
|
816
|
+
* 계속 두드리지 않게 하려는 것이고, 그 뒤는 재접속과 `subscribe()`의 몫이다.
|
|
817
|
+
*/
|
|
818
|
+
failed(err) {
|
|
819
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
820
|
+
if (this.repairTimer !== void 0 || this.repairs >= MaxRepairs) return;
|
|
821
|
+
const delay = RepairBaseMs * 2 ** this.repairs++;
|
|
822
|
+
this.repairTimer = setTimeout(() => {
|
|
823
|
+
this.repairTimer = void 0;
|
|
824
|
+
if (!this.wanted || this.subscribed || this.chat.state !== "open") return;
|
|
825
|
+
this.subscribe().catch((e) => this.failed(e));
|
|
826
|
+
}, delay);
|
|
827
|
+
}
|
|
732
828
|
/**
|
|
733
829
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
734
830
|
* for.
|
|
@@ -810,7 +906,7 @@ var Room = class extends Emitter {
|
|
|
810
906
|
if (data.seq > this.lastSeq) this.lastSeq = data.seq;
|
|
811
907
|
this.timeline.add(data).catch((err) => {
|
|
812
908
|
this.subscribed = false;
|
|
813
|
-
this.
|
|
909
|
+
this.failed(err);
|
|
814
910
|
});
|
|
815
911
|
break;
|
|
816
912
|
case "message.updated":
|
|
@@ -825,7 +921,9 @@ var Room = class extends Emitter {
|
|
|
825
921
|
case "reaction.added":
|
|
826
922
|
case "reaction.removed": {
|
|
827
923
|
const r = data;
|
|
828
|
-
this.
|
|
924
|
+
const me = this.chat.user?.id;
|
|
925
|
+
const mine = me !== void 0 && r.userId === me ? type === "reaction.added" : void 0;
|
|
926
|
+
this.timeline.reaction(r.messageId, r.emoji, r.count, mine);
|
|
829
927
|
break;
|
|
830
928
|
}
|
|
831
929
|
case "thread.updated": {
|
|
@@ -879,8 +977,11 @@ var ChatClient = class extends Emitter {
|
|
|
879
977
|
heartbeat;
|
|
880
978
|
retryTimer;
|
|
881
979
|
attempt = 0;
|
|
882
|
-
/**
|
|
883
|
-
|
|
980
|
+
/**
|
|
981
|
+
* The reconnect loop is off: set by close(), and by a failure that
|
|
982
|
+
* retrying cannot fix (see `stop`). Cleared by connect().
|
|
983
|
+
*/
|
|
984
|
+
stopped = false;
|
|
884
985
|
nextFrameId = 0;
|
|
885
986
|
opening;
|
|
886
987
|
rest;
|
|
@@ -958,7 +1059,7 @@ var ChatClient = class extends Emitter {
|
|
|
958
1059
|
* gap-filled via REST.
|
|
959
1060
|
*/
|
|
960
1061
|
async resume() {
|
|
961
|
-
if (this.
|
|
1062
|
+
if (this.stopped) return;
|
|
962
1063
|
if (this.state !== "open" || this.socket === void 0) {
|
|
963
1064
|
this.clearTimers();
|
|
964
1065
|
return this.connect();
|
|
@@ -970,9 +1071,7 @@ var ChatClient = class extends Emitter {
|
|
|
970
1071
|
return this.connect();
|
|
971
1072
|
}
|
|
972
1073
|
for (const room of this.handles.values()) {
|
|
973
|
-
void room.resume().catch((err) =>
|
|
974
|
-
room.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
975
|
-
});
|
|
1074
|
+
void room.resume().catch((err) => room.failed(err));
|
|
976
1075
|
}
|
|
977
1076
|
}
|
|
978
1077
|
/** The account-level routes: the rooms this user is in, and unread. */
|
|
@@ -1064,7 +1163,7 @@ var ChatClient = class extends Emitter {
|
|
|
1064
1163
|
}
|
|
1065
1164
|
/** Opens the connection and resolves when `hello` arrives. */
|
|
1066
1165
|
async connect() {
|
|
1067
|
-
this.
|
|
1166
|
+
this.stopped = false;
|
|
1068
1167
|
if (this.opening !== void 0) return this.opening;
|
|
1069
1168
|
if (this.state === "open" && this.socket !== void 0) return;
|
|
1070
1169
|
this.clearTimers();
|
|
@@ -1073,15 +1172,58 @@ var ChatClient = class extends Emitter {
|
|
|
1073
1172
|
});
|
|
1074
1173
|
return this.opening;
|
|
1075
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Re-authenticates: calls `token()` again and replaces the socket, keeping
|
|
1177
|
+
* every room handle and every subscription.
|
|
1178
|
+
*
|
|
1179
|
+
* For when the identity the server holds has to change mid-session --
|
|
1180
|
+
* a nickname change, a new avatar, fresh claims. The server reads the
|
|
1181
|
+
* token once, at `auth`, so `sender.name` on the next message is the old
|
|
1182
|
+
* one until the connection is re-authenticated. `close()` + `connect()`
|
|
1183
|
+
* does that too, but passes through `closed` (a consumer's "you are
|
|
1184
|
+
* offline" screen) and tears down the page listeners.
|
|
1185
|
+
*
|
|
1186
|
+
* States: `open` → `reconnecting` → `open`, never `closed`. Rooms
|
|
1187
|
+
* resubscribe from what they hold and catch up the gap, as after any
|
|
1188
|
+
* drop; `messages` is kept. Frames awaiting an ack on the old socket are
|
|
1189
|
+
* rejected with `closed`. `chat.user` is the new identity once this
|
|
1190
|
+
* resolves. If `token()` fails the client keeps retrying in the
|
|
1191
|
+
* background like any reconnect (and this rejects); a `ChatError` from
|
|
1192
|
+
* `token()` stops it at `closed`, as on connect.
|
|
1193
|
+
*
|
|
1194
|
+
* On a client that is not connected (never connected, or `close()`d) it
|
|
1195
|
+
* is `connect()`.
|
|
1196
|
+
*/
|
|
1197
|
+
async reconnect() {
|
|
1198
|
+
if (this.opening !== void 0) await this.opening.catch(() => {
|
|
1199
|
+
});
|
|
1200
|
+
const socket = this.socket;
|
|
1201
|
+
if (socket === void 0 || this.state !== "open") return this.connect();
|
|
1202
|
+
this.clearTimers();
|
|
1203
|
+
this.retired.add(socket);
|
|
1204
|
+
this.socket = void 0;
|
|
1205
|
+
this.failPending(new ChatError("closed", "the connection was replaced by reconnect()"));
|
|
1206
|
+
try {
|
|
1207
|
+
socket.close(1e3, "client reconnecting");
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
this.opening = this.openOnce("reconnecting").finally(() => {
|
|
1211
|
+
this.opening = void 0;
|
|
1212
|
+
});
|
|
1213
|
+
return this.opening;
|
|
1214
|
+
}
|
|
1215
|
+
/** Sockets `reconnect()` replaced: their late events are not this client's any more. */
|
|
1216
|
+
retired = /* @__PURE__ */ new WeakSet();
|
|
1076
1217
|
/**
|
|
1077
1218
|
* Closes for good.
|
|
1078
1219
|
*
|
|
1079
1220
|
* The distinction from a dropped socket is the whole point of the state
|
|
1080
|
-
* machine:
|
|
1081
|
-
*
|
|
1221
|
+
* machine: `closed` means nothing is coming back, and a consumer showing
|
|
1222
|
+
* "disconnected, retrying" versus "disconnected" needs it. The other
|
|
1223
|
+
* ways to reach it are refusals retrying cannot fix -- see `stop`.
|
|
1082
1224
|
*/
|
|
1083
1225
|
async close() {
|
|
1084
|
-
this.
|
|
1226
|
+
this.stopped = true;
|
|
1085
1227
|
this.teardowns.forEach((t) => {
|
|
1086
1228
|
try {
|
|
1087
1229
|
t();
|
|
@@ -1098,7 +1240,53 @@ var ChatClient = class extends Emitter {
|
|
|
1098
1240
|
} catch {
|
|
1099
1241
|
}
|
|
1100
1242
|
}
|
|
1243
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1244
|
+
this.setState("closed");
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Stops for a reason retrying cannot fix, and says which.
|
|
1248
|
+
*
|
|
1249
|
+
* `close()` without the teardown of the page listeners -- a consumer
|
|
1250
|
+
* that logs back in calls `connect()` on the same client -- and with an
|
|
1251
|
+
* `error` event, because on a reconnect there is no caller to reject.
|
|
1252
|
+
*/
|
|
1253
|
+
stop(err) {
|
|
1254
|
+
this.stopped = true;
|
|
1255
|
+
this.clearTimers();
|
|
1256
|
+
const socket = this.socket;
|
|
1257
|
+
this.socket = void 0;
|
|
1258
|
+
if (socket !== void 0) {
|
|
1259
|
+
try {
|
|
1260
|
+
socket.close(1e3, "client stopped");
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
this.failPending(err);
|
|
1265
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1101
1266
|
this.setState("closed");
|
|
1267
|
+
this.emit("error", err);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Resolves once the connection is open. Used by `Room.subscribe`.
|
|
1271
|
+
*
|
|
1272
|
+
* 연결 전에 부른 `subscribe()`를 `closed`로 거절하면 소비자는 "언제 다시
|
|
1273
|
+
* 부를지"를 스스로 알아내야 하고, 첫 connect가 실패했다면 그 때는 영영 오지
|
|
1274
|
+
* 않는다. 붙을 때까지 기다리면 그 신호가 필요 없다. 멈춘 클라이언트(close,
|
|
1275
|
+
* 인증 거절)는 붙을 일이 없으니 거절한다.
|
|
1276
|
+
*/
|
|
1277
|
+
whenOpen() {
|
|
1278
|
+
if (this.state === "open" && this.socket !== void 0) return Promise.resolve();
|
|
1279
|
+
if (this.stopped) return Promise.reject(new ChatError("closed", "the connection is closed"));
|
|
1280
|
+
return new Promise((resolve, reject) => void this.openWaiters.push({ resolve, reject }));
|
|
1281
|
+
}
|
|
1282
|
+
openWaiters = [];
|
|
1283
|
+
settleOpenWaiters(err) {
|
|
1284
|
+
const waiters = this.openWaiters;
|
|
1285
|
+
this.openWaiters = [];
|
|
1286
|
+
for (const w of waiters) {
|
|
1287
|
+
if (err === void 0) w.resolve();
|
|
1288
|
+
else w.reject(err);
|
|
1289
|
+
}
|
|
1102
1290
|
}
|
|
1103
1291
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
1104
1292
|
send(type, data, timeoutMs = 15e3) {
|
|
@@ -1139,16 +1327,20 @@ var ChatClient = class extends Emitter {
|
|
|
1139
1327
|
this.state = next;
|
|
1140
1328
|
this.emit("state", next);
|
|
1141
1329
|
}
|
|
1142
|
-
async openOnce() {
|
|
1143
|
-
this.setState(
|
|
1330
|
+
async openOnce(state = this.attempt === 0 ? "connecting" : "reconnecting") {
|
|
1331
|
+
this.setState(state);
|
|
1144
1332
|
let authData;
|
|
1145
1333
|
try {
|
|
1146
1334
|
authData = await this.authData();
|
|
1147
1335
|
} catch (err) {
|
|
1336
|
+
if (isChatError(err)) {
|
|
1337
|
+
this.stop(err);
|
|
1338
|
+
throw err;
|
|
1339
|
+
}
|
|
1148
1340
|
this.scheduleReconnect();
|
|
1149
1341
|
throw err;
|
|
1150
1342
|
}
|
|
1151
|
-
if (this.
|
|
1343
|
+
if (this.stopped) {
|
|
1152
1344
|
throw new ChatError("closed", "the client was closed while connecting");
|
|
1153
1345
|
}
|
|
1154
1346
|
const socket = new this.makeSocket(`${this.options.url}/v1/ws?v=1`);
|
|
@@ -1162,6 +1354,7 @@ var ChatClient = class extends Emitter {
|
|
|
1162
1354
|
else reject(err);
|
|
1163
1355
|
};
|
|
1164
1356
|
socket.addEventListener("message", (ev) => {
|
|
1357
|
+
if (this.retired.has(socket)) return;
|
|
1165
1358
|
const frame = JSON.parse(String(ev.data));
|
|
1166
1359
|
if (frame.type === "hello") {
|
|
1167
1360
|
this.onHello(frame.data);
|
|
@@ -1169,14 +1362,20 @@ var ChatClient = class extends Emitter {
|
|
|
1169
1362
|
return;
|
|
1170
1363
|
}
|
|
1171
1364
|
if (frame.type === "error" && !settled) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1365
|
+
const err = errorFrom(frame.data);
|
|
1366
|
+
if (err.code === "rate_limited" || err.code === "internal") {
|
|
1367
|
+
if (err.retryAfterMs !== void 0 && err.retryAfterMs > 0) this.plannedDelayMs = err.retryAfterMs;
|
|
1368
|
+
finish(err);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
finish(err);
|
|
1372
|
+
this.stop(err);
|
|
1175
1373
|
return;
|
|
1176
1374
|
}
|
|
1177
1375
|
this.onFrame(frame);
|
|
1178
1376
|
});
|
|
1179
1377
|
socket.addEventListener("close", () => {
|
|
1378
|
+
if (this.retired.has(socket)) return;
|
|
1180
1379
|
if (this.socket !== void 0 && this.socket !== socket) return;
|
|
1181
1380
|
this.clearTimers();
|
|
1182
1381
|
this.socket = void 0;
|
|
@@ -1204,12 +1403,11 @@ var ChatClient = class extends Emitter {
|
|
|
1204
1403
|
const reconnected = this.everOpened;
|
|
1205
1404
|
this.everOpened = true;
|
|
1206
1405
|
this.setState("open");
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1406
|
+
this.settleOpenWaiters();
|
|
1407
|
+
for (const room of this.handles.values()) {
|
|
1408
|
+
void room.resume(reconnected).catch((err) => {
|
|
1409
|
+
room.failed(err);
|
|
1410
|
+
});
|
|
1213
1411
|
}
|
|
1214
1412
|
}
|
|
1215
1413
|
onFrame(frame) {
|
|
@@ -1279,7 +1477,7 @@ var ChatClient = class extends Emitter {
|
|
|
1279
1477
|
}, Math.max(1e3, intervalMs));
|
|
1280
1478
|
}
|
|
1281
1479
|
scheduleReconnect() {
|
|
1282
|
-
if (this.
|
|
1480
|
+
if (this.stopped) return;
|
|
1283
1481
|
this.setState("reconnecting");
|
|
1284
1482
|
const delay = this.nextDelay();
|
|
1285
1483
|
this.retryTimer = setTimeout(() => {
|
|
@@ -1331,6 +1529,9 @@ var ChatClient = class extends Emitter {
|
|
|
1331
1529
|
const token = await this.options.token();
|
|
1332
1530
|
this.lastToken = token;
|
|
1333
1531
|
return token;
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
if (isChatError(err)) this.stop(err);
|
|
1534
|
+
throw err;
|
|
1334
1535
|
} finally {
|
|
1335
1536
|
this.refreshing = void 0;
|
|
1336
1537
|
}
|
|
@@ -1352,6 +1553,9 @@ var ChatClient = class extends Emitter {
|
|
|
1352
1553
|
this.retryTimer = void 0;
|
|
1353
1554
|
}
|
|
1354
1555
|
};
|
|
1556
|
+
function isChatError(err) {
|
|
1557
|
+
return err instanceof ChatError || err instanceof Error && err.name === "ChatError";
|
|
1558
|
+
}
|
|
1355
1559
|
function createChatClient(options) {
|
|
1356
1560
|
return new ChatClient(options);
|
|
1357
1561
|
}
|