@dooboostore/simple-boot-http-server 1.0.23 → 1.0.24

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.
Files changed (167) hide show
  1. package/README.MD +27 -1
  2. package/dist/cjs/SimpleBootHttpServer.js +79 -39
  3. package/dist/cjs/SimpleBootHttpServer.js.map +3 -3
  4. package/dist/cjs/codes/HttpHeaders.js +14 -3
  5. package/dist/cjs/codes/HttpHeaders.js.map +2 -2
  6. package/dist/cjs/codes/Mimes.js +2 -0
  7. package/dist/cjs/codes/Mimes.js.map +2 -2
  8. package/dist/cjs/decorators/MethodMapping.js +7 -6
  9. package/dist/cjs/decorators/MethodMapping.js.map +3 -3
  10. package/dist/cjs/endpoints/WebSocketEndPoint.js +16 -0
  11. package/dist/cjs/endpoints/WebSocketEndPoint.js.map +7 -0
  12. package/dist/cjs/filters/CacheFilter.js +75 -35
  13. package/dist/cjs/filters/CacheFilter.js.map +3 -3
  14. package/dist/cjs/filters/IntentSchemeFilter.js +98 -0
  15. package/dist/cjs/filters/IntentSchemeFilter.js.map +7 -0
  16. package/dist/cjs/filters/ResourceFilter.js +4 -1
  17. package/dist/cjs/filters/ResourceFilter.js.map +2 -2
  18. package/dist/cjs/filters/TopicProtocolFilter.js +343 -0
  19. package/dist/cjs/filters/TopicProtocolFilter.js.map +7 -0
  20. package/dist/cjs/filters/index.js +2 -0
  21. package/dist/cjs/filters/index.js.map +2 -2
  22. package/dist/cjs/index.js +28 -0
  23. package/dist/cjs/index.js.map +2 -2
  24. package/dist/cjs/models/RequestResponse.js +51 -89
  25. package/dist/cjs/models/RequestResponse.js.map +3 -3
  26. package/dist/cjs/models/datas/body/ReqPathData.js +29 -0
  27. package/dist/cjs/models/datas/body/ReqPathData.js.map +7 -0
  28. package/dist/cjs/option/HttpServerOption.js +25 -5
  29. package/dist/cjs/option/HttpServerOption.js.map +2 -2
  30. package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js +141 -0
  31. package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js.map +7 -0
  32. package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js +35 -0
  33. package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js.map +7 -0
  34. package/dist/cjs/proxy/TopicProtocolApiServiceProxy.js +324 -0
  35. package/dist/cjs/proxy/TopicProtocolApiServiceProxy.js.map +7 -0
  36. package/dist/cjs/proxy/index.js +26 -0
  37. package/dist/cjs/proxy/index.js.map +7 -0
  38. package/dist/cjs/resolvers/ResourceResolver.js +2 -2
  39. package/dist/cjs/resolvers/ResourceResolver.js.map +2 -2
  40. package/dist/cjs/session/SessionManager.js +2 -2
  41. package/dist/cjs/session/SessionManager.js.map +2 -2
  42. package/dist/cjs/websocket/WebSocketClient.js +554 -0
  43. package/dist/cjs/websocket/WebSocketClient.js.map +7 -0
  44. package/dist/cjs/websocket/WebSocketManager.js +652 -0
  45. package/dist/cjs/websocket/WebSocketManager.js.map +7 -0
  46. package/dist/cjs/websocket/index.js +24 -0
  47. package/dist/cjs/websocket/index.js.map +7 -0
  48. package/dist/esm/SimpleBootHttpServer.js +59 -19
  49. package/dist/esm/SimpleBootHttpServer.js.map +2 -2
  50. package/dist/esm/codes/HttpHeaders.js +12 -2
  51. package/dist/esm/codes/HttpHeaders.js.map +2 -2
  52. package/dist/esm/codes/Mimes.js +2 -0
  53. package/dist/esm/codes/Mimes.js.map +2 -2
  54. package/dist/esm/decorators/MethodMapping.js +2 -1
  55. package/dist/esm/decorators/MethodMapping.js.map +2 -2
  56. package/dist/esm/endpoints/WebSocketEndPoint.js +1 -0
  57. package/dist/esm/endpoints/WebSocketEndPoint.js.map +7 -0
  58. package/dist/esm/filters/CacheFilter.js +67 -27
  59. package/dist/esm/filters/CacheFilter.js.map +2 -2
  60. package/dist/esm/filters/IntentSchemeFilter.js +75 -0
  61. package/dist/esm/filters/IntentSchemeFilter.js.map +7 -0
  62. package/dist/esm/filters/ResourceFilter.js +4 -1
  63. package/dist/esm/filters/ResourceFilter.js.map +2 -2
  64. package/dist/esm/filters/TopicProtocolFilter.js +310 -0
  65. package/dist/esm/filters/TopicProtocolFilter.js.map +7 -0
  66. package/dist/esm/filters/index.js +1 -0
  67. package/dist/esm/filters/index.js.map +2 -2
  68. package/dist/esm/index.js +12 -0
  69. package/dist/esm/index.js.map +2 -2
  70. package/dist/esm/models/RequestResponse.js +46 -84
  71. package/dist/esm/models/RequestResponse.js.map +2 -2
  72. package/dist/esm/models/datas/body/ReqPathData.js +6 -0
  73. package/dist/esm/models/datas/body/ReqPathData.js.map +7 -0
  74. package/dist/esm/option/HttpServerOption.js +24 -4
  75. package/dist/esm/option/HttpServerOption.js.map +2 -2
  76. package/dist/esm/proxy/SymbolIntentApiServiceProxy.js +118 -0
  77. package/dist/esm/proxy/SymbolIntentApiServiceProxy.js.map +7 -0
  78. package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js +12 -0
  79. package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js.map +7 -0
  80. package/dist/esm/proxy/TopicProtocolApiServiceProxy.js +301 -0
  81. package/dist/esm/proxy/TopicProtocolApiServiceProxy.js.map +7 -0
  82. package/dist/esm/proxy/index.js +4 -0
  83. package/dist/esm/proxy/index.js.map +7 -0
  84. package/dist/esm/resolvers/ResourceResolver.js +1 -1
  85. package/dist/esm/resolvers/ResourceResolver.js.map +1 -1
  86. package/dist/esm/session/SessionManager.js +1 -1
  87. package/dist/esm/session/SessionManager.js.map +1 -1
  88. package/dist/esm/websocket/WebSocketClient.js +529 -0
  89. package/dist/esm/websocket/WebSocketClient.js.map +7 -0
  90. package/dist/esm/websocket/WebSocketManager.js +622 -0
  91. package/dist/esm/websocket/WebSocketManager.js.map +7 -0
  92. package/dist/esm/websocket/index.js +3 -0
  93. package/dist/esm/websocket/index.js.map +7 -0
  94. package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js +13190 -4886
  95. package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js.map +4 -4
  96. package/dist/types/SimpleBootHttpServer.d.ts +5 -3
  97. package/dist/types/SimpleBootHttpServer.d.ts.map +1 -1
  98. package/dist/types/codes/HttpHeaders.d.ts +3 -0
  99. package/dist/types/codes/HttpHeaders.d.ts.map +1 -1
  100. package/dist/types/codes/Mimes.d.ts +3 -1
  101. package/dist/types/codes/Mimes.d.ts.map +1 -1
  102. package/dist/types/decorators/MethodMapping.d.ts +3 -2
  103. package/dist/types/decorators/MethodMapping.d.ts.map +1 -1
  104. package/dist/types/endpoints/WebSocketEndPoint.d.ts +17 -0
  105. package/dist/types/endpoints/WebSocketEndPoint.d.ts.map +1 -0
  106. package/dist/types/filters/CacheFilter.d.ts +6 -0
  107. package/dist/types/filters/CacheFilter.d.ts.map +1 -1
  108. package/dist/types/filters/IntentSchemeFilter.d.ts +20 -0
  109. package/dist/types/filters/IntentSchemeFilter.d.ts.map +1 -0
  110. package/dist/types/filters/ResourceFilter.d.ts.map +1 -1
  111. package/dist/types/filters/TopicProtocolFilter.d.ts +34 -0
  112. package/dist/types/filters/TopicProtocolFilter.d.ts.map +1 -0
  113. package/dist/types/filters/index.d.ts +1 -0
  114. package/dist/types/filters/index.d.ts.map +1 -1
  115. package/dist/types/index.d.ts +12 -0
  116. package/dist/types/index.d.ts.map +1 -1
  117. package/dist/types/models/RequestResponse.d.ts +9 -6
  118. package/dist/types/models/RequestResponse.d.ts.map +1 -1
  119. package/dist/types/models/datas/body/ReqPathData.d.ts +4 -0
  120. package/dist/types/models/datas/body/ReqPathData.d.ts.map +1 -0
  121. package/dist/types/option/HttpServerOption.d.ts +30 -20
  122. package/dist/types/option/HttpServerOption.d.ts.map +1 -1
  123. package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts +18 -0
  124. package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts.map +1 -0
  125. package/dist/types/proxy/SymbolIntentApiServiceProxyBase.d.ts +5 -0
  126. package/dist/types/proxy/SymbolIntentApiServiceProxyBase.d.ts.map +1 -0
  127. package/dist/types/proxy/TopicProtocolApiServiceProxy.d.ts +28 -0
  128. package/dist/types/proxy/TopicProtocolApiServiceProxy.d.ts.map +1 -0
  129. package/dist/types/proxy/index.d.ts +4 -0
  130. package/dist/types/proxy/index.d.ts.map +1 -0
  131. package/dist/types/session/SessionManager.d.ts +2 -17
  132. package/dist/types/session/SessionManager.d.ts.map +1 -1
  133. package/dist/types/websocket/WebSocketClient.d.ts +88 -0
  134. package/dist/types/websocket/WebSocketClient.d.ts.map +1 -0
  135. package/dist/types/websocket/WebSocketManager.d.ts +163 -0
  136. package/dist/types/websocket/WebSocketManager.d.ts.map +1 -0
  137. package/dist/types/websocket/index.d.ts +3 -0
  138. package/dist/types/websocket/index.d.ts.map +1 -0
  139. package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js +13262 -4958
  140. package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js.map +4 -4
  141. package/dist/umd-bundle/websocket-client.umd.js +11194 -0
  142. package/dist/umd-bundle/websocket-client.umd.js.map +7 -0
  143. package/package.json +24 -15
  144. package/src/SimpleBootHttpServer.ts +366 -312
  145. package/src/codes/HttpHeaders.ts +13 -2
  146. package/src/codes/Mimes.ts +27 -25
  147. package/src/decorators/MethodMapping.ts +4 -3
  148. package/src/endpoints/WebSocketEndPoint.ts +12 -0
  149. package/src/filters/CacheFilter.ts +112 -67
  150. package/src/filters/IntentSchemeFilter.ts +69 -0
  151. package/src/filters/ResourceFilter.ts +6 -1
  152. package/src/filters/TopicProtocolFilter.ts +349 -0
  153. package/src/filters/index.ts +1 -0
  154. package/src/index.ts +12 -12
  155. package/src/models/RequestResponse.ts +108 -240
  156. package/src/models/datas/body/ReqPathData.ts +3 -0
  157. package/src/option/HttpServerOption.ts +73 -23
  158. package/src/proxy/SymbolIntentApiServiceProxy.ts +120 -0
  159. package/src/proxy/SymbolIntentApiServiceProxyBase.ts +16 -0
  160. package/src/proxy/TopicProtocolApiServiceProxy.ts +328 -0
  161. package/src/proxy/index.ts +3 -0
  162. package/src/resolvers/ResourceResolver.ts +1 -1
  163. package/src/session/SessionManager.ts +3 -3
  164. package/src/websocket/WebSocketClient.ts +637 -0
  165. package/src/websocket/WebSocketManager.ts +713 -0
  166. package/src/websocket/index.ts +2 -0
  167. package/tsconfig.json +3 -1
