@noverachat/sdk-web 0.5.0 → 0.5.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.cjs CHANGED
@@ -826,7 +826,15 @@ var MessageCollection = class {
826
826
  this._messages.splice(0, this.hydratedCount);
827
827
  this.hydratedCount = 0;
828
828
  }
829
- this._messages.unshift(...page.items.map(chatMessageFromHistory));
829
+ const indexById = /* @__PURE__ */ new Map();
830
+ this._messages.forEach((m, i) => indexById.set(m.id, i));
831
+ const fresh = [];
832
+ for (const item of page.items.map(chatMessageFromHistory)) {
833
+ const at = indexById.get(item.id);
834
+ if (at === void 0) fresh.push(item);
835
+ else this._messages[at] = item;
836
+ }
837
+ this._messages.unshift(...fresh);
830
838
  this.oldestCursor = page.nextCursor;
831
839
  this._hasMore = page.hasMore;
832
840
  this.emit("replaced");
@@ -997,8 +1005,26 @@ var MessageCollection = class {
997
1005
  this.bus.removeAll();
998
1006
  }
999
1007
  // ── event handlers ──────────────────────────────────────────────
1008
+ /**
1009
+ * 수신 프레임을 컬렉션에 넣는다.
1010
+ *
1011
+ * 두 가지를 방어한다 — backfill(`chat.backfill()`)은 재연결 시 앵커
1012
+ * 이후를 **다시** 실어오므로, 이미 들고 있는 메시지가 섞여 오고 화면보다
1013
+ * 오래된 메시지도 함께 온다:
1014
+ * - **중복**: 같은 id가 이미 있으면 무시한다.
1015
+ * - **순서**: 뒤에서부터 자리를 찾아 꽂는다. 실시간 프레임은 항상 마지막
1016
+ * 메시지보다 새로우므로 비교 1번으로 끝나고(빠른 경로), 옛 메시지가
1017
+ * 섞여 올 때만 탐색이 일어난다.
1018
+ */
1000
1019
  onIncoming(f) {
1001
- this._messages.push(chatMessageFromLive(f));
1020
+ const msg = chatMessageFromLive(f);
1021
+ let i = this._messages.length - 1;
1022
+ for (; i >= 0; i--) {
1023
+ const cur = this._messages[i];
1024
+ if (cur.id === msg.id) return;
1025
+ if (!idGreater(cur.id, msg.id)) break;
1026
+ }
1027
+ this._messages.splice(i + 1, 0, msg);
1002
1028
  this.emit("inserted");
1003
1029
  }
