@simplysm/service-client 13.0.100 → 14.0.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/features/event-client.d.ts.map +1 -1
- package/dist/features/event-client.js +75 -67
- package/dist/features/event-client.js.map +1 -6
- package/dist/features/file-client.js +41 -39
- package/dist/features/file-client.js.map +1 -6
- package/dist/features/orm/orm-client-connector.js +37 -38
- package/dist/features/orm/orm-client-connector.js.map +1 -6
- package/dist/features/orm/orm-client-db-context-executor.d.ts.map +1 -1
- package/dist/features/orm/orm-client-db-context-executor.js +60 -60
- package/dist/features/orm/orm-client-db-context-executor.js.map +1 -6
- package/dist/features/orm/orm-connect-options.js +2 -1
- package/dist/features/orm/orm-connect-options.js.map +1 -6
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -6
- package/dist/protocol/client-protocol-wrapper.d.ts +1 -0
- package/dist/protocol/client-protocol-wrapper.d.ts.map +1 -1
- package/dist/protocol/client-protocol-wrapper.js +92 -70
- package/dist/protocol/client-protocol-wrapper.js.map +1 -6
- package/dist/service-client.d.ts +2 -0
- package/dist/service-client.d.ts.map +1 -1
- package/dist/service-client.js +113 -111
- package/dist/service-client.js.map +1 -6
- package/dist/transport/service-transport.js +121 -104
- package/dist/transport/service-transport.js.map +1 -6
- package/dist/transport/socket-provider.js +180 -155
- package/dist/transport/socket-provider.js.map +1 -6
- package/dist/types/connection-options.d.ts +1 -1
- package/dist/types/connection-options.d.ts.map +1 -1
- package/dist/types/connection-options.js +2 -1
- package/dist/types/connection-options.js.map +1 -6
- package/dist/types/progress.types.d.ts +1 -0
- package/dist/types/progress.types.d.ts.map +1 -1
- package/dist/types/progress.types.js +2 -1
- package/dist/types/progress.types.js.map +1 -6
- package/dist/workers/client-protocol.worker.js +38 -24
- package/dist/workers/client-protocol.worker.js.map +1 -6
- package/package.json +16 -9
- package/src/features/event-client.ts +19 -17
- package/src/features/file-client.ts +5 -5
- package/src/features/orm/orm-client-connector.ts +2 -2
- package/src/features/orm/orm-client-db-context-executor.ts +8 -7
- package/src/index.ts +5 -5
- package/src/protocol/client-protocol-wrapper.ts +24 -19
- package/src/service-client.ts +22 -15
- package/src/transport/service-transport.ts +19 -19
- package/src/transport/socket-provider.ts +38 -38
- package/src/types/connection-options.ts +1 -1
- package/src/types/progress.types.ts +1 -0
- package/src/workers/client-protocol.worker.ts +9 -9
- package/README.md +0 -126
- package/docs/features.md +0 -143
- package/docs/protocol.md +0 -29
- package/docs/service-client.md +0 -93
- package/docs/transport.md +0 -96
- package/docs/types.md +0 -55
package/src/service-client.ts
CHANGED
|
@@ -9,27 +9,29 @@ import { createServiceTransport, type ServiceTransport } from "./transport/servi
|
|
|
9
9
|
import { createSocketProvider, type SocketProvider } from "./transport/socket-provider";
|
|
10
10
|
import { createEventClient, type EventClient } from "./features/event-client";
|
|
11
11
|
import { createFileClient, type FileClient } from "./features/file-client";
|
|
12
|
-
import { createClientProtocolWrapper } from "./protocol/client-protocol-wrapper";
|
|
12
|
+
import { createClientProtocolWrapper, type ClientProtocolWrapper } from "./protocol/client-protocol-wrapper";
|
|
13
13
|
|
|
14
14
|
const logger = consola.withTag("service-client:ServiceClient");
|
|
15
15
|
|
|
16
16
|
interface ServiceClientEvents {
|
|
17
17
|
"request-progress": ServiceProgressState;
|
|
18
18
|
"response-progress": ServiceProgressState;
|
|
19
|
+
"server-progress": ServiceProgressState;
|
|
19
20
|
"state": "connected" | "closed" | "reconnecting";
|
|
20
21
|
"reload": Set<string>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
24
|
-
//
|
|
25
|
+
// 모듈
|
|
25
26
|
private readonly _socket: SocketProvider;
|
|
26
27
|
private readonly _transport: ServiceTransport;
|
|
27
28
|
private readonly _eventClient: EventClient;
|
|
28
29
|
private readonly _fileClient: FileClient;
|
|
29
30
|
|
|
31
|
+
private readonly _protocolWrapper: ClientProtocolWrapper;
|
|
30
32
|
private _authToken?: string;
|
|
31
33
|
|
|
32
|
-
//
|
|
34
|
+
// 상태 접근자
|
|
33
35
|
get connected() {
|
|
34
36
|
return this._socket.connected;
|
|
35
37
|
}
|
|
@@ -47,27 +49,27 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
47
49
|
const wsProtocol = options.ssl ? "wss" : "ws";
|
|
48
50
|
const wsUrl = `${wsProtocol}://${options.host}:${options.port}/ws`;
|
|
49
51
|
|
|
50
|
-
//
|
|
52
|
+
// 모듈 초기화
|
|
51
53
|
this._socket = createSocketProvider(wsUrl, this.name, this.options.maxReconnectCount ?? 10);
|
|
52
54
|
const protocol = createServiceProtocol();
|
|
53
|
-
|
|
54
|
-
this._transport = createServiceTransport(this._socket,
|
|
55
|
+
this._protocolWrapper = createClientProtocolWrapper(protocol);
|
|
56
|
+
this._transport = createServiceTransport(this._socket, this._protocolWrapper);
|
|
55
57
|
this._eventClient = createEventClient(this._transport);
|
|
56
58
|
this._fileClient = createFileClient(this.hostUrl, this.name);
|
|
57
59
|
|
|
58
|
-
//
|
|
60
|
+
// 이벤트 바인딩
|
|
59
61
|
this._socket.on("state", async (state) => {
|
|
60
62
|
this.emit("state", state);
|
|
61
63
|
|
|
62
|
-
//
|
|
64
|
+
// 재연결 시 이벤트 리스너 자동 복구
|
|
63
65
|
if (state === "connected") {
|
|
64
66
|
try {
|
|
65
67
|
if (this._authToken != null) {
|
|
66
|
-
await this.auth(this._authToken); //
|
|
68
|
+
await this.auth(this._authToken); // 재인증
|
|
67
69
|
}
|
|
68
70
|
await this._eventClient.resubscribeAll();
|
|
69
71
|
} catch (err) {
|
|
70
|
-
logger.error("
|
|
72
|
+
logger.error("이벤트 리스너 복구 실패", err);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
});
|
|
@@ -77,7 +79,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
//
|
|
82
|
+
// 타입 안전성을 위한 프록시 생성 메서드
|
|
81
83
|
getService<TService>(serviceName: string): ServiceProxy<TService> {
|
|
82
84
|
return new Proxy({} as ServiceProxy<TService>, {
|
|
83
85
|
get: (_target, prop) => {
|
|
@@ -95,6 +97,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
95
97
|
|
|
96
98
|
async close(): Promise<void> {
|
|
97
99
|
await this._socket.close();
|
|
100
|
+
this._protocolWrapper.dispose();
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async send(
|
|
@@ -117,6 +120,10 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
117
120
|
this.emit("response-progress", state);
|
|
118
121
|
progress?.response?.(state);
|
|
119
122
|
},
|
|
123
|
+
server: (state) => {
|
|
124
|
+
this.emit("server-progress", state);
|
|
125
|
+
progress?.server?.(state);
|
|
126
|
+
},
|
|
120
127
|
},
|
|
121
128
|
);
|
|
122
129
|
}
|
|
@@ -131,7 +138,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
131
138
|
info: TInfo,
|
|
132
139
|
cb: (data: TData) => PromiseLike<void>,
|
|
133
140
|
): Promise<string> {
|
|
134
|
-
if (!this.connected) throw new Error("
|
|
141
|
+
if (!this.connected) throw new Error("서버에 연결되지 않았습니다.");
|
|
135
142
|
return this._eventClient.addListener(eventDef, info, cb);
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -150,7 +157,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
150
157
|
async uploadFile(files: File[] | FileList | { name: string; data: BlobPart }[]) {
|
|
151
158
|
if (this._authToken == null) {
|
|
152
159
|
throw new Error(
|
|
153
|
-
"
|
|
160
|
+
"인증 토큰이 없습니다. 파일 업로드 전에 auth()를 호출하여 인증해 주세요.",
|
|
154
161
|
);
|
|
155
162
|
}
|
|
156
163
|
return this._fileClient.upload(files, this._authToken);
|
|
@@ -161,11 +168,11 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
//
|
|
171
|
+
// TService의 모든 메서드 반환 타입을 Promise로 래핑하는 타입 변환기
|
|
165
172
|
export type ServiceProxy<TService> = {
|
|
166
173
|
[K in keyof TService]: TService[K] extends (...args: infer P) => infer R
|
|
167
174
|
? (...args: P) => Promise<Awaited<R>>
|
|
168
|
-
: never; //
|
|
175
|
+
: never; // 함수가 아닌 속성은 제외
|
|
169
176
|
};
|
|
170
177
|
|
|
171
178
|
export function createServiceClient(name: string, options: ServiceConnectionOptions): ServiceClient {
|
|
@@ -41,34 +41,34 @@ export function createServiceTransport(
|
|
|
41
41
|
}
|
|
42
42
|
>();
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// 응답 progress totalSize 저장 (완료 시 100% 전송용)
|
|
45
45
|
const responseProgressTotalSize = new Map<string, number>();
|
|
46
46
|
|
|
47
47
|
socket.on("message", onMessage);
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// 소켓 연결 끊김 시 메모리 해제를 위해 모든 대기 중인 요청을 reject
|
|
50
50
|
socket.on("state", (state) => {
|
|
51
51
|
if (state === "closed" || state === "reconnecting") {
|
|
52
|
-
cancelAllRequests("
|
|
52
|
+
cancelAllRequests("소켓 연결이 끊어졌습니다");
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
async function send(message: ServiceClientMessage, progress?: ServiceProgress): Promise<unknown> {
|
|
57
57
|
const uuid = Uuid.generate().toString();
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// 응답 대기 시작 (안전을 위해 요청 전송 전에 리스너 등록)
|
|
60
60
|
const responsePromise = new Promise((resolve, reject) => {
|
|
61
61
|
pendingRequests.set(uuid, { resolve, reject, progress });
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Promise가 고아가 되었을 때 unhandled rejection 방지 (예: HMR 정리 중)
|
|
65
65
|
responsePromise.catch(() => {});
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// 요청 전송
|
|
68
68
|
try {
|
|
69
69
|
const { chunks, totalSize } = await protocol.encode(uuid, message);
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// progress 초기화
|
|
72
72
|
if (chunks.length > 1) {
|
|
73
73
|
progress?.request?.({
|
|
74
74
|
uuid,
|
|
@@ -77,18 +77,18 @@ export function createServiceTransport(
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// 전송
|
|
81
81
|
for (const chunk of chunks) {
|
|
82
82
|
await socket.send(chunk);
|
|
83
83
|
}
|
|
84
84
|
} catch (err) {
|
|
85
|
-
//
|
|
85
|
+
// 전송 실패 시 즉시 정리
|
|
86
86
|
pendingRequests.get(uuid)?.reject(err as Error);
|
|
87
87
|
pendingRequests.delete(uuid);
|
|
88
88
|
throw err;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
//
|
|
91
|
+
// 응답 결과 반환
|
|
92
92
|
return responsePromise;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -99,7 +99,7 @@ export function createServiceTransport(
|
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
101
|
if (decoded.type === "progress") {
|
|
102
|
-
//
|
|
102
|
+
// totalSize 기억 (완료 시 100% 전송용)
|
|
103
103
|
responseProgressTotalSize.set(decoded.uuid, decoded.totalSize);
|
|
104
104
|
|
|
105
105
|
listenerInfo?.progress?.response?.({
|
|
@@ -110,13 +110,13 @@ export function createServiceTransport(
|
|
|
110
110
|
} else {
|
|
111
111
|
if (decoded.message.name === "progress") {
|
|
112
112
|
const body = decoded.message.body as { totalSize: number; completedSize: number };
|
|
113
|
-
listenerInfo?.progress?.
|
|
113
|
+
listenerInfo?.progress?.server?.({
|
|
114
114
|
uuid: decoded.uuid,
|
|
115
115
|
totalSize: body.totalSize,
|
|
116
116
|
completedSize: body.completedSize,
|
|
117
117
|
});
|
|
118
118
|
} else if (decoded.message.name === "response") {
|
|
119
|
-
//
|
|
119
|
+
// 분할 메시지였을 경우 100% progress 전송
|
|
120
120
|
const totalSize = responseProgressTotalSize.get(decoded.uuid);
|
|
121
121
|
if (totalSize != null) {
|
|
122
122
|
responseProgressTotalSize.delete(decoded.uuid);
|
|
@@ -127,15 +127,15 @@ export function createServiceTransport(
|
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
//
|
|
130
|
+
// 응답 수신으로 Map에서 제거
|
|
131
131
|
pendingRequests.delete(decoded.uuid);
|
|
132
132
|
|
|
133
133
|
listenerInfo?.resolve(decoded.message.body as ServiceResponseMessage);
|
|
134
134
|
} else if (decoded.message.name === "error") {
|
|
135
|
-
//
|
|
135
|
+
// progress totalSize 정리
|
|
136
136
|
responseProgressTotalSize.delete(decoded.uuid);
|
|
137
137
|
|
|
138
|
-
//
|
|
138
|
+
// 에러 수신으로 Map에서 제거
|
|
139
139
|
pendingRequests.delete(decoded.uuid);
|
|
140
140
|
|
|
141
141
|
listenerInfo?.reject(toError(decoded.message.body));
|
|
@@ -148,7 +148,7 @@ export function createServiceTransport(
|
|
|
148
148
|
const body = decoded.message.body as { keys: string[]; data: unknown };
|
|
149
149
|
emitter.emit("event", { keys: body.keys, data: body.data });
|
|
150
150
|
} else {
|
|
151
|
-
throw new Error("
|
|
151
|
+
throw new Error("서버로부터 잘못된 메시지를 수신했습니다.");
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
} catch (err) {
|
|
@@ -156,10 +156,10 @@ export function createServiceTransport(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// 모든 대기 중인 요청 취소
|
|
160
160
|
function cancelAllRequests(reason: string): void {
|
|
161
161
|
for (const listenerInfo of pendingRequests.values()) {
|
|
162
|
-
listenerInfo.reject(new Error(
|
|
162
|
+
listenerInfo.reject(new Error(`요청 취소됨: ${reason}`));
|
|
163
163
|
}
|
|
164
164
|
pendingRequests.clear();
|
|
165
165
|
responseProgressTotalSize.clear();
|
|
@@ -30,15 +30,15 @@ export function createSocketProvider(
|
|
|
30
30
|
clientName: string,
|
|
31
31
|
maxReconnectCount: number,
|
|
32
32
|
): SocketProvider {
|
|
33
|
-
//
|
|
34
|
-
const HEARTBEAT_TIMEOUT = 30000; //
|
|
35
|
-
const HEARTBEAT_INTERVAL = 5000; //
|
|
36
|
-
const RECONNECT_DELAY = 3000; //
|
|
33
|
+
// 설정 상수
|
|
34
|
+
const HEARTBEAT_TIMEOUT = 30000; // 30초 동안 메시지가 없으면 연결 끊김으로 간주
|
|
35
|
+
const HEARTBEAT_INTERVAL = 5000; // 5초마다 ping 전송
|
|
36
|
+
const RECONNECT_DELAY = 3000; // 3초마다 재연결 시도
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// 1바이트 버퍼 사전 할당 (메모리 절약)
|
|
39
39
|
const PING_PACKET = new Uint8Array([0x01]);
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// 상태
|
|
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; // 연결 성공 시 카운트 초기화
|
|
62
62
|
emitter.emit("state", "connected");
|
|
63
63
|
} catch (err) {
|
|
64
|
-
//
|
|
64
|
+
// 초기 연결 실패 시 예외를 던짐 (호출자가 처리할 수 있도록)
|
|
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
|
-
//
|
|
75
|
+
// 완전히 닫힐 때까지 대기 (정상 종료)
|
|
76
76
|
await wait.until(() => 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 wait.until(() => isConnected(), undefined, 50);
|
|
84
84
|
} catch {
|
|
85
|
-
throw new Error("
|
|
85
|
+
throw new Error("서버에 연결되지 않았습니다. 인터넷 연결을 확인해 주세요.");
|
|
86
86
|
}
|
|
87
87
|
const currentWs = ws;
|
|
88
88
|
if (currentWs == null) {
|
|
89
|
-
throw new Error("WebSocket
|
|
89
|
+
throw new Error("WebSocket이 연결되지 않았습니다.");
|
|
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
|
-
//
|
|
112
|
+
// 연결 중 에러 발생 시 reject
|
|
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
|
-
//
|
|
121
|
+
// 이 시점에서 ws는 항상 할당됨 (ws.onopen에서 할당됨)
|
|
122
122
|
const currentWs = ws;
|
|
123
123
|
if (currentWs == null) {
|
|
124
|
-
throw new Error("WebSocket
|
|
124
|
+
throw new Error("WebSocket 초기화 실패");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
currentWs.onmessage = (event) => {
|
|
128
|
-
lastHeartbeatTime = Date.now(); //
|
|
128
|
+
lastHeartbeatTime = Date.now(); // 하트비트 갱신
|
|
129
129
|
|
|
130
130
|
const data = event.data as ArrayBuffer;
|
|
131
131
|
const bytes = new Uint8Array(data);
|
|
132
132
|
|
|
133
|
-
// Raw Ping/Pong
|
|
134
|
-
//
|
|
135
|
-
// (
|
|
133
|
+
// Raw Ping/Pong 처리 (먼저 확인)
|
|
134
|
+
// 1바이트이고 첫 번째 바이트가 0x02 (Pong)이면 무시
|
|
135
|
+
// (하트비트 타임스탬프만 갱신되었으므로 추가 작업 불필요)
|
|
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
|
+
// 루프 기반 재연결 (스택 안전성을 위해 재귀 대신 사용)
|
|
151
151
|
while (reconnectCount < maxReconnectCount) {
|
|
152
152
|
reconnectCount++;
|
|
153
153
|
emitter.emit("state", "reconnecting");
|
|
154
|
-
logger.warn("WebSocket
|
|
154
|
+
logger.warn("WebSocket 연결 끊김. 재연결 시도 중...", {
|
|
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"); // 재연결 성공 알림
|
|
166
|
+
logger.info("WebSocket 재연결 성공");
|
|
167
|
+
return; // 재연결 성공 시 루프 종료
|
|
168
168
|
} catch {
|
|
169
|
-
//
|
|
169
|
+
// 실패 시 루프 계속
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//
|
|
174
|
-
logger.error("
|
|
173
|
+
// 최대 재시도 횟수 초과
|
|
174
|
+
logger.error("재연결 재시도 한도 초과. 연결을 포기합니다.");
|
|
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
|
+
// 타임아웃 확인
|
|
184
184
|
if (Date.now() - lastHeartbeatTime > HEARTBEAT_TIMEOUT) {
|
|
185
|
-
logger.warn("
|
|
185
|
+
logger.warn("하트비트 타임아웃. 연결이 끊어졌습니다.");
|
|
186
186
|
|
|
187
|
-
//
|
|
187
|
+
// 반복 실행 방지를 위해 타임아웃 시 즉시 타이머 중지
|
|
188
188
|
stopHeartbeat();
|
|
189
189
|
|
|
190
|
-
//
|
|
190
|
+
// 소켓 종료를 기다리지 않음 (onclose가 발생하지 않을 수 있음); 강제 정리 후 재연결
|
|
191
191
|
if (ws != null) {
|
|
192
192
|
const tempWs = ws;
|
|
193
|
-
ws = undefined; //
|
|
193
|
+
ws = undefined; // 연결 끊김으로 간주
|
|
194
194
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
195
|
+
// 이전 소켓에서 이벤트 핸들러 제거
|
|
196
|
+
// 늦게 발생하는 onclose 이벤트로 인한 중복 재연결 방지
|
|
197
197
|
tempWs.onclose = null;
|
|
198
198
|
tempWs.onerror = null;
|
|
199
199
|
tempWs.onmessage = null;
|
|
200
200
|
|
|
201
|
-
//
|
|
201
|
+
// 소켓 닫기 시도 (에러 무시)
|
|
202
202
|
try {
|
|
203
203
|
tempWs.close();
|
|
204
204
|
} catch {
|
|
205
|
-
//
|
|
205
|
+
// 무시
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
//
|
|
208
|
+
// 수동 종료가 아닌 경우 강제 재연결 로직 실행
|
|
209
209
|
if (!isManualClose) {
|
|
210
210
|
void tryReconnect();
|
|
211
211
|
}
|
|
@@ -213,13 +213,13 @@ export function createSocketProvider(
|
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// ping 전송
|
|
217
217
|
const currentWs = ws;
|
|
218
218
|
if (isConnected() && currentWs != null) {
|
|
219
219
|
try {
|
|
220
220
|
currentWs.send(PING_PACKET);
|
|
221
221
|
} catch (err) {
|
|
222
|
-
logger.warn("
|
|
222
|
+
logger.warn("ping 전송 실패", err);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
}, HEARTBEAT_INTERVAL);
|
|
@@ -17,34 +17,34 @@ self.onmessage = (event: MessageEvent) => {
|
|
|
17
17
|
let transferList: Transferable[] = [];
|
|
18
18
|
|
|
19
19
|
if (type === "encode") {
|
|
20
|
-
// [Main -> Worker]
|
|
21
|
-
// message
|
|
20
|
+
// [Main -> Worker] 인코딩 요청 (data: { uuid, message })
|
|
21
|
+
// message는 이미 Plain Object임 (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[]는 전송 가능하므로 결과로 반환
|
|
29
29
|
result = chunks;
|
|
30
|
-
//
|
|
30
|
+
// 결과 chunk의 내부 ArrayBuffer를 전송 목록에 추가
|
|
31
31
|
transferList = chunks.map((chunk) => chunk.buffer as ArrayBuffer);
|
|
32
32
|
} else {
|
|
33
|
-
// [Main -> Worker]
|
|
34
|
-
// data
|
|
33
|
+
// [Main -> Worker] 디코딩 요청 (data: Uint8Array)
|
|
34
|
+
// data는 Uint8Array로 전달됨
|
|
35
35
|
const bytes = new Uint8Array(data as ArrayBuffer);
|
|
36
36
|
const decodeResult = protocol.decode(bytes);
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// 결과 객체를 전송 가능한 형태로 변환 (zero-copy 준비)
|
|
39
39
|
const encoded = transfer.encode(decodeResult);
|
|
40
40
|
result = encoded.result;
|
|
41
41
|
transferList = encoded.transferList;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// [Worker -> Main]
|
|
44
|
+
// [Worker -> Main] 성공 응답
|
|
45
45
|
self.postMessage({ id, type: "success", result }, { transfer: transferList });
|
|
46
46
|
} catch (err) {
|
|
47
|
-
// [Worker -> Main]
|
|
47
|
+
// [Worker -> Main] 에러 응답
|
|
48
48
|
self.postMessage({
|
|
49
49
|
id,
|
|
50
50
|
type: "error",
|
package/README.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-client
|
|
2
|
-
|
|
3
|
-
Simplysm package - Service module (client)
|
|
4
|
-
|
|
5
|
-
WebSocket-based service client for communicating with `@simplysm/service-server`. Provides type-safe service method calls, event pub/sub, file upload/download, and client-side ORM connectivity.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @simplysm/service-client
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## API Overview
|
|
14
|
-
|
|
15
|
-
### Types
|
|
16
|
-
| API | Type | Description |
|
|
17
|
-
|-----|------|-------------|
|
|
18
|
-
| `ServiceConnectionOptions` | interface | Connection options (host, port, ssl, reconnect) |
|
|
19
|
-
| `ServiceProgress` | interface | Progress callback config for requests |
|
|
20
|
-
| `ServiceProgressState` | interface | Progress state (uuid, totalSize, completedSize) |
|
|
21
|
-
|
|
22
|
-
-> See [docs/types.md](./docs/types.md) for details.
|
|
23
|
-
|
|
24
|
-
### Transport
|
|
25
|
-
| API | Type | Description |
|
|
26
|
-
|-----|------|-------------|
|
|
27
|
-
| `SocketProvider` | interface | WebSocket connection provider |
|
|
28
|
-
| `SocketProviderEvents` | interface | Socket events (message, state) |
|
|
29
|
-
| `createSocketProvider` | function | Create socket with heartbeat and auto-reconnect |
|
|
30
|
-
| `ServiceTransport` | interface | Message routing and request/response correlation |
|
|
31
|
-
| `ServiceTransportEvents` | interface | Transport events (reload, event) |
|
|
32
|
-
| `createServiceTransport` | function | Create transport instance |
|
|
33
|
-
|
|
34
|
-
-> See [docs/transport.md](./docs/transport.md) for details.
|
|
35
|
-
|
|
36
|
-
### Protocol
|
|
37
|
-
| API | Type | Description |
|
|
38
|
-
|-----|------|-------------|
|
|
39
|
-
| `ClientProtocolWrapper` | interface | Protocol wrapper with Web Worker offloading |
|
|
40
|
-
| `createClientProtocolWrapper` | function | Create protocol wrapper |
|
|
41
|
-
|
|
42
|
-
-> See [docs/protocol.md](./docs/protocol.md) for details.
|
|
43
|
-
|
|
44
|
-
### Features (Event, File, ORM)
|
|
45
|
-
| API | Type | Description |
|
|
46
|
-
|-----|------|-------------|
|
|
47
|
-
| `EventClient` | interface | Event listener management |
|
|
48
|
-
| `createEventClient` | function | Create event client |
|
|
49
|
-
| `FileClient` | interface | File upload/download |
|
|
50
|
-
| `createFileClient` | function | Create file client |
|
|
51
|
-
| `OrmConnectOptions` | interface | ORM connection options |
|
|
52
|
-
| `OrmClientConnector` | interface | Client-side ORM connector |
|
|
53
|
-
| `createOrmClientConnector` | function | Create ORM connector |
|
|
54
|
-
| `OrmClientDbContextExecutor` | class | DbContext executor via service protocol |
|
|
55
|
-
|
|
56
|
-
-> See [docs/features.md](./docs/features.md) for details.
|
|
57
|
-
|
|
58
|
-
### Main
|
|
59
|
-
| API | Type | Description |
|
|
60
|
-
|-----|------|-------------|
|
|
61
|
-
| `ServiceClient` | class | Main client (connect, auth, service calls, events, files) |
|
|
62
|
-
| `ServiceClientEvents` | interface | Client events (progress, state, reload) |
|
|
63
|
-
| `ServiceProxy` | type | Type wrapper for remote service methods |
|
|
64
|
-
| `createServiceClient` | function | Factory to create ServiceClient |
|
|
65
|
-
|
|
66
|
-
-> See [docs/service-client.md](./docs/service-client.md) for details.
|
|
67
|
-
|
|
68
|
-
## Usage Examples
|
|
69
|
-
|
|
70
|
-
### Connect and Call Service Methods
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { createServiceClient } from "@simplysm/service-client";
|
|
74
|
-
|
|
75
|
-
const client = createServiceClient("my-app", {
|
|
76
|
-
host: "localhost",
|
|
77
|
-
port: 3000,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await client.connect();
|
|
81
|
-
await client.auth(jwtToken);
|
|
82
|
-
|
|
83
|
-
// Type-safe service proxy
|
|
84
|
-
interface UserService {
|
|
85
|
-
getProfile(): Promise<{ name: string; email: string }>;
|
|
86
|
-
updateName(name: string): Promise<void>;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const userService = client.getService<UserService>("User");
|
|
90
|
-
const profile = await userService.getProfile();
|
|
91
|
-
await userService.updateName("New Name");
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Client-Side ORM
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { createOrmClientConnector } from "@simplysm/service-client";
|
|
98
|
-
|
|
99
|
-
const orm = createOrmClientConnector(client);
|
|
100
|
-
|
|
101
|
-
const users = await orm.connect(
|
|
102
|
-
{ dbContextDef: MyDb, connOpt: { configName: "main" } },
|
|
103
|
-
async (db) => {
|
|
104
|
-
return db.user().where((u) => [expr.eq(u.status, "active")]).execute();
|
|
105
|
-
},
|
|
106
|
-
);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Event Subscription
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { defineEvent } from "@simplysm/service-common";
|
|
113
|
-
|
|
114
|
-
const OrderUpdated = defineEvent<{ orderId: number }, { status: string }>("OrderUpdated");
|
|
115
|
-
|
|
116
|
-
const key = await client.addListener(
|
|
117
|
-
OrderUpdated,
|
|
118
|
-
{ orderId: 123 },
|
|
119
|
-
async (data) => {
|
|
120
|
-
console.log("Order status:", data.status);
|
|
121
|
-
},
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// Later: unsubscribe
|
|
125
|
-
await client.removeListener(key);
|
|
126
|
-
```
|