@simplysm/service-server 14.0.49 → 14.0.51

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 (35) hide show
  1. package/README.md +53 -82
  2. package/dist/auth/jwt-manager.js.map +1 -1
  3. package/dist/services/orm-service.js.map +1 -1
  4. package/dist/transport/socket/websocket-handler.js.map +1 -1
  5. package/docs/auth/auth-token-payload.md +18 -0
  6. package/docs/auth/sign-jwt.md +30 -0
  7. package/docs/auth/verify-jwt.md +35 -0
  8. package/docs/core/auth.md +58 -0
  9. package/docs/core/define-service.md +76 -0
  10. package/docs/core/execute-service-method.md +38 -0
  11. package/docs/core/service-context.md +79 -0
  12. package/docs/{legacy.md → legacy/handle-v1-connection.md} +5 -5
  13. package/docs/main/create-service-server.md +32 -0
  14. package/docs/main/service-server.md +106 -0
  15. package/docs/{protocol.md → protocol/server-protocol-wrapper.md} +11 -9
  16. package/docs/services/app-structure-service.md +54 -0
  17. package/docs/services/auto-update-service.md +29 -0
  18. package/docs/services/orm-service.md +38 -0
  19. package/docs/transport-http/handle-http-request.md +33 -0
  20. package/docs/transport-http/handle-static-file.md +29 -0
  21. package/docs/transport-http/handle-upload.md +33 -0
  22. package/docs/transport-socket/service-socket.md +64 -0
  23. package/docs/transport-socket/websocket-handler.md +57 -0
  24. package/docs/{types.md → types/service-server-options.md} +4 -4
  25. package/docs/{utils.md → utils/get-config.md} +8 -6
  26. package/package.json +6 -6
  27. package/src/auth/jwt-manager.ts +1 -1
  28. package/src/services/orm-service.ts +1 -1
  29. package/src/transport/socket/websocket-handler.ts +4 -4
  30. package/docs/auth.md +0 -64
  31. package/docs/core.md +0 -174
  32. package/docs/main.md +0 -88
  33. package/docs/services.md +0 -94
  34. package/docs/transport-http.md +0 -93
  35. package/docs/transport-socket.md +0 -119
