@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.js
CHANGED
|
@@ -424,6 +424,8 @@ function createTimeline(options) {
|
|
|
424
424
|
|
|
425
425
|
// src/room.ts
|
|
426
426
|
var HistoryPageSize = 100;
|
|
427
|
+
var MaxRepairs = 5;
|
|
428
|
+
var RepairBaseMs = 1e3;
|
|
427
429
|
var Room = class extends Emitter {
|
|
428
430
|
/** Resolved on subscribe when the room was addressed by key. */
|
|
429
431
|
id;
|
|
@@ -461,6 +463,9 @@ var Room = class extends Emitter {
|
|
|
461
463
|
*/
|
|
462
464
|
wanted = false;
|
|
463
465
|
subscribing;
|
|
466
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
467
|
+
repairTimer;
|
|
468
|
+
repairs = 0;
|
|
464
469
|
constructor(chat, rest, address) {
|
|
465
470
|
super();
|
|
466
471
|
this.chat = chat;
|
|
@@ -527,7 +532,12 @@ var Room = class extends Emitter {
|
|
|
527
532
|
this.wanted = true;
|
|
528
533
|
if (this.subscribing !== void 0) return this.subscribing;
|
|
529
534
|
if (this.subscribed) return;
|
|
530
|
-
const attempt = this.doSubscribe().
|
|
535
|
+
const attempt = this.doSubscribe().then(() => {
|
|
536
|
+
const successor = this.subscribing;
|
|
537
|
+
if (successor !== void 0 && successor !== attempt) return successor;
|
|
538
|
+
if (this.subscribed) this.repairs = 0;
|
|
539
|
+
return void 0;
|
|
540
|
+
}).finally(() => {
|
|
531
541
|
if (this.subscribing === attempt) this.subscribing = void 0;
|
|
532
542
|
});
|
|
533
543
|
this.subscribing = attempt;
|
|
@@ -536,6 +546,10 @@ var Room = class extends Emitter {
|
|
|
536
546
|
async doSubscribe() {
|
|
537
547
|
const gen = this.generation;
|
|
538
548
|
const current = () => gen === this.generation;
|
|
549
|
+
if (this.chat.state !== "open") {
|
|
550
|
+
await this.chat.whenOpen();
|
|
551
|
+
if (!current()) return;
|
|
552
|
+
}
|
|
539
553
|
const data = this.id !== void 0 ? { roomId: this.id } : { key: this.key };
|
|
540
554
|
const since = this.timeline.contiguousSeq;
|
|
541
555
|
if (since > 0) data["since"] = since;
|
|
@@ -665,13 +679,33 @@ var Room = class extends Emitter {
|
|
|
665
679
|
* transient 500 takes a room out of the live feed for the life of the
|
|
666
680
|
* page.
|
|
667
681
|
*/
|
|
668
|
-
async resume() {
|
|
682
|
+
async resume(restart = true) {
|
|
669
683
|
if (!this.wanted) return;
|
|
684
|
+
if (!restart) return this.subscribe();
|
|
670
685
|
this.generation++;
|
|
671
686
|
this.subscribing = void 0;
|
|
672
687
|
this.subscribed = false;
|
|
673
688
|
await this.subscribe();
|
|
674
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Reports a failure no caller was waiting for, and repairs.
|
|
692
|
+
*
|
|
693
|
+
* 백그라운드 실패(재접속 뒤 다시 구독, 구멍 메우기)에는 거절을 받을 호출자가
|
|
694
|
+
* 없다. 이벤트만 내고 두면 방은 다음 재접속이나 탭 복귀까지 구멍을 안고 —
|
|
695
|
+
* 구독 프레임부터 실패했다면 라이브 피드 밖에서 — 머문다. 그래서 스스로 다시
|
|
696
|
+
* 구독한다. 횟수를 묶는 것은 고칠 수 없는 실패(권한, 없어진 방)가 서버를
|
|
697
|
+
* 계속 두드리지 않게 하려는 것이고, 그 뒤는 재접속과 `subscribe()`의 몫이다.
|
|
698
|
+
*/
|
|
699
|
+
failed(err) {
|
|
700
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
701
|
+
if (this.repairTimer !== void 0 || this.repairs >= MaxRepairs) return;
|
|
702
|
+
const delay = RepairBaseMs * 2 ** this.repairs++;
|
|
703
|
+
this.repairTimer = setTimeout(() => {
|
|
704
|
+
this.repairTimer = void 0;
|
|
705
|
+
if (!this.wanted || this.subscribed || this.chat.state !== "open") return;
|
|
706
|
+
this.subscribe().catch((e) => this.failed(e));
|
|
707
|
+
}, delay);
|
|
708
|
+
}
|
|
675
709
|
/**
|
|
676
710
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
677
711
|
* for.
|
|
@@ -753,7 +787,7 @@ var Room = class extends Emitter {
|
|
|
753
787
|
if (data.seq > this.lastSeq) this.lastSeq = data.seq;
|
|
754
788
|
this.timeline.add(data).catch((err) => {
|
|
755
789
|
this.subscribed = false;
|
|
756
|
-
this.
|
|
790
|
+
this.failed(err);
|
|
757
791
|
});
|
|
758
792
|
break;
|
|
759
793
|
case "message.updated":
|
|
@@ -822,8 +856,11 @@ var ChatClient = class extends Emitter {
|
|
|
822
856
|
heartbeat;
|
|
823
857
|
retryTimer;
|
|
824
858
|
attempt = 0;
|
|
825
|
-
/**
|
|
826
|
-
|
|
859
|
+
/**
|
|
860
|
+
* The reconnect loop is off: set by close(), and by a failure that
|
|
861
|
+
* retrying cannot fix (see `stop`). Cleared by connect().
|
|
862
|
+
*/
|
|
863
|
+
stopped = false;
|
|
827
864
|
nextFrameId = 0;
|
|
828
865
|
opening;
|
|
829
866
|
rest;
|
|
@@ -901,7 +938,7 @@ var ChatClient = class extends Emitter {
|
|
|
901
938
|
* gap-filled via REST.
|
|
902
939
|
*/
|
|
903
940
|
async resume() {
|
|
904
|
-
if (this.
|
|
941
|
+
if (this.stopped) return;
|
|
905
942
|
if (this.state !== "open" || this.socket === void 0) {
|
|
906
943
|
this.clearTimers();
|
|
907
944
|
return this.connect();
|
|
@@ -913,9 +950,7 @@ var ChatClient = class extends Emitter {
|
|
|
913
950
|
return this.connect();
|
|
914
951
|
}
|
|
915
952
|
for (const room of this.handles.values()) {
|
|
916
|
-
void room.resume().catch((err) =>
|
|
917
|
-
room.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
918
|
-
});
|
|
953
|
+
void room.resume().catch((err) => room.failed(err));
|
|
919
954
|
}
|
|
920
955
|
}
|
|
921
956
|
/** The account-level routes: the rooms this user is in, and unread. */
|
|
@@ -1007,7 +1042,7 @@ var ChatClient = class extends Emitter {
|
|
|
1007
1042
|
}
|
|
1008
1043
|
/** Opens the connection and resolves when `hello` arrives. */
|
|
1009
1044
|
async connect() {
|
|
1010
|
-
this.
|
|
1045
|
+
this.stopped = false;
|
|
1011
1046
|
if (this.opening !== void 0) return this.opening;
|
|
1012
1047
|
if (this.state === "open" && this.socket !== void 0) return;
|
|
1013
1048
|
this.clearTimers();
|
|
@@ -1020,11 +1055,12 @@ var ChatClient = class extends Emitter {
|
|
|
1020
1055
|
* Closes for good.
|
|
1021
1056
|
*
|
|
1022
1057
|
* The distinction from a dropped socket is the whole point of the state
|
|
1023
|
-
* machine:
|
|
1024
|
-
*
|
|
1058
|
+
* machine: `closed` means nothing is coming back, and a consumer showing
|
|
1059
|
+
* "disconnected, retrying" versus "disconnected" needs it. The other
|
|
1060
|
+
* ways to reach it are refusals retrying cannot fix -- see `stop`.
|
|
1025
1061
|
*/
|
|
1026
1062
|
async close() {
|
|
1027
|
-
this.
|
|
1063
|
+
this.stopped = true;
|
|
1028
1064
|
this.teardowns.forEach((t) => {
|
|
1029
1065
|
try {
|
|
1030
1066
|
t();
|
|
@@ -1041,7 +1077,53 @@ var ChatClient = class extends Emitter {
|
|
|
1041
1077
|
} catch {
|
|
1042
1078
|
}
|
|
1043
1079
|
}
|
|
1080
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1081
|
+
this.setState("closed");
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Stops for a reason retrying cannot fix, and says which.
|
|
1085
|
+
*
|
|
1086
|
+
* `close()` without the teardown of the page listeners -- a consumer
|
|
1087
|
+
* that logs back in calls `connect()` on the same client -- and with an
|
|
1088
|
+
* `error` event, because on a reconnect there is no caller to reject.
|
|
1089
|
+
*/
|
|
1090
|
+
stop(err) {
|
|
1091
|
+
this.stopped = true;
|
|
1092
|
+
this.clearTimers();
|
|
1093
|
+
const socket = this.socket;
|
|
1094
|
+
this.socket = void 0;
|
|
1095
|
+
if (socket !== void 0) {
|
|
1096
|
+
try {
|
|
1097
|
+
socket.close(1e3, "client stopped");
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
this.failPending(err);
|
|
1102
|
+
this.settleOpenWaiters(new ChatError("closed", "the connection is closed"));
|
|
1044
1103
|
this.setState("closed");
|
|
1104
|
+
this.emit("error", err);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Resolves once the connection is open. Used by `Room.subscribe`.
|
|
1108
|
+
*
|
|
1109
|
+
* 연결 전에 부른 `subscribe()`를 `closed`로 거절하면 소비자는 "언제 다시
|
|
1110
|
+
* 부를지"를 스스로 알아내야 하고, 첫 connect가 실패했다면 그 때는 영영 오지
|
|
1111
|
+
* 않는다. 붙을 때까지 기다리면 그 신호가 필요 없다. 멈춘 클라이언트(close,
|
|
1112
|
+
* 인증 거절)는 붙을 일이 없으니 거절한다.
|
|
1113
|
+
*/
|
|
1114
|
+
whenOpen() {
|
|
1115
|
+
if (this.state === "open" && this.socket !== void 0) return Promise.resolve();
|
|
1116
|
+
if (this.stopped) return Promise.reject(new ChatError("closed", "the connection is closed"));
|
|
1117
|
+
return new Promise((resolve, reject) => void this.openWaiters.push({ resolve, reject }));
|
|
1118
|
+
}
|
|
1119
|
+
openWaiters = [];
|
|
1120
|
+
settleOpenWaiters(err) {
|
|
1121
|
+
const waiters = this.openWaiters;
|
|
1122
|
+
this.openWaiters = [];
|
|
1123
|
+
for (const w of waiters) {
|
|
1124
|
+
if (err === void 0) w.resolve();
|
|
1125
|
+
else w.reject(err);
|
|
1126
|
+
}
|
|
1045
1127
|
}
|
|
1046
1128
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
1047
1129
|
send(type, data, timeoutMs = 15e3) {
|
|
@@ -1088,10 +1170,14 @@ var ChatClient = class extends Emitter {
|
|
|
1088
1170
|
try {
|
|
1089
1171
|
authData = await this.authData();
|
|
1090
1172
|
} catch (err) {
|
|
1173
|
+
if (isChatError(err)) {
|
|
1174
|
+
this.stop(err);
|
|
1175
|
+
throw err;
|
|
1176
|
+
}
|
|
1091
1177
|
this.scheduleReconnect();
|
|
1092
1178
|
throw err;
|
|
1093
1179
|
}
|
|
1094
|
-
if (this.
|
|
1180
|
+
if (this.stopped) {
|
|
1095
1181
|
throw new ChatError("closed", "the client was closed while connecting");
|
|
1096
1182
|
}
|
|
1097
1183
|
const socket = new this.makeSocket(`${this.options.url}/v1/ws?v=1`);
|
|
@@ -1112,9 +1198,14 @@ var ChatClient = class extends Emitter {
|
|
|
1112
1198
|
return;
|
|
1113
1199
|
}
|
|
1114
1200
|
if (frame.type === "error" && !settled) {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1201
|
+
const err = errorFrom(frame.data);
|
|
1202
|
+
if (err.code === "rate_limited" || err.code === "internal") {
|
|
1203
|
+
if (err.retryAfterMs !== void 0 && err.retryAfterMs > 0) this.plannedDelayMs = err.retryAfterMs;
|
|
1204
|
+
finish(err);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
finish(err);
|
|
1208
|
+
this.stop(err);
|
|
1118
1209
|
return;
|
|
1119
1210
|
}
|
|
1120
1211
|
this.onFrame(frame);
|
|
@@ -1147,12 +1238,11 @@ var ChatClient = class extends Emitter {
|
|
|
1147
1238
|
const reconnected = this.everOpened;
|
|
1148
1239
|
this.everOpened = true;
|
|
1149
1240
|
this.setState("open");
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1241
|
+
this.settleOpenWaiters();
|
|
1242
|
+
for (const room of this.handles.values()) {
|
|
1243
|
+
void room.resume(reconnected).catch((err) => {
|
|
1244
|
+
room.failed(err);
|
|
1245
|
+
});
|
|
1156
1246
|
}
|
|
1157
1247
|
}
|
|
1158
1248
|
onFrame(frame) {
|
|
@@ -1222,7 +1312,7 @@ var ChatClient = class extends Emitter {
|
|
|
1222
1312
|
}, Math.max(1e3, intervalMs));
|
|
1223
1313
|
}
|
|
1224
1314
|
scheduleReconnect() {
|
|
1225
|
-
if (this.
|
|
1315
|
+
if (this.stopped) return;
|
|
1226
1316
|
this.setState("reconnecting");
|
|
1227
1317
|
const delay = this.nextDelay();
|
|
1228
1318
|
this.retryTimer = setTimeout(() => {
|
|
@@ -1274,6 +1364,9 @@ var ChatClient = class extends Emitter {
|
|
|
1274
1364
|
const token = await this.options.token();
|
|
1275
1365
|
this.lastToken = token;
|
|
1276
1366
|
return token;
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
if (isChatError(err)) this.stop(err);
|
|
1369
|
+
throw err;
|
|
1277
1370
|
} finally {
|
|
1278
1371
|
this.refreshing = void 0;
|
|
1279
1372
|
}
|
|
@@ -1295,6 +1388,9 @@ var ChatClient = class extends Emitter {
|
|
|
1295
1388
|
this.retryTimer = void 0;
|
|
1296
1389
|
}
|
|
1297
1390
|
};
|
|
1391
|
+
function isChatError(err) {
|
|
1392
|
+
return err instanceof ChatError || err instanceof Error && err.name === "ChatError";
|
|
1393
|
+
}
|
|
1298
1394
|
function createChatClient(options) {
|
|
1299
1395
|
return new ChatClient(options);
|
|
1300
1396
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kispi/chat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Client SDK for the chat server: one WebSocket, many rooms, ordered history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"README.en.md"
|
|
10
11
|
],
|
|
11
12
|
"exports": {
|
|
12
13
|
".": {
|