@kispi/chat 0.2.0 → 0.2.1
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 +404 -0
- package/README.md +46 -10
- package/dist/index.cjs +120 -24
- package/dist/index.d.cts +80 -33
- package/dist/index.d.ts +80 -33
- package/dist/index.js +120 -24
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -481,6 +481,8 @@ function createTimeline(options) {
|
|
|
481
481
|
|
|
482
482
|
// src/room.ts
|
|
483
483
|
var HistoryPageSize = 100;
|
|
484
|
+
var MaxRepairs = 5;
|
|
485
|
+
var RepairBaseMs = 1e3;
|
|
484
486
|
var Room = class extends Emitter {
|
|
485
487
|
/** Resolved on subscribe when the room was addressed by key. */
|
|
486
488
|
id;
|
|
@@ -518,6 +520,9 @@ var Room = class extends Emitter {
|
|
|
518
520
|
*/
|
|
519
521
|
wanted = false;
|
|
520
522
|
subscribing;
|
|
523
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
524
|
+
repairTimer;
|
|
525
|
+
repairs = 0;
|
|
521
526
|
constructor(chat, rest, address) {
|
|
522
527
|
super();
|
|
523
528
|
this.chat = chat;
|
|
@@ -584,7 +589,12 @@ var Room = class extends Emitter {
|
|
|
584
589
|
this.wanted = true;
|
|
585
590
|
if (this.subscribing !== void 0) return this.subscribing;
|
|
586
591
|
if (this.subscribed) return;
|
|
587
|
-
const attempt = this.doSubscribe().
|
|
592
|
+
const attempt = this.doSubscribe().then(() => {
|
|
593
|
+
const successor = this.subscribing;
|
|
594
|
+
if (successor !== void 0 && successor !== attempt) return successor;
|
|
595
|
+
if (this.subscribed) this.repairs = 0;
|
|
596
|
+
return void 0;
|
|
597
|
+
}).finally(() => {
|
|
588
598
|
if (this.subscribing === attempt) this.subscribing = void 0;
|
|
589
599
|
});
|
|
590
600
|
this.subscribing = attempt;
|
|
@@ -593,6 +603,10 @@ var Room = class extends Emitter {
|
|
|
593
603
|
async doSubscribe() {
|
|
594
604
|
const gen = this.generation;
|
|
595
605
|
const current = () => gen === this.generation;
|
|
606
|
+
if (this.chat.state !== "open") {
|
|
607
|
+
await this.chat.whenOpen();
|
|
608
|
+
if (!current()) return;
|
|
609
|
+
}
|
|
596
610
|
const data = this.id !== void 0 ? { roomId: this.id } : { key: this.key };
|
|
597
611
|
const since = this.timeline.contiguousSeq;
|
|
598
612
|
if (since > 0) data["since"] = since;
|
|
@@ -722,13 +736,33 @@ var Room = class extends Emitter {
|
|
|
722
736
|
* transient 500 takes a room out of the live feed for the life of the
|
|
723
737
|
* page.
|
|
724
738
|
*/
|
|
725
|
-
async resume() {
|
|
739
|
+
async resume(restart = true) {
|
|
726
740
|
if (!this.wanted) return;
|
|
741
|
+
if (!restart) return this.subscribe();
|
|
727
742
|
this.generation++;
|
|
728
743
|
this.subscribing = void 0;
|
|
729
744
|
this.subscribed = false;
|
|
730
745
|
await this.subscribe();
|
|
731
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Reports a failure no caller was waiting for, and repairs.
|
|
749
|
+
*
|
|
750
|
+
* 백그라운드 실패(재접속 뒤 다시 구독, 구멍 메우기)에는 거절을 받을 호출자가
|
|
751
|
+
* 없다. 이벤트만 내고 두면 방은 다음 재접속이나 탭 복귀까지 구멍을 안고 —
|
|
752
|
+
* 구독 프레임부터 실패했다면 라이브 피드 밖에서 — 머문다. 그래서 스스로 다시
|
|
753
|
+
* 구독한다. 횟수를 묶는 것은 고칠 수 없는 실패(권한, 없어진 방)가 서버를
|
|
754
|
+
* 계속 두드리지 않게 하려는 것이고, 그 뒤는 재접속과 `subscribe()`의 몫이다.
|
|
755
|
+
*/
|
|
756
|
+
failed(err) {
|
|
757
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
758
|
+
if (this.repairTimer !== void 0 || this.repairs >= MaxRepairs) return;
|
|
759
|
+
const delay = RepairBaseMs * 2 ** this.repairs++;
|
|
760
|
+
this.repairTimer = setTimeout(() => {
|
|
761
|
+
this.repairTimer = void 0;
|
|
762
|
+
if (!this.wanted || this.subscribed || this.chat.state !== "open") return;
|
|
763
|
+
this.subscribe().catch((e) => this.failed(e));
|
|
764
|
+
}, delay);
|
|
765
|
+
}
|
|
732
766
|
/**
|
|
733
767
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
734
768
|
* for.
|
|
@@ -810,7 +844,7 @@ var Room = class extends Emitter {
|
|
|
810
844
|
if (data.seq > this.lastSeq) this.lastSeq = data.seq;
|
|
811
845
|
this.timeline.add(data).catch((err) => {
|
|
812
846
|
this.subscribed = false;
|
|
813
|
-
this.
|
|
847
|
+
this.failed(err);
|
|
814
848
|
});
|
|
815
849
|
break;
|
|
816
850
|
case "message.updated":
|
|
@@ -879,8 +913,11 @@ var ChatClient = class extends Emitter {
|
|
|
879
913
|
heartbeat;
|
|
880
914
|
retryTimer;
|
|
881
915
|
attempt = 0;
|
|
882
|
-
/**
|
|
883
|
-
|
|
916
|
+
/**
|
|
917
|
+
* The reconnect loop is off: set by close(), and by a failure that
|
|
918
|
+
* retrying cannot fix (see `stop`). Cleared by connect().
|
|
919
|
+
*/
|
|
920
|
+
stopped = false;
|
|
884
921
|
nextFrameId = 0;
|
|
885
922
|
opening;
|
|
886
923
|
rest;
|
|
@@ -958,7 +995,7 @@ var ChatClient = class extends Emitter {
|
|
|
958
995
|
* gap-filled via REST.
|
|
959
996
|
*/
|
|
960
997
|
async resume() {
|
|
961
|
-
if (this.
|
|
998
|
+
if (this.stopped) return;
|
|
962
999
|
if (this.state !== "open" || this.socket === void 0) {
|
|
963
1000
|
this.clearTimers();
|
|
964
1001
|
return this.connect();
|
|
@@ -970,9 +1007,7 @@ var ChatClient = class extends Emitter {
|
|
|
970
1007
|
return this.connect();
|
|
971
1008
|
}
|
|
972
1009
|
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
|
-
});
|
|
1010
|
+
void room.resume().catch((err) => room.failed(err));
|
|
976
1011
|
}
|
|
977
1012
|
}
|
|
978
1013
|
/** The account-level routes: the rooms this user is in, and unread. */
|
|
@@ -1064,7 +1099,7 @@ var ChatClient = class extends Emitter {
|
|
|
1064
1099
|
}
|
|
1065
1100
|
/** Opens the connection and resolves when `hello` arrives. */
|
|
1066
1101
|
async connect() {
|
|
1067
|
-
this.
|
|
1102
|
+
this.stopped = false;
|
|
1068
1103
|
if (this.opening !== void 0) return this.opening;
|
|
1069
1104
|
if (this.state === "open" && this.socket !== void 0) return;
|
|
1070
1105
|
this.clearTimers();
|
|
@@ -1077,11 +1112,12 @@ var ChatClient = class extends Emitter {
|
|
|
1077
1112
|
* Closes for good.
|
|
1078
1113
|
*
|
|
1079
1114
|
* The distinction from a dropped socket is the whole point of the state
|
|
1080
|
-
* machine:
|
|
1081
|
-
*
|
|
1115
|
+
* machine: `closed` means nothing is coming back, and a consumer showing
|
|
1116
|
+
* "disconnected, retrying" versus "disconnected" needs it. The other
|
|
1117
|
+
* ways to reach it are refusals retrying cannot fix -- see `stop`.
|
|
1082
1118
|
*/
|
|
1083
1119
|
async close() {
|
|
1084
|
-
this.
|
|
1120
|
+
this.stopped = true;
|
|
1085
1121
|
this.teardowns.forEach((t) => {
|
|
1086
1122
|
try {
|
|
1087
1123
|
t();
|
|
@@ -1098,7 +1134,53 @@ var ChatClient = class extends Emitter {
|
|
|
1098
1134
|
} catch {
|
|
1099
1135
|
}
|
|
1100
1136
|
}
|
|
1137
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1138
|
+
this.setState("closed");
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Stops for a reason retrying cannot fix, and says which.
|
|
1142
|
+
*
|
|
1143
|
+
* `close()` without the teardown of the page listeners -- a consumer
|
|
1144
|
+
* that logs back in calls `connect()` on the same client -- and with an
|
|
1145
|
+
* `error` event, because on a reconnect there is no caller to reject.
|
|
1146
|
+
*/
|
|
1147
|
+
stop(err) {
|
|
1148
|
+
this.stopped = true;
|
|
1149
|
+
this.clearTimers();
|
|
1150
|
+
const socket = this.socket;
|
|
1151
|
+
this.socket = void 0;
|
|
1152
|
+
if (socket !== void 0) {
|
|
1153
|
+
try {
|
|
1154
|
+
socket.close(1e3, "client stopped");
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
this.failPending(err);
|
|
1159
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1101
1160
|
this.setState("closed");
|
|
1161
|
+
this.emit("error", err);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Resolves once the connection is open. Used by `Room.subscribe`.
|
|
1165
|
+
*
|
|
1166
|
+
* 연결 전에 부른 `subscribe()`를 `closed`로 거절하면 소비자는 "언제 다시
|
|
1167
|
+
* 부를지"를 스스로 알아내야 하고, 첫 connect가 실패했다면 그 때는 영영 오지
|
|
1168
|
+
* 않는다. 붙을 때까지 기다리면 그 신호가 필요 없다. 멈춘 클라이언트(close,
|
|
1169
|
+
* 인증 거절)는 붙을 일이 없으니 거절한다.
|
|
1170
|
+
*/
|
|
1171
|
+
whenOpen() {
|
|
1172
|
+
if (this.state === "open" && this.socket !== void 0) return Promise.resolve();
|
|
1173
|
+
if (this.stopped) return Promise.reject(new ChatError("closed", "the connection is closed"));
|
|
1174
|
+
return new Promise((resolve, reject) => void this.openWaiters.push({ resolve, reject }));
|
|
1175
|
+
}
|
|
1176
|
+
openWaiters = [];
|
|
1177
|
+
settleOpenWaiters(err) {
|
|
1178
|
+
const waiters = this.openWaiters;
|
|
1179
|
+
this.openWaiters = [];
|
|
1180
|
+
for (const w of waiters) {
|
|
1181
|
+
if (err === void 0) w.resolve();
|
|
1182
|
+
else w.reject(err);
|
|
1183
|
+
}
|
|
1102
1184
|
}
|
|
1103
1185
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
1104
1186
|
send(type, data, timeoutMs = 15e3) {
|
|
@@ -1145,10 +1227,14 @@ var ChatClient = class extends Emitter {
|
|
|
1145
1227
|
try {
|
|
1146
1228
|
authData = await this.authData();
|
|
1147
1229
|
} catch (err) {
|
|
1230
|
+
if (isChatError(err)) {
|
|
1231
|
+
this.stop(err);
|
|
1232
|
+
throw err;
|
|
1233
|
+
}
|
|
1148
1234
|
this.scheduleReconnect();
|
|
1149
1235
|
throw err;
|
|
1150
1236
|
}
|
|
1151
|
-
if (this.
|
|
1237
|
+
if (this.stopped) {
|
|
1152
1238
|
throw new ChatError("closed", "the client was closed while connecting");
|
|
1153
1239
|
}
|
|
1154
1240
|
const socket = new this.makeSocket(`${this.options.url}/v1/ws?v=1`);
|
|
@@ -1169,9 +1255,14 @@ var ChatClient = class extends Emitter {
|
|
|
1169
1255
|
return;
|
|
1170
1256
|
}
|
|
1171
1257
|
if (frame.type === "error" && !settled) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1258
|
+
const err = errorFrom(frame.data);
|
|
1259
|
+
if (err.code === "rate_limited" || err.code === "internal") {
|
|
1260
|
+
if (err.retryAfterMs !== void 0 && err.retryAfterMs > 0) this.plannedDelayMs = err.retryAfterMs;
|
|
1261
|
+
finish(err);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
finish(err);
|
|
1265
|
+
this.stop(err);
|
|
1175
1266
|
return;
|
|
1176
1267
|
}
|
|
1177
1268
|
this.onFrame(frame);
|
|
@@ -1204,12 +1295,11 @@ var ChatClient = class extends Emitter {
|
|
|
1204
1295
|
const reconnected = this.everOpened;
|
|
1205
1296
|
this.everOpened = true;
|
|
1206
1297
|
this.setState("open");
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1298
|
+
this.settleOpenWaiters();
|
|
1299
|
+
for (const room of this.handles.values()) {
|
|
1300
|
+
void room.resume(reconnected).catch((err) => {
|
|
1301
|
+
room.failed(err);
|
|
1302
|
+
});
|
|
1213
1303
|
}
|
|
1214
1304
|
}
|
|
1215
1305
|
onFrame(frame) {
|
|
@@ -1279,7 +1369,7 @@ var ChatClient = class extends Emitter {
|
|
|
1279
1369
|
}, Math.max(1e3, intervalMs));
|
|
1280
1370
|
}
|
|
1281
1371
|
scheduleReconnect() {
|
|
1282
|
-
if (this.
|
|
1372
|
+
if (this.stopped) return;
|
|
1283
1373
|
this.setState("reconnecting");
|
|
1284
1374
|
const delay = this.nextDelay();
|
|
1285
1375
|
this.retryTimer = setTimeout(() => {
|
|
@@ -1331,6 +1421,9 @@ var ChatClient = class extends Emitter {
|
|
|
1331
1421
|
const token = await this.options.token();
|
|
1332
1422
|
this.lastToken = token;
|
|
1333
1423
|
return token;
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
if (isChatError(err)) this.stop(err);
|
|
1426
|
+
throw err;
|
|
1334
1427
|
} finally {
|
|
1335
1428
|
this.refreshing = void 0;
|
|
1336
1429
|
}
|
|
@@ -1352,6 +1445,9 @@ var ChatClient = class extends Emitter {
|
|
|
1352
1445
|
this.retryTimer = void 0;
|
|
1353
1446
|
}
|
|
1354
1447
|
};
|
|
1448
|
+
function isChatError(err) {
|
|
1449
|
+
return err instanceof ChatError || err instanceof Error && err.name === "ChatError";
|
|
1450
|
+
}
|
|
1355
1451
|
function createChatClient(options) {
|
|
1356
1452
|
return new ChatClient(options);
|
|
1357
1453
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,28 @@ declare class Emitter<Events extends Record<string, unknown>> {
|
|
|
14
14
|
clear(): void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Every failure this SDK raises, with the server's own code on it.
|
|
19
|
+
*
|
|
20
|
+
* The code matters more than the message. A consumer branches on
|
|
21
|
+
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
22
|
+
* bare `Error` carrying prose forces them to match on strings the server
|
|
23
|
+
* is free to reword.
|
|
24
|
+
*/
|
|
25
|
+
declare class ChatError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
/** Present on rate limits, in milliseconds. */
|
|
28
|
+
readonly retryAfterMs?: number;
|
|
29
|
+
/** The consumer's own code, when a before_publish hook denied this. */
|
|
30
|
+
readonly appCode?: string;
|
|
31
|
+
/** The HTTP status, when this came from a REST call. */
|
|
32
|
+
status?: number;
|
|
33
|
+
constructor(code: string, message: string, extra?: {
|
|
34
|
+
retryAfterMs?: number;
|
|
35
|
+
appCode?: string;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
40
|
* The wire shapes this SDK reads and writes.
|
|
19
41
|
*
|
|
@@ -166,11 +188,11 @@ type RoomEvents = {
|
|
|
166
188
|
* socket's read loop where there is no caller to throw to -- and
|
|
167
189
|
* swallowing it is what leaves a hole nothing ever fills.
|
|
168
190
|
*
|
|
169
|
-
* The
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
191
|
+
* The room repairs itself: it re-subscribes with backoff (1 s doubling,
|
|
192
|
+
* up to five tries while connected), and every reconnect tries again.
|
|
193
|
+
* `room.subscribe()` is still the retry to offer a person pressing a
|
|
194
|
+
* button -- a room that emitted this is left not-subscribed on purpose,
|
|
195
|
+
* precisely so that call does something.
|
|
174
196
|
*/
|
|
175
197
|
error: Error;
|
|
176
198
|
/**
|
|
@@ -252,6 +274,9 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
252
274
|
*/
|
|
253
275
|
private wanted;
|
|
254
276
|
private subscribing;
|
|
277
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
278
|
+
private repairTimer;
|
|
279
|
+
private repairs;
|
|
255
280
|
constructor(chat: ChatClient, rest: Rest, address: {
|
|
256
281
|
id?: string;
|
|
257
282
|
key?: string;
|
|
@@ -387,7 +412,17 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
387
412
|
* transient 500 takes a room out of the live feed for the life of the
|
|
388
413
|
* page.
|
|
389
414
|
*/
|
|
390
|
-
resume(): Promise<void>;
|
|
415
|
+
resume(restart?: boolean): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Reports a failure no caller was waiting for, and repairs.
|
|
418
|
+
*
|
|
419
|
+
* 백그라운드 실패(재접속 뒤 다시 구독, 구멍 메우기)에는 거절을 받을 호출자가
|
|
420
|
+
* 없다. 이벤트만 내고 두면 방은 다음 재접속이나 탭 복귀까지 구멍을 안고 —
|
|
421
|
+
* 구독 프레임부터 실패했다면 라이브 피드 밖에서 — 머문다. 그래서 스스로 다시
|
|
422
|
+
* 구독한다. 횟수를 묶는 것은 고칠 수 없는 실패(권한, 없어진 방)가 서버를
|
|
423
|
+
* 계속 두드리지 않게 하려는 것이고, 그 뒤는 재접속과 `subscribe()`의 몫이다.
|
|
424
|
+
*/
|
|
425
|
+
failed(err: unknown): void;
|
|
391
426
|
/**
|
|
392
427
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
393
428
|
* for.
|
|
@@ -474,7 +509,9 @@ type ChatClientOptions = {
|
|
|
474
509
|
* Called again on every connect, and again when a REST call is
|
|
475
510
|
* answered 401 (the call is then retried once), so an expired token is
|
|
476
511
|
* replaced rather than reused. A throw here is retried with backoff
|
|
477
|
-
* like a dropped socket
|
|
512
|
+
* like a dropped socket -- **unless it throws a `ChatError`**, which
|
|
513
|
+
* means "do not retry" (a ban, a session that ended): the client goes
|
|
514
|
+
* to `closed` and emits it as `error`.
|
|
478
515
|
*
|
|
479
516
|
* **Never sign these in the browser** -- signing needs the `sk_`, and
|
|
480
517
|
* a `sk_` in a browser is the whole app.
|
|
@@ -497,6 +534,15 @@ type ChatClientEvents = {
|
|
|
497
534
|
* SDK learns about it.
|
|
498
535
|
*/
|
|
499
536
|
frame: Frame;
|
|
537
|
+
/**
|
|
538
|
+
* 클라이언트가 스스로 `closed`로 멈췄고, 이게 그 이유다.
|
|
539
|
+
*
|
|
540
|
+
* 멈추는 길은 셋이다: `close()`, 서버의 인증 거절, `token()`이 던진
|
|
541
|
+
* `ChatError`. 첫 connect라면 거절로도 알 수 있지만 재접속 중이나 REST의
|
|
542
|
+
* 토큰 갱신 중에는 받을 호출자가 없어서, 이것 없이는 `state`가 `closed`로
|
|
543
|
+
* 바뀌는 것만 보이고 왜인지는 사라진다. `close()`로 닫을 때는 내지 않는다.
|
|
544
|
+
*/
|
|
545
|
+
error: ChatError;
|
|
500
546
|
};
|
|
501
547
|
declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
502
548
|
state: ConnectionState;
|
|
@@ -509,8 +555,11 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
509
555
|
private heartbeat;
|
|
510
556
|
private retryTimer;
|
|
511
557
|
private attempt;
|
|
512
|
-
/**
|
|
513
|
-
|
|
558
|
+
/**
|
|
559
|
+
* The reconnect loop is off: set by close(), and by a failure that
|
|
560
|
+
* retrying cannot fix (see `stop`). Cleared by connect().
|
|
561
|
+
*/
|
|
562
|
+
private stopped;
|
|
514
563
|
private nextFrameId;
|
|
515
564
|
private opening;
|
|
516
565
|
private readonly rest;
|
|
@@ -641,10 +690,30 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
641
690
|
* Closes for good.
|
|
642
691
|
*
|
|
643
692
|
* The distinction from a dropped socket is the whole point of the state
|
|
644
|
-
* machine:
|
|
645
|
-
*
|
|
693
|
+
* machine: `closed` means nothing is coming back, and a consumer showing
|
|
694
|
+
* "disconnected, retrying" versus "disconnected" needs it. The other
|
|
695
|
+
* ways to reach it are refusals retrying cannot fix -- see `stop`.
|
|
646
696
|
*/
|
|
647
697
|
close(): Promise<void>;
|
|
698
|
+
/**
|
|
699
|
+
* Stops for a reason retrying cannot fix, and says which.
|
|
700
|
+
*
|
|
701
|
+
* `close()` without the teardown of the page listeners -- a consumer
|
|
702
|
+
* that logs back in calls `connect()` on the same client -- and with an
|
|
703
|
+
* `error` event, because on a reconnect there is no caller to reject.
|
|
704
|
+
*/
|
|
705
|
+
private stop;
|
|
706
|
+
/**
|
|
707
|
+
* Resolves once the connection is open. Used by `Room.subscribe`.
|
|
708
|
+
*
|
|
709
|
+
* 연결 전에 부른 `subscribe()`를 `closed`로 거절하면 소비자는 "언제 다시
|
|
710
|
+
* 부를지"를 스스로 알아내야 하고, 첫 connect가 실패했다면 그 때는 영영 오지
|
|
711
|
+
* 않는다. 붙을 때까지 기다리면 그 신호가 필요 없다. 멈춘 클라이언트(close,
|
|
712
|
+
* 인증 거절)는 붙을 일이 없으니 거절한다.
|
|
713
|
+
*/
|
|
714
|
+
whenOpen(): Promise<void>;
|
|
715
|
+
private openWaiters;
|
|
716
|
+
private settleOpenWaiters;
|
|
648
717
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
649
718
|
send(type: string, data?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
650
719
|
/**
|
|
@@ -700,28 +769,6 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
700
769
|
}
|
|
701
770
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
702
771
|
|
|
703
|
-
/**
|
|
704
|
-
* Every failure this SDK raises, with the server's own code on it.
|
|
705
|
-
*
|
|
706
|
-
* The code matters more than the message. A consumer branches on
|
|
707
|
-
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
708
|
-
* bare `Error` carrying prose forces them to match on strings the server
|
|
709
|
-
* is free to reword.
|
|
710
|
-
*/
|
|
711
|
-
declare class ChatError extends Error {
|
|
712
|
-
readonly code: string;
|
|
713
|
-
/** Present on rate limits, in milliseconds. */
|
|
714
|
-
readonly retryAfterMs?: number;
|
|
715
|
-
/** The consumer's own code, when a before_publish hook denied this. */
|
|
716
|
-
readonly appCode?: string;
|
|
717
|
-
/** The HTTP status, when this came from a REST call. */
|
|
718
|
-
status?: number;
|
|
719
|
-
constructor(code: string, message: string, extra?: {
|
|
720
|
-
retryAfterMs?: number;
|
|
721
|
-
appCode?: string;
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
772
|
/**
|
|
726
773
|
* The message list, in `seq` order, with its holes filled.
|
|
727
774
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,28 @@ declare class Emitter<Events extends Record<string, unknown>> {
|
|
|
14
14
|
clear(): void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Every failure this SDK raises, with the server's own code on it.
|
|
19
|
+
*
|
|
20
|
+
* The code matters more than the message. A consumer branches on
|
|
21
|
+
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
22
|
+
* bare `Error` carrying prose forces them to match on strings the server
|
|
23
|
+
* is free to reword.
|
|
24
|
+
*/
|
|
25
|
+
declare class ChatError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
/** Present on rate limits, in milliseconds. */
|
|
28
|
+
readonly retryAfterMs?: number;
|
|
29
|
+
/** The consumer's own code, when a before_publish hook denied this. */
|
|
30
|
+
readonly appCode?: string;
|
|
31
|
+
/** The HTTP status, when this came from a REST call. */
|
|
32
|
+
status?: number;
|
|
33
|
+
constructor(code: string, message: string, extra?: {
|
|
34
|
+
retryAfterMs?: number;
|
|
35
|
+
appCode?: string;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
40
|
* The wire shapes this SDK reads and writes.
|
|
19
41
|
*
|
|
@@ -166,11 +188,11 @@ type RoomEvents = {
|
|
|
166
188
|
* socket's read loop where there is no caller to throw to -- and
|
|
167
189
|
* swallowing it is what leaves a hole nothing ever fills.
|
|
168
190
|
*
|
|
169
|
-
* The
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
191
|
+
* The room repairs itself: it re-subscribes with backoff (1 s doubling,
|
|
192
|
+
* up to five tries while connected), and every reconnect tries again.
|
|
193
|
+
* `room.subscribe()` is still the retry to offer a person pressing a
|
|
194
|
+
* button -- a room that emitted this is left not-subscribed on purpose,
|
|
195
|
+
* precisely so that call does something.
|
|
174
196
|
*/
|
|
175
197
|
error: Error;
|
|
176
198
|
/**
|
|
@@ -252,6 +274,9 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
252
274
|
*/
|
|
253
275
|
private wanted;
|
|
254
276
|
private subscribing;
|
|
277
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
278
|
+
private repairTimer;
|
|
279
|
+
private repairs;
|
|
255
280
|
constructor(chat: ChatClient, rest: Rest, address: {
|
|
256
281
|
id?: string;
|
|
257
282
|
key?: string;
|
|
@@ -387,7 +412,17 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
387
412
|
* transient 500 takes a room out of the live feed for the life of the
|
|
388
413
|
* page.
|
|
389
414
|
*/
|
|
390
|
-
resume(): Promise<void>;
|
|
415
|
+
resume(restart?: boolean): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Reports a failure no caller was waiting for, and repairs.
|
|
418
|
+
*
|
|
419
|
+
* 백그라운드 실패(재접속 뒤 다시 구독, 구멍 메우기)에는 거절을 받을 호출자가
|
|
420
|
+
* 없다. 이벤트만 내고 두면 방은 다음 재접속이나 탭 복귀까지 구멍을 안고 —
|
|
421
|
+
* 구독 프레임부터 실패했다면 라이브 피드 밖에서 — 머문다. 그래서 스스로 다시
|
|
422
|
+
* 구독한다. 횟수를 묶는 것은 고칠 수 없는 실패(권한, 없어진 방)가 서버를
|
|
423
|
+
* 계속 두드리지 않게 하려는 것이고, 그 뒤는 재접속과 `subscribe()`의 몫이다.
|
|
424
|
+
*/
|
|
425
|
+
failed(err: unknown): void;
|
|
391
426
|
/**
|
|
392
427
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
393
428
|
* for.
|
|
@@ -474,7 +509,9 @@ type ChatClientOptions = {
|
|
|
474
509
|
* Called again on every connect, and again when a REST call is
|
|
475
510
|
* answered 401 (the call is then retried once), so an expired token is
|
|
476
511
|
* replaced rather than reused. A throw here is retried with backoff
|
|
477
|
-
* like a dropped socket
|
|
512
|
+
* like a dropped socket -- **unless it throws a `ChatError`**, which
|
|
513
|
+
* means "do not retry" (a ban, a session that ended): the client goes
|
|
514
|
+
* to `closed` and emits it as `error`.
|
|
478
515
|
*
|
|
479
516
|
* **Never sign these in the browser** -- signing needs the `sk_`, and
|
|
480
517
|
* a `sk_` in a browser is the whole app.
|
|
@@ -497,6 +534,15 @@ type ChatClientEvents = {
|
|
|
497
534
|
* SDK learns about it.
|
|
498
535
|
*/
|
|
499
536
|
frame: Frame;
|
|
537
|
+
/**
|
|
538
|
+
* 클라이언트가 스스로 `closed`로 멈췄고, 이게 그 이유다.
|
|
539
|
+
*
|
|
540
|
+
* 멈추는 길은 셋이다: `close()`, 서버의 인증 거절, `token()`이 던진
|
|
541
|
+
* `ChatError`. 첫 connect라면 거절로도 알 수 있지만 재접속 중이나 REST의
|
|
542
|
+
* 토큰 갱신 중에는 받을 호출자가 없어서, 이것 없이는 `state`가 `closed`로
|
|
543
|
+
* 바뀌는 것만 보이고 왜인지는 사라진다. `close()`로 닫을 때는 내지 않는다.
|
|
544
|
+
*/
|
|
545
|
+
error: ChatError;
|
|
500
546
|
};
|
|
501
547
|
declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
502
548
|
state: ConnectionState;
|
|
@@ -509,8 +555,11 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
509
555
|
private heartbeat;
|
|
510
556
|
private retryTimer;
|
|
511
557
|
private attempt;
|
|
512
|
-
/**
|
|
513
|
-
|
|
558
|
+
/**
|
|
559
|
+
* The reconnect loop is off: set by close(), and by a failure that
|
|
560
|
+
* retrying cannot fix (see `stop`). Cleared by connect().
|
|
561
|
+
*/
|
|
562
|
+
private stopped;
|
|
514
563
|
private nextFrameId;
|
|
515
564
|
private opening;
|
|
516
565
|
private readonly rest;
|
|
@@ -641,10 +690,30 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
641
690
|
* Closes for good.
|
|
642
691
|
*
|
|
643
692
|
* The distinction from a dropped socket is the whole point of the state
|
|
644
|
-
* machine:
|
|
645
|
-
*
|
|
693
|
+
* machine: `closed` means nothing is coming back, and a consumer showing
|
|
694
|
+
* "disconnected, retrying" versus "disconnected" needs it. The other
|
|
695
|
+
* ways to reach it are refusals retrying cannot fix -- see `stop`.
|
|
646
696
|
*/
|
|
647
697
|
close(): Promise<void>;
|
|
698
|
+
/**
|
|
699
|
+
* Stops for a reason retrying cannot fix, and says which.
|
|
700
|
+
*
|
|
701
|
+
* `close()` without the teardown of the page listeners -- a consumer
|
|
702
|
+
* that logs back in calls `connect()` on the same client -- and with an
|
|
703
|
+
* `error` event, because on a reconnect there is no caller to reject.
|
|
704
|
+
*/
|
|
705
|
+
private stop;
|
|
706
|
+
/**
|
|
707
|
+
* Resolves once the connection is open. Used by `Room.subscribe`.
|
|
708
|
+
*
|
|
709
|
+
* 연결 전에 부른 `subscribe()`를 `closed`로 거절하면 소비자는 "언제 다시
|
|
710
|
+
* 부를지"를 스스로 알아내야 하고, 첫 connect가 실패했다면 그 때는 영영 오지
|
|
711
|
+
* 않는다. 붙을 때까지 기다리면 그 신호가 필요 없다. 멈춘 클라이언트(close,
|
|
712
|
+
* 인증 거절)는 붙을 일이 없으니 거절한다.
|
|
713
|
+
*/
|
|
714
|
+
whenOpen(): Promise<void>;
|
|
715
|
+
private openWaiters;
|
|
716
|
+
private settleOpenWaiters;
|
|
648
717
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
649
718
|
send(type: string, data?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
650
719
|
/**
|
|
@@ -700,28 +769,6 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
700
769
|
}
|
|
701
770
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
702
771
|
|
|
703
|
-
/**
|
|
704
|
-
* Every failure this SDK raises, with the server's own code on it.
|
|
705
|
-
*
|
|
706
|
-
* The code matters more than the message. A consumer branches on
|
|
707
|
-
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
708
|
-
* bare `Error` carrying prose forces them to match on strings the server
|
|
709
|
-
* is free to reword.
|
|
710
|
-
*/
|
|
711
|
-
declare class ChatError extends Error {
|
|
712
|
-
readonly code: string;
|
|
713
|
-
/** Present on rate limits, in milliseconds. */
|
|
714
|
-
readonly retryAfterMs?: number;
|
|
715
|
-
/** The consumer's own code, when a before_publish hook denied this. */
|
|
716
|
-
readonly appCode?: string;
|
|
717
|
-
/** The HTTP status, when this came from a REST call. */
|
|
718
|
-
status?: number;
|
|
719
|
-
constructor(code: string, message: string, extra?: {
|
|
720
|
-
retryAfterMs?: number;
|
|
721
|
-
appCode?: string;
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
772
|
/**
|
|
726
773
|
* The message list, in `seq` order, with its holes filled.
|
|
727
774
|
*
|