@noverachat/sdk-web 0.5.2 → 0.6.0
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 +29 -1
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +29 -1
- package/package.json +1 -1
- package/src/chat-message.ts +14 -0
- package/src/client.ts +14 -1
- package/src/features/message-collection.ts +5 -0
- package/src/features/room.ts +11 -0
package/dist/index.cjs
CHANGED
|
@@ -620,6 +620,7 @@ function chatMessageFromHistory(m) {
|
|
|
620
620
|
messageType: m.messageType,
|
|
621
621
|
sender: m.sender ?? null,
|
|
622
622
|
customType: m.customType ?? null,
|
|
623
|
+
data: m.data ?? null,
|
|
623
624
|
fileId: m.fileId ?? null,
|
|
624
625
|
file: m.file ?? null,
|
|
625
626
|
files: m.files ?? null,
|
|
@@ -644,6 +645,7 @@ function chatMessageFromLive(f) {
|
|
|
644
645
|
messageType: f.message_type,
|
|
645
646
|
sender: null,
|
|
646
647
|
customType: f.custom_type ?? null,
|
|
648
|
+
data,
|
|
647
649
|
fileId: f.file_id ?? null,
|
|
648
650
|
file: f.file ? fileInlineFromWire(f.file) : null,
|
|
649
651
|
files: f.files ? f.files.map(fileInlineFromWire) : null,
|
|
@@ -666,6 +668,7 @@ function pendingChatMessage(p) {
|
|
|
666
668
|
messageType: "TEXT",
|
|
667
669
|
sender: null,
|
|
668
670
|
customType: p.customType ?? null,
|
|
671
|
+
data: p.data ?? null,
|
|
669
672
|
fileId: null,
|
|
670
673
|
file: null,
|
|
671
674
|
files: null,
|
|
@@ -689,6 +692,7 @@ function pendingFileChatMessage(p) {
|
|
|
689
692
|
messageType: "FILE",
|
|
690
693
|
sender: null,
|
|
691
694
|
customType: null,
|
|
695
|
+
data: null,
|
|
692
696
|
fileId: null,
|
|
693
697
|
file: {
|
|
694
698
|
id: "",
|
|
@@ -912,6 +916,7 @@ var MessageCollection = class {
|
|
|
912
916
|
content: text,
|
|
913
917
|
...opts?.replyToId ? { replyToId: opts.replyToId } : {},
|
|
914
918
|
...opts?.customType ? { customType: opts.customType } : {},
|
|
919
|
+
...opts?.data ? { data: opts.data } : {},
|
|
915
920
|
...opts?.customMeta ? { customMeta: opts.customMeta } : {}
|
|
916
921
|
});
|
|
917
922
|
tempId2 = sent.tempId;
|
|
@@ -2066,6 +2071,16 @@ var Room = class _Room {
|
|
|
2066
2071
|
this.pendingAcks.delete(tempId2);
|
|
2067
2072
|
this.mySentTempIds.delete(tempId2);
|
|
2068
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* ack 를 기다리던 전송을 전부 실패 처리한다 — `chat.disconnect()` 처럼
|
|
2076
|
+
* 응답이 올 길이 사라졌을 때. 메시지 컬렉션은 건드리지 않는다(낙관적
|
|
2077
|
+
* 버블은 `failed` 로 바뀌어 재전송 UI 를 띄울 수 있다).
|
|
2078
|
+
*/
|
|
2079
|
+
_failPendingSends(reason) {
|
|
2080
|
+
for (const tempId2 of [...this.pendingAcks.keys()]) {
|
|
2081
|
+
this._rejectPending(tempId2, new Error(reason));
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2069
2084
|
/**
|
|
2070
2085
|
* Try to re-send a pending frame after a transient server reject
|
|
2071
2086
|
* (currently only NC-RATE-001 — the tenant's per-second token bucket
|
|
@@ -2226,9 +2241,22 @@ var NoveraChat = class {
|
|
|
2226
2241
|
async connect() {
|
|
2227
2242
|
await this.ws.connect();
|
|
2228
2243
|
}
|
|
2244
|
+
/**
|
|
2245
|
+
* 소켓을 닫는다. **Room 파사드와 그 메시지 상태는 유지한다** — `connect()`
|
|
2246
|
+
* 로 다시 붙으면 같은 파사드가 그대로 이어서 프레임을 받는다.
|
|
2247
|
+
*
|
|
2248
|
+
* 예전엔 여기서 `rooms.clear()` 를 했는데, 그러면 UI 가 들고 있던 컬렉션이
|
|
2249
|
+
* 다음 접근 때 빈 컬렉션으로 교체돼 이미 로드한 히스토리가 사라졌다
|
|
2250
|
+
* (React StrictMode 의 connect → disconnect → connect 에서 채팅방이 빈
|
|
2251
|
+
* 화면으로 뜨는 증상). 소켓은 재사용되는 `ReconnectingWs` 인스턴스라
|
|
2252
|
+
* 파사드가 참조를 유지해도 문제가 없다.
|
|
2253
|
+
*
|
|
2254
|
+
* 다만 ack 를 기다리던 전송은 응답 받을 길이 없으므로 여기서 실패로
|
|
2255
|
+
* 정리한다(낙관적 버블이 영원히 `sending` 으로 남지 않도록).
|
|
2256
|
+
*/
|
|
2229
2257
|
disconnect() {
|
|
2230
2258
|
this.ws.close();
|
|
2231
|
-
this.rooms.
|
|
2259
|
+
for (const room of this.rooms.values()) room._failPendingSends("disconnected");
|
|
2232
2260
|
}
|
|
2233
2261
|
room(roomId) {
|
|
2234
2262
|
let r = this.rooms.get(roomId);
|
package/dist/index.d.cts
CHANGED
|
@@ -723,6 +723,15 @@ interface ChatMessage {
|
|
|
723
723
|
sender: SenderMini | null;
|
|
724
724
|
/** App-defined subtype (e.g. a message-priority convention). */
|
|
725
725
|
customType: string | null;
|
|
726
|
+
/**
|
|
727
|
+
* 서버가 그대로 통과시키는 앱 정의 페이로드 원문.
|
|
728
|
+
*
|
|
729
|
+
* 테넌트가 `customType` 과 짝지어 쓰는 규약이 여기 담긴다 — 예: 이미지
|
|
730
|
+
* 메시지를 `customType: "IMAGE"` + `data: { kind, fileId, url, thumbUrl }`
|
|
731
|
+
* 로 표현하는 식. SDK 는 내용을 해석하지 않고 원문을 넘긴다.
|
|
732
|
+
* ([customMeta] 는 이 안의 예약 키 `_customMeta` 를 꺼낸 편의 필드다.)
|
|
733
|
+
*/
|
|
734
|
+
data: Record<string, unknown> | null;
|
|
726
735
|
/** Snowflake id (string) of the attached file, when this is a file message. */
|
|
727
736
|
fileId: string | null;
|
|
728
737
|
/** Inline metadata of the attached file — enough to render a placeholder
|
|
@@ -758,6 +767,7 @@ declare function pendingChatMessage(p: {
|
|
|
758
767
|
content: string;
|
|
759
768
|
replyToId?: string;
|
|
760
769
|
customType?: string;
|
|
770
|
+
data?: Record<string, unknown>;
|
|
761
771
|
customMeta?: Record<string, unknown>;
|
|
762
772
|
}): ChatMessage;
|
|
763
773
|
/** An optimistic bubble for a FILE message whose bytes are still uploading.
|
|
@@ -898,6 +908,10 @@ declare class MessageCollection {
|
|
|
898
908
|
send(text: string, opts?: {
|
|
899
909
|
replyToId?: string;
|
|
900
910
|
customType?: string;
|
|
911
|
+
/** 앱 정의 페이로드 원문 — `customType` 과 짝지어 쓰는 테넌트 규약용
|
|
912
|
+
* (예: `customType:"IMAGE"` + `data:{kind,fileId,url,thumbUrl}`).
|
|
913
|
+
* 낙관적 버블에도 그대로 실려 전송 즉시 렌더할 수 있다. */
|
|
914
|
+
data?: Record<string, unknown>;
|
|
901
915
|
customMeta?: Record<string, unknown>;
|
|
902
916
|
}): void;
|
|
903
917
|
/** Send a file optimistically. A FILE bubble with `uploadProgress`
|
|
@@ -1348,6 +1362,12 @@ declare class Room {
|
|
|
1348
1362
|
* fail the matching outbound promise immediately instead of waiting
|
|
1349
1363
|
* for the 15s ack timeout. */
|
|
1350
1364
|
_rejectPending(tempId: string, err: Error): void;
|
|
1365
|
+
/**
|
|
1366
|
+
* ack 를 기다리던 전송을 전부 실패 처리한다 — `chat.disconnect()` 처럼
|
|
1367
|
+
* 응답이 올 길이 사라졌을 때. 메시지 컬렉션은 건드리지 않는다(낙관적
|
|
1368
|
+
* 버블은 `failed` 로 바뀌어 재전송 UI 를 띄울 수 있다).
|
|
1369
|
+
*/
|
|
1370
|
+
_failPendingSends(reason: string): void;
|
|
1351
1371
|
/**
|
|
1352
1372
|
* Try to re-send a pending frame after a transient server reject
|
|
1353
1373
|
* (currently only NC-RATE-001 — the tenant's per-second token bucket
|
|
@@ -1426,6 +1446,19 @@ declare class NoveraChat {
|
|
|
1426
1446
|
private lastPresenceKey;
|
|
1427
1447
|
constructor(opts: ClientOptions);
|
|
1428
1448
|
connect(): Promise<void>;
|
|
1449
|
+
/**
|
|
1450
|
+
* 소켓을 닫는다. **Room 파사드와 그 메시지 상태는 유지한다** — `connect()`
|
|
1451
|
+
* 로 다시 붙으면 같은 파사드가 그대로 이어서 프레임을 받는다.
|
|
1452
|
+
*
|
|
1453
|
+
* 예전엔 여기서 `rooms.clear()` 를 했는데, 그러면 UI 가 들고 있던 컬렉션이
|
|
1454
|
+
* 다음 접근 때 빈 컬렉션으로 교체돼 이미 로드한 히스토리가 사라졌다
|
|
1455
|
+
* (React StrictMode 의 connect → disconnect → connect 에서 채팅방이 빈
|
|
1456
|
+
* 화면으로 뜨는 증상). 소켓은 재사용되는 `ReconnectingWs` 인스턴스라
|
|
1457
|
+
* 파사드가 참조를 유지해도 문제가 없다.
|
|
1458
|
+
*
|
|
1459
|
+
* 다만 ack 를 기다리던 전송은 응답 받을 길이 없으므로 여기서 실패로
|
|
1460
|
+
* 정리한다(낙관적 버블이 영원히 `sending` 으로 남지 않도록).
|
|
1461
|
+
*/
|
|
1429
1462
|
disconnect(): void;
|
|
1430
1463
|
room(roomId: string): Room;
|
|
1431
1464
|
/** Subscribe to client-level events. Currently `roomEvent` — membership /
|
package/dist/index.d.ts
CHANGED
|
@@ -723,6 +723,15 @@ interface ChatMessage {
|
|
|
723
723
|
sender: SenderMini | null;
|
|
724
724
|
/** App-defined subtype (e.g. a message-priority convention). */
|
|
725
725
|
customType: string | null;
|
|
726
|
+
/**
|
|
727
|
+
* 서버가 그대로 통과시키는 앱 정의 페이로드 원문.
|
|
728
|
+
*
|
|
729
|
+
* 테넌트가 `customType` 과 짝지어 쓰는 규약이 여기 담긴다 — 예: 이미지
|
|
730
|
+
* 메시지를 `customType: "IMAGE"` + `data: { kind, fileId, url, thumbUrl }`
|
|
731
|
+
* 로 표현하는 식. SDK 는 내용을 해석하지 않고 원문을 넘긴다.
|
|
732
|
+
* ([customMeta] 는 이 안의 예약 키 `_customMeta` 를 꺼낸 편의 필드다.)
|
|
733
|
+
*/
|
|
734
|
+
data: Record<string, unknown> | null;
|
|
726
735
|
/** Snowflake id (string) of the attached file, when this is a file message. */
|
|
727
736
|
fileId: string | null;
|
|
728
737
|
/** Inline metadata of the attached file — enough to render a placeholder
|
|
@@ -758,6 +767,7 @@ declare function pendingChatMessage(p: {
|
|
|
758
767
|
content: string;
|
|
759
768
|
replyToId?: string;
|
|
760
769
|
customType?: string;
|
|
770
|
+
data?: Record<string, unknown>;
|
|
761
771
|
customMeta?: Record<string, unknown>;
|
|
762
772
|
}): ChatMessage;
|
|
763
773
|
/** An optimistic bubble for a FILE message whose bytes are still uploading.
|
|
@@ -898,6 +908,10 @@ declare class MessageCollection {
|
|
|
898
908
|
send(text: string, opts?: {
|
|
899
909
|
replyToId?: string;
|
|
900
910
|
customType?: string;
|
|
911
|
+
/** 앱 정의 페이로드 원문 — `customType` 과 짝지어 쓰는 테넌트 규약용
|
|
912
|
+
* (예: `customType:"IMAGE"` + `data:{kind,fileId,url,thumbUrl}`).
|
|
913
|
+
* 낙관적 버블에도 그대로 실려 전송 즉시 렌더할 수 있다. */
|
|
914
|
+
data?: Record<string, unknown>;
|
|
901
915
|
customMeta?: Record<string, unknown>;
|
|
902
916
|
}): void;
|
|
903
917
|
/** Send a file optimistically. A FILE bubble with `uploadProgress`
|
|
@@ -1348,6 +1362,12 @@ declare class Room {
|
|
|
1348
1362
|
* fail the matching outbound promise immediately instead of waiting
|
|
1349
1363
|
* for the 15s ack timeout. */
|
|
1350
1364
|
_rejectPending(tempId: string, err: Error): void;
|
|
1365
|
+
/**
|
|
1366
|
+
* ack 를 기다리던 전송을 전부 실패 처리한다 — `chat.disconnect()` 처럼
|
|
1367
|
+
* 응답이 올 길이 사라졌을 때. 메시지 컬렉션은 건드리지 않는다(낙관적
|
|
1368
|
+
* 버블은 `failed` 로 바뀌어 재전송 UI 를 띄울 수 있다).
|
|
1369
|
+
*/
|
|
1370
|
+
_failPendingSends(reason: string): void;
|
|
1351
1371
|
/**
|
|
1352
1372
|
* Try to re-send a pending frame after a transient server reject
|
|
1353
1373
|
* (currently only NC-RATE-001 — the tenant's per-second token bucket
|
|
@@ -1426,6 +1446,19 @@ declare class NoveraChat {
|
|
|
1426
1446
|
private lastPresenceKey;
|
|
1427
1447
|
constructor(opts: ClientOptions);
|
|
1428
1448
|
connect(): Promise<void>;
|
|
1449
|
+
/**
|
|
1450
|
+
* 소켓을 닫는다. **Room 파사드와 그 메시지 상태는 유지한다** — `connect()`
|
|
1451
|
+
* 로 다시 붙으면 같은 파사드가 그대로 이어서 프레임을 받는다.
|
|
1452
|
+
*
|
|
1453
|
+
* 예전엔 여기서 `rooms.clear()` 를 했는데, 그러면 UI 가 들고 있던 컬렉션이
|
|
1454
|
+
* 다음 접근 때 빈 컬렉션으로 교체돼 이미 로드한 히스토리가 사라졌다
|
|
1455
|
+
* (React StrictMode 의 connect → disconnect → connect 에서 채팅방이 빈
|
|
1456
|
+
* 화면으로 뜨는 증상). 소켓은 재사용되는 `ReconnectingWs` 인스턴스라
|
|
1457
|
+
* 파사드가 참조를 유지해도 문제가 없다.
|
|
1458
|
+
*
|
|
1459
|
+
* 다만 ack 를 기다리던 전송은 응답 받을 길이 없으므로 여기서 실패로
|
|
1460
|
+
* 정리한다(낙관적 버블이 영원히 `sending` 으로 남지 않도록).
|
|
1461
|
+
*/
|
|
1429
1462
|
disconnect(): void;
|
|
1430
1463
|
room(roomId: string): Room;
|
|
1431
1464
|
/** Subscribe to client-level events. Currently `roomEvent` — membership /
|
package/dist/index.js
CHANGED
|
@@ -585,6 +585,7 @@ function chatMessageFromHistory(m) {
|
|
|
585
585
|
messageType: m.messageType,
|
|
586
586
|
sender: m.sender ?? null,
|
|
587
587
|
customType: m.customType ?? null,
|
|
588
|
+
data: m.data ?? null,
|
|
588
589
|
fileId: m.fileId ?? null,
|
|
589
590
|
file: m.file ?? null,
|
|
590
591
|
files: m.files ?? null,
|
|
@@ -609,6 +610,7 @@ function chatMessageFromLive(f) {
|
|
|
609
610
|
messageType: f.message_type,
|
|
610
611
|
sender: null,
|
|
611
612
|
customType: f.custom_type ?? null,
|
|
613
|
+
data,
|
|
612
614
|
fileId: f.file_id ?? null,
|
|
613
615
|
file: f.file ? fileInlineFromWire(f.file) : null,
|
|
614
616
|
files: f.files ? f.files.map(fileInlineFromWire) : null,
|
|
@@ -631,6 +633,7 @@ function pendingChatMessage(p) {
|
|
|
631
633
|
messageType: "TEXT",
|
|
632
634
|
sender: null,
|
|
633
635
|
customType: p.customType ?? null,
|
|
636
|
+
data: p.data ?? null,
|
|
634
637
|
fileId: null,
|
|
635
638
|
file: null,
|
|
636
639
|
files: null,
|
|
@@ -654,6 +657,7 @@ function pendingFileChatMessage(p) {
|
|
|
654
657
|
messageType: "FILE",
|
|
655
658
|
sender: null,
|
|
656
659
|
customType: null,
|
|
660
|
+
data: null,
|
|
657
661
|
fileId: null,
|
|
658
662
|
file: {
|
|
659
663
|
id: "",
|
|
@@ -877,6 +881,7 @@ var MessageCollection = class {
|
|
|
877
881
|
content: text,
|
|
878
882
|
...opts?.replyToId ? { replyToId: opts.replyToId } : {},
|
|
879
883
|
...opts?.customType ? { customType: opts.customType } : {},
|
|
884
|
+
...opts?.data ? { data: opts.data } : {},
|
|
880
885
|
...opts?.customMeta ? { customMeta: opts.customMeta } : {}
|
|
881
886
|
});
|
|
882
887
|
tempId2 = sent.tempId;
|
|
@@ -2031,6 +2036,16 @@ var Room = class _Room {
|
|
|
2031
2036
|
this.pendingAcks.delete(tempId2);
|
|
2032
2037
|
this.mySentTempIds.delete(tempId2);
|
|
2033
2038
|
}
|
|
2039
|
+
/**
|
|
2040
|
+
* ack 를 기다리던 전송을 전부 실패 처리한다 — `chat.disconnect()` 처럼
|
|
2041
|
+
* 응답이 올 길이 사라졌을 때. 메시지 컬렉션은 건드리지 않는다(낙관적
|
|
2042
|
+
* 버블은 `failed` 로 바뀌어 재전송 UI 를 띄울 수 있다).
|
|
2043
|
+
*/
|
|
2044
|
+
_failPendingSends(reason) {
|
|
2045
|
+
for (const tempId2 of [...this.pendingAcks.keys()]) {
|
|
2046
|
+
this._rejectPending(tempId2, new Error(reason));
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2034
2049
|
/**
|
|
2035
2050
|
* Try to re-send a pending frame after a transient server reject
|
|
2036
2051
|
* (currently only NC-RATE-001 — the tenant's per-second token bucket
|
|
@@ -2191,9 +2206,22 @@ var NoveraChat = class {
|
|
|
2191
2206
|
async connect() {
|
|
2192
2207
|
await this.ws.connect();
|
|
2193
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* 소켓을 닫는다. **Room 파사드와 그 메시지 상태는 유지한다** — `connect()`
|
|
2211
|
+
* 로 다시 붙으면 같은 파사드가 그대로 이어서 프레임을 받는다.
|
|
2212
|
+
*
|
|
2213
|
+
* 예전엔 여기서 `rooms.clear()` 를 했는데, 그러면 UI 가 들고 있던 컬렉션이
|
|
2214
|
+
* 다음 접근 때 빈 컬렉션으로 교체돼 이미 로드한 히스토리가 사라졌다
|
|
2215
|
+
* (React StrictMode 의 connect → disconnect → connect 에서 채팅방이 빈
|
|
2216
|
+
* 화면으로 뜨는 증상). 소켓은 재사용되는 `ReconnectingWs` 인스턴스라
|
|
2217
|
+
* 파사드가 참조를 유지해도 문제가 없다.
|
|
2218
|
+
*
|
|
2219
|
+
* 다만 ack 를 기다리던 전송은 응답 받을 길이 없으므로 여기서 실패로
|
|
2220
|
+
* 정리한다(낙관적 버블이 영원히 `sending` 으로 남지 않도록).
|
|
2221
|
+
*/
|
|
2194
2222
|
disconnect() {
|
|
2195
2223
|
this.ws.close();
|
|
2196
|
-
this.rooms.
|
|
2224
|
+
for (const room of this.rooms.values()) room._failPendingSends("disconnected");
|
|
2197
2225
|
}
|
|
2198
2226
|
room(roomId) {
|
|
2199
2227
|
let r = this.rooms.get(roomId);
|
package/package.json
CHANGED
package/src/chat-message.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface ChatMessage {
|
|
|
44
44
|
sender: SenderMini | null;
|
|
45
45
|
/** App-defined subtype (e.g. a message-priority convention). */
|
|
46
46
|
customType: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* 서버가 그대로 통과시키는 앱 정의 페이로드 원문.
|
|
49
|
+
*
|
|
50
|
+
* 테넌트가 `customType` 과 짝지어 쓰는 규약이 여기 담긴다 — 예: 이미지
|
|
51
|
+
* 메시지를 `customType: "IMAGE"` + `data: { kind, fileId, url, thumbUrl }`
|
|
52
|
+
* 로 표현하는 식. SDK 는 내용을 해석하지 않고 원문을 넘긴다.
|
|
53
|
+
* ([customMeta] 는 이 안의 예약 키 `_customMeta` 를 꺼낸 편의 필드다.)
|
|
54
|
+
*/
|
|
55
|
+
data: Record<string, unknown> | null;
|
|
47
56
|
/** Snowflake id (string) of the attached file, when this is a file message. */
|
|
48
57
|
fileId: string | null;
|
|
49
58
|
/** Inline metadata of the attached file — enough to render a placeholder
|
|
@@ -96,6 +105,7 @@ export function chatMessageFromHistory(m: MessageOut): ChatMessage {
|
|
|
96
105
|
messageType: m.messageType,
|
|
97
106
|
sender: m.sender ?? null,
|
|
98
107
|
customType: m.customType ?? null,
|
|
108
|
+
data: m.data ?? null,
|
|
99
109
|
fileId: m.fileId ?? null,
|
|
100
110
|
file: m.file ?? null,
|
|
101
111
|
files: m.files ?? null,
|
|
@@ -127,6 +137,7 @@ export function chatMessageFromLive(f: WsChatReceive): ChatMessage {
|
|
|
127
137
|
messageType: f.message_type,
|
|
128
138
|
sender: null,
|
|
129
139
|
customType: f.custom_type ?? null,
|
|
140
|
+
data,
|
|
130
141
|
fileId: f.file_id ?? null,
|
|
131
142
|
file: f.file ? fileInlineFromWire(f.file) : null,
|
|
132
143
|
files: f.files ? f.files.map(fileInlineFromWire) : null,
|
|
@@ -146,6 +157,7 @@ export function pendingChatMessage(p: {
|
|
|
146
157
|
content: string;
|
|
147
158
|
replyToId?: string;
|
|
148
159
|
customType?: string;
|
|
160
|
+
data?: Record<string, unknown>;
|
|
149
161
|
customMeta?: Record<string, unknown>;
|
|
150
162
|
}): ChatMessage {
|
|
151
163
|
return {
|
|
@@ -157,6 +169,7 @@ export function pendingChatMessage(p: {
|
|
|
157
169
|
messageType: "TEXT",
|
|
158
170
|
sender: null,
|
|
159
171
|
customType: p.customType ?? null,
|
|
172
|
+
data: p.data ?? null,
|
|
160
173
|
fileId: null,
|
|
161
174
|
file: null,
|
|
162
175
|
files: null,
|
|
@@ -188,6 +201,7 @@ export function pendingFileChatMessage(p: {
|
|
|
188
201
|
messageType: "FILE",
|
|
189
202
|
sender: null,
|
|
190
203
|
customType: null,
|
|
204
|
+
data: null,
|
|
191
205
|
fileId: null,
|
|
192
206
|
file: {
|
|
193
207
|
id: "",
|
package/src/client.ts
CHANGED
|
@@ -149,9 +149,22 @@ export class NoveraChat {
|
|
|
149
149
|
await this.ws.connect();
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 소켓을 닫는다. **Room 파사드와 그 메시지 상태는 유지한다** — `connect()`
|
|
154
|
+
* 로 다시 붙으면 같은 파사드가 그대로 이어서 프레임을 받는다.
|
|
155
|
+
*
|
|
156
|
+
* 예전엔 여기서 `rooms.clear()` 를 했는데, 그러면 UI 가 들고 있던 컬렉션이
|
|
157
|
+
* 다음 접근 때 빈 컬렉션으로 교체돼 이미 로드한 히스토리가 사라졌다
|
|
158
|
+
* (React StrictMode 의 connect → disconnect → connect 에서 채팅방이 빈
|
|
159
|
+
* 화면으로 뜨는 증상). 소켓은 재사용되는 `ReconnectingWs` 인스턴스라
|
|
160
|
+
* 파사드가 참조를 유지해도 문제가 없다.
|
|
161
|
+
*
|
|
162
|
+
* 다만 ack 를 기다리던 전송은 응답 받을 길이 없으므로 여기서 실패로
|
|
163
|
+
* 정리한다(낙관적 버블이 영원히 `sending` 으로 남지 않도록).
|
|
164
|
+
*/
|
|
152
165
|
disconnect(): void {
|
|
153
166
|
this.ws.close();
|
|
154
|
-
this.rooms.
|
|
167
|
+
for (const room of this.rooms.values()) room._failPendingSends("disconnected");
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
room(roomId: string): Room {
|
|
@@ -308,6 +308,10 @@ export class MessageCollection {
|
|
|
308
308
|
opts?: {
|
|
309
309
|
replyToId?: string;
|
|
310
310
|
customType?: string;
|
|
311
|
+
/** 앱 정의 페이로드 원문 — `customType` 과 짝지어 쓰는 테넌트 규약용
|
|
312
|
+
* (예: `customType:"IMAGE"` + `data:{kind,fileId,url,thumbUrl}`).
|
|
313
|
+
* 낙관적 버블에도 그대로 실려 전송 즉시 렌더할 수 있다. */
|
|
314
|
+
data?: Record<string, unknown>;
|
|
311
315
|
customMeta?: Record<string, unknown>;
|
|
312
316
|
},
|
|
313
317
|
): void {
|
|
@@ -319,6 +323,7 @@ export class MessageCollection {
|
|
|
319
323
|
content: text,
|
|
320
324
|
...(opts?.replyToId ? { replyToId: opts.replyToId } : {}),
|
|
321
325
|
...(opts?.customType ? { customType: opts.customType } : {}),
|
|
326
|
+
...(opts?.data ? { data: opts.data } : {}),
|
|
322
327
|
...(opts?.customMeta ? { customMeta: opts.customMeta } : {}),
|
|
323
328
|
});
|
|
324
329
|
tempId = sent.tempId;
|
package/src/features/room.ts
CHANGED
|
@@ -1246,6 +1246,17 @@ export class Room {
|
|
|
1246
1246
|
this.mySentTempIds.delete(tempId);
|
|
1247
1247
|
}
|
|
1248
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* ack 를 기다리던 전송을 전부 실패 처리한다 — `chat.disconnect()` 처럼
|
|
1251
|
+
* 응답이 올 길이 사라졌을 때. 메시지 컬렉션은 건드리지 않는다(낙관적
|
|
1252
|
+
* 버블은 `failed` 로 바뀌어 재전송 UI 를 띄울 수 있다).
|
|
1253
|
+
*/
|
|
1254
|
+
_failPendingSends(reason: string): void {
|
|
1255
|
+
for (const tempId of [...this.pendingAcks.keys()]) {
|
|
1256
|
+
this._rejectPending(tempId, new Error(reason));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1249
1260
|
/**
|
|
1250
1261
|
* Try to re-send a pending frame after a transient server reject
|
|
1251
1262
|
* (currently only NC-RATE-001 — the tenant's per-second token bucket
|