1004
1030
  onUpdated(f) {
@@ -2163,6 +2189,16 @@ var NoveraChat = class {
2163
2189
  readDebounceMs;
2164
2190
  logger;
2165
2191
  connectedOnce = false;
2192
+ /**
2193
+ * 방별 "실시간 파이프라인으로 본 최대 message_id" — 재연결 핸드셰이크의
2194
+ * `?since=`와 backfill 시작점.
2195
+ *
2196
+ * **의도적으로 낮게 유지한다.** 히스토리 로드(REST)는 이 값을 올리지
2197
+ * 않는다 — 앵커가 낮으면 backfill이 이미 가진 것까지 다시 주는 쪽으로
2198
+ * (안전), 높으면 못 받은 것을 영영 건너뛰는 쪽으로(치명) 실패하기
2199
+ * 때문이다. 중복은 수신 측(`MessageCollection.onIncoming`)의 id dedupe가
2200
+ * 흡수하므로, "빠뜨린 갱신"으로 보고 올리지 말 것.
2201
+ */
2166
2202
  lastMessageIdByRoom = /* @__PURE__ */ new Map();
2167
2203
  /** Most recently dispatched presence event key, `${userId}:${at}:${online}`.
2168
2204
  * Used to drop the N-1 shared-room copies of a single online/offline
package/dist/index.d.cts CHANGED
@@ -912,6 +912,17 @@ declare class MessageCollection {
912
912
  /** Unsubscribe from the room and flush the pending read watermark. The
913
913
  * collection must not be used afterwards. */
914
914
  dispose(): void;
915
+ /**
916
+ * 수신 프레임을 컬렉션에 넣는다.
917
+ *
918
+ * 두 가지를 방어한다 — backfill(`chat.backfill()`)은 재연결 시 앵커
919
+ * 이후를 **다시** 실어오므로, 이미 들고 있는 메시지가 섞여 오고 화면보다
920
+ * 오래된 메시지도 함께 온다:
921
+ * - **중복**: 같은 id가 이미 있으면 무시한다.
922
+ * - **순서**: 뒤에서부터 자리를 찾아 꽂는다. 실시간 프레임은 항상 마지막
923
+ * 메시지보다 새로우므로 비교 1번으로 끝나고(빠른 경로), 옛 메시지가
924
+ * 섞여 올 때만 탐색이 일어난다.
925
+ */
915
926
  private onIncoming;
916
927
  private onUpdated;
917
928
  private onDeleted;
@@ -1386,6 +1397,16 @@ declare class NoveraChat {
1386
1397
  private readDebounceMs;
1387
1398
  private logger;
1388
1399
  private connectedOnce;
1400
+ /**
1401
+ * 방별 "실시간 파이프라인으로 본 최대 message_id" — 재연결 핸드셰이크의
1402
+ * `?since=`와 backfill 시작점.
1403
+ *
1404
+ * **의도적으로 낮게 유지한다.** 히스토리 로드(REST)는 이 값을 올리지
1405
+ * 않는다 — 앵커가 낮으면 backfill이 이미 가진 것까지 다시 주는 쪽으로
1406
+ * (안전), 높으면 못 받은 것을 영영 건너뛰는 쪽으로(치명) 실패하기
1407
+ * 때문이다. 중복은 수신 측(`MessageCollection.onIncoming`)의 id dedupe가
1408
+ * 흡수하므로, "빠뜨린 갱신"으로 보고 올리지 말 것.
1409
+ */
1389
1410
  private lastMessageIdByRoom;
1390
1411
  /** Most recently dispatched presence event key, `${userId}:${at}:${online}`.
1391
1412
  * Used to drop the N-1 shared-room copies of a single online/offline
package/dist/index.d.ts CHANGED
@@ -912,6 +912,17 @@ declare class MessageCollection {
912
912
  /** Unsubscribe from the room and flush the pending read watermark. The
913
913
  * collection must not be used afterwards. */
914
914
  dispose(): void;
915
+ /**
916
+ * 수신 프레임을 컬렉션에 넣는다.
917
+ *
918
+ * 두 가지를 방어한다 — backfill(`chat.backfill()`)은 재연결 시 앵커
919
+ * 이후를 **다시** 실어오므로, 이미 들고 있는 메시지가 섞여 오고 화면보다
920
+ * 오래된 메시지도 함께 온다:
921
+ * - **중복**: 같은 id가 이미 있으면 무시한다.
922
+ * - **순서**: 뒤에서부터 자리를 찾아 꽂는다. 실시간 프레임은 항상 마지막
923
+ * 메시지보다 새로우므로 비교 1번으로 끝나고(빠른 경로), 옛 메시지가
924
+ * 섞여 올 때만 탐색이 일어난다.
925
+ */
915
926
  private onIncoming;
916
927
  private onUpdated;
917
928
  private onDeleted;
@@ -1386,6 +1397,16 @@ declare class NoveraChat {
1386
1397
  private readDebounceMs;
1387
1398
  private logger;
1388
1399
  private connectedOnce;
1400
+ /**
1401
+ * 방별 "실시간 파이프라인으로 본 최대 message_id" — 재연결 핸드셰이크의
1402
+ * `?since=`와 backfill 시작점.
1403
+ *
1404
+ * **의도적으로 낮게 유지한다.** 히스토리 로드(REST)는 이 값을 올리지
1405
+ * 않는다 — 앵커가 낮으면 backfill이 이미 가진 것까지 다시 주는 쪽으로
1406
+ * (안전), 높으면 못 받은 것을 영영 건너뛰는 쪽으로(치명) 실패하기
1407
+ * 때문이다. 중복은 수신 측(`MessageCollection.onIncoming`)의 id dedupe가
1408
+ * 흡수하므로, "빠뜨린 갱신"으로 보고 올리지 말 것.
1409
+ */
1389
1410
  private lastMessageIdByRoom;
1390
1411
  /** Most recently dispatched presence event key, `${userId}:${at}:${online}`.
1391
1412
  * Used to drop the N-1 shared-room copies of a single online/offline
package/dist/index.js CHANGED
@@ -791,7 +791,15 @@ var MessageCollection = class {
791
791
  this._messages.splice(0, this.hydratedCount);
792
792
  this.hydratedCount = 0;
793
793
  }
794
- this._messages.unshift(...page.items.map(chatMessageFromHistory));
794
+ const indexById = /* @__PURE__ */ new Map();
795
+ this._messages.forEach((m, i) => indexById.set(m.id, i));
796
+ const fresh = [];
797
+ for (const item of page.items.map(chatMessageFromHistory)) {
798
+ const at = indexById.get(item.id);
799
+ if (at === void 0) fresh.push(item);
800
+ else this._messages[at] = item;
801
+ }
802
+ this._messages.unshift(...fresh);
795
803
  this.oldestCursor = page.nextCursor;
796
804
  this._hasMore = page.hasMore;
797
805
  this.emit("replaced");
@@ -962,8 +970,26 @@ var MessageCollection = class {
962
970
  this.bus.removeAll();
963
971
  }
964
972
  // ── event handlers ──────────────────────────────────────────────
973
+ /**
974
+ * 수신 프레임을 컬렉션에 넣는다.
975
+ *
976
+ * 두 가지를 방어한다 — backfill(`chat.backfill()`)은 재연결 시 앵커
977
+ * 이후를 **다시** 실어오므로, 이미 들고 있는 메시지가 섞여 오고 화면보다
978
+ * 오래된 메시지도 함께 온다:
979
+ * - **중복**: 같은 id가 이미 있으면 무시한다.
980
+ * - **순서**: 뒤에서부터 자리를 찾아 꽂는다. 실시간 프레임은 항상 마지막
981
+ * 메시지보다 새로우므로 비교 1번으로 끝나고(빠른 경로), 옛 메시지가
982
+ * 섞여 올 때만 탐색이 일어난다.
983
+ */
965
984
  onIncoming(f) {
966
- this._messages.push(chatMessageFromLive(f));
985
+ const msg = chatMessageFromLive(f);
986
+ let i = this._messages.length - 1;
987
+ for (; i >= 0; i--) {
988
+ const cur = this._messages[i];
989
+ if (cur.id === msg.id) return;
990
+ if (!idGreater(cur.id, msg.id)) break;
991
+ }
992
+ this._messages.splice(i + 1, 0, msg);
967
993
  this.emit("inserted");
968
994
  }
969
995
  onUpdated(f) {
@@ -2128,6 +2154,16 @@ var NoveraChat = class {
2128
2154
  readDebounceMs;
2129
2155
  logger;
2130
2156
  connectedOnce = false;
2157
+ /**
2158
+ * 방별 "실시간 파이프라인으로 본 최대 message_id" — 재연결 핸드셰이크의
2159
+ * `?since=`와 backfill 시작점.
2160
+ *
2161
+ * **의도적으로 낮게 유지한다.** 히스토리 로드(REST)는 이 값을 올리지
2162
+ * 않는다 — 앵커가 낮으면 backfill이 이미 가진 것까지 다시 주는 쪽으로
2163
+ * (안전), 높으면 못 받은 것을 영영 건너뛰는 쪽으로(치명) 실패하기
2164
+ * 때문이다. 중복은 수신 측(`MessageCollection.onIncoming`)의 id dedupe가
2165
+ * 흡수하므로, "빠뜨린 갱신"으로 보고 올리지 말 것.
2166
+ */
2131
2167
  lastMessageIdByRoom = /* @__PURE__ */ new Map();
2132
2168
  /** Most recently dispatched presence event key, `${userId}:${at}:${online}`.
2133
2169
  * Used to drop the N-1 shared-room copies of a single online/offline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noverachat/sdk-web",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "NoveraChat browser/Node SDK — WebSocket + REST client with optimistic UI, reconnection and gap-fill",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.ts CHANGED
@@ -57,6 +57,16 @@ export class NoveraChat {
57
57
  private readDebounceMs: number;
58
58
  private logger: ClientOptions["logger"];
59
59
  private connectedOnce = false;
60
+ /**
61
+ * 방별 "실시간 파이프라인으로 본 최대 message_id" — 재연결 핸드셰이크의
62
+ * `?since=`와 backfill 시작점.
63
+ *
64
+ * **의도적으로 낮게 유지한다.** 히스토리 로드(REST)는 이 값을 올리지
65
+ * 않는다 — 앵커가 낮으면 backfill이 이미 가진 것까지 다시 주는 쪽으로
66
+ * (안전), 높으면 못 받은 것을 영영 건너뛰는 쪽으로(치명) 실패하기
67
+ * 때문이다. 중복은 수신 측(`MessageCollection.onIncoming`)의 id dedupe가
68
+ * 흡수하므로, "빠뜨린 갱신"으로 보고 올리지 말 것.
69
+ */
60
70
  private lastMessageIdByRoom = new Map<string, MessageIdStr>();
61
71
  /** Most recently dispatched presence event key, `${userId}:${at}:${online}`.
62
72
  * Used to drop the N-1 shared-room copies of a single online/offline
@@ -223,7 +223,18 @@ export class MessageCollection {
223
223
  this._messages.splice(0, this.hydratedCount); // 스냅샷 통째 폐기 → 서버가 이긴다
224
224
  this.hydratedCount = 0;
225
225
  }
226
- this._messages.unshift(...page.items.map(chatMessageFromHistory));
226
+ // id 기준 upsert — 이미 들고 있는 메시지는 서버 버전으로 교체하고, 없는
227
+ // 것만 앞에 잇는다. `load()`가 두 번 불려도(React StrictMode의 mount →
228
+ // cleanup → mount 사이클처럼) 같은 페이지가 겹쳐 쌓이지 않는다.
229
+ const indexById = new Map<string, number>();
230
+ this._messages.forEach((m, i) => indexById.set(m.id, i));
231
+ const fresh: ChatMessage[] = [];
232
+ for (const item of page.items.map(chatMessageFromHistory)) {
233
+ const at = indexById.get(item.id);
234
+ if (at === undefined) fresh.push(item);
235
+ else this._messages[at] = item; // 겹치는 구간은 서버가 이긴다
236
+ }
237
+ this._messages.unshift(...fresh);
227
238
  this.oldestCursor = page.nextCursor;
228
239
  this._hasMore = page.hasMore;
229
240
  this.emit("replaced");
@@ -439,8 +450,26 @@ export class MessageCollection {
439
450
 
440
451
  // ── event handlers ──────────────────────────────────────────────
441
452
 
453
+ /**
454
+ * 수신 프레임을 컬렉션에 넣는다.
455
+ *
456
+ * 두 가지를 방어한다 — backfill(`chat.backfill()`)은 재연결 시 앵커
457
+ * 이후를 **다시** 실어오므로, 이미 들고 있는 메시지가 섞여 오고 화면보다
458
+ * 오래된 메시지도 함께 온다:
459
+ * - **중복**: 같은 id가 이미 있으면 무시한다.
460
+ * - **순서**: 뒤에서부터 자리를 찾아 꽂는다. 실시간 프레임은 항상 마지막
461
+ * 메시지보다 새로우므로 비교 1번으로 끝나고(빠른 경로), 옛 메시지가
462
+ * 섞여 올 때만 탐색이 일어난다.
463
+ */
442
464
  private onIncoming(f: WsChatReceive): void {
443
- this._messages.push(chatMessageFromLive(f));
465
+ const msg = chatMessageFromLive(f);
466
+ let i = this._messages.length - 1;
467
+ for (; i >= 0; i--) {
468
+ const cur = this._messages[i]!;
469
+ if (cur.id === msg.id) return; // 이미 있음 — 재전송분
470
+ if (!idGreater(cur.id, msg.id)) break; // 여기가 삽입 자리
471
+ }
472
+ this._messages.splice(i + 1, 0, msg);
444
473
  this.emit("inserted");
445
474
  }
446
475