@@ -0,0 +1,106 @@
1
+ # ServiceServer
2
+
3
+ Fastify를 래핑한 서비스 서버 클래스. WebSocket/HTTP 이중 전송, JWT 인증, 이벤트 브로드캐스트, graceful shutdown을 처리한다. `EventEmitter<{ ready: void; close: void }>`를 확장한다.
4
+
5
+ ```typescript
6
+ class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
7
+ ready: void;
8
+ close: void;
9
+ }> {
10
+ isOpen: boolean;
11
+ readonly fastify: FastifyInstance;
12
+ readonly options: ServiceServerOptions;
13
+
14
+ constructor(options: ServiceServerOptions);
15
+
16
+ async listen(): Promise<void>;
17
+ async close(): Promise<void>;
18
+ getEvent<TEventDef extends ServiceEventDef>(eventName: string): ServerEventProxy<TEventDef>;
19
+ async emitEvent<TEventDef extends ServiceEventDef>(
20
+ eventName: string,
21
+ infoSelector: (item: TEventDef["$info"]) => boolean,
22
+ data: TEventDef["$data"],
23
+ ): Promise<void>;
24
+ async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
25
+ async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
26
+ }
27
+ ```
28
+
29
+ ## Members
30
+
31
+ | Member | Kind | Type | Description |
32
+ |--------|------|------|-------------|
33
+ | `isOpen` | property | `boolean` | 서버가 리스닝 중인지 여부 |
34
+ | `fastify` | property | `FastifyInstance` | 내부 Fastify 인스턴스. 직접 접근이 필요할 때 사용 |
35
+ | `options` | property | `ServiceServerOptions` | 생성 시 전달된 옵션 |
36
+ | `listen()` | method | `Promise<void>` | 서버를 시작한다. 플러그인 등록, 라우트 설정, SIGINT/SIGTERM 핸들러 등록을 수행한다. 완료 시 `ready` 이벤트를 발생시킨다 |
37
+ | `close()` | method | `Promise<void>` | 모든 WebSocket 연결을 닫고 Fastify 서버를 종료한다. 완료 시 `close` 이벤트를 발생시킨다 |
38
+ | `getEvent(eventName)` | method | `ServerEventProxy<TEventDef>` | 타입 안전한 이벤트 프록시를 반환한다. `emit(infoSelector, data)` 메서드를 포함 |
39
+ | `emitEvent(eventName, infoSelector, data)` | method | `Promise<void>` | `infoSelector`에 매칭되는 WebSocket 클라이언트에 이벤트를 브로드캐스트한다 |
40
+ | `signAuthToken(payload)` | method | `Promise<string>` | JWT 토큰을 서명한다. `options.auth`가 설정되지 않으면 에러를 던진다 |
41
+ | `verifyAuthToken(token)` | method | `Promise<AuthTokenPayload<TAuthInfo>>` | JWT 토큰을 검증하고 페이로드를 반환한다. `options.auth`가 설정되지 않으면 에러를 던진다 |
42
+
43
+ `listen()` 시 등록되는 라우트:
44
+
45
+ | Route | Method | Handler |
46
+ |-------|--------|---------|
47
+ | `/api/:service/:method` | GET/POST | `handleHttpRequest` — 서비스 메서드 호출 |
48
+ | `/upload` | POST | `handleUpload` — multipart 파일 업로드 |
49
+ | `/`, `/ws` | WebSocket | WebSocket 핸들러. `ver=2` 쿼리 시 V2 프로토콜, 그 외 V1 레거시 |
50
+ | `/*` | GET/POST/PUT/DELETE/PATCH/HEAD | `handleStaticFile` — 정적 파일 서빙 |
51
+
52
+ `listen()` 시 Fastify 플러그인 등록 순서: `@fastify/websocket` → `@fastify/helmet` → `@fastify/multipart` → `@fastify/static` → `@fastify/cors`
53
+
54
+ Graceful shutdown: `SIGINT`/`SIGTERM` 시그널 수신 시 `close()`를 호출하고, 10초 내에 종료되지 않으면 `process.exit(1)`로 강제 종료한다.
55
+
56
+ ## Related Types
57
+
58
+ ### `ServerEventProxy`
59
+
60
+ `getEvent()`가 반환하는 타입 안전한 이벤트 프록시 인터페이스.
61
+
62
+ ```typescript
63
+ interface ServerEventProxy<TEventDef extends ServiceEventDef> {
64
+ emit(
65
+ infoSelector: (item: TEventDef["$info"]) => boolean,
66
+ data: TEventDef["$data"],
67
+ ): Promise<void>;
68
+ }
69
+ ```
70
+
71
+ | Method | Description |
72
+ |--------|-------------|
73
+ | `emit(infoSelector, data)` | `infoSelector`에 매칭되는 이벤트 리스너를 가진 WebSocket 클라이언트에 이벤트를 브로드캐스트한다 |
74
+
75
+ ## Usage
76
+
77
+ ```typescript
78
+ import { createServiceServer, defineService, auth } from "@simplysm/service-server";
79
+
80
+ const HealthService = defineService("Health", (ctx) => ({
81
+ check: () => ({ status: "ok" }),
82
+ }));
83
+
84
+ const UserService = defineService("User", auth((ctx) => ({
85
+ getProfile: () => ctx.authInfo,
86
+ })));
87
+
88
+ const server = createServiceServer<{ userId: string }>({
89
+ rootPath: "/app",
90
+ port: 3000,
91
+ auth: { jwtSecret: "my-secret" },
92
+ services: [HealthService, UserService],
93
+ });
94
+
95
+ await server.listen();
96
+
97
+ // JWT 토큰 발급
98
+ const token = await server.signAuthToken({
99
+ roles: ["admin"],
100
+ data: { userId: "123" },
101
+ });
102
+
103
+ // 이벤트 브로드캐스트
104
+ const evt = server.getEvent<typeof MyEvent>("MyEvent");
105
+ await evt.emit((info) => info.userId === "123", { name: "새 이름" });
106
+ ```
@@ -1,6 +1,4 @@
1
- # Protocol
2
-
3
- ## `ServerProtocolWrapper`
1
+ # ServerProtocolWrapper
4
2
 
5
3
  메시지 인코딩/디코딩 래퍼 인터페이스. 무거운 메시지는 worker 스레드에 자동으로 위임하고, 가벼운 작업은 메인 스레드에서 처리한다.
6
4
 
@@ -12,11 +10,13 @@ interface ServerProtocolWrapper {
12
10
  }
