@simplysm/service-server 13.0.95 → 13.0.97

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "13.0.95",
3
+ "version": "13.0.97",
4
4
  "description": "Simplysm package - service module (server)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -30,17 +30,17 @@
30
30
  "bufferutil": "^4.1.0",
31
31
  "consola": "^3.4.2",
32
32
  "fastify": "^5.8.2",
33
- "jose": "^6.2.1",
33
+ "jose": "^6.2.2",
34
34
  "mime": "^4.1.0",
35
- "nodemailer": "^8.0.2",
35
+ "nodemailer": "^8.0.3",
36
36
  "semver": "^7.7.4",
37
37
  "utf-8-validate": "^6.0.6",
38
38
  "ws": "^8.19.0",
39
- "@simplysm/orm-node": "13.0.95",
40
- "@simplysm/core-common": "13.0.95",
41
- "@simplysm/orm-common": "13.0.95",
42
- "@simplysm/core-node": "13.0.95",
43
- "@simplysm/service-common": "13.0.95"
39
+ "@simplysm/core-common": "13.0.97",
40
+ "@simplysm/core-node": "13.0.97",
41
+ "@simplysm/orm-node": "13.0.97",
42
+ "@simplysm/orm-common": "13.0.97",
43
+ "@simplysm/service-common": "13.0.97"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/nodemailer": "^7.0.11",
package/README.md DELETED
@@ -1,294 +0,0 @@
1
- # @simplysm/service-server
2
-
3
- Fastify 기반 서비스 서버. WebSocket RPC, HTTP API, JWT 인증, 이벤트 브로드캐스트, 파일 업로드, 정적 파일 서빙을 제공한다.
4
-
5
- ## 설치
6
-
7
- ```bash
8
- npm install @simplysm/service-server
9
- ```
10
-
11
- **주요 의존성:** `fastify`, `@fastify/cors`, `@fastify/websocket`, `@fastify/static`, `@fastify/multipart`, `@fastify/helmet`, `jose`, `nodemailer`, `ws`
12
-
13
- **내부 의존성:** `@simplysm/core-common`, `@simplysm/core-node`, `@simplysm/orm-common`, `@simplysm/orm-node`, `@simplysm/service-common`
14
-
15
- ## 서버 생성 및 실행
16
-
17
- ```typescript
18
- import { createServiceServer, defineService, auth } from "@simplysm/service-server";
19
-
20
- const server = createServiceServer({
21
- rootPath: "/app",
22
- port: 3000,
23
- ssl: { pfxBytes, passphrase: "cert-pass" }, // 선택
24
- auth: { jwtSecret: "my-secret" }, // 선택
25
- services: [UserService, OrmService],
26
- });
27
-
28
- await server.listen();
29
- // ... 종료 시
30
- await server.close();
31
- ```
32
-
33
- ### ServiceServerOptions
34
-
35
- ```typescript
36
- interface ServiceServerOptions {
37
- rootPath: string; // 루트 경로 (www/, .config.json 기준)
38
- port: number; // 리스닝 포트
39
- ssl?: { // HTTPS 설정 (선택)
40
- pfxBytes: Uint8Array;
41
- passphrase: string;
42
- };
43
- auth?: { // JWT 인증 설정 (선택)
44
- jwtSecret: string;
45
- };
46
- services: ServiceDefinition[]; // 등록할 서비스 목록
47
- }
48
- ```
49
-
50
- ## 서비스 정의
51
-
52
- `defineService`로 서비스를 정의하고, `auth`로 인증을 요구한다.
53
-
54
- ```typescript
55
- import { defineService, auth } from "@simplysm/service-server";
56
- import type { ServiceContext, ServiceMethods } from "@simplysm/service-server";
57
-
58
- // 기본 서비스 (인증 불필요)
59
- const UserService = defineService("User", (ctx: ServiceContext) => ({
60
- async findAll() {
61
- return [{ id: 1, name: "Alice" }];
62
- },
63
- async findById(id: number) {
64
- return { id, name: "Alice" };
65
- },
66
- }));
67
-
68
- // 서비스 전체에 인증 필수
69
- const AdminService = defineService("Admin", auth((ctx) => ({
70
- async getStats() {
71
- const authInfo = ctx.authInfo; // 인증 정보 접근
72
- return { users: 100 };
73
- },
74
- })));
75
-
76
- // 서비스 전체에 역할 기반 인증
77
- const SecureService = defineService("Secure", auth(["admin", "manager"], (ctx) => ({
78
- async deleteUser(id: number) { /* ... */ },
79
- })));
80
-
81
- // 메서드 단위 인증 (서비스 레벨은 공개, 특정 메서드만 인증)
82
- const MixedService = defineService("Mixed", (ctx) => ({
83
- async publicMethod() { /* 누구나 호출 가능 */ },
84
- adminOnly: auth(["admin"], async () => { /* admin만 호출 가능 */ }),
85
- }));
86
-
87
- // 클라이언트 타입 공유용
88
- export type UserServiceType = ServiceMethods<typeof UserService>;
89
- ```
90
-
91
- ### auth 래퍼
92
-
93
- `auth` 함수는 서비스 팩토리 또는 개별 메서드에 적용할 수 있다.
94
-
95
- ```typescript
96
- // 서비스 레벨: 모든 메서드에 로그인 필수
97
- auth((ctx) => ({ ... }))
98
-
99
- // 서비스 레벨: 특정 역할 필수
100
- auth(["admin"], (ctx) => ({ ... }))
101
-
102
- // 메서드 레벨: 해당 메서드만 로그인 필수
103
- auth(() => result)
104
-
105
- // 메서드 레벨: 해당 메서드만 특정 역할 필수
106
- auth(["admin"], () => result)
107
- ```
108
-
109
- 인증 검사 우선순위: 메서드 레벨 > 서비스 레벨. 메서드에 `auth`가 있으면 서비스 레벨 설정을 무시한다.
110
-
111
- ## ServiceContext
112
-
113
- 서비스 메서드에 주입되는 컨텍스트 객체.
114
-
115
- ```typescript
116
- interface ServiceContext<TAuthInfo = unknown> {
117
- server: ServiceServer<TAuthInfo>; // 서버 인스턴스
118
- socket?: ServiceSocket; // WebSocket 연결 (소켓 호출 시)
119
- http?: { // HTTP 요청 (HTTP 호출 시)
120
- clientName: string;
121
- authTokenPayload?: AuthTokenPayload<TAuthInfo>;
122
- };
123
-
124
- get authInfo(): TAuthInfo | undefined; // 인증 데이터 (socket 또는 http에서 추출)
125
- get clientName(): string | undefined; // 클라이언트 이름
126
- get clientPath(): string | undefined; // rootPath/www/{clientName} 경로
127
- getConfig<T>(section: string): Promise<T>; // .config.json에서 설정 읽기
128
- }
129
- ```
130
-
131
- `getConfig`는 `rootPath/.config.json`을 기본으로 읽고, `clientPath/.config.json`이 있으면 머지한다.
132
-
133
- ## JWT 인증
134
-
135
- `jose` 라이브러리 기반. HS256 알고리즘, 토큰 유효기간 12시간.
136
-
137
- ```typescript
138
- // 토큰 발행
139
- const token = await server.signAuthToken({
140
- roles: ["admin"],
141
- data: { userId: 1, name: "Alice" },
142
- });
143
-
144
- // 토큰 검증
145
- const payload = await server.verifyAuthToken(token);
146
- // { roles: ["admin"], data: { userId: 1, name: "Alice" } }
147
- ```
148
-
149
- ### AuthTokenPayload
150
-
151
- ```typescript
152
- interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
153
- roles: string[]; // 역할 목록 (권한 검사용)
154
- data: TAuthInfo; // 사용자 정의 인증 데이터
155
- }
156
- ```
157
-
158
- ### JWT 유틸리티 함수
159
-
160
- 서버 인스턴스 없이 직접 사용할 수 있는 저수준 함수.
161
-
162
- ```typescript
163
- import { signJwt, verifyJwt, decodeJwt } from "@simplysm/service-server";
164
-
165
- const token = await signJwt("secret", { roles: ["user"], data: { id: 1 } });
166
- const payload = await verifyJwt("secret", token); // 검증 + 디코드 (만료 시 에러)
167
- const decoded = decodeJwt(token); // 검증 없이 디코드
168
- ```
169
-
170
- ## 이벤트 브로드캐스트
171
-
172
- WebSocket으로 연결된 클라이언트에게 이벤트를 전송한다.
173
-
174
- ```typescript
175
- import { defineEvent } from "@simplysm/service-common";
176
-
177
- const OrderCreated = defineEvent<{ shopId: string }, { orderId: string; amount: number }>(
178
- "order-created"
179
- );
180
-
181
- // 조건에 맞는 구독자에게 이벤트 전송
182
- await server.emitEvent(
183
- OrderCreated,
184
- (info) => info.shopId === "shop-1",
185
- { orderId: "order-123", amount: 50000 }
186
- );
187
-
188
- // 클라이언트 리로드 알림 (개발 모드용)
189
- await server.broadcastReload("my-app", new Set(["file1.ts", "file2.ts"]));
190
- ```
191
-
192
- ## HTTP API
193
-
194
- 자동으로 등록되는 HTTP 엔드포인트:
195
-
196
- | 메서드 | 경로 | 설명 |
197
- |--------|------|------|
198
- | GET | `/api/:service/:method?json=...` | 서비스 호출 (params: JSON 인코딩 배열) |
199
- | POST | `/api/:service/:method` | 서비스 호출 (params: body 배열) |
200
- | POST | `/upload` | 파일 업로드 (multipart, 인증 필수) |
201
- | GET | `/...` | 정적 파일 서빙 (`rootPath/www/`) |
202
-
203
- ### HTTP 헤더
204
-
205
- - `x-sd-client-name` (필수): 클라이언트 이름
206
- - `Authorization: Bearer <token>` (선택): JWT 인증 토큰
207
-
208
- ### 파일 업로드
209
-
210
- 인증 필수. multipart/form-data로 전송. 파일은 `rootPath/www/uploads/`에 UUID 이름으로 저장된다.
211
-
212
- ```typescript
213
- // 응답 타입
214
- interface ServiceUploadResult {
215
- path: string; // "uploads/{uuid}.ext"
216
- filename: string; // 원본 파일명
217
- size: number; // 바이트 크기
218
- }
219
- ```
220
-
221
- ## 내장 서비스
222
-
223
- 상세 API는 [docs/builtin-services.md](docs/builtin-services.md) 참조.
224
-
225
- ### OrmService
226
-
227
- 인증 필수. WebSocket 전용. 소켓별 DB 커넥션 풀을 관리한다. 소켓 종료 시 자동 정리.
228
-
229
- ```typescript
230
- import { OrmService } from "@simplysm/service-server";
231
-
232
- const server = createServiceServer({
233
- services: [OrmService],
234
- // ...
235
- });
236
- ```
237
-
238
- `.config.json`에 DB 설정 필요:
239
-
240
- ```json
241
- {
242
- "orm": {
243
- "main": {
244
- "dialect": "mysql",
245
- "host": "localhost",
246
- "port": 3306,
247
- "username": "root",
248
- "password": "password"
249
- }
250
- }
251
- }
252
- ```
253
-
254
- ### AutoUpdateService
255
-
256
- `clientPath/{platform}/updates/` 디렉토리에서 최신 버전을 탐색한다. `.apk`(Android), `.exe`(Windows) 지원. V1 레거시 클라이언트 호환.
257
-
258
- ```typescript
259
- import { AutoUpdateService } from "@simplysm/service-server";
260
- ```
261
-
262
- ### SmtpClientService
263
-
264
- nodemailer 기반 이메일 전송 서비스. 직접 설정 또는 `.config.json` 기반 전송을 지원한다.
265
-
266
- ```typescript
267
- import { SmtpClientService } from "@simplysm/service-server";
268
- ```
269
-
270
- ## 설정 파일
271
-
272
- `rootPath/.config.json`에서 설정을 읽는다. LRU 캐시(1시간 만료, 10분 GC 주기)와 파일 감시로 자동 리로드된다.
273
-
274
- ```typescript
275
- const dbConfig = await ctx.getConfig<DbConfig>("orm.main");
276
- ```
277
-
278
- 클라이언트별 설정이 있으면 (`rootPath/www/{clientName}/.config.json`) 루트 설정에 머지된다.
279
-
280
- ## 서버 이벤트
281
-
282
- ```typescript
283
- server.on("ready", () => { /* 서버 시작 완료 */ });
284
- server.on("close", () => { /* 서버 종료 완료 */ });
285
- ```
286
-
287
- ## Graceful Shutdown
288
-
289
- SIGINT/SIGTERM 시그널 수신 시 자동으로 graceful shutdown을 수행한다. 10초 타임아웃 후 강제 종료.
290
-
291
- ## 상세 API 레퍼런스
292
-
293
- - [내장 서비스 상세](docs/builtin-services.md) -- OrmService, AutoUpdateService, SmtpClientService 메서드 시그니처
294
- - [전송 계층 및 프로토콜](docs/transport-protocol.md) -- WebSocket 핸들러, ServiceSocket, 프로토콜 래퍼
@@ -1,240 +0,0 @@
1
- # 내장 서비스 상세 API
2
-
3
- ## OrmService
4
-
5
- 서비스 이름: `"Orm"`. 인증 필수 (`auth` 래핑). WebSocket 전용 -- HTTP로는 사용 불가.
6
-
7
- 소켓별로 DB 커넥션을 `WeakMap<ServiceSocket, Map<number, DbConn>>`으로 관리한다. 소켓 `close` 이벤트 시 해당 소켓의 모든 커넥션을 자동 정리한다.
8
-
9
- ### 메서드
10
-
11
- ```typescript
12
- // DB 정보 조회 (연결 없이)
13
- async getInfo(opt: DbConnOptions & { configName: string }): Promise<{
14
- dialect: Dialect; // "mysql" | "postgresql" | "mssql" (mssql-azure는 mssql로 반환)
15
- database?: string;
16
- schema?: string;
17
- }>
18
-
19
- // DB 연결 생성 (connId 반환)
20
- async connect(opt: DbConnOptions & { configName: string }): Promise<number>
21
-
22
- // DB 연결 종료
23
- async close(connId: number): Promise<void>
24
-
25
- // 트랜잭션 제어
26
- async beginTransaction(connId: number, isolationLevel?: IsolationLevel): Promise<void>
27
- async commitTransaction(connId: number): Promise<void>
28
- async rollbackTransaction(connId: number): Promise<void>
29
-
30
- // 파라미터화된 쿼리 실행
31
- async executeParametrized(
32
- connId: number,
33
- query: string,
34
- params?: unknown[],
35
- ): Promise<unknown[][]>
36
-
37
- // QueryDef 기반 쿼리 실행 (쿼리 빌더 사용)
38
- async executeDefs(
39
- connId: number,
40
- defs: QueryDef[],
41
- options?: (ResultMeta | undefined)[],
42
- ): Promise<unknown[][]>
43
-
44
- // 벌크 인서트
45
- async bulkInsert(
46
- connId: number,
47
- tableName: string,
48
- columnDefs: Record<string, ColumnMeta>,
49
- records: Record<string, unknown>[],
50
- ): Promise<void>
51
- ```
52
-
53
- ### DbConnOptions
54
-
55
- `@simplysm/service-common`에서 가져온다.
56
-
57
- ```typescript
58
- type DbConnOptions = {
59
- configName?: string;
60
- config?: Record<string, unknown>; // .config.json 설정 오버라이드
61
- };
62
- ```
63
-
64
- ### 설정 예시
65
-
66
- `.config.json`:
67
-
68
- ```json
69
- {
70
- "orm": {
71
- "main": {
72
- "dialect": "mysql",
73
- "host": "localhost",
74
- "port": 3306,
75
- "username": "root",
76
- "password": "password",
77
- "database": "mydb"
78
- },
79
- "secondary": {
80
- "dialect": "postgresql",
81
- "host": "pg-host",
82
- "port": 5432,
83
- "username": "admin",
84
- "password": "password",
85
- "database": "mydb",
86
- "schema": "public"
87
- }
88
- }
89
- }
90
- ```
91
-
92
- ### 타입 내보내기
93
-
94
- ```typescript
95
- import type { OrmServiceType } from "@simplysm/service-server";
96
- // 클라이언트에서 타입 공유: client.getService<OrmServiceType>("Orm")
97
- ```
98
-
99
- ---
100
-
101
- ## AutoUpdateService
102
-
103
- 서비스 이름: `"AutoUpdate"`. 인증 불필요. `clientPath/{platform}/updates/` 디렉토리에서 semver 기준 최신 버전 파일을 탐색한다.
104
-
105
- ### 메서드
106
-
107
- ```typescript
108
- async getLastVersion(platform: string): Promise<{
109
- version: string; // 최신 버전 문자열 (예: "1.2.3")
110
- downloadPath: string; // 다운로드 경로 (예: "/my-app/android/updates/1.2.3.apk")
111
- } | undefined>
112
- ```
113
-
114
- ### 지원 플랫폼
115
-
116
- | platform | 파일 확장자 | 디렉토리 경로 |
117
- |----------|------------|---------------|
118
- | `"android"` | `.apk` | `www/{clientName}/android/updates/` |
119
- | 기타 | `.exe` | `www/{clientName}/{platform}/updates/` |
120
-
121
- 파일명은 버전 번호여야 한다 (예: `1.2.3.apk`, `2.0.0.exe`). `semver.maxSatisfying`으로 최신 버전을 결정한다.
122
-
123
- ### V1 레거시 호환
124
-
125
- V1 프로토콜 클라이언트(WebSocket `ver` 쿼리 파라미터 없음)도 `SdAutoUpdateService.getLastVersion` 명령으로 자동 업데이트를 요청할 수 있다. V1 클라이언트의 다른 요청은 `UPGRADE_REQUIRED` 에러를 반환한다.
126
-
127
- ### 타입 내보내기
128
-
129
- ```typescript
130
- import type { AutoUpdateServiceType } from "@simplysm/service-server";
131
- ```
132
-
133
- ---
134
-
135
- ## SmtpClientService
136
-
137
- 서비스 이름: `"SmtpClient"`. 인증 불필요. nodemailer 기반 이메일 전송.
138
-
139
- ### 메서드
140
-
141
- ```typescript
142
- // 직접 SMTP 설정으로 전송
143
- async send(options: SmtpClientSendOption): Promise<string>
144
- // 반환값: messageId
145
-
146
- // .config.json의 smtp 설정으로 전송
147
- async sendByConfig(configName: string, options: SmtpClientSendByDefaultOption): Promise<string>
148
- // 반환값: messageId
149
- ```
150
-
151
- ### SmtpClientSendOption
152
-
153
- `@simplysm/service-common`에서 가져온다.
154
-
155
- ```typescript
156
- interface SmtpClientSendOption {
157
- host: string;
158
- port: number;
159
- secure?: boolean;
160
- user?: string;
161
- pass?: string;
162
- from: string;
163
- to: string | string[];
164
- cc?: string | string[];
165
- bcc?: string | string[];
166
- subject: string;
167
- html?: string;
168
- text?: string;
169
- attachments?: Array<{ filename: string; content: Uint8Array }>;
170
- }
171
- ```
172
-
173
- ### SmtpClientSendByDefaultOption
174
-
175
- ```typescript
176
- interface SmtpClientSendByDefaultOption {
177
- to: string | string[];
178
- cc?: string | string[];
179
- bcc?: string | string[];
180
- subject: string;
181
- html?: string;
182
- text?: string;
183
- attachments?: Array<{ filename: string; content: Uint8Array }>;
184
- }
185
- ```
186
-
187
- ### 설정 예시
188
-
189
- `.config.json`:
190
-
191
- ```json
192
- {
193
- "smtp": {
194
- "default": {
195
- "host": "smtp.example.com",
196
- "port": 587,
197
- "secure": false,
198
- "user": "noreply@example.com",
199
- "pass": "password",
200
- "senderName": "My App",
201
- "senderEmail": "noreply@example.com"
202
- }
203
- }
204
- }
205
- ```
206
-
207
- `senderEmail`이 없으면 `user`가 발신자 이메일로 사용된다.
208
-
209
- ### 사용 예시
210
-
211
- ```typescript
212
- // 서버에서 직접 사용
213
- const ctx = createServiceContext(server);
214
- const methods = SmtpClientService.factory(ctx);
215
-
216
- // 직접 설정
217
- await methods.send({
218
- host: "smtp.example.com",
219
- port: 587,
220
- user: "user@example.com",
221
- pass: "pass",
222
- from: '"My App" <noreply@example.com>',
223
- to: "recipient@example.com",
224
- subject: "테스트",
225
- html: "<p>Hello</p>",
226
- });
227
-
228
- // .config.json 기반
229
- await methods.sendByConfig("default", {
230
- to: "recipient@example.com",
231
- subject: "테스트",
232
- html: "<p>Hello</p>",
233
- });
234
- ```
235
-
236
- ### 타입 내보내기
237
-
238
- ```typescript
239
- import type { SmtpClientServiceType } from "@simplysm/service-server";
240
- ```
@@ -1,200 +0,0 @@
1
- # 전송 계층 및 프로토콜
2
-
3
- ## WebSocket 전송
4
-
5
- ### WebSocketHandler
6
-
7
- 다수의 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며, 이벤트 브로드캐스트를 처리한다.
8
-
9
- ```typescript
10
- interface WebSocketHandler {
11
- addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
12
- closeAll(): void;
13
- broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
14
- emit<TInfo, TData>(
15
- eventDef: ServiceEventDef<TInfo, TData>,
16
- infoSelector: (item: TInfo) => boolean,
17
- data: TData,
18
- ): Promise<void>;
19
- }
20
-
21
- function createWebSocketHandler(
22
- runMethod: (def: {
23
- serviceName: string;
24
- methodName: string;
25
- params: unknown[];
26
- socket?: ServiceSocket;
27
- }) => Promise<unknown>,
28
- jwtSecret: string | undefined,
29
- ): WebSocketHandler;
30
- ```
31
-
32
- ### WebSocket 연결 흐름
33
-
34
- 1. 클라이언트가 `/` 또는 `/ws`로 WebSocket 연결
35
- 2. 쿼리 파라미터로 프로토콜 버전 구분:
36
- - `ver=2&clientId=...&clientName=...` -- V2 프로토콜 (현재)
37
- - 쿼리 없음 -- V1 레거시 (AutoUpdate만 지원)
38
- 3. V2: `createServiceSocket`으로 소켓 래핑, 메시지 라우팅 시작
39
- 4. 같은 `clientId`로 재연결 시 이전 소켓을 종료하고 새 소켓으로 교체
40
-
41
- ### WebSocket 메시지 프로토콜
42
-
43
- V2 클라이언트 메시지 포맷 (`ServiceClientMessage`):
44
-
45
- | `name` | `body` | 설명 |
46
- |--------|--------|------|
47
- | `"{Service}.{Method}"` | `unknown[]` (파라미터 배열) | 서비스 메서드 호출 |
48
- | `"auth"` | `string` (JWT 토큰) | 인증 토큰 설정 |
49
- | `"evt:add"` | `{ key, name, info }` | 이벤트 리스너 등록 |
50
- | `"evt:remove"` | `{ key }` | 이벤트 리스너 제거 |
51
- | `"evt:gets"` | `{ name }` | 이벤트 리스너 목록 조회 |
52
- | `"evt:emit"` | `{ keys, data }` | 대상 키에 이벤트 전송 |
53
-
54
- V2 서버 응답 (`ServiceServerMessage`):
55
-
56
- | `name` | `body` | 설명 |
57
- |--------|--------|------|
58
- | `"response"` | `unknown` (결과) | 성공 응답 |
59
- | `"error"` | `{ name, message, code, stack }` | 에러 응답 |
60
- | `"progress"` | `{ totalSize, completedSize }` | 전송 진행률 |
61
- | `"evt:on"` | `{ keys, data }` | 이벤트 수신 |
62
- | `"reload"` | `{ clientName, changedFileSet }` | 리로드 알림 |
63
-
64
- ---
65
-
66
- ### ServiceSocket
67
-
68
- 단일 WebSocket 연결을 관리하는 인터페이스. 프로토콜 인코딩/디코딩, ping/pong keep-alive(5초 간격), 이벤트 리스너 추적을 담당한다.
69
-
70
- ```typescript
71
- interface ServiceSocket {
72
- readonly connectedAtDateTime: DateTime;
73
- readonly clientName: string;
74
- readonly connReq: FastifyRequest;
75
- authTokenPayload?: AuthTokenPayload;
76
-
77
- close(): void;
78
- send(uuid: string, msg: ServiceServerMessage): Promise<number>;
79
-
80
- // 이벤트 리스너 관리
81
- addListener(key: string, eventName: string, info: unknown): void;
82
- removeListener(key: string): void;
83
- getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
84
- filterEventTargetKeys(targetKeys: string[]): string[];
85
-
86
- // 이벤트 핸들러 등록
87
- on(event: "error", handler: (err: Error) => void): void;
88
- on(event: "close", handler: (code: number) => void): void;
89
- on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
90
- }
91
-
92
- function createServiceSocket(
93
- socket: WebSocket,
94
- clientId: string,
95
- clientName: string,
96
- connReq: FastifyRequest,
97
- ): ServiceSocket;
98
- ```
99
-
100
- ---
101
-
102
- ## HTTP 전송
103
-
104
- ### handleHttpRequest
105
-
106
- HTTP API 요청 처리 함수. 서버 내부에서 `/api/:service/:method` 라우트에 등록된다.
107
-
108
- ```typescript
109
- async function handleHttpRequest<TAuthInfo>(
110
- req: FastifyRequest,
111
- reply: FastifyReply,
112
- jwtSecret: string | undefined,
113
- runMethod: (def: {
114
- serviceName: string;
115
- methodName: string;
116
- params: unknown[];
117
- http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
118
- }) => Promise<unknown>,
119
- ): Promise<void>;
120
- ```
121
-
122
- - GET: `?json=` 쿼리 파라미터에서 파라미터 배열을 JSON 파싱
123
- - POST: request body를 배열로 파싱
124
- - `x-sd-client-name` 헤더 필수
125
- - `Authorization: Bearer <token>` 헤더가 있으면 JWT 검증 후 `authTokenPayload`로 전달
126
-
127
- ### handleUpload
128
-
129
- 파일 업로드 처리 함수. `/upload` 라우트에 등록된다. 인증 필수.
130
-
131
- ```typescript
132
- async function handleUpload(
133
- req: FastifyRequest,
134
- reply: FastifyReply,
135
- rootPath: string,
136
- jwtSecret: string | undefined,
137
- ): Promise<void>;
138
- ```
139
-
140
- - multipart/form-data 필수
141
- - 파일을 `rootPath/www/uploads/{UUID}.{ext}`로 저장
142
- - 업로드 실패 시 불완전한 파일 자동 삭제
143
- - 응답: `ServiceUploadResult[]`
144
-
145
- ### handleStaticFile
146
-
147
- 정적 파일 서빙 함수. 와일드카드 라우트 `/*`에 등록된다.
148
-
149
- ```typescript
150
- async function handleStaticFile(
151
- req: FastifyRequest,
152
- reply: FastifyReply,
153
- rootPath: string,
154
- urlPath: string,
155
- ): Promise<void>;
156
- ```
157
-
158
- - `rootPath/www/` 디렉토리 기준
159
- - 디렉토리 접근 시 trailing slash 리다이렉트 + `index.html` 서빙
160
- - `.`으로 시작하는 파일은 403 거부
161
- - path traversal 방지 (보안 가드)
162
-
163
- ---
164
-
165
- ## 프로토콜 래퍼
166
-
167
- 메시지 인코딩/디코딩을 자동으로 워커 스레드에 오프로드한다. 30KB 이상의 메시지 또는 `Uint8Array` 포함 메시지는 워커에서 처리하여 메인 스레드 블로킹을 방지한다.
168
-
169
- ```typescript
170
- interface ServerProtocolWrapper {
171
- encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
172
- decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
173
- dispose(): void;
174
- }
175
-
176
- function createServerProtocolWrapper(): ServerProtocolWrapper;
177
- ```
178
-
179
- ### 워커 오프로드 조건
180
-
181
- - **인코딩**: 메시지 body가 `Uint8Array`이거나, 배열 내에 `Uint8Array` 요소가 있으면 워커 사용
182
- - **디코딩**: 메시지 크기가 30KB 초과이면 워커 사용
183
- - 워커는 lazy singleton으로 관리 (최대 4GB 메모리 제한)
184
-
185
- ---
186
-
187
- ## 설정 관리
188
-
189
- ### getConfig
190
-
191
- `.config.json` 파일을 읽고 캐시하는 함수.
192
-
193
- ```typescript
194
- async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
195
- ```
196
-
197
- - LRU 캐시: 1시간 만료, 10분마다 GC
198
- - 파일 감시(FsWatcher)로 변경 시 자동 리로드
199
- - 파일 삭제 시 캐시에서 제거 및 감시 해제
200
- - 캐시 만료 시 감시도 함께 해제