@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/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 retry to offer is **`room.subscribe()`**. A room that emitted
164
- * this is left not-subscribed on purpose, precisely so that call does
165
- * something: it re-subscribes and reloads. Marking it subscribed and
166
- * telling the consumer to retry would be advice that returns
167
- * immediately having done nothing, which is worse than no advice.
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
- /** The room's message list changed, for any reason. */
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, so an expired token is replaced
434
- * rather than reused. **Never sign these in the browser** -- signing
435
- * needs the `sk_`, and a `sk_` in a browser is the whole app.
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
- /** Set by close(), and the only thing that stops the reconnect loop. */
468
- private closedByCaller;
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: this is the only path that reaches `closed`, and a consumer
600
- * showing "disconnected, retrying" versus "disconnected" needs it.
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 retry to offer is **`room.subscribe()`**. A room that emitted
164
- * this is left not-subscribed on purpose, precisely so that call does
165
- * something: it re-subscribes and reloads. Marking it subscribed and
166
- * telling the consumer to retry would be advice that returns
167
- * immediately having done nothing, which is worse than no advice.
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
- /** The room's message list changed, for any reason. */
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, so an expired token is replaced
434
- * rather than reused. **Never sign these in the browser** -- signing
435
- * needs the `sk_`, and a `sk_` in a browser is the whole app.
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
- /** Set by close(), and the only thing that stops the reconnect loop. */
468
- private closedByCaller;
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: this is the only path that reaches `closed`, and a consumer
600
- * showing "disconnected, retrying" versus "disconnected" needs it.
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;