13
11
  ```
14
12
 
15
- | Method | Description |
16
- |--------|-------------|
17
- | `encode(uuid, message)` | 메시지를 인코딩한다. 바이너리 데이터(Uint8Array)가 포함된 메시지는 worker 스레드에 위임한다 |
18
- | `decode(bytes)` | 메시지를 디코딩한다. 30KB 초과 메시지는 worker 스레드에 위임한다 |
19
- | `dispose()` | 내부 프로토콜 리소스를 해제한다 |
13
+ ## Members
14
+
15
+ | Member | Kind | Type | Description |
16
+ |--------|------|------|-------------|
17
+ | `encode(uuid, message)` | method | `Promise<{ chunks: Bytes[]; totalSize: number }>` | 메시지를 인코딩한다. 바이너리 데이터(Uint8Array)가 포함된 메시지는 worker 스레드에 위임한다 |
18
+ | `decode(bytes)` | method | `Promise<ServiceMessageDecodeResult<ServiceMessage>>` | 메시지를 디코딩한다. 30KB 초과 메시지는 worker 스레드에 위임한다 |
19
+ | `dispose()` | method | `void` | 내부 프로토콜 리소스를 해제한다 |
20
20
 
21
21
  Worker 위임 기준:
22
22
  - **encode**: 메시지 body가 `Uint8Array`이거나, 배열 내에 `Uint8Array` 요소가 있을 때 worker 사용
@@ -24,7 +24,9 @@ Worker 위임 기준:
24
24
 
25
25
  Worker는 지연 싱글턴으로 생성되며, `maxOldGenerationSizeMb: 4096`의 리소스 제한이 설정되어 있다.
26
26
 
27
- ## `createServerProtocolWrapper`
27
+ ## Related Types
28
+
29
+ ### `createServerProtocolWrapper`
28
30
 
29
31
  `ServerProtocolWrapper` 인스턴스를 생성한다. 내부적으로 `@simplysm/service-common`의 `createServiceProtocol()`을 사용한다.
30
32
 
@@ -0,0 +1,54 @@
1
+ # AppStructureService
2
+
3
+ 앱 구조 정보 서비스를 생성하는 팩토리 함수. `defineService`를 래핑하여 `Record<string, AppStructureItem[]>` 맵을 받아 서비스 정의를 반환한다. 인증 불필요.
4
+
5
+ ```typescript
6
+ function AppStructureService(
7
+ itemsMap: Record<string, AppStructureItem[]>,
8
+ ): ServiceDefinition;
9
+ ```
10
+
11
+ ## Parameters
12
+
13
+ | Param | Type | Description |
14
+ |-------|------|-------------|
15
+ | `itemsMap` | `Record<string, AppStructureItem[]>` | 앱 구조 아이템 맵. `AppStructureItem`은 `@simplysm/service-common`에서 import한다 |
16
+
17
+ ## Returns
18
+
19
+ `ServiceDefinition` — `defineService("AppStructure", ...)`로 생성된 서비스 정의.
20
+
21
+ 제공 메서드:
22
+
23
+ | Method | Signature | Description |
24
+ |--------|-----------|-------------|
25
+ | `getItems` | `() => Record<string, AppStructureItem[]>` | 생성 시 전달된 `itemsMap`을 그대로 반환한다 |
26
+
27
+ ## Related Types
28
+
29
+ ### `AppStructureServiceType`
30
+
31
+ `AppStructureService`가 반환하는 서비스의 메서드 시그니처 타입.
32
+
33
+ ```typescript
34
+ type AppStructureServiceType = ServiceMethods<ReturnType<typeof AppStructureService>>;
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```typescript
40
+ import { AppStructureService } from "@simplysm/service-server";
41
+ import type { AppStructureItem } from "@simplysm/service-common";
42
+
43
+ const itemsMap: Record<string, AppStructureItem[]> = {
44
+ "my-client": [
45
+ { title: "홈", path: "/home" },
46
+ { title: "설정", path: "/settings" },
47
+ ],
48
+ };
49
+
50
+ const server = createServiceServer({
51
+ services: [AppStructureService(itemsMap)],
52
+ // ...
53
+ });
54
+ ```
@@ -0,0 +1,29 @@
1
+ # AutoUpdateService
2
+
3
+ 자동 업데이트 서비스 정의. `defineService("AutoUpdate", (ctx) => ...)`로 정의되어 있다. 인증 불필요.
4
+
5
+ ```typescript
6
+ const AutoUpdateService: ServiceDefinition;
7
+ ```
8
+
9
+ ## Members
10
+
11
+ | Method | Signature | Description |
12
+ |--------|-----------|-------------|
13
+ | `getLastVersion` | `(platform: string) => Promise<{ version: string; downloadPath: string } \| undefined>` | `{clientPath}/{platform}/updates/` 디렉토리에서 최신 버전 파일을 찾아 반환한다 |
14
+
15
+ `getLastVersion` 동작:
16
+ - `platform`이 `"android"`이면 `.apk` 파일을, 그 외에는 `.exe` 파일을 탐색한다
17
+ - 파일명이 `{version}.{ext}` 형식이어야 하며 (예: `1.2.3.apk`), `semver.maxSatisfying`으로 최대 버전을 결정한다
18
+ - `clientPath`가 없으면 에러를 던진다
19
+ - 업데이트 디렉토리나 매칭 파일이 없으면 `undefined`를 반환한다
20
+
21
+ ## Related Types
22
+
23
+ ### `AutoUpdateServiceType`
24
+
25
+ `AutoUpdateService`의 메서드 시그니처 타입.
26
+
27
+ ```typescript
28
+ type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>;
29
+ ```
@@ -0,0 +1,38 @@
1
+ # OrmService
2
+
3
+ ORM 브리지 서비스 정의. WebSocket 전용이며 `auth()`로 래핑되어 로그인이 필수다. `defineService("Orm", auth((ctx) => ...))`로 정의되어 있다.
4
+
5
+ ```typescript
6
+ const OrmService: ServiceDefinition;
7
+ ```
8
+
9
+ 소켓별 DB 연결 관리:
10
+ - `WeakMap<ServiceSocket, Map<number, DbConn>>`으로 소켓별 연결 상태를 관리한다
11
+ - 소켓이 닫히면 해당 소켓의 열린 DB 연결을 모두 자동 종료한다
12
+ - `getConfig("orm")`에서 `configName`으로 DB 연결 설정을 읽는다
13
+
14
+ HTTP 요청 시 "WebSocket 연결이 필요합니다" 에러를 던진다.
15
+
16
+ ## Members
17
+
18
+ | Method | Signature | Description |
19
+ |--------|-----------|-------------|
20
+ | `getInfo` | `(opt: DbConnOptions & { configName: string }) => Promise<{ dialect: Dialect; database?: string; schema?: string }>` | DB 연결 정보를 반환한다. `mssql-azure` dialect은 `mssql`로 변환된다 |
21
+ | `connect` | `(opt: DbConnOptions & { configName: string }) => Promise<number>` | DB에 연결하고 연결 ID를 반환한다 |
22
+ | `close` | `(connId: number) => Promise<void>` | DB 연결을 종료한다 |
23
+ | `beginTransaction` | `(connId: number, isolationLevel?: IsolationLevel) => Promise<void>` | 트랜잭션을 시작한다 |
24
+ | `commitTransaction` | `(connId: number) => Promise<void>` | 트랜잭션을 커밋한다 |
25
+ | `rollbackTransaction` | `(connId: number) => Promise<void>` | 트랜잭션을 롤백한다 |
26
+ | `executeParametrized` | `(connId: number, query: string, params?: unknown[]) => Promise<unknown[][]>` | 파라미터화된 쿼리를 실행한다 |
27
+ | `executeDefs` | `(connId: number, defs: QueryDef[], options?: (ResultMeta \| undefined)[]) => Promise<unknown[][]>` | QueryDef 배열을 SQL로 변환하여 실행한다. `options`가 모두 `undefined`이면 쿼리를 합쳐 한 번에 실행한다 |
28
+ | `bulkInsert` | `(connId: number, tableName: string, columnDefs: Record<string, ColumnMeta>, records: Record<string, unknown>[]) => Promise<void>` | 대량 삽입을 수행한다 |
29
+
30
+ ## Related Types
31
+
32
+ ### `OrmServiceType`
33
+
34
+ `OrmService`의 메서드 시그니처 타입. 클라이언트 측 타입 공유에 사용한다.
35
+
36
+ ```typescript
37
+ type OrmServiceType = ServiceMethods<typeof OrmService>;
38
+ ```
@@ -0,0 +1,33 @@
1
+ # handleHttpRequest
2
+
3
+ GET/POST `/api/:service/:method` 경로의 HTTP 요청을 처리한다. `x-sd-client-name` 헤더가 필수이며, `Authorization` 헤더가 있으면 JWT 토큰을 검증한다.
4
+
5
+ ```typescript
6
+ async function handleHttpRequest<TAuthInfo = unknown>(
7
+ req: FastifyRequest,
8
+ reply: FastifyReply,
9
+ jwtSecret: string | undefined,
10
+ runMethod: (def: {
11
+ serviceName: string;
12
+ methodName: string;
13
+ params: unknown[];
14
+ http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
15
+ }) => Promise<unknown>,
16
+ ): Promise<void>;
17
+ ```
18
+
19
+ ## Parameters
20
+
21
+ | Param | Type | Description |
22
+ |-------|------|-------------|
23
+ | `req` | `FastifyRequest` | Fastify 요청 객체 |
24
+ | `reply` | `FastifyReply` | Fastify 응답 객체 |
25
+ | `jwtSecret` | `string \| undefined` | JWT 시크릿. `Authorization` 헤더가 있는데 시크릿이 없으면 에러를 던진다 |
26
+ | `runMethod` | `(def) => Promise<unknown>` | 서비스 메서드 실행 콜백 |
27
+
28
+ 요청 매개변수 파싱:
29
+ - **GET**: `?json=` 쿼리 파라미터에서 JSON 배열을 파싱한다
30
+ - **POST**: 요청 본문이 JSON 배열이어야 한다. 배열이 아니면 400을 반환한다
31
+ - **그 외**: 405 Method Not Allowed를 반환한다
32
+
33
+ 인증 실패 시 401 응답을 반환한다.
@@ -0,0 +1,29 @@
1
+ # handleStaticFile
2
+
3
+ 정적 파일 서빙을 처리한다. 경로 탐색 공격 방지와 숨김 파일 접근 차단이 포함되어 있다.
4
+
5
+ ```typescript
6
+ async function handleStaticFile(
7
+ req: FastifyRequest,
8
+ reply: FastifyReply,
9
+ rootPath: string,
10
+ urlPath: string,
11
+ ): Promise<void>;
12
+ ```
13
+
14
+ ## Parameters
15
+
16
+ | Param | Type | Description |
17
+ |-------|------|-------------|
18
+ | `req` | `FastifyRequest` | Fastify 요청 객체 |
19
+ | `reply` | `FastifyReply` | Fastify 응답 객체 |
20
+ | `rootPath` | `string` | 서버 루트 경로. `{rootPath}/www/` 하위에서 파일을 찾는다 |
21
+ | `urlPath` | `string` | 요청 URL 경로 (슬래시 제거됨) |
22
+
23
+ 보안 처리:
24
+ - `{rootPath}/www/` 외부 경로 접근 시 에러를 던진다 (경로 탐색 공격 방지)
25
+ - `.`으로 시작하는 파일은 403 Forbidden을 반환한다
26
+
27
+ 디렉토리 처리:
28
+ - 디렉토리 요청 시 끝에 슬래시가 없으면 슬래시를 추가하여 리다이렉트한다
29
+ - 디렉토리에 대해 `index.html`을 자동으로 서빙한다
@@ -0,0 +1,33 @@
1
+ # handleUpload
2
+
3
+ `/upload` 경로의 multipart 파일 업로드를 처리한다. 인증 필수 (Authorization 헤더 필수).
4
+
5
+ ```typescript
6
+ async function handleUpload(
7
+ req: FastifyRequest,
8
+ reply: FastifyReply,
9
+ rootPath: string,
10
+ jwtSecret: string | undefined,
11
+ ): Promise<void>;
12
+ ```
13
+
14
+ ## Parameters
15
+
16
+ | Param | Type | Description |
17
+ |-------|------|-------------|
18
+ | `req` | `FastifyRequest` | Fastify 요청 객체 |
19
+ | `reply` | `FastifyReply` | Fastify 응답 객체 |
20
+ | `rootPath` | `string` | 서버 루트 경로. 파일은 `{rootPath}/www/uploads/`에 저장된다 |
21
+ | `jwtSecret` | `string \| undefined` | JWT 시크릿 |
22
+
23
+ 동작:
24
+ - 파일명은 UUID로 변환되고 원래 확장자를 유지한다
25
+ - 파일 크기 제한 초과 시 에러를 던진다
26
+ - 에러 발생 시 불완전한 파일과 이미 저장된 파일을 모두 정리한다
27
+
28
+ 응답: `ServiceUploadResult[]` (from `@simplysm/service-common`)
29
+
30
+ ```typescript
31
+ // ServiceUploadResult 구조
32
+ { path: "uploads/{uuid}.ext", filename: "원본파일명.ext", size: number }
33
+ ```
@@ -0,0 +1,64 @@
1
+ # ServiceSocket
2
+
3
+ 프로토콜 인코딩/디코딩, ping/pong 연결 유지, 이벤트 리스너 추적이 포함된 단일 WebSocket 연결 인터페이스.
4
+
5
+ ```typescript
6
+ interface ServiceSocket {
7
+ readonly connectedAtDateTime: DateTime;
8
+ readonly clientName: string;
9
+ readonly connReq: FastifyRequest;
10
+ authTokenPayload?: AuthTokenPayload;
11
+
12
+ close(): void;
13
+ send(uuid: string, msg: ServiceServerMessage): Promise<number>;
14
+ addListener(key: string, eventName: string, info: unknown): void;
15
+ removeListener(key: string): void;
16
+ getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
17
+ filterEventTargetKeys(targetKeys: string[]): string[];
18
+ on(event: "error", handler: (err: Error) => void): void;
19
+ on(event: "close", handler: (code: number) => void): void;
20
+ on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
21
+ }
22
+ ```
23
+
24
+ ## Members
25
+
26
+ | Member | Kind | Type | Description |
27
+ |--------|------|------|-------------|
28
+ | `connectedAtDateTime` | property | `DateTime` | 연결 시점의 DateTime 객체 |
29
+ | `clientName` | property | `string` | 클라이언트 앱 이름 |
30
+ | `connReq` | property | `FastifyRequest` | 연결 시점의 Fastify 요청 객체 |
31
+ | `authTokenPayload` | property | `AuthTokenPayload` (optional) | WebSocket `auth` 메시지로 검증된 토큰 페이로드 |
32
+ | `close()` | method | `void` | WebSocket 연결을 종료한다 (`socket.terminate()`) |
33
+ | `send(uuid, msg)` | method | `Promise<number>` | 메시지를 프로토콜 인코딩하여 전송한다. 전송된 바이트 수를 반환한다. 소켓이 닫혀있으면 0을 반환한다 |
34
+ | `addListener(key, eventName, info)` | method | `void` | key/name/info로 이벤트 리스너를 등록한다 |
35
+ | `removeListener(key)` | method | `void` | key로 이벤트 리스너를 제거한다 |
36
+ | `getEventListeners(eventName)` | method | `Array<{ key: string; info: unknown }>` | 특정 이벤트 이름에 해당하는 모든 리스너의 배열을 반환한다 |
37
+ | `filterEventTargetKeys(targetKeys)` | method | `string[]` | 이 소켓의 리스너에 존재하는 대상 키만 필터링하여 반환한다 |
38
+ | `on(event, handler)` | method | `void` | `"error"`, `"close"`, `"message"` 이벤트 핸들러를 등록한다 |
39
+
40
+ ping/pong: 5초 간격으로 ping을 전송하고, 클라이언트의 `0x01` 바이트(ping) 수신 시 `0x02` 바이트(pong)를 응답한다. pong 미수신 시 연결을 종료한다.
41
+
42
+ 프로토콜 메시지 처리: `createServerProtocolWrapper`를 사용하여 메시지를 인코딩/디코딩한다. 수신 메시지의 디코딩 결과가 `"progress"` 타입이면 진행률 메시지를 클라이언트에 전송한다.
43
+
44
+ ## Related Types
45
+
46
+ ### `createServiceSocket`
47
+
48
+ `ServiceSocket` 인스턴스를 생성한다.
49
+
50
+ ```typescript
51
+ function createServiceSocket(
52
+ socket: WebSocket,
53
+ clientId: string,
54
+ clientName: string,
55
+ connReq: FastifyRequest,
56
+ ): ServiceSocket;
57
+ ```
58
+
59
+ | Param | Type | Description |
60
+ |-------|------|-------------|
61
+ | `socket` | `WebSocket` | 기저 WebSocket 인스턴스 (`ws` 라이브러리) |
62
+ | `clientId` | `string` | 클라이언트 고유 식별자 |
63
+ | `clientName` | `string` | 클라이언트 앱 이름 |
64
+ | `connReq` | `FastifyRequest` | 연결 시점의 Fastify 요청 객체 |
@@ -0,0 +1,57 @@
1
+ # WebSocketHandler
2
+
3
+ 다중 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며, 이벤트 브로드캐스팅을 처리하는 인터페이스.
4
+
5
+ ```typescript
6
+ interface WebSocketHandler {
7
+ addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
8
+ closeAll(): void;
9
+ emit<TEventDef extends ServiceEventDef>(
10
+ eventName: string,
11
+ infoSelector: (item: TEventDef["$info"]) => boolean,
12
+ data: TEventDef["$data"],
13
+ ): Promise<void>;
14
+ }
15
+ ```
16
+
17
+ ## Members
18
+
19
+ | Member | Kind | Type | Description |
20
+ |--------|------|------|-------------|
21
+ | `addSocket(socket, clientId, clientName, connReq)` | method | `void` | 새 WebSocket 연결을 추가한다. 동일 `clientId`의 이전 연결이 있으면 해제한다 |
22
+ | `closeAll()` | method | `void` | 모든 활성 연결을 닫는다 |
23
+ | `emit(eventName, infoSelector, data)` | method | `Promise<void>` | `infoSelector`에 매칭되는 클라이언트에 이벤트를 브로드캐스트한다 |
24
+
25
+ 메시지 라우팅 (`processRequest` 내부):
26
+
27
+ | `message.name` | 처리 |
28
+ |-----------------|------|
29
+ | `"SvcName.methodName"` (`.` 포함) | `runMethod`로 서비스 메서드 실행 |
30
+ | `"evt:add"` | 이벤트 리스너 등록 (`key`, `name`, `info`) |
31
+ | `"evt:remove"` | 이벤트 리스너 제거 (`key`) |
32
+ | `"evt:gets"` | 전체 소켓의 특정 이벤트 리스너 조회 |
33
+ | `"evt:emit"` | 지정된 키 대상 이벤트 발송 |
34
+ | `"auth"` | JWT 토큰 검증 후 소켓에 `authTokenPayload` 저장 |
35
+
36
+ ## Related Types
37
+
38
+ ### `createWebSocketHandler`
39
+
40
+ `WebSocketHandler` 인스턴스를 생성한다.
41
+
42
+ ```typescript
43
+ function createWebSocketHandler(
44
+ runMethod: (def: {
45
+ serviceName: string;
46
+ methodName: string;
47
+ params: unknown[];
48
+ socket?: ServiceSocket;
49
+ }) => Promise<unknown>,
50
+ jwtSecret: string | undefined,
51
+ ): WebSocketHandler;
52
+ ```
53
+
54
+ | Param | Type | Description |
55
+ |-------|------|-------------|
56
+ | `runMethod` | `(def: { serviceName, methodName, params, socket? }) => Promise<unknown>` | 서비스 메서드 실행 콜백 |
57
+ | `jwtSecret` | `string \| undefined` | JWT 시크릿. `undefined`이면 `auth` 메시지 처리 시 에러를 던진다 |
@@ -1,6 +1,4 @@
1
- # Types
2
-
3
- ## `ServiceServerOptions`
1
+ # ServiceServerOptions
4
2
 
