@simplysm/service-client 13.0.69 → 13.0.71

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.
@@ -17,10 +17,10 @@ const workerResolvers = new LazyGcMap<
17
17
  string,
18
18
  { resolve: (res: unknown) => void; reject: (err: Error) => void }
19
19
  >({
20
- gcInterval: 5 * 1000, // 5초마다 만료 검사
21
- expireTime: 60 * 1000, // 60초가 지나면 만료 (타임아웃)
20
+ gcInterval: 5 * 1000, // Check for expired entries every 5s
21
+ expireTime: 60 * 1000, // Expire after 60s (timeout)
22
22
  onExpire: (key, item) => {
23
- // 만료 reject 호출 (메모리 방지 핵심)
23
+ // Reject on expiry (critical for preventing memory leaks)
24
24
  item.reject(new Error(`Worker task timed out (uuid: ${key})`));
25
25
  },
26
26
  });
@@ -40,8 +40,8 @@ function getWorker(): Worker | undefined {
40
40
  }
41
41
 
42
42
  if (!worker) {
43
- // Vite/Esbuild/Webpack 모던 번들러는 문법을 통해 Worker 별도 파일로 분리/로드함
44
- // 주의: import.meta.resolve 대신 상대경로 사용 (Vite 호환)
43
+ // Modern bundlers (Vite/Esbuild/Webpack) use this syntax to split/load the Worker as a separate file
44
+ // Note: use relative path instead of import.meta.resolve (Vite compatibility)
45
45
  worker = new Worker(new URL("../workers/client-protocol.worker.ts", import.meta.url), {
46
46
  type: "module",
47
47
  });
@@ -71,8 +71,8 @@ function getWorker(): Worker | undefined {
71
71
  }
72
72
 
73
73
  /**
74
- * Worker에 작업 위임 결과 대기
75
- * 주의: workerAvailable이 true일 때만 호출해야
74
+ * Delegate work to Worker and await result
75
+ * Note: only call when workerAvailable is true
76
76
  */
77
77
  async function runWorker(
78
78
  type: "encode" | "decode",
@@ -83,20 +83,20 @@ async function runWorker(
83
83
  const id = Uuid.new().toString();
84
84
 
85
85
  workerResolvers.set(id, { resolve, reject });
86
- // workerAvailable 체크 호출되므로 worker 항상 존재
86
+ // Called after workerAvailable check, so worker always exists
87
87
  getWorker()!.postMessage({ id, type, data }, { transfer });
88
88
  });
89
89
  }
90
90
 