@@ -0,0 +1,637 @@
1
+ import {RandomUtils} from '@dooboostore/core';
2
+ import {TopicRequest, TopicResponse, TopicServerEventRequest, TopicServerEventResponse, TopicServerEventUnSubscribeRequest, TopicSubscribeRequest, TopicType, TopicUnsubscribeRequest} from './WebSocketManager';
3
+ import {MessageOperator, Observable, Subject} from '@dooboostore/core';
4
+
5
+ export * from '@dooboostore/core';
6
+
7
+ type TopicObserverNextCallBack<T> = <R = any>(data: T, meta: { socket: WebSocket }) => void;
8
+ type TopicObserverEventCallBack = { [event: string]: <T, R = any>(data: T, meta: { socket: WebSocket }) => Promise<R> | void | any } | (<T, R = any>(event: string, data: T, meta: { socket: WebSocket }) => Promise<R> | void | any);
9
+ type TopicObserverErrorCallBack = <R = any>(err: any) => void;
10
+ export type TopicObserverCallBack<T> = { next: TopicObserverNextCallBack<T>; event?: TopicObserverEventCallBack; error?: TopicObserverErrorCallBack; complete?: () => void } | TopicObserverNextCallBack<T>;
11
+
12
+ export interface WebSocketClientConfig {
13
+ retryConnectionCount?: number;
14
+ retryConnectionDelay?: number;
15
+ maxPendingMessages?: number;
16
+ maxPendingBinaryMessages?: number;
17
+ }
18
+
19
+ type OnUnsubscribeCallback = (type: 'server' | 'manual') => void;
20
+ export type Subscription = {
21
+ closed: boolean;
22
+ unsubscribe: () => void;
23
+ };
24
+
25
+ export class WebSocketClient {
26
+ private websocket: WebSocket;
27
+ private topics = new Map<string, { observer: TopicObserverCallBack<any>; subscription: Subscription; onUnsubscribes: OnUnsubscribeCallback[]; target: string }>();
28
+ private observer?: (data: TopicResponse | TopicServerEventRequest | TopicServerEventUnSubscribeRequest) => void;
29
+ private pendingMessages: any[] = [];
30
+ private pendingBinaryMessages: Array<ArrayBuffer | Blob | Uint8Array> = [];
31
+ private isConnected = false;
32
+ private onOpenCallback: () => void;
33
+ private onCloseCallback: () => void;
34
+ private _subject = new Subject<TopicResponse | TopicServerEventRequest | TopicServerEventUnSubscribeRequest>();
35
+ // Reconnection properties
36
+ private connectionUrl: string | URL;
37
+ private connectionProtocols?: string | string[];
38
+ private retryConnectionCount: number;
39
+ private retryConnectionDelay: number;
40
+ private currentRetryCount = 0;
41
+ private reconnectTimeout?: number;
42
+ private isManualClose = false;
43
+ private isConnecting = false;
44
+ private maxPendingMessages: number;
45
+ private maxPendingBinaryMessages: number;
46
+
47
+ constructor(connection: { url: string | URL; protocols?: string | string[] }, config?: WebSocketClientConfig);
48
+ constructor(connection: string, config?: WebSocketClientConfig);
49
+ constructor(connection: URL, config?: WebSocketClientConfig);
50
+ constructor(connection: { url: string | URL; protocols?: string | string[] } | string | URL, config?: WebSocketClientConfig) {
51
+ // Store connection info for reconnection
52
+ if (typeof connection === 'string' || connection instanceof URL) {
53
+ this.connectionUrl = connection;
54
+ } else {
55
+ this.connectionUrl = connection.url;
56
+ this.connectionProtocols = connection.protocols;
57
+ }
58
+
59
+ // Set config with defaults
60
+ this.retryConnectionCount = config?.retryConnectionCount ?? 0;
61
+ this.retryConnectionDelay = config?.retryConnectionDelay ?? 1000;
62
+ this.maxPendingMessages = config?.maxPendingMessages ?? 1000;
63
+ this.maxPendingBinaryMessages = config?.maxPendingBinaryMessages ?? 100;
64
+
65
+ this.connect();
66
+ }
67
+
68
+ private get observable() {
69
+ return this._subject.asObservable();
70
+ }
71
+
72
+ private connect() {
73
+ if (this.isConnecting) {
74
+ return;
75
+ }
76
+ if (this.websocket && (this.websocket.readyState === WebSocket.OPEN || this.websocket.readyState === WebSocket.CONNECTING)) {
77
+ return;
78
+ }
79
+ this.isConnecting = true;
80
+ if (this.connectionProtocols) {
81
+ this.websocket = new WebSocket(this.connectionUrl, this.connectionProtocols);
82
+ } else {
83
+ this.websocket = new WebSocket(this.connectionUrl);
84
+ }
85
+
86
+ this.websocket.binaryType = 'arraybuffer';
87
+
88
+ this.websocket.addEventListener('open', () => {
89
+ this.isConnecting = false;
90
+ this.isConnected = true;
91
+ this.currentRetryCount = 0; // Reset retry count on successful connection
92
+ if (this.reconnectTimeout) {
93
+ clearTimeout(this.reconnectTimeout);
94
+ this.reconnectTimeout = undefined;
95
+ }
96
+ console.log('WebSocket connected');
97
+ this.onOpenCallback?.();
98
+
99
+ // Re-subscribe to all existing topics after reconnection
100
+ this.topics.forEach((topicData, uuid) => {
101
+ // closed된 subscription은 재구독하지 않음
102
+ if (!topicData.subscription.closed) {
103
+ const subscribeTopic: TopicSubscribeRequest = {
104
+ type: 'subscribe',
105
+ uuid,
106
+ target: topicData.target
107
+ };
108
+ // console.log('????')
109
+ this.websocket.send(JSON.stringify(subscribeTopic));
110
+ }
111
+ });
112
+
113
+ // 대기 중인 모든 메시지 전송
114
+ while (this.pendingMessages.length > 0) {
115
+ const message = this.pendingMessages.shift();
116
+ this.websocket.send(JSON.stringify(message));
117
+ }
118
+
119
+ while (this.pendingBinaryMessages.length > 0) {
120
+ const message = this.pendingBinaryMessages.shift();
121
+ if (message) {
122
+ this.websocket.send(message);
123
+ }
124
+ }
125
+ });
126
+
127
+ this.websocket.addEventListener('close', event => {
128
+ this.isConnecting = false;
129
+ this.isConnected = false;
130
+ console.log('WebSocket closed', event.code, event.reason);
131
+ this.onCloseCallback?.();
132
+
133
+ // Attempt reconnection if not manually closed
134
+ if (!this.isManualClose) {
135
+ this.attemptReconnect();
136
+ }
137
+ });
138
+
139
+ this.websocket.addEventListener('error', error => {
140
+ console.error('WebSocket error:', error);
141
+ });
142
+
143
+ this.websocket.addEventListener('message', async event => {
144
+ const topic = await this.parseIncomingTopic(event.data);
145
+ if (!topic) {
146
+ return;
147
+ }
148
+ this._subject.next(topic);
149
+ this.observer?.(topic);
150
+ if (topic.uuid) {
151
+ const topicData = this.topics.get(topic.uuid);
152
+ if (topicData) {
153
+ const observer = topicData.observer;
154
+ const nextCallback = typeof observer === 'function' ? observer : observer.next;
155
+ const errorCallback = typeof observer === 'function' ? undefined : observer.error;
156
+ const eventCallback = typeof observer === 'function' ? undefined : observer.event;
157
+ if ('state' in topic && topic.state === 'success') {
158
+ await nextCallback(topic.body, { socket: this.websocket });
159
+ } else if ('state' in topic && topic.state === 'error' && errorCallback) {
160
+ await errorCallback(topic.body);
161
+ } else if ('type' in topic && topic.type === 'unsubscribe') {
162
+ topicData.subscription.closed = true;
163
+ this.topics.delete(topic.uuid);
164
+ topicData.onUnsubscribes.forEach(it => it('server'));
165
+ } else if ('type' in topic && topic.type === 'event' && eventCallback) {
166
+ /////////// event ///////////
167
+ let eventResponse = typeof eventCallback === 'function' ? await eventCallback(topic.event, topic.body, { socket: this.websocket }) : await eventCallback[topic.event]?.(topic.body, { socket: this.websocket });
168
+
169
+ if (eventResponse !== undefined && eventResponse !== null) {
170
+ const value: TopicServerEventResponse = {
171
+ uuid: topic.uuid,
172
+ eventUUID: topic.eventUUID,
173
+ type: topic.type,
174
+ body: eventResponse
175
+ };
176
+ this.websocket.send(JSON.stringify(value));
177
+ }
178
+ }
179
+ }
180
+ }
181
+ });
182
+ }
183
+
184
+ private attemptReconnect() {
185
+ if (this.currentRetryCount >= this.retryConnectionCount) {
186
+ console.error(`WebSocket reconnection failed after ${this.retryConnectionCount} attempts`);
187
+ return;
188
+ }
189
+ if (this.reconnectTimeout) {
190
+ return;
191
+ }
192
+
193
+ this.currentRetryCount++;
194
+ console.log(`Attempting to reconnect... (${this.currentRetryCount}/${this.retryConnectionCount})`);
195
+
196
+ this.reconnectTimeout = window.setTimeout(() => {
197
+ this.reconnectTimeout = undefined;
198
+ this.connect();
199
+ // Subscribe messages are already in pendingMessages and will be sent automatically
200
+ }, this.retryConnectionDelay);
201
+ }
202
+
203
+ private sendMessage(message: TopicRequest | TopicUnsubscribeRequest | TopicSubscribeRequest) {
204
+ if (this.isConnected && this.websocket.readyState === WebSocket.OPEN) {
205
+ this.websocket.send(JSON.stringify(message));
206
+ return;
207
+ }
208
+ if (message.type === 'subscribe' || message.type === 'unsubscribe') {
209
+ return;
210
+ }
211
+ if (this.maxPendingMessages <= 0) {
212
+ return;
213
+ }
214
+ if (this.pendingMessages.length >= this.maxPendingMessages) {
215
+ this.pendingMessages.shift();
216
+ }
217
+ this.pendingMessages.push(message);
218
+ }
219
+
220
+ sendBinary(data: ArrayBuffer | Blob | Uint8Array) {
221
+ if (this.isConnected && this.websocket.readyState === WebSocket.OPEN) {
222
+ this.websocket.send(data);
223
+ return;
224
+ }
225
+ if (this.maxPendingBinaryMessages <= 0) {
226
+ return;
227
+ }
228
+ if (this.pendingBinaryMessages.length >= this.maxPendingBinaryMessages) {
229
+ this.pendingBinaryMessages.shift();
230
+ }
231
+ this.pendingBinaryMessages.push(data);
232
+ }
233
+
234
+ private async parseIncomingTopic(data: any): Promise<TopicResponse | TopicServerEventRequest | TopicServerEventUnSubscribeRequest | undefined> {
235
+ if (typeof data === 'string') {
236
+ try {
237
+ return JSON.parse(data);
238
+ } catch (error) {
239
+ console.warn('Invalid WebSocket payload:', error);
240
+ return undefined;
241
+ }
242
+ }
243
+
244
+ if (data instanceof ArrayBuffer) {
245
+ return this.parseBinaryTopic(data);
246
+ }
247
+
248
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
249
+ const buffer = await data.arrayBuffer();
250
+ return this.parseBinaryTopic(buffer);
251
+ }
252
+
253
+ return undefined;
254
+ }
255
+
256
+ private parseBinaryTopic(data: ArrayBuffer): TopicResponse | TopicServerEventRequest | TopicServerEventUnSubscribeRequest | undefined {
257
+ if (data.byteLength < 4) {
258
+ return undefined;
259
+ }
260
+ const view = new DataView(data);
261
+ const jsonLength = view.getUint32(0, true);
262
+ const jsonStart = 4;
263
+ const jsonEnd = jsonStart + jsonLength;
264
+ if (data.byteLength < jsonEnd) {
265
+ return undefined;
266
+ }
267
+
268
+ const jsonBytes = new Uint8Array(data, jsonStart, jsonLength);
269
+ let meta: any;
270
+ try {
271
+ const decoder = new TextDecoder();
272
+ meta = JSON.parse(decoder.decode(jsonBytes));
273
+ } catch (error) {
274
+ console.warn('Invalid WebSocket binary payload:', error);
275
+ return undefined;
276
+ }
277
+
278
+ const filesMeta = Array.isArray(meta.files) ? meta.files : [];
279
+ const fileMap = new Map<string, any>();
280
+ filesMeta.forEach((file: any) => {
281
+ if (!file || typeof file.id !== 'string' || typeof file.size !== 'number' || typeof file.offset !== 'number') {
282
+ return;
283
+ }
284
+ const start = jsonEnd + file.offset;
285
+ const end = start + file.size;
286
+ if (start < jsonEnd || end > data.byteLength) {
287
+ return;
288
+ }
289
+ const slice = data.slice(start, end);
290
+ const mime = file.mime || 'application/octet-stream';
291
+ let fileValue: any;
292
+ if (typeof File !== 'undefined') {
293
+ fileValue = new File([slice], file.name || 'file', { type: mime });
294
+ } else {
295
+ fileValue = { name: file.name || 'file', mime, size: file.size, buffer: slice };
296
+ }
297
+ fileMap.set(file.id, fileValue);
298
+ });
299
+
300
+ const replaceFileRefs = (value: any): any => {
301
+ if (value && typeof value === 'object' && '$file' in value && typeof value.$file === 'string') {
302
+ const file = fileMap.get(value.$file);
303
+ if (file) {
304
+ return file;
305
+ }
306
+ }
307
+ if (Array.isArray(value)) {
308
+ return value.map(item => replaceFileRefs(item));
309
+ }
310
+ if (value && typeof value === 'object') {
311
+ const result: Record<string, any> = {};
312
+ Object.keys(value).forEach(key => {
313
+ result[key] = replaceFileRefs(value[key]);
314
+ });
315
+ return result;
316
+ }
317
+ return value;
318
+ };
319
+
320
+ meta.body = replaceFileRefs(meta.body);
321
+ return meta as TopicResponse | TopicServerEventRequest | TopicServerEventUnSubscribeRequest;
322
+ }
323
+
324
+ private isBinaryPayload(value: any) {
325
+ if (!value) {
326
+ return false;
327
+ }
328
+ if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
329
+ return true;
330
+ }
331
+ if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
332
+ return true;
333
+ }
334
+ if (typeof Blob !== 'undefined' && value instanceof Blob) {
335
+ return true;
336
+ }
337
+ if (value && typeof value === 'object' && 'buffer' in value) {
338
+ const bufferValue = (value as any).buffer;
339
+ return this.isBinaryPayload(bufferValue);
340
+ }
341
+ return false;
342
+ }
343
+
344
+ private hasBinaryPayload(value: any): boolean {
345
+ if (this.isBinaryPayload(value)) {
346
+ return true;
347
+ }
348
+ if (Array.isArray(value)) {
349
+ return value.some(item => this.hasBinaryPayload(item));
350
+ }
351
+ if (value && typeof value === 'object') {
352
+ return Object.keys(value).some(key => this.hasBinaryPayload(value[key]));
353
+ }
354
+ return false;
355
+ }
356
+
357
+ private async buildBinaryMessage(body: any, meta: { type?: TopicType; uuid: string; requestUUID: string }) {
358
+ const fileRefs = new Map<any, string>();
359
+ const fileEntries: Array<{ id: string; name: string; mime: string; buffer: Promise<ArrayBuffer | SharedArrayBuffer> }> = [];
360
+ let fileIndex = 0;
361
+
362
+ const registerFile = (value: any) => {
363
+ const existing = fileRefs.get(value);
364
+ if (existing) {
365
+ return existing;
366
+ }
367
+ const id = `f${++fileIndex}`;
368
+ fileRefs.set(value, id);
369
+
370
+ let name = 'file';
371
+ let mime = 'application/octet-stream';
372
+ let buffer: Promise<ArrayBuffer | SharedArrayBuffer>;
373
+
374
+ if (typeof Blob !== 'undefined' && value instanceof Blob) {
375
+ const fileLike = value as File | Blob;
376
+ if ('name' in fileLike && typeof (fileLike as File).name === 'string') {
377
+ name = (fileLike as File).name || name;
378
+ }
379
+ mime = fileLike.type || mime;
380
+ buffer = fileLike.arrayBuffer();
381
+ } else if (value && typeof value === 'object' && 'buffer' in value) {
382
+ const fileLike = value as any;
383
+ if (typeof fileLike.name === 'string') {
384
+ name = fileLike.name || name;
385
+ }
386
+ if (typeof fileLike.mime === 'string') {
387
+ mime = fileLike.mime || mime;
388
+ }
389
+ const innerBuffer = fileLike.buffer;
390
+ if (typeof Blob !== 'undefined' && innerBuffer instanceof Blob) {
391
+ buffer = innerBuffer.arrayBuffer();
392
+ } else if (typeof Uint8Array !== 'undefined' && innerBuffer instanceof Uint8Array) {
393
+ const sliced = innerBuffer.buffer.slice(innerBuffer.byteOffset, innerBuffer.byteOffset + innerBuffer.byteLength);
394
+ buffer = Promise.resolve(sliced instanceof ArrayBuffer ? sliced : new Uint8Array(sliced).buffer);
395
+ } else if (typeof ArrayBuffer !== 'undefined' && innerBuffer instanceof ArrayBuffer) {
396
+ buffer = Promise.resolve(innerBuffer as ArrayBuffer);
397
+ } else {
398
+ buffer = Promise.resolve(new ArrayBuffer(0));
399
+ }
400
+ } else if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
401
+ const view = value as Uint8Array;
402
+ const sliced = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
403
+ buffer = Promise.resolve(sliced instanceof ArrayBuffer ? sliced : new Uint8Array(sliced).buffer);
404
+ } else if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
405
+ buffer = Promise.resolve(value as ArrayBuffer);
406
+ } else {
407
+ buffer = Promise.resolve(new ArrayBuffer(0));
408
+ }
409
+
410
+ fileEntries.push({ id, name, mime, buffer });
411
+ return id;
412
+ };
413
+
414
+ const replaceFiles = (value: any): any => {
415
+ if (this.isBinaryPayload(value)) {
416
+ const id = registerFile(value);
417
+ return { $file: id };
418
+ }
419
+ if (Array.isArray(value)) {
420
+ return value.map(item => replaceFiles(item));
421
+ }
422
+ if (value && typeof value === 'object') {
423
+ const result: Record<string, any> = {};
424
+ Object.keys(value).forEach(key => {
425
+ result[key] = replaceFiles(value[key]);
426
+ });
427
+ return result;
428
+ }
429
+ return value;
430
+ };
431
+
432
+ const payload = replaceFiles(body);
433
+ const buffers = await Promise.all(fileEntries.map(entry => entry.buffer));
434
+
435
+ let offset = 0;
436
+ const files = fileEntries.map((entry, index) => {
437
+ const size = buffers[index].byteLength;
438
+ const fileMeta = {
439
+ id: entry.id,
440
+ name: entry.name,
441
+ mime: entry.mime,
442
+ size,
443
+ offset
444
+ };
445
+ offset += size;
446
+ return fileMeta;
447
+ });
448
+
449
+ const json = JSON.stringify({
450
+ type: meta.type,
451
+ uuid: meta.uuid,
452
+ requestUUID: meta.requestUUID,
453
+ body: payload,
454
+ files
455
+ });
456
+
457
+ const encoder = new TextEncoder();
458
+ const jsonBytes = encoder.encode(json);
459
+ const header = new Uint8Array(4);
460
+ new DataView(header.buffer).setUint32(0, jsonBytes.byteLength, true);
461
+
462
+ const totalSize = header.byteLength + jsonBytes.byteLength + offset;
463
+ const combined = new Uint8Array(totalSize);
464
+ combined.set(header, 0);
465
+ combined.set(jsonBytes, header.byteLength);
466
+
467
+ let cursor = header.byteLength + jsonBytes.byteLength;
468
+ buffers.forEach(buffer => {
469
+ const view = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : new Uint8Array(buffer);
470
+ combined.set(view, cursor);
471
+ cursor += buffer.byteLength;
472
+ });
473
+
474
+ return combined.buffer;
475
+ }
476
+
477
+ subject(target: string, option?: { type?: TopicType; subScribeBody?: any; observer?: TopicObserverCallBack<any>; onUnsubscribe?: OnUnsubscribeCallback }) {
478
+ const self = this;
479
+ const uuid = RandomUtils.uuid4();
480
+ const onUnsubscribes: OnUnsubscribeCallback[] = [];
481
+ if (option?.onUnsubscribe) {
482
+ onUnsubscribes.push(option.onUnsubscribe);
483
+ }
484
+ if (option?.observer && typeof option.observer !== 'function' && option.observer.complete) {
485
+ onUnsubscribes.push(() => (option.observer as any).complete());
486
+ }
487
+ const topicSubject = new Subject<any>();
488
+ onUnsubscribes.push(() => topicSubject.complete());
489
+
490
+ let isClosed = false;
491
+ let activeSubscription: Subscription | undefined;
492
+
493
+ const unsubscribeLogic = () => {
494
+ if (isClosed) return;
495
+ const unSubscribeTopic: TopicUnsubscribeRequest = { type: 'unsubscribe', uuid };
496
+ self.sendMessage(unSubscribeTopic);
497
+ isClosed = true;
498
+ self.topics.delete(uuid);
499
+ onUnsubscribes.forEach(it => it('manual'));
500
+ if (activeSubscription) {
501
+ activeSubscription.closed = true;
502
+ }
503
+ };
504
+
505
+ const doSubscribe = <T>(observer?: TopicObserverCallBack<T>) => {
506
+ const subscription: Subscription = {
507
+ closed: isClosed,
508
+ unsubscribe: unsubscribeLogic
509
+ };
510
+
511
+ const wrappedObserver: TopicObserverCallBack<T> = {
512
+ next: (data, meta) => {
513
+ topicSubject.next(data);
514
+ if (observer) {
515
+ return typeof observer === 'function' ? observer(data, meta) : observer.next(data, meta);
516
+ }
517
+ },
518
+ error: err => {
519
+ topicSubject.error(err);
520
+ if (observer && typeof observer !== 'function' && observer.error) {
521
+ observer.error(err);
522
+ }
523
+ },
524
+ event: observer && typeof observer !== 'function' ? observer.event : undefined
525
+ };
526
+
527
+ self.topics.set(uuid, { observer: wrappedObserver, target, subscription, onUnsubscribes });
528
+ const subscribeTopic: TopicSubscribeRequest = { type: 'subscribe', uuid, target: target, body: option?.subScribeBody };
529
+
530
+ self.sendMessage(subscribeTopic);
531
+ // console.log('----------')
532
+ activeSubscription = subscription;
533
+ return subscription;
534
+ };
535
+
536
+ if (option?.observer) {
537
+ doSubscribe(option.observer);
538
+ }
539
+
540
+ return {
541
+ get closed() {
542
+ return isClosed;
543
+ },
544
+ unsubscribe: unsubscribeLogic,
545
+ get observable() {
546
+ return new Observable<any>(subscriber => {
547
+ if (!activeSubscription && !isClosed) {
548
+ doSubscribe();
549
+ }
550
+ return topicSubject.subscribe({
551
+ next: value => subscriber.next(value),
552
+ error: err => subscriber.error(err),
553
+ complete: () => subscriber.complete()
554
+ });
555
+ });
556
+ },
557
+ send: (body?: any) => {
558
+ if (!activeSubscription && !self.topics.has(uuid) && !isClosed) {
559
+ doSubscribe();
560
+ }
561
+ const requestUUID = RandomUtils.uuid4();
562
+ if (self.hasBinaryPayload(body)) {
563
+ self.buildBinaryMessage(body, { type: option?.type, uuid, requestUUID }).then(buffer => self.sendBinary(buffer));
564
+ } else {
565
+ const value: TopicRequest = { type: option?.type, uuid, requestUUID, body: body };
566
+ self.sendMessage(value);
567
+ }
568
+ return topicSubject.pipe(MessageOperator.filter(data => data && data.requestUUID === requestUUID));
569
+ },
570
+ onUnsubscribe: (callback: OnUnsubscribeCallback) => {
571
+ onUnsubscribes.push(callback);
572
+ }
573
+ };
574
+ }
575
+
576
+ onOpen(callback: () => void) {
577
+ this.onOpenCallback = callback;
578
+ }
579
+
580
+ onClose(callback: () => void) {
581
+ this.onCloseCallback = callback;
582
+ }
583
+
584
+ subscribe<T = any>(observer: (data: TopicResponse<T> | TopicServerEventRequest<T>) => void) {
585
+ this.observer = observer;
586
+ }
587
+
588
+ close() {
589
+ this.isManualClose = true;
590
+ if (this.reconnectTimeout) {
591
+ clearTimeout(this.reconnectTimeout);
592
+ this.reconnectTimeout = undefined;
593
+ }
594
+ this.pendingMessages = [];
595
+ this.pendingBinaryMessages = [];
596
+ this.isConnecting = false;
597
+ this.topics.forEach(topicData => {
598
+ topicData.subscription.closed = true;
599
+ topicData.onUnsubscribes.forEach(it => it('manual'));
600
+ });
601
+ this.topics.clear();
602
+ this.websocket.close();
603
+ }
604
+
605
+ reconnect() {
606
+ // 기존 연결 종료
607
+ this.isManualClose = true;
608
+ if (this.reconnectTimeout) {
609
+ clearTimeout(this.reconnectTimeout);
610
+ }
611
+
612
+ // close 이벤트 후에 재연결하도록 설정
613
+ const reconnectAfterClose = () => {
614
+ console.log('close websocket');
615
+ this.isManualClose = false;
616
+ this.currentRetryCount = 0;
617
+ this.connect();
618
+ };
619
+
620
+ // 이미 닫혀있으면 바로 재연결
621
+ if (this.websocket.readyState === WebSocket.CLOSED || this.websocket.readyState === WebSocket.CLOSING) {
622
+ reconnectAfterClose();
623
+ } else {
624
+ // 연결이 닫힐 때까지 기다렸다가 재연결
625
+ this.websocket.addEventListener('close', reconnectAfterClose, { once: true });
626
+ this.websocket.close();
627
+ }
628
+ }
629
+
630
+ getReadyState(): number {
631
+ return this.websocket.readyState;
632
+ }
633
+
634
+ isConnectionOpen(): boolean {
635
+ return this.isConnected && this.websocket.readyState === WebSocket.OPEN;
636
+ }
637
+ }