5
3
  서버 생성 시 전달하는 옵션 인터페이스.
6
4
 
@@ -19,12 +17,14 @@ interface ServiceServerOptions {
19
17
  }
20
18
  ```
21
19
 
20
+ ## Fields
21
+
22
22
  | Field | Type | Description |
23
23
  |-------|------|-------------|
24
24
  | `rootPath` | `string` | 서버 루트 경로. 정적 파일은 `{rootPath}/www/`에서 서빙되고, 설정 파일은 `{rootPath}/.config.json`에서 읽는다 |
25
25
  | `port` | `number` | 리스닝 포트 번호 |
26
26
  | `ssl` | `{ pfxBytes: Uint8Array; passphrase: string }` (optional) | HTTPS 설정. PFX 인증서 바이트와 비밀번호. 설정하지 않으면 HTTP로 동작한다 |
27
- | `auth` | `{ jwtSecret: string } \| false` (optional) | JWT 인증 설정. `{ jwtSecret }`이면 인증 활성화, `false`이면 인증 의도적 비활성화, `undefined`이면 인증 미사용 |
27
+ | `auth` | `{ jwtSecret: string } \| false` (optional) | JWT 인증 설정. 가지 상태가 있다 |
28
28
  | `services` | `ServiceDefinition[]` | 등록할 서비스 정의 배열 |
29
29
 
30
30
  `auth` 필드의 세 가지 상태:
@@ -1,6 +1,4 @@
1
- # Utils
2
-
3
- ## `getConfig`
1
+ # getConfig
4
2
 
5
3
  `.config.json` 파일을 읽고 캐싱한다. 파일 변경 시 자동 리로드되며, 캐시는 1시간 후 만료된다.
6
4
 
@@ -8,11 +6,15 @@
8
6
  async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
9
7
  ```