91
91
  export function createClientProtocolWrapper(protocol: ServiceProtocol): ClientProtocolWrapper {
92
- // 기준값: 30KB
92
+ // Threshold: 30KB
93
93
  const SIZE_THRESHOLD = 30 * 1024;
94
94
 
95
95
  function shouldUseWorkerForEncode(msg: ServiceMessage): boolean {
96
96
  if (!("body" in msg)) return false;
97
97
  const body = msg.body;
98
98
 
99
- // Uint8Array 있거나, 배열 길이가 길면 워커 사용
99
+ // Use worker if Uint8Array is present or array length is large
100
100
  if (body instanceof Uint8Array) return true;
101
101
  if (typeof body === "string" && body.length > SIZE_THRESHOLD) return true;
102
102
  if (Array.isArray(body)) {
@@ -110,14 +110,14 @@ export function createClientProtocolWrapper(protocol: ServiceProtocol): ClientPr
110
110
  uuid: string,
111
111
  message: ServiceMessage,
112
112
  ): Promise<{ chunks: Bytes[]; totalSize: number }> {
113
- // Worker가 없거나 작은 데이터는 메인 스레드에서 처리
113
+ // Process on main thread if no Worker or small data
114
114
  if (!isWorkerAvailable() || !shouldUseWorkerForEncode(message)) {
115
115
  return protocol.encode(uuid, message);
116
116
  }
117
117
 
118
118
  // [Worker]
119
- // Encode 객체를 보내야 하므로 Structured Clone 발생함.
120
- // 하지만 JSON.stringify 비용을 메인 스레드에서 제거하는 이득이 큼.
119
+ // Encode requires sending an object, so Structured Clone occurs.
120
+ // But the benefit of offloading JSON.stringify cost from the main thread is greater.
121
121
  return (await runWorker("encode", { uuid, message })) as {
122
122
  chunks: Bytes[];
123
123
  totalSize: number;
@@ -127,16 +127,16 @@ export function createClientProtocolWrapper(protocol: ServiceProtocol): ClientPr
127
127
  async function decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>> {
128
128
  const totalSize = bytes.length;
129
129
 
130
- // Worker가 없거나 작은 데이터는 메인 스레드에서 처리
130
+ // Process on main thread if no Worker or small data
131
131
  if (!isWorkerAvailable() || totalSize <= SIZE_THRESHOLD) {
132
132
  return protocol.decode(bytes);
133
133
  }
134
134
 
135
135
  // [Worker]
136
- // Zero-Copy 전송 (buffer 소유권이 Worker로 넘어감)
136
+ // Zero-copy transfer (buffer ownership moves to Worker)
137
137
  const rawResult = await runWorker("decode", bytes, [bytes.buffer]);
138
138
 
139
- // Worker에서 결과(Plain Object) 클래스 인스턴스(DateTime 등)로 복원
139
+ // Restore class instances (DateTime, etc.) from Worker's plain object result
140
140
  return transferableDecode(rawResult) as ServiceMessageDecodeResult<ServiceMessage>;
141
141
  }
142
142
 
@@ -21,7 +21,7 @@ interface ServiceClientEvents {
21
21
  }
22
22
 
23
23
  export class ServiceClient extends EventEmitter<ServiceClientEvents> {
24
- // 모듈들
24
+ // Modules
25
25
  private readonly _socket: SocketProvider;
26
26
  private readonly _transport: ServiceTransport;
27
27
  private readonly _eventClient: EventClient;
@@ -29,7 +29,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
29
29
 
30
30
  private _authToken?: string;
31
31
 
32
- // 상태 접근자
32
+ // State accessors
33
33
  get connected() {
34
34
  return this._socket.connected;
35
35
  }
@@ -47,7 +47,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
47
47
  const wsProtocol = options.ssl ? "wss" : "ws";
48
48
  const wsUrl = `${wsProtocol}://${options.host}:${options.port}/ws`;
49
49
 
50
- // 모듈 초기화
50
+ // Initialize modules
51
51
  this._socket = createSocketProvider(wsUrl, this.name, this.options.maxReconnectCount ?? 10);
52
52
  const protocol = createServiceProtocol();
53
53
  const protocolWrapper = createClientProtocolWrapper(protocol);
@@ -55,19 +55,19 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
55
55
  this._eventClient = createEventClient(this._transport);
56
56
  this._fileClient = createFileClient(this.hostUrl, this.name);
57
57
 
58
- // 이벤트 바인딩
58
+ // Event bindings
59
59
  this._socket.on("state", async (state) => {
60
60
  this.emit("state", state);
61
61
 
62
- // 재연결 이벤트 리스너 자동 복구
62
+ // Auto-recover event listeners on reconnect
63
63
  if (state === "connected") {
64
64
  try {
65
65
  if (this._authToken != null) {
66
- await this.auth(this._authToken); // 재인증
66
+ await this.auth(this._authToken); // Re-authenticate
67
67
  }
68
68
  await this._eventClient.reRegisterAll();
69
69
  } catch (err) {
70
- logger.error("이벤트 리스너 복구 실패", err);
70
+ logger.error("Failed to recover event listeners", err);
71
71
  }
72
72
  }
73
73
  });
@@ -77,7 +77,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
77
77
  });
78
78
  }
79
79
 
80
- // 타입 안전성을 위한 Proxy 생성 메소드
80
+ // Proxy creation method for type safety
81
81
  getService<TService>(serviceName: string): RemoteService<TService> {
82
82
  return new Proxy({} as RemoteService<TService>, {
83
83
  get: (_target, prop) => {
@@ -131,7 +131,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
131
131
  info: TInfo,
132
132
  cb: (data: TData) => PromiseLike<void>,
133
133
  ): Promise<string> {
134
- if (!this.connected) throw new Error("서버와 연결되어있지 않습니다.");
134
+ if (!this.connected) throw new Error("Not connected to the server.");
135
135
  return this._eventClient.addListener(eventDef, info, cb);
136
136
  }
137
137
 
@@ -150,7 +150,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
150
150
  async uploadFile(files: File[] | FileList | { name: string; data: BlobPart }[]) {
151
151
  if (this._authToken == null) {
152
152
  throw new Error(
153
- "인증 토큰이 없습니다. 파일 업로드를 위해서는 먼저 auth() 호출하여 인증해야 합니다.",
153
+ "No authentication token found. Call auth() to authenticate before uploading files.",
154
154
  );
155
155
  }
156
156
  return this._fileClient.upload(files, this._authToken);
@@ -161,11 +161,11 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
161
161
  }
162
162
  }
163
163
 
164
- // TService의 모든 메소드 반환형을 Promise로 감싸주는 타입 변환기
164
+ // Type transformer that wraps all method return types of TService with Promise
165
165
  export type RemoteService<TService> = {
166
166
  [K in keyof TService]: TService[K] extends (...args: infer P) => infer R
167
167
  ? (...args: P) => Promise<Awaited<R>>
168
- : never; // 함수가 아닌 프로퍼티는 안씀
168
+ : never; // Non-function properties are excluded
169
169
  };
170
170
 
171
171
  export function createServiceClient(name: string, options: ServiceConnectionConfig): ServiceClient {
@@ -41,12 +41,12 @@ export function createServiceTransport(
41
41
  }
42
42
  >();
43
43
 
44
- // 응답 progress totalSize 저장 (complete 100% emit용)
44
+ // Store response progress totalSize (for emitting 100% on complete)
45
45
  const responseProgressTotalSize = new Map<string, number>();
46
46
 
47
47
  socket.on("message", onMessage);
48
48
 
49
- // 소켓이 끊기면 대기 중인 모든 요청을 에러 처리하여 메모리 해제
49
+ // When socket disconnects, reject all pending requests to free memory
50
50
  socket.on("state", (state) => {
51
51
  if (state === "closed" || state === "reconnecting") {
52
52
  cancelAllRequests("Socket connection lost");
@@ -56,16 +56,19 @@ export function createServiceTransport(
56
56
  async function send(message: ServiceClientMessage, progress?: ServiceProgress): Promise<unknown> {
57
57
  const uuid = Uuid.new().toString();
58
58
 
59
- // 응답 대기 시작 (요청 보내기 전에 리스너를 먼저 등록해야 안전함)
59
+ // Start awaiting response (register listener before sending request for safety)
60
60
  const responsePromise = new Promise((resolve, reject) => {
61
61
  pendingRequests.set(uuid, { resolve, reject, progress });
62
62
  });
63
63
 
64
- // 요청 전송
64
+ // Prevent unhandled rejection when the promise is orphaned (e.g., during HMR cleanup)
65
+ responsePromise.catch(() => {});
66
+
67
+ // Send request
65
68
  try {
66
69
  const { chunks, totalSize } = await protocol.encode(uuid, message);
67
70
 
68
- // 진행률 초기화
71
+ // Initialize progress
69
72
  if (chunks.length > 1) {
70
73
  progress?.request?.({
71
74
  uuid,
@@ -74,18 +77,18 @@ export function createServiceTransport(
74
77
  });
75
78
  }
76
79
 
77
- // 전송
80
+ // Send
78
81
  for (const chunk of chunks) {
79
82
  await socket.send(chunk);
80
83
  }
81
84
  } catch (err) {
82
- // 전송 실패 즉시 정리
85
+ // Clean up immediately on send failure
83
86
  pendingRequests.get(uuid)?.reject(err as Error);
84
87
  pendingRequests.delete(uuid);
85
88
  throw err;
86
89
  }
87
90
 
88
- // 응답 결과 반환
91
+ // Return response result
89
92
  return responsePromise;
90
93
  }
91
94
 
@@ -96,7 +99,7 @@ export function createServiceTransport(
96
99
 
97
100
  try {
98
101
  if (decoded.type === "progress") {
99
- // totalSize 기억 (complete 100% emit용)
102
+ // Remember totalSize (for emitting 100% on complete)
100
103
  responseProgressTotalSize.set(decoded.uuid, decoded.totalSize);
101
104
 
102
105
  listenerInfo?.progress?.response?.({
@@ -113,7 +116,7 @@ export function createServiceTransport(
113
116
  completedSize: body.completedSize,
114
117
  });
115
118
  } else if (decoded.message.name === "response") {
116
- // split된 메시지였으면 100% progress emit
119
+ // Emit 100% progress if it was a split message
117
120
  const totalSize = responseProgressTotalSize.get(decoded.uuid);
118
121
  if (totalSize != null) {
119
122
  responseProgressTotalSize.delete(decoded.uuid);
@@ -124,15 +127,15 @@ export function createServiceTransport(
124
127
  });
125
128
  }
126
129
 
127
- // 응답을 받았으므로 Map에서 제거
130
+ // Remove from Map since response received
128
131
  pendingRequests.delete(decoded.uuid);
129
132
 
130
133
  listenerInfo?.resolve(decoded.message.body as ServiceResponseMessage);
131
134
  } else if (decoded.message.name === "error") {
132
- // progress totalSize 정리
135
+ // Clean up progress totalSize
133
136
  responseProgressTotalSize.delete(decoded.uuid);
134
137
 
135
- // 에러를 받았으므로 Map에서 제거
138
+ // Remove from Map since error received
136
139
  pendingRequests.delete(decoded.uuid);
137
140
 
138
141
  listenerInfo?.reject(toError(decoded.message.body));
@@ -145,7 +148,7 @@ export function createServiceTransport(
145
148
  const body = decoded.message.body as { keys: string[]; data: unknown };
146
149
  emitter.emit("event", { keys: body.keys, data: body.data });
147
150
  } else {
148
- throw new Error("요청이 되었습니다.");
151
+ throw new Error("Invalid message received from server.");
149
152
  }
150
153
  }
151
154
  } catch (err) {
@@ -153,7 +156,7 @@ export function createServiceTransport(
153
156
  }
154
157
  }
155
158
 
156
- // 모든 대기 요청 취소 처리
159
+ // Cancel all pending requests
157
160
  function cancelAllRequests(reason: string): void {
158
161
  for (const listenerInfo of pendingRequests.values()) {
159
162
  listenerInfo.reject(new Error(`Request canceled: ${reason}`));
@@ -30,15 +30,15 @@ export function createSocketProvider(
30
30
  clientName: string,
31
31
  maxReconnectCount: number,
32
32
  ): SocketProvider {
33
- // 설정상수
34
- const HEARTBEAT_TIMEOUT = 30000; // 30초간 아무런 메시지가 없으면 끊김으로 간주
35
- const HEARTBEAT_INTERVAL = 5000; // 5초마다 전송
36
- const RECONNECT_DELAY = 3000; // 3초마다 재연결 시도
33
+ // Configuration constants
34
+ const HEARTBEAT_TIMEOUT = 30000; // Consider disconnected if no message for 30s
35
+ const HEARTBEAT_INTERVAL = 5000; // Send ping every 5s
36
+ const RECONNECT_DELAY = 3000; // Retry reconnect every 3s
37
37
 
38
- // 1바이트 버퍼 미리 생성 (메모리 절약)
38
+ // Pre-allocate 1-byte buffer (saves memory)
39
39
  const PING_PACKET = new Uint8Array([0x01]);
40
40
 
41
- // 상태
41
+ // State
42
42
  let ws: WebSocket | undefined;
43
43
  let isManualClose = false;
44
44
  let reconnectCount = 0;
@@ -58,10 +58,10 @@ export function createSocketProvider(
58
58
  try {
59
59
  await createSocket();
60
60
  startHeartbeat();
61
- reconnectCount = 0; // 연결 성공 카운트 초기화
61
+ reconnectCount = 0; // Reset count on successful connection
62
62
  emitter.emit("state", "connected");
63
63
  } catch (err) {
64
- // 최초 연결 실패는 에러를 던짐 (호출자가 있게)
64
+ // Throw on initial connection failure (so the caller can handle it)
65
65
  throw err;
66
66
  }
67
67
  }
@@ -72,7 +72,7 @@ export function createSocketProvider(
72
72
  const currentWs = ws;
73
73
  if (currentWs != null) {
74
74
  currentWs.close();
75
- // 완전히 닫힐 때까지 대기 (Graceful Shutdown)
75
+ // Wait until fully closed (graceful shutdown)
76
76
  await waitUntil(() => currentWs.readyState === WebSocket.CLOSED, 100, 30).catch(() => {});
77
77
  }
78
78
  emitter.emit("state", "closed");
@@ -82,11 +82,11 @@ export function createSocketProvider(
82
82
  try {
83
83
  await waitUntil(() => isConnected(), undefined, 50);
84
84
  } catch {
85
- throw new Error("서버와 연결되어있지 않습니다. 인터넷 연결을 확인하세요.");
85
+ throw new Error("Not connected to the server. Please check your internet connection.");
86
86
  }
87
87
  const currentWs = ws;
88
88
  if (currentWs == null) {
89
- throw new Error("WebSocket 연결되어있지 않습니다.");
89
+ throw new Error("WebSocket is not connected.");
90
90
  }
91
91
  currentWs.send(data);
92
92
  }
@@ -109,7 +109,7 @@ export function createSocketProvider(
109
109
  };
110
110
 
111
111
  newWs.onerror = (event: Event) => {
112
- // 연결 에러 발생 시 reject
112
+ // Reject on error during connection
113
113
  if (!isConnected()) {
114
114
  const errorEvent = event as ErrorEvent;
115
115
  const msg = errorEvent.message;
@@ -118,21 +118,21 @@ export function createSocketProvider(
118
118
  };
119
119
  });
120
120
 
121
- // 시점에서 ws 항상 할당되어 있음 (ws.onopen에서 할당)
121
+ // At this point ws is always assigned (assigned in ws.onopen)
122
122
  const currentWs = ws;
123
123
  if (currentWs == null) {
124
- throw new Error("WebSocket 초기화 실패");
124
+ throw new Error("WebSocket initialization failed");
125
125
  }
126
126
 
127
127
  currentWs.onmessage = (event) => {
128
- lastHeartbeatTime = Date.now(); // 하트비트 갱신
128
+ lastHeartbeatTime = Date.now(); // Update heartbeat
129
129
 
130
130
  const data = event.data as ArrayBuffer;
131
131
  const bytes = new Uint8Array(data);
132
132
 
133
- // Raw Ping/Pong 처리 (가장 먼저 체크)
134
- // 1바이트이고 바이트가 0x02(Pong)이면 무시
135
- // (하트비트 타임스탬프만 갱신하고 끝냄)
133
+ // Raw Ping/Pong handling (checked first)
134
+ // If 1 byte and first byte is 0x02 (Pong), ignore
135
+ // (only heartbeat timestamp was updated, nothing else to do)
136
136
  if (bytes.length === 1 && bytes[0] === 0x02) return;
137
137
 
138
138
  emitter.emit("message", bytes);
@@ -147,11 +147,11 @@ export function createSocketProvider(
147
147
  }
148
148
 
149
149
  async function tryReconnect(): Promise<void> {
150
- // 루프 기반 재연결 (재귀 대신 사용하여 스택 안전성 확보)
150
+ // Loop-based reconnect (used instead of recursion for stack safety)
151
151
  while (reconnectCount < maxReconnectCount) {
152
152
  reconnectCount++;
153
153
  emitter.emit("state", "reconnecting");
154
- logger.warn("WebSocket 연결 끊김. 재연결 시도...", {
154
+ logger.warn("WebSocket disconnected. Attempting reconnect...", {
155
155
  reconnectCount,
156
156
  maxReconnectCount,
157
157
  });
@@ -162,16 +162,16 @@ export function createSocketProvider(
162
162
  await createSocket();
163
163
  startHeartbeat();
164
164
  reconnectCount = 0;
165
- emitter.emit("state", "connected"); // 재연결 성공 알림
166
- logger.info("WebSocket 재연결 성공");
167
- return; // 재연결 성공 종료
165
+ emitter.emit("state", "connected"); // Notify reconnect success
166
+ logger.info("WebSocket reconnected successfully");
167
+ return; // Exit on successful reconnect
168
168
  } catch {
169
- // 실패 루프 계속
169
+ // Continue loop on failure
170
170
  }
171
171
  }
172
172
 
173
- // 최대 재시도 횟수 초과
174
- logger.error("재연결 시도 횟수 초과. 연결을 포기합니다.");
173
+ // Max retry count exceeded
174
+ logger.error("Reconnect retry limit exceeded. Giving up.");
175
175
  emitter.emit("state", "closed");
176
176
  }
177
177
 
@@ -180,32 +180,32 @@ export function createSocketProvider(
180
180
  lastHeartbeatTime = Date.now();
181
181
 
182
182
  heartbeatTimer = setInterval(() => {
183
- // 타임아웃 체크
183
+ // Timeout check
184
184
  if (Date.now() - lastHeartbeatTime > HEARTBEAT_TIMEOUT) {
185
185
  logger.warn("Heartbeat Timeout. Connection lost.");
186
186
 
187
- // 타임아웃이 발생했으므로 즉시 타이머를 멈춰서 반복 실행을 막습니다.
187
+ // Stop the timer immediately on timeout to prevent repeated execution.
188
188
  stopHeartbeat();
189
189
 
190
- // 소켓이 닫히기를 기다리지 말고(onclose 미발생 대비), 강제로 정리 재연결합니다.
190
+ // Don't wait for socket close (onclose may not fire); force cleanup and reconnect.
191
191
  if (ws != null) {
192
192
  const tempWs = ws;
193
- ws = undefined; // 연결 상태 끊김으로 간주
193
+ ws = undefined; // Consider connection as disconnected
194
194
 
195
- // 기존 소켓의 이벤트 핸들러 제거
196
- // 뒤늦게 발생한 onclose에 따른 중복 재연결 방지
195
+ // Remove event handlers from old socket
196
+ // Prevent duplicate reconnect from late onclose events
197
197
  tempWs.onclose = null;
198
198
  tempWs.onerror = null;
199
199
  tempWs.onmessage = null;
200
200
 
201
- // 소켓 닫기 시도 (에러 무시)
201
+ // Attempt to close socket (ignore errors)
202
202
  try {
203
203
  tempWs.close();
204
204
  } catch {
205
205
  // ignore
206
206
  }
207
207
 
208
- // 수동 종료가 아니라면 재연결 로직 강제 실행
208
+ // Force reconnect logic if not a manual close
209
209
  if (!isManualClose) {
210
210
  void tryReconnect();
211
211
  }
@@ -213,7 +213,7 @@ export function createSocketProvider(
213
213
  return;
214
214
  }
215
215
 
216
- // ping 전송
216
+ // Send ping
217
217
  const currentWs = ws;
218
218
  if (isConnected() && currentWs != null) {
219
219
  try {
@@ -2,6 +2,6 @@ export interface ServiceConnectionConfig {
2
2
  port: number;
3
3
  host: string;
4
4
  ssl?: boolean;
5
- /** 0 입력시 reconnect안함. 바로 끊김 */
5
+ /** Set to 0 to disable reconnect; disconnects immediately */
6
6
  maxReconnectCount?: number;
7
7
  }
@@ -17,34 +17,34 @@ self.onmessage = (event: MessageEvent) => {
17
17
  let transferList: Transferable[] = [];
18
18
 
19
19
  if (type === "encode") {
20
- // [Main -> Worker] 인코딩 요청 (data: { uuid, message })
21
- // message 이미 Plain Object 넘어옴 (Structured Clone)
20
+ // [Main -> Worker] Encode request (data: { uuid, message })
21
+ // message is already a Plain Object (via Structured Clone)
22
22
  const { uuid, message } = data as {
23
23
  uuid: string;
24
24
  message: Parameters<typeof protocol.encode>[1];
25
25
  };
26
26
  const { chunks } = protocol.encode(uuid, message);
27
27
 
28
- // Buffer[] 전송 가능하므로 결과로 반환
28
+ // Buffer[] is transferable, return as result
29
29
  result = chunks;
30
- // 결과물 청크들의 내부 ArrayBuffer를 소유권 이전 목록에 추가
30
+ // Add internal ArrayBuffers of result chunks to transfer list
31
31
  transferList = chunks.map((chunk) => chunk.buffer as ArrayBuffer);
32
32
  } else {
33
- // [Main -> Worker] 디코딩 요청 (data: Uint8Array)
34
- // data Uint8Array로 넘어옴
33
+ // [Main -> Worker] Decode request (data: Uint8Array)
34
+ // data arrives as Uint8Array
35
35
  const bytes = new Uint8Array(data as ArrayBuffer);
36
36
  const decodeResult = protocol.decode(bytes);
37
37
 
38
- // 결과물(객체)을 전송 가능한 형태로 변환 (Zero-Copy 준비)
38
+ // Convert result object to transferable form (prepare for zero-copy)
39
39
  const encoded = transferableEncode(decodeResult);
40
40
  result = encoded.result;
41
41
  transferList = encoded.transferList;
42
42
  }
43
43
 
44
- // [Worker -> Main] 성공 응답
44
+ // [Worker -> Main] Success response
45
45
  self.postMessage({ id, type: "success", result }, { transfer: transferList });
46
46
  } catch (err) {
47
- // [Worker -> Main] 에러 응답
47
+ // [Worker -> Main] Error response
48
48
  self.postMessage({
49
49
  id,
50
50
  type: "error",