@kispi/chat 0.1.2 → 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 +142 -11
- package/dist/{chunk-T3QOUFOM.js → chunk-5QQRM7BH.js} +2 -0
- package/dist/index.cjs +265 -38
- package/dist/index.d.cts +173 -36
- package/dist/index.d.ts +173 -36
- package/dist/index.js +264 -39
- package/dist/server/index.cjs +59 -10
- package/dist/server/index.d.cts +52 -1
- package/dist/server/index.d.ts +52 -1
- package/dist/server/index.js +57 -11
- package/package.json +3 -2
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
|
*
|
|
@@ -75,6 +97,11 @@ type RestOptions = {
|
|
|
75
97
|
key: string;
|
|
76
98
|
/** Returns the current user JWT, or undefined before the first connect. */
|
|
77
99
|
token: () => Promise<string | undefined>;
|
|
100
|
+
/**
|
|
101
|
+
* Fetches a fresh token after a 401, or undefined when there is none to
|
|
102
|
+
* fetch. Optional so a Rest built without it behaves as before.
|
|
103
|
+
*/
|
|
104
|
+
refreshToken?: () => Promise<string | undefined>;
|
|
78
105
|
fetch: typeof globalThis.fetch;
|
|
79
106
|
};
|
|
80
107
|
declare class Rest {
|
|
@@ -86,6 +113,7 @@ declare class Rest {
|
|
|
86
113
|
put<T>(path: string, body?: unknown): Promise<T>;
|
|
87
114
|
delete<T>(path: string): Promise<T>;
|
|
88
115
|
private call;
|
|
116
|
+
private callWith;
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
/** A message as the server renders it. */
|
|
@@ -160,11 +188,11 @@ type RoomEvents = {
|
|
|
160
188
|
* socket's read loop where there is no caller to throw to -- and
|
|
161
189
|
* swallowing it is what leaves a hole nothing ever fills.
|
|
162
190
|
*
|
|
163
|
-
* The
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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.
|
|
168
196
|
*/
|
|
169
197
|
error: Error;
|
|
170
198
|
/**
|
|
@@ -197,7 +225,12 @@ type RoomEvents = {
|
|
|
197
225
|
'room.updated': any;
|
|
198
226
|
'room.deleted': any;
|
|
199
227
|
custom: any;
|
|
200
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* The room's message list changed, for any reason.
|
|
230
|
+
*
|
|
231
|
+
* **A new array every time**, and one that is never mutated after it is
|
|
232
|
+
* emitted, so it can go straight into state that compares by reference.
|
|
233
|
+
*/
|
|
201
234
|
messages: Message[];
|
|
202
235
|
};
|
|
203
236
|
declare class Room extends Emitter<RoomEvents> {
|
|
@@ -241,12 +274,34 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
241
274
|
*/
|
|
242
275
|
private wanted;
|
|
243
276
|
private subscribing;
|
|
277
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
278
|
+
private repairTimer;
|
|
279
|
+
private repairs;
|
|
244
280
|
constructor(chat: ChatClient, rest: Rest, address: {
|
|
245
281
|
id?: string;
|
|
246
282
|
key?: string;
|
|
247
283
|
});
|
|
248
|
-
/** Everything this client knows about the room, in seq order. */
|
|
284
|
+
/** Everything this client knows about the room, in seq order. A new array whenever it changes. */
|
|
249
285
|
get messages(): Message[];
|
|
286
|
+
/**
|
|
287
|
+
* Reads the page just before the oldest message held and **prepends it to
|
|
288
|
+
* `messages`**.
|
|
289
|
+
*
|
|
290
|
+
* The scroll-back call. Unlike `history()`, the rows join the room's own
|
|
291
|
+
* list, so ordering, de-duplication, deletes, reactions and reconnects
|
|
292
|
+
* apply to them like any other row, and the `messages` event fires once.
|
|
293
|
+
* A `reset` (or `reload()`) drops them with everything else; a read that
|
|
294
|
+
* lands after one is discarded rather than stitched onto the new list.
|
|
295
|
+
*
|
|
296
|
+
* `hasMore` is false once the top of the room -- or of its retention --
|
|
297
|
+
* is reached. Concurrent calls share one request.
|
|
298
|
+
*/
|
|
299
|
+
loadOlder(options?: {
|
|
300
|
+
limit?: number;
|
|
301
|
+
}): Promise<{
|
|
302
|
+
messages: Message[];
|
|
303
|
+
hasMore: boolean;
|
|
304
|
+
}>;
|
|
250
305
|
/**
|
|
251
306
|
* Reloads recent history, discarding what is held.
|
|
252
307
|
*
|
|
@@ -277,7 +332,8 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
277
332
|
*
|
|
278
333
|
* The list this room keeps is the live one; this is for a consumer
|
|
279
334
|
* scrolling back, which owns its own window and does not want the
|
|
280
|
-
* bottom of the room rearranged under it.
|
|
335
|
+
* bottom of the room rearranged under it. **Scrolling the room's own
|
|
336
|
+
* list back is `loadOlder()`**, which keeps one list instead of two.
|
|
281
337
|
*
|
|
282
338
|
* `view` defaults to the server's, which is every message including
|
|
283
339
|
* thread replies. **`view: 'main'` is a display filter, not a sync
|
|
@@ -356,7 +412,17 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
356
412
|
* transient 500 takes a room out of the live feed for the life of the
|
|
357
413
|
* page.
|
|
358
414
|
*/
|
|
359
|
-
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;
|
|
360
426
|
/**
|
|
361
427
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
362
428
|
* for.
|
|
@@ -379,6 +445,16 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
379
445
|
* mistake.
|
|
380
446
|
*/
|
|
381
447
|
private requireId;
|
|
448
|
+
/**
|
|
449
|
+
* The room's id, waiting for a subscribe in flight to learn it.
|
|
450
|
+
*
|
|
451
|
+
* `roomByKey(k).subscribe()`를 await하지 않고 곧바로 `send`하는 것은 자연스러운
|
|
452
|
+
* 코드이고, 그때 id는 구독 ack가 와야 생긴다. 거절(`closed`)하면 소비자는
|
|
453
|
+
* "보내도 되는 때"를 알릴 신호를 따로 찾아야 한다 — 이미 날아가고 있는
|
|
454
|
+
* 구독을 기다리면 그 신호가 필요 없다. 구독이 실패하면 그 실패가 그대로
|
|
455
|
+
* 나간다. 구독한 적이 없으면 기다릴 것이 없으니 예전처럼 거절한다.
|
|
456
|
+
*/
|
|
457
|
+
private resolveId;
|
|
382
458
|
/** Publishes and resolves when the server acks. */
|
|
383
459
|
send(input: SendInput, options?: SendOptions): Promise<PublishAck>;
|
|
384
460
|
/** Routes a frame the client decided belongs to this room. */
|
|
@@ -430,9 +506,15 @@ type ChatClientOptions = {
|
|
|
430
506
|
* has no way to decide who anybody is on its own, and the modes that
|
|
431
507
|
* pretended otherwise were removed.
|
|
432
508
|
*
|
|
433
|
-
* Called again on every connect,
|
|
434
|
-
*
|
|
435
|
-
*
|
|
509
|
+
* Called again on every connect, and again when a REST call is
|
|
510
|
+
* answered 401 (the call is then retried once), so an expired token is
|
|
511
|
+
* replaced rather than reused. A throw here is retried with backoff
|
|
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`.
|
|
515
|
+
*
|
|
516
|
+
* **Never sign these in the browser** -- signing needs the `sk_`, and
|
|
517
|
+
* a `sk_` in a browser is the whole app.
|
|
436
518
|
*/
|
|
437
519
|
token: () => string | Promise<string>;
|
|
438
520
|
/** external mode. **The server refuses this today** -- the verification half is unbuilt. */
|
|
@@ -452,6 +534,15 @@ type ChatClientEvents = {
|
|
|
452
534
|
* SDK learns about it.
|
|
453
535
|
*/
|
|
454
536
|
frame: Frame;
|
|
537
|
+
/**
|
|
538
|
+
* 클라이언트가 스스로 `closed`로 멈췄고, 이게 그 이유다.
|
|
539
|
+
*
|
|
540
|
+
* 멈추는 길은 셋이다: `close()`, 서버의 인증 거절, `token()`이 던진
|
|
541
|
+
* `ChatError`. 첫 connect라면 거절로도 알 수 있지만 재접속 중이나 REST의
|
|
542
|
+
* 토큰 갱신 중에는 받을 호출자가 없어서, 이것 없이는 `state`가 `closed`로
|
|
543
|
+
* 바뀌는 것만 보이고 왜인지는 사라진다. `close()`로 닫을 때는 내지 않는다.
|
|
544
|
+
*/
|
|
545
|
+
error: ChatError;
|
|
455
546
|
};
|
|
456
547
|
declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
457
548
|
state: ConnectionState;
|
|
@@ -464,8 +555,11 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
464
555
|
private heartbeat;
|
|
465
556
|
private retryTimer;
|
|
466
557
|
private attempt;
|
|
467
|
-
/**
|
|
468
|
-
|
|
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;
|
|
469
563
|
private nextFrameId;
|
|
470
564
|
private opening;
|
|
471
565
|
private readonly rest;
|
|
@@ -596,10 +690,30 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
596
690
|
* Closes for good.
|
|
597
691
|
*
|
|
598
692
|
* The distinction from a dropped socket is the whole point of the state
|
|
599
|
-
* machine:
|
|
600
|
-
*
|
|
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`.
|
|
601
696
|
*/
|
|
602
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;
|
|
603
717
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
604
718
|
send(type: string, data?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
605
719
|
/**
|
|
@@ -639,32 +753,22 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
639
753
|
*/
|
|
640
754
|
private nextDelay;
|
|
641
755
|
private authData;
|
|
756
|
+
/**
|
|
757
|
+
* REST가 401을 받았을 때 토큰을 새로 받는다.
|
|
758
|
+
*
|
|
759
|
+
* 소켓은 접속할 때 한 번 인증하고 그 뒤로는 토큰을 다시 보지 않지만, REST는
|
|
760
|
+
* 요청마다 본다. 그래서 한 시간짜리 토큰이면 한 시간 뒤 라이브는 멀쩡한데
|
|
761
|
+
* 히스토리·구멍 메우기만 401이 된다. 동시에 실패한 요청들이 `token()`을
|
|
762
|
+
* 각자 부르지 않도록 진행 중인 것 하나를 나눠 쓴다.
|
|
763
|
+
*/
|
|
764
|
+
private refreshToken;
|
|
765
|
+
private refreshing;
|
|
642
766
|
private failPending;
|
|
643
767
|
private clearHeartbeat;
|
|
644
768
|
private clearTimers;
|
|
645
769
|
}
|
|
646
770
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
647
771
|
|
|
648
|
-
/**
|
|
649
|
-
* Every failure this SDK raises, with the server's own code on it.
|
|
650
|
-
*
|
|
651
|
-
* The code matters more than the message. A consumer branches on
|
|
652
|
-
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
653
|
-
* bare `Error` carrying prose forces them to match on strings the server
|
|
654
|
-
* is free to reword.
|
|
655
|
-
*/
|
|
656
|
-
declare class ChatError extends Error {
|
|
657
|
-
readonly code: string;
|
|
658
|
-
/** Present on rate limits, in milliseconds. */
|
|
659
|
-
readonly retryAfterMs?: number;
|
|
660
|
-
/** The consumer's own code, when a before_publish hook denied this. */
|
|
661
|
-
readonly appCode?: string;
|
|
662
|
-
constructor(code: string, message: string, extra?: {
|
|
663
|
-
retryAfterMs?: number;
|
|
664
|
-
appCode?: string;
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
772
|
/**
|
|
669
773
|
* The message list, in `seq` order, with its holes filled.
|
|
670
774
|
*
|
|
@@ -724,7 +828,20 @@ declare class Timeline {
|
|
|
724
828
|
* rows into the new list.
|
|
725
829
|
*/
|
|
726
830
|
private epoch;
|
|
831
|
+
/**
|
|
832
|
+
* `items` has been handed out -- by the getter or by `onChange`.
|
|
833
|
+
*
|
|
834
|
+
* 한 번 내준 배열은 다시 건드리지 않는다. 같은 배열을 제자리에서 고쳐
|
|
835
|
+
* 다시 내주면 Svelte `$state.raw`, React `useState`, Vue `shallowRef`처럼
|
|
836
|
+
* 참조가 바뀌어야 다시 그리는 쪽은 변화를 보지 못하고 화면이 멈춘다. 그래서
|
|
837
|
+
* 내준 뒤의 첫 변경은 복사본에서 한다(copy-on-write). 행 객체도 같은 규칙이라
|
|
838
|
+
* 바뀐 행만 새 객체다.
|
|
839
|
+
*/
|
|
840
|
+
private shared;
|
|
841
|
+
/** The `loadOlder` in flight, so a scroll handler firing twice asks once. */
|
|
842
|
+
private older;
|
|
727
843
|
constructor(options: TimelineOptions);
|
|
844
|
+
/** A new array whenever the list changes; never mutated once returned. */
|
|
728
845
|
get messages(): Message[];
|
|
729
846
|
/** The highest seq this timeline holds, hole or no hole. */
|
|
730
847
|
get highestSeq(): number;
|
|
@@ -807,6 +924,24 @@ declare class Timeline {
|
|
|
807
924
|
thread(rootId: string, count: number, lastSeq?: number): void;
|
|
808
925
|
/** Applies a `message.deleted`, remembering it if the row is not here. */
|
|
809
926
|
remove(id: string): void;
|
|
927
|
+
/**
|
|
928
|
+
* Prepends the page just below the oldest message held.
|
|
929
|
+
*
|
|
930
|
+
* 스크롤을 올려 과거를 읽는 소비자가 `history()` 결과를 라이브 목록과 따로
|
|
931
|
+
* 들고 합치고, 중복을 걸러 내고, reset과 재접속을 따로 처리해야 했다. 같은
|
|
932
|
+
* 목록에 끼워 넣으면 그 일이 전부 이미 있는 규칙(seq 자리에 넣기, 같은 seq는
|
|
933
|
+
* 한 행, reset이면 epoch로 버리기)으로 끝난다.
|
|
934
|
+
*
|
|
935
|
+
* `hasMore`는 더 올라갈 것이 있는지다. 서버의 seq는 1부터 구멍 없이 붙으므로
|
|
936
|
+
* 맨 위가 1이거나 페이지가 덜 찼으면 끝이다(보존 기간이 지운 앞부분도 덜 찬
|
|
937
|
+
* 페이지로 드러난다). 한 번에 하나만 돈다 — 스크롤 핸들러가 두 번 불러도
|
|
938
|
+
* 요청은 하나다(진행 중인 호출과 같은 결과를 받으므로 뒤 호출의 `limit`은 쓰이지 않는다).
|
|
939
|
+
*/
|
|
940
|
+
loadOlder(limit?: number): Promise<{
|
|
941
|
+
messages: Message[];
|
|
942
|
+
hasMore: boolean;
|
|
943
|
+
}>;
|
|
944
|
+
private readOlder;
|
|
810
945
|
/**
|
|
811
946
|
* Fetches everything between what we have and `upTo`.
|
|
812
947
|
*
|
|
@@ -827,6 +962,8 @@ declare class Timeline {
|
|
|
827
962
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
828
963
|
private insert;
|
|
829
964
|
private applyDelete;
|
|
965
|
+
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
966
|
+
private own;
|
|
830
967
|
private changed;
|
|
831
968
|
}
|
|
832
969
|
declare function createTimeline(options: TimelineOptions): Timeline;
|
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
|
*
|
|
@@ -75,6 +97,11 @@ type RestOptions = {
|
|
|
75
97
|
key: string;
|
|
76
98
|
/** Returns the current user JWT, or undefined before the first connect. */
|
|
77
99
|
token: () => Promise<string | undefined>;
|
|
100
|
+
/**
|
|
101
|
+
* Fetches a fresh token after a 401, or undefined when there is none to
|
|
102
|
+
* fetch. Optional so a Rest built without it behaves as before.
|
|
103
|
+
*/
|
|
104
|
+
refreshToken?: () => Promise<string | undefined>;
|
|
78
105
|
fetch: typeof globalThis.fetch;
|
|
79
106
|
};
|
|
80
107
|
declare class Rest {
|
|
@@ -86,6 +113,7 @@ declare class Rest {
|
|
|
86
113
|
put<T>(path: string, body?: unknown): Promise<T>;
|
|
87
114
|
delete<T>(path: string): Promise<T>;
|
|
88
115
|
private call;
|
|
116
|
+
private callWith;
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
/** A message as the server renders it. */
|
|
@@ -160,11 +188,11 @@ type RoomEvents = {
|
|
|
160
188
|
* socket's read loop where there is no caller to throw to -- and
|
|
161
189
|
* swallowing it is what leaves a hole nothing ever fills.
|
|
162
190
|
*
|
|
163
|
-
* The
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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.
|
|
168
196
|
*/
|
|
169
197
|
error: Error;
|
|
170
198
|
/**
|
|
@@ -197,7 +225,12 @@ type RoomEvents = {
|
|
|
197
225
|
'room.updated': any;
|
|
198
226
|
'room.deleted': any;
|
|
199
227
|
custom: any;
|
|
200
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* The room's message list changed, for any reason.
|
|
230
|
+
*
|
|
231
|
+
* **A new array every time**, and one that is never mutated after it is
|
|
232
|
+
* emitted, so it can go straight into state that compares by reference.
|
|
233
|
+
*/
|
|
201
234
|
messages: Message[];
|
|
202
235
|
};
|
|
203
236
|
declare class Room extends Emitter<RoomEvents> {
|
|
@@ -241,12 +274,34 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
241
274
|
*/
|
|
242
275
|
private wanted;
|
|
243
276
|
private subscribing;
|
|
277
|
+
/** Self-repair: the armed retry, and how many have run since the last success. */
|
|
278
|
+
private repairTimer;
|
|
279
|
+
private repairs;
|
|
244
280
|
constructor(chat: ChatClient, rest: Rest, address: {
|
|
245
281
|
id?: string;
|
|
246
282
|
key?: string;
|
|
247
283
|
});
|
|
248
|
-
/** Everything this client knows about the room, in seq order. */
|
|
284
|
+
/** Everything this client knows about the room, in seq order. A new array whenever it changes. */
|
|
249
285
|
get messages(): Message[];
|
|
286
|
+
/**
|
|
287
|
+
* Reads the page just before the oldest message held and **prepends it to
|
|
288
|
+
* `messages`**.
|
|
289
|
+
*
|
|
290
|
+
* The scroll-back call. Unlike `history()`, the rows join the room's own
|
|
291
|
+
* list, so ordering, de-duplication, deletes, reactions and reconnects
|
|
292
|
+
* apply to them like any other row, and the `messages` event fires once.
|
|
293
|
+
* A `reset` (or `reload()`) drops them with everything else; a read that
|
|
294
|
+
* lands after one is discarded rather than stitched onto the new list.
|
|
295
|
+
*
|
|
296
|
+
* `hasMore` is false once the top of the room -- or of its retention --
|
|
297
|
+
* is reached. Concurrent calls share one request.
|
|
298
|
+
*/
|
|
299
|
+
loadOlder(options?: {
|
|
300
|
+
limit?: number;
|
|
301
|
+
}): Promise<{
|
|
302
|
+
messages: Message[];
|
|
303
|
+
hasMore: boolean;
|
|
304
|
+
}>;
|
|
250
305
|
/**
|
|
251
306
|
* Reloads recent history, discarding what is held.
|
|
252
307
|
*
|
|
@@ -277,7 +332,8 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
277
332
|
*
|
|
278
333
|
* The list this room keeps is the live one; this is for a consumer
|
|
279
334
|
* scrolling back, which owns its own window and does not want the
|
|
280
|
-
* bottom of the room rearranged under it.
|
|
335
|
+
* bottom of the room rearranged under it. **Scrolling the room's own
|
|
336
|
+
* list back is `loadOlder()`**, which keeps one list instead of two.
|
|
281
337
|
*
|
|
282
338
|
* `view` defaults to the server's, which is every message including
|
|
283
339
|
* thread replies. **`view: 'main'` is a display filter, not a sync
|
|
@@ -356,7 +412,17 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
356
412
|
* transient 500 takes a room out of the live feed for the life of the
|
|
357
413
|
* page.
|
|
358
414
|
*/
|
|
359
|
-
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;
|
|
360
426
|
/**
|
|
361
427
|
* Sends `subscribe` and recovers from the one error it has an answer
|
|
362
428
|
* for.
|
|
@@ -379,6 +445,16 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
379
445
|
* mistake.
|
|
380
446
|
*/
|
|
381
447
|
private requireId;
|
|
448
|
+
/**
|
|
449
|
+
* The room's id, waiting for a subscribe in flight to learn it.
|
|
450
|
+
*
|
|
451
|
+
* `roomByKey(k).subscribe()`를 await하지 않고 곧바로 `send`하는 것은 자연스러운
|
|
452
|
+
* 코드이고, 그때 id는 구독 ack가 와야 생긴다. 거절(`closed`)하면 소비자는
|
|
453
|
+
* "보내도 되는 때"를 알릴 신호를 따로 찾아야 한다 — 이미 날아가고 있는
|
|
454
|
+
* 구독을 기다리면 그 신호가 필요 없다. 구독이 실패하면 그 실패가 그대로
|
|
455
|
+
* 나간다. 구독한 적이 없으면 기다릴 것이 없으니 예전처럼 거절한다.
|
|
456
|
+
*/
|
|
457
|
+
private resolveId;
|
|
382
458
|
/** Publishes and resolves when the server acks. */
|
|
383
459
|
send(input: SendInput, options?: SendOptions): Promise<PublishAck>;
|
|
384
460
|
/** Routes a frame the client decided belongs to this room. */
|
|
@@ -430,9 +506,15 @@ type ChatClientOptions = {
|
|
|
430
506
|
* has no way to decide who anybody is on its own, and the modes that
|
|
431
507
|
* pretended otherwise were removed.
|
|
432
508
|
*
|
|
433
|
-
* Called again on every connect,
|
|
434
|
-
*
|
|
435
|
-
*
|
|
509
|
+
* Called again on every connect, and again when a REST call is
|
|
510
|
+
* answered 401 (the call is then retried once), so an expired token is
|
|
511
|
+
* replaced rather than reused. A throw here is retried with backoff
|
|
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`.
|
|
515
|
+
*
|
|
516
|
+
* **Never sign these in the browser** -- signing needs the `sk_`, and
|
|
517
|
+
* a `sk_` in a browser is the whole app.
|
|
436
518
|
*/
|
|
437
519
|
token: () => string | Promise<string>;
|
|
438
520
|
/** external mode. **The server refuses this today** -- the verification half is unbuilt. */
|
|
@@ -452,6 +534,15 @@ type ChatClientEvents = {
|
|
|
452
534
|
* SDK learns about it.
|
|
453
535
|
*/
|
|
454
536
|
frame: Frame;
|
|
537
|
+
/**
|
|
538
|
+
* 클라이언트가 스스로 `closed`로 멈췄고, 이게 그 이유다.
|
|
539
|
+
*
|
|
540
|
+
* 멈추는 길은 셋이다: `close()`, 서버의 인증 거절, `token()`이 던진
|
|
541
|
+
* `ChatError`. 첫 connect라면 거절로도 알 수 있지만 재접속 중이나 REST의
|
|
542
|
+
* 토큰 갱신 중에는 받을 호출자가 없어서, 이것 없이는 `state`가 `closed`로
|
|
543
|
+
* 바뀌는 것만 보이고 왜인지는 사라진다. `close()`로 닫을 때는 내지 않는다.
|
|
544
|
+
*/
|
|
545
|
+
error: ChatError;
|
|
455
546
|
};
|
|
456
547
|
declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
457
548
|
state: ConnectionState;
|
|
@@ -464,8 +555,11 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
464
555
|
private heartbeat;
|
|
465
556
|
private retryTimer;
|
|
466
557
|
private attempt;
|
|
467
|
-
/**
|
|
468
|
-
|
|
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;
|
|
469
563
|
private nextFrameId;
|
|
470
564
|
private opening;
|
|
471
565
|
private readonly rest;
|
|
@@ -596,10 +690,30 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
596
690
|
* Closes for good.
|
|
597
691
|
*
|
|
598
692
|
* The distinction from a dropped socket is the whole point of the state
|
|
599
|
-
* machine:
|
|
600
|
-
*
|
|
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`.
|
|
601
696
|
*/
|
|
602
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;
|
|
603
717
|
/** Sends a frame and resolves with its ack, or rejects with its error. */
|
|
604
718
|
send(type: string, data?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
605
719
|
/**
|
|
@@ -639,32 +753,22 @@ declare class ChatClient extends Emitter<ChatClientEvents> {
|
|
|
639
753
|
*/
|
|
640
754
|
private nextDelay;
|
|
641
755
|
private authData;
|
|
756
|
+
/**
|
|
757
|
+
* REST가 401을 받았을 때 토큰을 새로 받는다.
|
|
758
|
+
*
|
|
759
|
+
* 소켓은 접속할 때 한 번 인증하고 그 뒤로는 토큰을 다시 보지 않지만, REST는
|
|
760
|
+
* 요청마다 본다. 그래서 한 시간짜리 토큰이면 한 시간 뒤 라이브는 멀쩡한데
|
|
761
|
+
* 히스토리·구멍 메우기만 401이 된다. 동시에 실패한 요청들이 `token()`을
|
|
762
|
+
* 각자 부르지 않도록 진행 중인 것 하나를 나눠 쓴다.
|
|
763
|
+
*/
|
|
764
|
+
private refreshToken;
|
|
765
|
+
private refreshing;
|
|
642
766
|
private failPending;
|
|
643
767
|
private clearHeartbeat;
|
|
644
768
|
private clearTimers;
|
|
645
769
|
}
|
|
646
770
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
647
771
|
|
|
648
|
-
/**
|
|
649
|
-
* Every failure this SDK raises, with the server's own code on it.
|
|
650
|
-
*
|
|
651
|
-
* The code matters more than the message. A consumer branches on
|
|
652
|
-
* `rate_limited` versus `moderation_denied` versus `unauthorized`, and a
|
|
653
|
-
* bare `Error` carrying prose forces them to match on strings the server
|
|
654
|
-
* is free to reword.
|
|
655
|
-
*/
|
|
656
|
-
declare class ChatError extends Error {
|
|
657
|
-
readonly code: string;
|
|
658
|
-
/** Present on rate limits, in milliseconds. */
|
|
659
|
-
readonly retryAfterMs?: number;
|
|
660
|
-
/** The consumer's own code, when a before_publish hook denied this. */
|
|
661
|
-
readonly appCode?: string;
|
|
662
|
-
constructor(code: string, message: string, extra?: {
|
|
663
|
-
retryAfterMs?: number;
|
|
664
|
-
appCode?: string;
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
772
|
/**
|
|
669
773
|
* The message list, in `seq` order, with its holes filled.
|
|
670
774
|
*
|
|
@@ -724,7 +828,20 @@ declare class Timeline {
|
|
|
724
828
|
* rows into the new list.
|
|
725
829
|
*/
|
|
726
830
|
private epoch;
|
|
831
|
+
/**
|
|
832
|
+
* `items` has been handed out -- by the getter or by `onChange`.
|
|
833
|
+
*
|
|
834
|
+
* 한 번 내준 배열은 다시 건드리지 않는다. 같은 배열을 제자리에서 고쳐
|
|
835
|
+
* 다시 내주면 Svelte `$state.raw`, React `useState`, Vue `shallowRef`처럼
|
|
836
|
+
* 참조가 바뀌어야 다시 그리는 쪽은 변화를 보지 못하고 화면이 멈춘다. 그래서
|
|
837
|
+
* 내준 뒤의 첫 변경은 복사본에서 한다(copy-on-write). 행 객체도 같은 규칙이라
|
|
838
|
+
* 바뀐 행만 새 객체다.
|
|
839
|
+
*/
|
|
840
|
+
private shared;
|
|
841
|
+
/** The `loadOlder` in flight, so a scroll handler firing twice asks once. */
|
|
842
|
+
private older;
|
|
727
843
|
constructor(options: TimelineOptions);
|
|
844
|
+
/** A new array whenever the list changes; never mutated once returned. */
|
|
728
845
|
get messages(): Message[];
|
|
729
846
|
/** The highest seq this timeline holds, hole or no hole. */
|
|
730
847
|
get highestSeq(): number;
|
|
@@ -807,6 +924,24 @@ declare class Timeline {
|
|
|
807
924
|
thread(rootId: string, count: number, lastSeq?: number): void;
|
|
808
925
|
/** Applies a `message.deleted`, remembering it if the row is not here. */
|
|
809
926
|
remove(id: string): void;
|
|
927
|
+
/**
|
|
928
|
+
* Prepends the page just below the oldest message held.
|
|
929
|
+
*
|
|
930
|
+
* 스크롤을 올려 과거를 읽는 소비자가 `history()` 결과를 라이브 목록과 따로
|
|
931
|
+
* 들고 합치고, 중복을 걸러 내고, reset과 재접속을 따로 처리해야 했다. 같은
|
|
932
|
+
* 목록에 끼워 넣으면 그 일이 전부 이미 있는 규칙(seq 자리에 넣기, 같은 seq는
|
|
933
|
+
* 한 행, reset이면 epoch로 버리기)으로 끝난다.
|
|
934
|
+
*
|
|
935
|
+
* `hasMore`는 더 올라갈 것이 있는지다. 서버의 seq는 1부터 구멍 없이 붙으므로
|
|
936
|
+
* 맨 위가 1이거나 페이지가 덜 찼으면 끝이다(보존 기간이 지운 앞부분도 덜 찬
|
|
937
|
+
* 페이지로 드러난다). 한 번에 하나만 돈다 — 스크롤 핸들러가 두 번 불러도
|
|
938
|
+
* 요청은 하나다(진행 중인 호출과 같은 결과를 받으므로 뒤 호출의 `limit`은 쓰이지 않는다).
|
|
939
|
+
*/
|
|
940
|
+
loadOlder(limit?: number): Promise<{
|
|
941
|
+
messages: Message[];
|
|
942
|
+
hasMore: boolean;
|
|
943
|
+
}>;
|
|
944
|
+
private readOlder;
|
|
810
945
|
/**
|
|
811
946
|
* Fetches everything between what we have and `upTo`.
|
|
812
947
|
*
|
|
@@ -827,6 +962,8 @@ declare class Timeline {
|
|
|
827
962
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
828
963
|
private insert;
|
|
829
964
|
private applyDelete;
|
|
965
|
+
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
966
|
+
private own;
|
|
830
967
|
private changed;
|
|
831
968
|
}
|
|
832
969
|
declare function createTimeline(options: TimelineOptions): Timeline;
|