10
8
 
11
- | Parameter | Type | Description |
12
- |-----------|------|-------------|
9
+ ## Parameters
10
+
11
+ | Param | Type | Description |
12
+ |-------|------|-------------|
13
13
  | `filePath` | `string` | `.config.json` 파일의 절대 경로 |
14
14
 
15
- 반환값: 파싱된 설정 객체. 파일이 존재하지 않으면 `undefined`.
15
+ ## Returns
16
+
17
+ `Promise<TConfig | undefined>` — 파싱된 설정 객체. 파일이 존재하지 않으면 `undefined`.
16
18
 
17
19
  캐싱 동작:
18
20
  - `LazyGcMap`을 사용하여 캐시를 관리한다 (10분 간격 GC, 1시간 후 만료)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "14.0.49",
3
+ "version": "14.0.51",
4
4
  "description": "심플리즘 패키지 - 서비스 (server)",
5
5
  "author": "심플리즘",
6
6
  "license": "Apache-2.0",
@@ -31,11 +31,11 @@
31
31
  "semver": "^7.7.4",
32
32
  "utf-8-validate": "^6.0.6",
33
33
  "ws": "^8.20.0",
34
- "@simplysm/orm-node": "14.0.49",
35
- "@simplysm/core-node": "14.0.49",
36
- "@simplysm/orm-common": "14.0.49",
37
- "@simplysm/service-common": "14.0.49",
38
- "@simplysm/core-common": "14.0.49"
34
+ "@simplysm/service-common": "14.0.51",
35
+ "@simplysm/core-node": "14.0.51",
36
+ "@simplysm/core-common": "14.0.51",
37
+ "@simplysm/orm-common": "14.0.51",
38
+ "@simplysm/orm-node": "14.0.51"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/semver": "^7.7.1",
@@ -32,5 +32,5 @@ export async function verifyJwt<TAuthInfo = unknown>(
32
32
  }
33
33
 
34
34
  export function decodeJwt<TAuthInfo = unknown>(token: string): AuthTokenPayload<TAuthInfo> {
35
- return jose.decodeJwt(token) as AuthTokenPayload<TAuthInfo>;
35
+ return jose.decodeJwt(token);
36
36
  }
@@ -37,7 +37,7 @@ export const OrmService = defineService(
37
37
  if (config == null) {
38
38
  throw new Error(`ORM 설정을 찾을 수 없습니다: ${opt.configName}`);
39
39
  }
40
- return { ...config, ...opt.config } as DbConnConfig;
40
+ return { ...config, ...opt.config };
41
41
  };
42
42
 
43
43
  const getConn = (connId: number): DbConn => {
@@ -80,21 +80,21 @@ export function createWebSocketHandler(
80
80
 
81
81
  return await serviceSocket.send(uuid, { name: "response", body: result });
82
82
  } else if (message.name === "evt:add") {
83
- const { key, name, info } = message.body as { key: string; name: string; info: unknown };
83
+ const { key, name, info } = message.body;
84
84
  serviceSocket.addListener(key, name, info);
85
85
  return await serviceSocket.send(uuid, { name: "response" });
86
86
  } else if (message.name === "evt:remove") {
87
- const { key } = message.body as { key: string };
87
+ const { key } = message.body;
88
88
  serviceSocket.removeListener(key);
89
89
  return await serviceSocket.send(uuid, { name: "response" });
90
90
  } else if (message.name === "evt:gets") {
91
- const { name } = message.body as { name: string };
91
+ const { name } = message.body;
92
92
  const infos = Array.from(socketMap.values()).flatMap((subSock) =>
93
93
  subSock.getEventListeners(name),
94
94
  );
95
95
  return await serviceSocket.send(uuid, { name: "response", body: infos });
96
96
  } else if (message.name === "evt:emit") {
97
- const { keys, data } = message.body as { keys: string[]; data: unknown };
97
+ const { keys, data } = message.body;
98
98
 
99
99
  await Promise.allSettled(
100
100
  Array.from(socketMap.values()).map(async (subSock) => {