@simplysm/sd-claude 14.0.88 → 14.0.89
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/claude/references/sd-simplysm14/README.md +17 -17
- package/claude/references/sd-simplysm14/apis/angular/README.md +27 -53
- package/claude/references/sd-simplysm14/apis/angular/controls.md +37 -105
- package/claude/references/sd-simplysm14/apis/angular/crud.md +46 -43
- package/claude/references/sd-simplysm14/apis/angular/directives.md +22 -32
- package/claude/references/sd-simplysm14/apis/angular/features.md +40 -55
- package/claude/references/sd-simplysm14/apis/angular/infra.md +40 -40
- package/claude/references/sd-simplysm14/apis/angular/layout.md +25 -53
- package/claude/references/sd-simplysm14/apis/angular/overlay.md +70 -82
- package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +44 -39
- package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +21 -36
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +52 -65
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +65 -70
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +33 -35
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +7 -7
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +29 -29
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +45 -50
- package/claude/references/sd-simplysm14/apis/core-browser/README.md +42 -55
- package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +62 -0
- package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +13 -12
- package/claude/references/sd-simplysm14/apis/core-common/README.md +222 -98
- package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +102 -53
- package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +128 -0
- package/claude/references/sd-simplysm14/apis/core-common/datetime.md +98 -64
- package/claude/references/sd-simplysm14/apis/core-common/errors.md +91 -0
- package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +34 -28
- package/claude/references/sd-simplysm14/apis/core-common/obj.md +104 -40
- package/claude/references/sd-simplysm14/apis/core-node/README.md +11 -8
- package/claude/references/sd-simplysm14/apis/core-node/consola.md +23 -31
- package/claude/references/sd-simplysm14/apis/core-node/cpx.md +33 -22
- package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +28 -25
- package/claude/references/sd-simplysm14/apis/core-node/fsx.md +39 -53
- package/claude/references/sd-simplysm14/apis/core-node/pathx.md +26 -29
- package/claude/references/sd-simplysm14/apis/core-node/worker.md +27 -29
- package/claude/references/sd-simplysm14/apis/excel/README.md +14 -14
- package/claude/references/sd-simplysm14/apis/lint/README.md +27 -21
- package/claude/references/sd-simplysm14/apis/lint/rules.md +89 -49
- package/claude/references/sd-simplysm14/apis/orm-common/README.md +5 -59
- package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +98 -67
- package/claude/references/sd-simplysm14/apis/orm-common/expr.md +107 -92
- package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +99 -65
- package/claude/references/sd-simplysm14/apis/orm-common/schema.md +83 -98
- package/claude/references/sd-simplysm14/apis/orm-common/types.md +62 -52
- package/claude/references/sd-simplysm14/apis/orm-node/README.md +62 -25
- package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +27 -27
- package/claude/references/sd-simplysm14/apis/sd-cli/README.md +12 -15
- package/claude/references/sd-simplysm14/apis/sd-cli/SdTsCompiler.md +92 -45
- package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +226 -108
- package/claude/references/sd-simplysm14/apis/service-client/README.md +84 -86
- package/claude/references/sd-simplysm14/apis/service-client/orm.md +14 -11
- package/claude/references/sd-simplysm14/apis/service-client/transport.md +33 -10
- package/claude/references/sd-simplysm14/apis/service-common/README.md +37 -23
- package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +9 -9
- package/claude/references/sd-simplysm14/apis/service-common/protocol.md +13 -13
- package/claude/references/sd-simplysm14/apis/service-server/README.md +81 -65
- package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +32 -35
- package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +44 -33
- package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +34 -45
- package/claude/references/sd-simplysm14/apis/storage/README.md +24 -18
- package/claude/skills/sd-demo/SKILL.md +6 -0
- package/claude/skills/sd-impl/SKILL.md +4 -7
- package/claude/skills/sd-spec/SKILL.md +31 -858
- package/claude/skills/sd-spec/references/spec-authoring.md +519 -0
- package/claude/workflows/sd-docs.js +84 -0
- package/package.json +1 -1
- package/claude/references/sd-simplysm14/apis/orm-common/query-builder.md +0 -29
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/.specs/inventory/spec.md +0 -99
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/package.json +0 -12
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/index.ts +0 -3
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inbound/inbound.list.ts +0 -150
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inventory/inventory-master.list.ts +0 -143
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/outbound/outbound.list.ts +0 -150
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/pnpm-workspace.yaml +0 -2
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/sd.config.ts +0 -12
- package/claude/skills/sd-demo/evals/golden.jsonl +0 -1
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/package.json +0 -8
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/src/.gitkeep +0 -0
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tests/.gitkeep +0 -0
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tsconfig.json +0 -10
- package/claude/skills/sd-dev/evals/golden.jsonl +0 -1
- package/claude/skills/sd-docs/SKILL.md +0 -46
- package/claude/skills/sd-docs/evals/fixtures/new-write/.claude/references/sd-simplysm14/README.md +0 -7
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/src/index.ts +0 -3
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/package.json +0 -6
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/src/index.ts +0 -1
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/src/index.ts +0 -8
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/README.md +0 -7
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/apis/foo/README.md +0 -3
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/src/index.ts +0 -3
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/package.json +0 -6
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/src/index.ts +0 -1
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/src/index.ts +0 -8
- package/claude/skills/sd-docs/evals/golden.jsonl +0 -2
- package/claude/skills/sd-impl/evals/fixtures/case-a-new-screen/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/packages/app/src/screens/box-register/box-register.view.ts +0 -46
- package/claude/skills/sd-impl/evals/fixtures/case-c-new-cross/.specs/260513120000_warehouse/spec.md +0 -89
- package/claude/skills/sd-impl/evals/fixtures/case-d-spec-modify/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/golden.jsonl +0 -4
- package/claude/skills/sd-manual/evals/fixtures/new-manual/src/notification.ts +0 -25
- package/claude/skills/sd-manual/evals/fixtures/update-manual/.claude/references/sd-simplysm14/manuals/notification.md +0 -14
- package/claude/skills/sd-manual/evals/fixtures/update-manual/src/notification.ts +0 -37
- package/claude/skills/sd-manual/evals/golden.jsonl +0 -2
- package/claude/skills/sd-review/evals/fixtures/code-review/src/foo.ts +0 -7
- package/claude/skills/sd-review/evals/fixtures/doc-review/docs/foo.md +0 -4
- package/claude/skills/sd-review/evals/golden.jsonl +0 -2
- package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +0 -14
- package/claude/skills/sd-skill/evals/fixtures/new-skill/.gitkeep +0 -0
- package/claude/skills/sd-skill/evals/golden.jsonl +0 -2
- package/claude/skills/sd-spec/evals/fixtures/case-a-split//355/232/214/354/235/230/353/241/235.md +0 -20
- package/claude/skills/sd-spec/evals/fixtures/case-b-detail/.specs/260513120000_warehouse/spec.md +0 -95
- package/claude/skills/sd-spec/evals/golden.jsonl +0 -2
- package/claude/skills/sd-unpack/evals/fixtures/eml-with-text-attachment/meeting.eml +0 -21
- package/claude/skills/sd-unpack/evals/fixtures/simple-eml/meeting.eml +0 -10
- package/claude/skills/sd-unpack/evals/golden.jsonl +0 -2
- package/claude/skills/sd-use/evals/fixtures/empty/.gitkeep +0 -0
- package/claude/skills/sd-use/evals/golden.jsonl +0 -6
- /package/claude/{skills/sd-docs/references/doc-rules.md → workflows/sd-docs.rules.md} +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# @simplysm/service-common — protocol
|
|
2
2
|
|
|
3
|
-
서버·클라이언트 간 서비스 메시지의 바이너리 인코딩/디코딩과 청크 재조립을 담당하는 프로토콜(V2). 헤더 28바이트(UUID 16 + TotalSize 8 + Index 4) + JSON
|
|
3
|
+
서버·클라이언트 간 서비스 메시지의 바이너리 인코딩/디코딩과 청크 재조립을 담당하는 프로토콜(V2). 헤더 28바이트(UUID 16 + TotalSize 8 + Index 4) + JSON 본문 구조이며, 3MB 초과 시 300KB 청크로 자동 분할, 단일 메시지 최대 100MB.
|
|
4
4
|
|
|
5
5
|
## createServiceProtocol / ServiceProtocol
|
|
6
6
|
|
|
7
|
-
`createServiceProtocol(): ServiceProtocol` — stateful 청크
|
|
7
|
+
`createServiceProtocol(): ServiceProtocol` — stateful 청크 누적기(`LazyGcMap`)를 내장한 프로토콜 인스턴스 생성. 누적기는 GC 타이머를 가지므로 사용 종료 시 `dispose()` 필수.
|
|
8
8
|
|
|
9
9
|
`ServiceProtocol` 메서드:
|
|
10
10
|
|
|
11
|
-
- `encode(uuid, message): { chunks: Bytes[]; totalSize: number }` — 메시지를 `[name, body]` JSON→바이트로 직렬화 후 헤더 부착. `SPLIT_MESSAGE_SIZE`(3MB) 이하면 단일 청크, 초과면 `CHUNK_SIZE`(300KB)
|
|
12
|
-
- `accumulate(bytes): ServiceAccumulateResult` — 수신 청크 1개를 uuid별 누적기에 모음(stateful, 재조립 전용). 같은 index 중복 패킷은 무시. JSON 파싱은
|
|
13
|
-
- `parseMessage(resultBytes): ServiceMessage` — 재조립된 raw 바이트를 메시지 객체로 파싱(stateless). 누적
|
|
14
|
-
- `decode<T>(bytes): ServiceMessageDecodeResult<T>` — `accumulate` 후 완료 시 `parseMessage` 까지 수행하는 통합 동작. 가장 일반적인 수신 처리 경로.
|
|
11
|
+
- `encode(uuid: string, message: ServiceMessage): { chunks: Bytes[]; totalSize: number }` — 메시지를 `[name, body]` JSON→바이트로 직렬화 후 28바이트 헤더 부착. `SPLIT_MESSAGE_SIZE`(3MB) 이하면 단일 청크, 초과면 `CHUNK_SIZE`(300KB) 단위로 분할해 여러 청크. `MAX_TOTAL_SIZE`(100MB) 초과 시 `ArgumentError` throw. `uuid`=메시지 묶음 식별자(재조립 키).
|
|
12
|
+
- `accumulate(bytes: Bytes): ServiceAccumulateResult` — 수신 청크 1개를 uuid별 누적기에 모음(stateful, 재조립 전용). 같은 index 중복 패킷은 무시. JSON 파싱은 하지 않음. 미완성이면 `progress`, 전 청크 도착 시 raw 바이트 담은 `complete` 반환. 헤더 미만(<28B)·크기 초과·무결성 위반(completedSize > totalSize) 시 `ArgumentError` throw.
|
|
13
|
+
- `parseMessage(resultBytes: Bytes): ServiceMessage` — 재조립된 raw 바이트를 메시지 객체로 파싱(stateless). 누적 상태에 비의존이라 worker 등 다른 실행 컨텍스트에 위임 가능. 파싱 실패 시 `ArgumentError` throw.
|
|
14
|
+
- `decode<T extends ServiceMessage>(bytes: Bytes): ServiceMessageDecodeResult<T>` — `accumulate` 후 완료 시 `parseMessage` 까지 수행하는 통합 동작. 가장 일반적인 수신 처리 경로.
|
|
15
15
|
- `dispose(): void` — 내부 누적기 GC 타이머 해제·메모리 반환. 인스턴스 폐기 전 반드시 호출.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
@@ -24,8 +24,8 @@ try {
|
|
|
24
24
|
} finally { proto.dispose(); }
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`ServiceMessageDecodeResult<
|
|
28
|
-
- `{ type: "complete"; uuid; message:
|
|
27
|
+
`ServiceMessageDecodeResult<TMessage>` (유니언, `type` 판별):
|
|
28
|
+
- `{ type: "complete"; uuid; message: TMessage }` — 전 청크 수신, 메시지 재조립·파싱 완료.
|
|
29
29
|
- `{ type: "progress"; uuid; totalSize; completedSize }` — 일부 청크만 도착. 진행률 표시용.
|
|
30
30
|
|
|
31
31
|
`ServiceAccumulateResult` (유니언, `type` 판별):
|
|
@@ -43,9 +43,9 @@ try {
|
|
|
43
43
|
|
|
44
44
|
- `MAX_TOTAL_SIZE: 100MB` — 단일 메시지 허용 최대 크기. 초과 시 `encode`/`accumulate` throw.
|
|
45
45
|
- `SPLIT_MESSAGE_SIZE: 3MB` — 이 값 초과 시 청킹 시작(이하면 단일 청크).
|
|
46
|
-
- `CHUNK_SIZE: 300KB` — 분할 청크 1
|
|
46
|
+
- `CHUNK_SIZE: 300KB` — 분할 청크 1개의 본문 크기.
|
|
47
47
|
- `GC_INTERVAL: 10초` — 미완성 누적기 정리 주기.
|
|
48
|
-
- `EXPIRE_TIME: 60초` — 미완성 메시지 만료
|
|
48
|
+
- `EXPIRE_TIME: 60초` — 미완성 메시지 만료 시간(이후 GC 대상).
|
|
49
49
|
|
|
50
50
|
## 메시지 타입
|
|
51
51
|
|
|
@@ -61,9 +61,9 @@ try {
|
|
|
61
61
|
- `ServiceProgressMessage` `"progress"` — 서버가 청크 수신 진행 알림. `body: { totalSize, completedSize }`(바이트).
|
|
62
62
|
- `ServiceErrorMessage` `"error"` — 서버 에러 알림. `body: { name, message, code, stack?, detail?, cause? }`.
|
|
63
63
|
- `ServiceAuthMessage` `"auth"` — 클라이언트 인증. `body: string`(토큰).
|
|
64
|
-
- `ServiceRequestMessage` `` `${string}.${string}` `` — 클라이언트 서비스 메서드 호출(`service.method`). `body: unknown[]`(매개변수).
|
|
65
|
-
- `ServiceResponseMessage` `"response"` — 서버 응답. `body?: unknown`(
|
|
66
|
-
- `ServiceAddEventListenerMessage` `"evt:add"` — 리스너 등록. `body: { key, name, info }` — `key`=리스너 키(uuid, 제거에 필요), `name`=이벤트 이름, `info`=발생 필터링용 정보.
|
|
64
|
+
- `ServiceRequestMessage` `` `${string}.${string}` `` — 클라이언트 서비스 메서드 호출(`service.method`). `body: unknown[]`(매개변수 배열).
|
|
65
|
+
- `ServiceResponseMessage` `"response"` — 서버 응답. `body?: unknown`(결과, 없을 수 있어 optional).
|
|
66
|
+
- `ServiceAddEventListenerMessage` `"evt:add"` — 리스너 등록. `body: { key, name, info }` — `key`=리스너 키(uuid, 제거에 필요), `name`=이벤트 이름, `info`=발생 시 필터링용 정보.
|
|
67
67
|
- `ServiceRemoveEventListenerMessage` `"evt:remove"` — 리스너 제거. `body: { key }`(리스너 키).
|
|
68
68
|
- `ServiceGetEventListenerInfosMessage` `"evt:gets"` — 특정 이벤트 리스너 info 목록 요청. `body: { name }`(이벤트 이름).
|
|
69
69
|
- `ServiceEmitEventMessage` `"evt:emit"` — 클라이언트가 이벤트 발생 요청. `body: { keys, data }` — `keys`=대상 리스너 키 목록, `data`=데이터.
|
|
@@ -1,102 +1,118 @@
|
|
|
1
1
|
# @simplysm/service-server
|
|
2
2
|
|
|
3
|
-
Fastify 기반 서비스 서버. WebSocket
|
|
3
|
+
Fastify 기반 서비스 서버. WebSocket/HTTP 두 전송 계층으로 RPC 스타일 서비스 메서드를 노출하고, JWT 인증·정적 파일·업로드·이벤트 브로드캐스팅·내장 ORM/자동업데이트 서비스를 제공한다.
|
|
4
4
|
|
|
5
5
|
## 사용 트리거 인덱스
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
-
|
|
9
|
-
- **
|
|
10
|
-
-
|
|
11
|
-
- **
|
|
12
|
-
-
|
|
13
|
-
- **V1 레거시 (handleV1Connection
|
|
7
|
+
- **ServiceServer / createServiceServer / ServiceServerOptions** — 서버 인스턴스를 만들고 listen/close 할 때, 포트·SSL·auth·서비스 목록을 설정할 때. (아래 "서버 인스턴스" 인라인)
|
|
8
|
+
- **이벤트 브로드캐스트 (getEvent / emitEvent / ServerEventProxy)** — 서버에서 WebSocket 클라이언트들에게 이벤트를 푸시할 때. (아래 "이벤트 브로드캐스트" 인라인)
|
|
9
|
+
- **JWT 인증 (signAuthToken/verifyAuthToken, signJwt/verifyJwt/decodeJwt, AuthTokenPayload)** — 로그인 토큰을 발급·검증할 때. (아래 "JWT 인증" 인라인)
|
|
10
|
+
- **서비스 정의 (defineService / auth / ServiceContext / ServiceDefinition / ServiceMethods)** — 서버에 노출할 RPC 서비스를 작성하고 인증·권한을 거는 작업 컨텍스트. 자세히: [service-authoring.md](./service-authoring.md)
|
|
11
|
+
- **내장 서비스 (OrmService / AutoUpdateService)** — DB 원격 실행·앱 자동업데이트를 services 목록에 바로 꽂을 때. (아래 "내장 서비스" 인라인)
|
|
12
|
+
- **전송 계층 내부 (WebSocketHandler / ServiceSocket / HTTP·업로드·정적 핸들러 / 프로토콜 래퍼 / ConfigManager)** — 서버 내부 동작을 이해하거나 커스텀 통합할 때. 자세히: [transport-internals.md](./transport-internals.md)
|
|
13
|
+
- **V1 레거시 자동업데이트 (handleV1Connection 등)** — 구버전(ver≠2) 클라이언트를 지원해야 할 때. 자세히: [v1-legacy.md](./v1-legacy.md)
|
|
14
14
|
|
|
15
|
-
## 서버
|
|
15
|
+
## 서버 인스턴스
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```ts
|
|
18
|
+
class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }>
|
|
19
|
+
function createServiceServer<TAuthInfo = unknown>(options: ServiceServerOptions): ServiceServer<TAuthInfo>
|
|
20
|
+
```
|
|
18
21
|
|
|
19
|
-
`
|
|
22
|
+
`createServiceServer` 는 `new ServiceServer` 의 얇은 래퍼. `TAuthInfo` 는 인증 토큰 `data` 필드의 타입(`ctx.authInfo` 와 토큰 발급/검증에 전파됨).
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
- `isOpen: boolean` — listen 성공 후 true, close 후 false. 가동 여부 판단에 사용.
|
|
23
|
-
- `fastify: FastifyInstance` — 내부 Fastify 인스턴스. 임의 포트 조회(`fastify.server.address()`)·직접 라우트 추가에 사용.
|
|
24
|
-
- `listen(): Promise<void>` — 플러그인 등록 후 `0.0.0.0:options.port` 수신 시작. 완료 시 `ready` emit. `auth` 미설정인데 auth 요구 서비스가 있으면 throw. listen 중 SIGINT/SIGTERM graceful shutdown(10초 타임아웃 후 강제 종료) 1회 등록.
|
|
25
|
-
- `close(): Promise<void>` — 모든 WebSocket 연결을 닫고 Fastify 종료. `close` emit.
|
|
26
|
-
- `getEvent<TEventDef>(eventDef): ServerEventProxy<TEventDef>` — eventDef 를 바인딩한 emit 전용 프록시 획득. 같은 이벤트를 반복 emit 할 때 편의.
|
|
27
|
-
- `emitEvent<TEventDef>(eventDef, infoSelector, data): Promise<void>` — `infoSelector(info) === true` 인 리스너에게만 `data` 전송. `infoSelector` = 리스너 등록 시의 info 로 수신 대상 선별.
|
|
28
|
-
- `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>` — JWT 발급. `auth` 미설정 시 throw. 로그인 처리에 사용.
|
|
29
|
-
- `verifyAuthToken(token): Promise<AuthTokenPayload<TAuthInfo>>` — JWT 검증·페이로드 반환. `auth` 미설정 시 throw.
|
|
24
|
+
`ServiceServerOptions`:
|
|
30
25
|
|
|
31
|
-
`
|
|
26
|
+
- `rootPath: string` — 서버 루트 디렉토리. 정적 파일은 `<rootPath>/www`, 업로드는 `<rootPath>/www/uploads`, 설정은 `<rootPath>/.config.json` 및 `<rootPath>/www/<clientName>/.config.json` 에서 읽음.
|
|
27
|
+
- `port: number` — 리슨 포트. host 는 항상 `0.0.0.0`. `0` 이면 OS 가 임의 포트 배정(테스트용).
|
|
28
|
+
- `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — HTTPS 인증서. 지정 시 HTTPS 구동 + HSTS·crossOriginOpenerPolicy 활성, 미지정 시 HTTP 구동 + `upgrade-insecure-requests` CSP 해제. PFX 형식 인증서만 지원.
|
|
29
|
+
- `auth?: { jwtSecret: string } | false` — 인증 모드. `{ jwtSecret }` = JWT 검증 활성, `false` = auth 요구 서비스가 있어도 인증 검사 스킵(의도적 비활성화), 미지정(undefined) = auth 요구 서비스가 하나라도 있으면 `listen()` 시 throw.
|
|
30
|
+
- `services: ServiceDefinition[]` — 노출할 서비스 목록. `defineService` 결과를 나열.
|
|
31
|
+
- `legacyV1Handlers?: V1RequestHandler[]` — V1 레거시 클라이언트용 커스텀 요청 핸들러. 자세히: [v1-legacy.md](./v1-legacy.md).
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
메서드:
|
|
34
|
+
|
|
35
|
+
- `listen(): Promise<void>` — Fastify 플러그인(websocket/helmet/multipart/static/cors) 등록 후 리슨 시작. auth 미설정인데 auth 요구 서비스가 있으면 throw. SIGINT/SIGTERM graceful shutdown 핸들러 등록(10초 내 미종료 시 강제 종료). 완료 시 `isOpen=true` + `ready` 이벤트 발생.
|
|
36
|
+
- `close(): Promise<void>` — 모든 WebSocket 연결 종료 + Fastify 종료. `isOpen=false` + `close` 이벤트 발생.
|
|
37
|
+
- `isOpen: boolean` — 현재 리슨 중 여부.
|
|
38
|
+
- `fastify: FastifyInstance` — 내부 Fastify 인스턴스(예: `fastify.server.address()` 로 실제 포트 조회).
|
|
39
|
+
- `options: ServiceServerOptions` — 생성 시 전달한 옵션(읽기 전용 참조).
|
|
34
40
|
|
|
35
41
|
```ts
|
|
36
|
-
const server = createServiceServer<
|
|
37
|
-
rootPath:
|
|
42
|
+
const server = createServiceServer<MyAuthInfo>({
|
|
43
|
+
rootPath: import.meta.dirname,
|
|
38
44
|
port: 50080,
|
|
39
|
-
auth: { jwtSecret:
|
|
40
|
-
services: [OrmService, AutoUpdateService
|
|
45
|
+
auth: { jwtSecret: "secret" },
|
|
46
|
+
services: [MyService, OrmService, AutoUpdateService],
|
|
41
47
|
});
|
|
42
48
|
await server.listen();
|
|
43
|
-
const token = await server.signAuthToken({ roles: ["admin"], data: authInfo });
|
|
44
|
-
await server.getEvent(MyEventDef).emit((info) => info.room === "lobby", payload);
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
- `rootPath: string` — 서버 루트. 정적파일은 `<rootPath>/www`, 업로드는 `<rootPath>/www/uploads`, 설정은 `<rootPath>/.config.json` 및 `www/<client>/.config.json` 기준.
|
|
50
|
-
- `port: number` — 수신 포트. `0` 이면 OS가 임의 할당(테스트용, `fastify.server.address()` 로 확인).
|
|
51
|
-
- `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — HTTPS 설정. `pfxBytes` = PFX 인증서 바이트, `passphrase` = 암호. 설정 시 HSTS·COOP 켜짐, 미설정 시 HTTP(`upgrade-insecure-requests` 해제).
|
|
52
|
-
- `auth?: { jwtSecret: string } | false` — 인증 모드. `{ jwtSecret }` = JWT 활성(서명/검증 키), `false` = auth 요구 서비스를 두되 인증 검사를 의도적으로 스킵, `undefined`(미지정) = 인증 비활성이되 auth 요구 서비스가 있으면 `listen()` 에서 throw.
|
|
53
|
-
- `services: ServiceDefinition[]` — 등록할 서비스 목록(`defineService` 산출물).
|
|
54
|
-
- `legacyV1Handlers?: V1RequestHandler[]` — ver≠2 구버전 클라이언트용 커스텀 핸들러. 미지정 시 빈 배열.
|
|
51
|
+
## 이벤트 브로드캐스트
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
```ts
|
|
54
|
+
interface ServerEventProxy<TEventDef extends ServiceEventDef> {
|
|
55
|
+
emit(infoSelector: (item: TEventDef["$info"]) => boolean, data: TEventDef["$data"]): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
server.getEvent<TEventDef>(eventDef: TEventDef): ServerEventProxy<TEventDef>
|
|
58
|
+
server.emitEvent<TEventDef>(eventDef, infoSelector, data): Promise<void>
|
|
59
|
+
```
|
|
57
60
|
|
|
58
|
-
`
|
|
61
|
+
`ServiceEventDef` 는 `@simplysm/service-common` 의 이벤트 정의 타입(`eventName`/`$info`/`$data` 보유). 클라이언트는 이벤트 리스너 등록 시 `info` 를 같이 보내고, 서버는 등록된 모든 소켓의 리스너 중 `infoSelector` 가 true 인 대상에게만 `data` 를 푸시한다.
|
|
59
62
|
|
|
60
|
-
`
|
|
63
|
+
- `infoSelector: (item) => boolean` — 수신 대상 필터. 등록된 각 리스너의 `info` 를 받아 전송 여부를 결정. 특정 조건(예: 같은 화면을 보는 클라이언트)에만 보낼 때 사용.
|
|
64
|
+
- `getEvent` 는 `emit` 만 노출하는 프록시를 반환(내부적으로 `emitEvent` 호출) — 같은 eventDef 로 여러 번 emit 할 때 편함.
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
```ts
|
|
67
|
+
const evt = server.getEvent(MyDataChangedEvent);
|
|
68
|
+
await evt.emit((info) => info.boardId === 3, { updatedAt: new Date() });
|
|
69
|
+
```
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
- `verifyJwt<TAuthInfo>(jwtSecret, token): Promise<AuthTokenPayload<TAuthInfo>>` — 검증·디코드. 만료 시 "토큰이 만료되었습니다.", 그 외 실패 시 "유효하지 않은 토큰입니다." throw.
|
|
67
|
-
- `decodeJwt<TAuthInfo>(token): AuthTokenPayload<TAuthInfo>` — 서명 검증 없이 페이로드만 디코드.
|
|
71
|
+
## JWT 인증
|
|
68
72
|
|
|
69
73
|
```ts
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
|
|
75
|
+
roles: string[];
|
|
76
|
+
data: TAuthInfo;
|
|
77
|
+
}
|
|
78
|
+
server.signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>
|
|
79
|
+
server.verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>
|
|
80
|
+
|
|
81
|
+
function signJwt<T>(jwtSecret: string, payload: AuthTokenPayload<T>): Promise<string>
|
|
82
|
+
function verifyJwt<T>(jwtSecret: string, token: string): Promise<AuthTokenPayload<T>>
|
|
83
|
+
function decodeJwt<T>(token: string): AuthTokenPayload<T>
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
- `AuthTokenPayload.roles: string[]` — 보유 역할 목록. `auth(["admin"], ...)` 권한 검사 시 이 배열에 해당 권한이 포함되는지 확인.
|
|
87
|
+
- `AuthTokenPayload.data: TAuthInfo` — 임의 사용자 정보. 서비스 메서드에서 `ctx.authInfo` 로 읽힘.
|
|
88
|
+
- `signAuthToken`/`verifyAuthToken` — 서버 옵션의 `jwtSecret` 을 자동 사용하는 인스턴스 메서드. jwtSecret 미설정 시 throw.
|
|
89
|
+
- `signJwt` — HS256, 발급시각 자동 설정, **만료 12시간 고정**. secret 은 UTF-8 로 인코딩됨.
|
|
90
|
+
- `verifyJwt` — 검증 실패 시 만료면 `"토큰이 만료되었습니다."`, 그 외엔 `"유효하지 않은 토큰입니다."` throw(jose 에러 코드 `ERR_JWT_EXPIRED` 로 만료 여부 구분).
|
|
91
|
+
- `decodeJwt` — **서명 검증 없이** 페이로드만 디코드. 신뢰할 수 없는 토큰 검증 용도로는 쓰지 말 것.
|
|
92
|
+
|
|
93
|
+
## 내장 서비스
|
|
75
94
|
|
|
76
|
-
`services`
|
|
95
|
+
`defineService` 결과 상수. `services` 목록에 그대로 추가해 사용. 둘 다 이름 별칭(`["Orm","SdOrmService"]`, `["AutoUpdate","SdAutoUpdateService"]`)을 가져 신·구 클라이언트 모두 호출 가능.
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
### OrmService / OrmServiceType
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `commitTransaction(connId)` / `rollbackTransaction(connId): Promise<void>` — 커밋/롤백.
|
|
85
|
-
- `executeParametrized(connId, query, params?): Promise<unknown[][]>` — 파라미터 바인딩 SQL 실행.
|
|
86
|
-
- `executeDefs(connId, defs, options?): Promise<unknown[][]>` — 쿼리 정의 배열 실행. `options` 가 모두 null 이면 묶음 실행 후 빈 결과, 아니면 정의별 결과셋을 `options[i]`(ResultMeta) 로 파싱.
|
|
87
|
-
- `bulkInsert(connId, tableName, columnDefs, records): Promise<void>` — 대량 삽입. `columnDefs` = 컬럼별 메타, `records` = 행 객체 배열.
|
|
88
|
-
- `type OrmServiceType = ServiceMethods<typeof OrmService>` — 클라이언트 타입 공유용.
|
|
99
|
+
```ts
|
|
100
|
+
export const OrmService: ServiceDefinition
|
|
101
|
+
export type OrmServiceType = ServiceMethods<typeof OrmService>
|
|
102
|
+
```
|
|
89
103
|
|
|
90
|
-
`
|
|
104
|
+
`auth()` 로 래핑됨(로그인 필요). **WebSocket 전용** — HTTP 호출 시 throw(연결 ID 상태를 소켓에 묶어 관리하기 때문). DB 설정은 `ctx.getConfig("orm")` 의 `<configName>` 키에서 읽음. 소켓 종료 시 해당 소켓의 모든 열린 DB 연결을 자동 정리. 메서드: `getInfo`/`connect`(연결 ID 반환)/`close`/`beginTransaction`(`isolationLevel?`)/`commitTransaction`/`rollbackTransaction`/`executeParametrized`/`executeDefs`/`bulkInsert`. `dialect` 가 `"mssql-azure"` 면 `"mssql"` 로 정규화해 응답.
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
- `type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>` — 클라이언트 타입 공유용.
|
|
106
|
+
### AutoUpdateService / AutoUpdateServiceType
|
|
94
107
|
|
|
95
108
|
```ts
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
export const AutoUpdateService: ServiceDefinition
|
|
110
|
+
export type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>
|
|
98
111
|
```
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
인증 불필요. `getLastVersion(platform: string)` — `<clientPath>/<platform>/updates/` 에서 최신 버전 파일을 semver 로 골라 `{ version, downloadPath }` 반환, 없으면 undefined. `platform === "android"` 면 `.apk`, 그 외엔 `.exe` 파일만 후보(파일명이 버전 숫자 패턴 `^[0-9.]*$` 여야 함). `downloadPath` 는 `/` 로 시작하는 POSIX 경로.
|
|
101
114
|
|
|
102
|
-
|
|
115
|
+
```ts
|
|
116
|
+
client.getService<OrmServiceType>("Orm");
|
|
117
|
+
client.getService<AutoUpdateServiceType>("AutoUpdate");
|
|
118
|
+
```
|
|
@@ -4,71 +4,68 @@ RPC 서비스(클라이언트가 원격 호출할 메서드 묶음)를 정의하
|
|
|
4
4
|
|
|
5
5
|
## defineService
|
|
6
6
|
|
|
7
|
-
`defineService<TMethods>(name: string | string[], factory: (ctx: ServiceContext) => TMethods): ServiceDefinition<TMethods>` —
|
|
7
|
+
`defineService<TMethods>(name: string | string[], factory: (ctx: ServiceContext) => TMethods): ServiceDefinition<TMethods>` — 서비스 정의 생성.
|
|
8
8
|
|
|
9
|
-
- `name
|
|
10
|
-
- `factory
|
|
9
|
+
- `name` — 서비스 식별 이름. 문자열 1개 또는 배열(별칭 다중 등록, 첫 원소가 primary). 빈 배열이면 throw. 클라이언트는 `"<name>.<method>"` 형태로 호출.
|
|
10
|
+
- `factory` — 호출마다 `ctx`(요청 컨텍스트)를 받아 메서드 객체를 반환하는 함수. 요청별로 매번 호출되므로 요청 스코프 상태를 여기 둔다. 인스턴스 간 공유 상태는 팩토리 외부에 둘 것(예: `OrmService` 의 `WeakMap`).
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
|
|
13
|
+
const HealthService = defineService("Health", (ctx) => ({
|
|
14
14
|
check: () => ({ status: "ok" }),
|
|
15
15
|
}));
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
팩토리 전체를 `auth(...)` 로 감싸면 정의의 `authPermissions` 가 채워져 서비스 전 메서드에 인증이 강제된다(`getServiceAuthPermissions` 로 추출).
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
## auth
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
- `names: string[]` — 호출 매칭에 쓰이는 전체 이름 목록(별칭 포함).
|
|
24
|
-
- `factory: (ctx: ServiceContext) => TMethods` — 메서드 생성 팩토리.
|
|
25
|
-
- `authPermissions?: string[]` — 서비스 수준 인증 권한. 팩토리를 `auth` 로 감쌌을 때만 존재(빈 배열 = 로그인만 요구, undefined = 인증 없음).
|
|
22
|
+
메서드 또는 팩토리를 감싸 인증·권한을 부여하는 래퍼. 호출 동작은 그대로 유지하고 권한 메타데이터만 부착한다.
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
- `auth(fn)` — 권한 배열 없이 감쌈. 로그인만 필요(역할 무관).
|
|
25
|
+
- `auth(permissions: string[], fn)` — 지정 역할 중 하나라도 토큰 `roles` 에 있어야 통과. 빈 배열은 로그인만 요구하는 것과 동일.
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
적용 수준 두 가지(둘 다 같은 함수):
|
|
30
28
|
|
|
31
|
-
- `auth
|
|
32
|
-
- `auth
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
- 서비스 수준: `auth((ctx) => ({ ... }))` 또는 `auth(["admin"], (ctx) => ({ ... }))` — 모든 메서드에 적용.
|
|
30
|
+
- 메서드 수준: 객체 안에서 `someMethod: auth(() => result)` 또는 `auth(["admin"], () => result)` — 그 메서드만.
|
|
31
|
+
|
|
32
|
+
권한 해석 우선순위(`executeServiceMethod`): 메서드 수준 권한이 있으면 그것을, 없으면 서비스 수준 권한을 사용. 권한이 있는데 서버 `auth` 가 `undefined` 면 설정 오류로 throw, `false` 면 검사 스킵, 객체면 토큰 검증(미인증 시 `"로그인이 필요합니다."`, 권한 부족 시 `"권한이 부족합니다."` throw).
|
|
35
33
|
|
|
36
34
|
```ts
|
|
37
|
-
|
|
35
|
+
const UserService = defineService("User", auth((ctx) => ({
|
|
38
36
|
getProfile: () => ctx.authInfo,
|
|
39
|
-
|
|
37
|
+
adminOnly: auth(["admin"], () => "admin"),
|
|
40
38
|
})));
|
|
41
39
|
```
|
|
42
40
|
|
|
41
|
+
`getServiceAuthPermissions(fn: Function): string[] | undefined` — `auth()` 로 감싼 함수에서 권한 배열을 읽음. 감싸지 않았으면 undefined. 보통 내부에서만 사용.
|
|
42
|
+
|
|
43
43
|
## ServiceContext
|
|
44
44
|
|
|
45
|
-
`
|
|
45
|
+
팩토리가 받는 요청 컨텍스트. `ServiceContext<TAuthInfo>` 멤버:
|
|
46
46
|
|
|
47
|
-
- `server: ServiceServer<TAuthInfo>` —
|
|
48
|
-
- `socket?: ServiceSocket` — WebSocket
|
|
49
|
-
- `http?: { clientName: string; authTokenPayload
|
|
50
|
-
- `legacy?: { clientName
|
|
51
|
-
- `authInfo
|
|
52
|
-
- `clientName
|
|
53
|
-
- `clientPath
|
|
54
|
-
- `getConfig<T>(section): Promise<T>` — 루트 `.config.json`
|
|
47
|
+
- `server: ServiceServer<TAuthInfo>` — 서버 인스턴스. `server.options` 접근 등.
|
|
48
|
+
- `socket?: ServiceSocket` — WebSocket 요청이면 해당 소켓(HTTP/레거시 요청이면 undefined).
|
|
49
|
+
- `http?: { clientName: string; authTokenPayload? }` — HTTP 요청 메타(WebSocket 요청이면 undefined).
|
|
50
|
+
- `legacy?: { clientName? }` — V1 레거시 요청 메타.
|
|
51
|
+
- `authInfo` (getter) — `TAuthInfo | undefined`. 소켓/HTTP 토큰 페이로드의 `data`. 미인증이면 undefined.
|
|
52
|
+
- `clientName` (getter) — `string | undefined`. 소켓→HTTP→레거시 순으로 클라이언트 이름. `..`·`/`·`\`·빈 문자열 포함 시 보안상 throw.
|
|
53
|
+
- `clientPath` (getter) — `string | undefined`. `<rootPath>/www/<clientName>` 절대경로. clientName 없으면 undefined.
|
|
54
|
+
- `getConfig<T>(section: string): Promise<T>` — 루트 `.config.json` + 클라이언트별 `.config.json` 을 병합(클라이언트가 루트를 덮어씀)한 뒤 `section` 키를 반환. 해당 섹션 없으면 throw.
|
|
55
55
|
|
|
56
56
|
## ServiceMethods
|
|
57
57
|
|
|
58
|
-
`
|
|
58
|
+
`ServiceMethods<TDefinition>` — `ServiceDefinition<M>` 에서 메서드 시그니처 `M` 만 추출하는 타입 유틸. 서버 정의를 클라이언트와 공유해 호출 타입을 맞출 때.
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
61
|
export type UserServiceType = ServiceMethods<typeof UserService>;
|
|
62
62
|
// 클라이언트: client.getService<UserServiceType>("User");
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## ServiceDefinition
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
- `createServiceContext(server, socket?, http?, legacy?): ServiceContext` — 컨텍스트 수동 생성(전송 핸들러용). 인자별로 socket/http/legacy 출처를 지정.
|
|
69
|
-
- `executeServiceMethod(server, { serviceName, methodName, params, socket?, http? }): Promise<unknown>` — 서비스 검색→clientName 가드→컨텍스트 생성→팩토리 호출→메서드 검색→인증 검사→실행. 전송 계층이 RPC 를 실제 디스패치하는 진입점. 서비스/메서드 미존재 시 throw.
|
|
67
|
+
`defineService` 의 반환 타입. `{ name: string; names: string[]; factory: (ctx) => TMethods; authPermissions?: string[] }`. `name` 은 primary 이름, `names` 는 모든 별칭, `authPermissions` 는 팩토리가 `auth()` 로 감싸졌을 때만 채워짐. 보통 직접 만들지 않고 `defineService` 결과를 그대로 `services` 에 넣는다.
|
|
70
68
|
|
|
71
|
-
##
|
|
69
|
+
## createServiceContext
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
- 결측(authInfo/clientName)은 undefined 로 끝까지 전파. `?? ""` 등으로 치환 금지.
|
|
71
|
+
`createServiceContext<TAuthInfo>(server, socket?, http?, legacy?): ServiceContext<TAuthInfo>` — 위 컨텍스트 객체를 직접 생성. 서버 내부(요청 처리·V1 레거시 fallback)에서 사용하며, 커스텀 호출 경로를 손수 만들 때만 직접 호출.
|
|
@@ -1,51 +1,62 @@
|
|
|
1
1
|
# @simplysm/service-server — transport-internals
|
|
2
2
|
|
|
3
|
-
`ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜
|
|
3
|
+
`ServiceServer.listen()` 이 내부적으로 등록하는 저수준 전송·프로토콜 핸들러와 서비스 실행기. 보통 직접 호출하지 않으며, 커스텀 서버를 손수 조립하거나 동작을 디버깅·확장할 때만 참조한다.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## executeServiceMethod
|
|
6
6
|
|
|
7
|
-
`
|
|
7
|
+
`executeServiceMethod(server, def): Promise<unknown>` — 서비스 이름·메서드 이름·params 로 실제 메서드를 찾아 인증 검사 후 실행하는 핵심 디스패처. WebSocket/HTTP 핸들러가 공통으로 이걸 호출한다.
|
|
8
8
|
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
9
|
+
- `def.serviceName` / `def.methodName` — `services` 에서 매칭할 이름. 서비스 없으면 `"서비스 [..]를 찾을 수 없습니다."`, 메서드 없으면 `"메서드 [..]를 찾을 수 없습니다."` throw.
|
|
10
|
+
- `def.params: unknown[]` — 메서드 인자.
|
|
11
|
+
- `def.socket?` / `def.http?` — 요청 출처(둘 중 하나). clientName 에 `..`·`/`·`\` 포함 시 보안 throw.
|
|
11
12
|
|
|
12
|
-
`
|
|
13
|
+
인증 검사는 메서드/서비스 권한 + 서버 `auth` 설정 조합으로 수행(service-authoring.md 의 `auth` 항목 참조).
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
- `closeAll(): void` — 전 연결 종료.
|
|
16
|
-
- `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` — `infoSelector(info) === true` 인 리스너에 `evt:on` 푸시. `infoSelector` = 리스너 info 로 수신 대상 선별.
|
|
15
|
+
## createWebSocketHandler / WebSocketHandler
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
`createWebSocketHandler(runMethod, jwtSecret?): WebSocketHandler` — 여러 WebSocket 연결을 `clientId` 키로 관리하고 메시지를 라우팅·이벤트 브로드캐스트한다. `runMethod` 는 보통 `executeServiceMethod` 바인딩.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
`WebSocketHandler` 멤버:
|
|
21
20
|
|
|
22
|
-
`
|
|
21
|
+
- `addSocket(socket, clientId, clientName, connReq)` — 새 연결 등록. 같은 `clientId` 기존 연결은 닫고 교체. 연결 처리 중 에러 시 소켓 terminate.
|
|
22
|
+
- `closeAll()` — 모든 연결 종료(서버 close 시).
|
|
23
|
+
- `emit<TEventDef>(eventName, infoSelector, data): Promise<void>` — 등록 리스너 중 `infoSelector(info)` true 인 키에만 `evt:on` 전송.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
- `authTokenPayload?: AuthTokenPayload` — `auth` 메시지로 세팅되는 인증 페이로드. 읽기/쓰기 가능.
|
|
26
|
-
- `close(): void` — 연결 terminate.
|
|
27
|
-
- `send(uuid, msg): Promise<number>` — 메시지 인코딩 후 전송, 전송 바이트수 반환(소켓 미개방 시 0).
|
|
28
|
-
- `addListener(key, eventName, info): void` — key/이벤트명/info 로 리스너 등록.
|
|
29
|
-
- `removeListener(key): void` — key 로 리스너 제거.
|
|
30
|
-
- `getEventListeners(eventName): Array<{ key; info }>` — 해당 이벤트명 리스너 전체 조회.
|
|
31
|
-
- `filterEventTargetKeys(targetKeys): string[]` — 이 소켓에 존재하는 대상 key 만 필터.
|
|
32
|
-
- `on("error"|"close"|"message", handler): void` — 핸들러 등록. `error` → `(err)`, `close` → `(code)`, `message` → `({ uuid, msg })`.
|
|
25
|
+
처리하는 클라이언트 메시지 `name`: `"<service>.<method>"`(RPC 실행), `evt:add`/`evt:remove`/`evt:gets`/`evt:emit`(이벤트 리스너 등록·해제·조회·발신), `auth`(토큰 검증 후 소켓에 페이로드 저장; jwtSecret 없으면 throw). 그 외엔 `BAD_MESSAGE`, 실행 중 예외는 `INTERNAL_ERROR` 코드로 에러 응답(`DEV` env 시 stack 포함).
|
|
33
26
|
|
|
34
|
-
##
|
|
27
|
+
## createServiceSocket / ServiceSocket
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
- `handleUpload(req, reply, rootPath, jwtSecret): Promise<void>` — `/upload` multipart 처리. 인증 필수(토큰 없음·검증 실패 시 401). 파일을 `www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(path/filename/size) 반환. 멀티파트 아니면 400. 도중 실패 시 이미 저장한 파일·불완전 파일 전부 삭제 후 500(원자성).
|
|
38
|
-
- `handleStaticFile(req, reply, rootPath, urlPath): Promise<void>` — `www` 하위 정적 파일 서빙. `www` 밖 경로는 "접근이 거부되었습니다" throw(경로탐색 가드). 디렉토리는 슬래시 리다이렉트 후 `index.html`. `.` 시작 파일은 403. 없으면 404, 그 외 500(HTML 에러 페이지).
|
|
29
|
+
`createServiceSocket(socket: WebSocket, clientId, clientName, connReq): ServiceSocket` — 단일 WebSocket 연결을 감싸 프로토콜 인코딩/디코딩, 5초 주기 ping/pong keep-alive(무응답 시 terminate), 이벤트 리스너 추적을 담당.
|
|
39
30
|
|
|
40
|
-
|
|
31
|
+
`ServiceSocket` 멤버:
|
|
41
32
|
|
|
42
|
-
`
|
|
33
|
+
- `connectedAtDateTime: DateTime` / `clientName: string` / `connReq: FastifyRequest` — 연결 메타(읽기 전용).
|
|
34
|
+
- `authTokenPayload?: AuthTokenPayload` — `auth` 메시지 검증 후 저장되는 인증 페이로드(get/set).
|
|
35
|
+
- `close()` — 연결 terminate.
|
|
36
|
+
- `send(uuid, msg): Promise<number>` — 메시지 인코딩 후 전송, 전송 바이트 수 반환(소켓 닫혀 있으면 0).
|
|
37
|
+
- `addListener(key, eventName, info)` / `removeListener(key)` — 이벤트 리스너 등록·제거.
|
|
38
|
+
- `getEventListeners(eventName): Array<{ key, info }>` — 해당 이벤트의 리스너 목록.
|
|
39
|
+
- `filterEventTargetKeys(targetKeys): string[]` — 이 소켓에 실제 등록된 키만 필터.
|
|
40
|
+
- `on(event, handler)` — `"error"`(Error) / `"close"`(code: number) / `"message"`({ uuid, msg }) 핸들러 등록.
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
- `decode(bytes): Promise<ServiceMessageDecodeResult>` — 청크 재조립(stateful)은 항상 메인 단일 누적기, 재조립 완료 후 30KB 초과 JSON 파싱만 worker(청크 분산 재조립 버그 #35 방지).
|
|
46
|
-
- `dispose(): void` — 프로토콜 리소스 해제.
|
|
42
|
+
## handleHttpRequest
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
`handleHttpRequest<TAuthInfo>(req, reply, jwtSecret?, runMethod): Promise<void>` — `/api/:service/:method` 라우트 처리. `x-sd-client-name` 헤더 필수(없으면 throw), `Authorization: Bearer <token>` 있으면 검증(실패 시 401). GET 은 `?json=` 쿼리에서 params 파싱, POST 는 본문 배열(아니면 400), 그 외 메서드는 405. 결과를 그대로 응답.
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
## handleUpload
|
|
47
|
+
|
|
48
|
+
`handleUpload(req, reply, rootPath, jwtSecret?): Promise<void>` — `/upload` multipart 업로드 처리. multipart 아니면 400, 인증 토큰 누락·검증 실패 시 401. 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장하고 `ServiceUploadResult[]`(`{ path, filename, size }`) 반환. 크기 제한 초과나 도중 에러 시 이미 저장된 파일을 모두 삭제(원자적 정리)하고 500.
|
|
49
|
+
|
|
50
|
+
## handleStaticFile
|
|
51
|
+
|
|
52
|
+
`handleStaticFile(req, reply, rootPath, urlPath): Promise<void>` — `<rootPath>/www/` 하위 정적 파일 제공. `www` 밖 경로 탐색 시도는 throw. 디렉토리는 끝에 `/` 붙여 리다이렉트 후 `index.html` 제공. `.` 으로 시작하는 숨김 파일은 403, 없는 파일은 404, 그 외 전송 에러는 500(각각 HTML 에러 페이지).
|
|
53
|
+
|
|
54
|
+
## createServerProtocolWrapper / ServerProtocolWrapper
|
|
55
|
+
|
|
56
|
+
`createServerProtocolWrapper(): ServerProtocolWrapper` — 메시지 인코딩/디코딩 래퍼. 무거운 작업(Uint8Array 본문, 30KB 초과 JSON 파싱)은 공유 worker 스레드에 위임하고 가벼운 작업은 메인에서 처리. 청크 재조립(stateful)은 항상 메인 단일 누적기에서 수행한다(분산 시 재조립 불가 회피, #35).
|
|
57
|
+
|
|
58
|
+
`ServerProtocolWrapper` 멤버:
|
|
59
|
+
|
|
60
|
+
- `encode(uuid, message): Promise<{ chunks: Bytes[]; totalSize: number }>` — 인코딩. 본문이 Uint8Array 거나 Uint8Array 요소를 포함한 배열이면 worker 사용.
|
|
61
|
+
- `decode(bytes): Promise<ServiceMessageDecodeResult>` — 누적·디코딩. 진행 중이면 `{ type: "progress", ... }`, 완료 시 `{ type: "complete", uuid, message }`(30KB 초과 시 worker 파싱).
|
|
62
|
+
- `dispose()` — 프로토콜 리소스 해제(소켓 종료 시).
|
|
@@ -1,50 +1,39 @@
|
|
|
1
1
|
# @simplysm/service-server — v1-legacy
|
|
2
2
|
|
|
3
|
-
ver
|
|
3
|
+
`ver !== "2"`(구버전) WebSocket 클라이언트를 받기 위한 레거시 핸들러. 주로 구버전 앱의 자동 업데이트(`SdAutoUpdateService.getLastVersion`) 요청을 처리한다. `ServiceServer` 는 ver=2 가 아닌 연결을 자동으로 이 핸들러로 넘긴다(`AutoUpdate` 서비스나 `legacyV1Handlers` 가 있을 때만, 둘 다 없으면 연결 거부). `ServiceServerOptions.legacyV1Handlers` 로 커스텀 핸들러를 끼울 때만 직접 다룬다.
|
|
4
4
|
|
|
5
5
|
## handleV1Connection
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `V1AutoUpdateMethods` —
|
|
21
|
-
- `
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ctx.request.command === "Legacy.ping"
|
|
41
|
-
? { handled: true, body: "pong" }
|
|
42
|
-
: { handled: false },
|
|
43
|
-
],
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## 주의사항
|
|
48
|
-
|
|
49
|
-
- `handlers` 가 있는데 `serviceContext`(또는 factory)가 없으면 핸들러 실행 시 "serviceContext가 필요합니다." throw — 핸들러를 쓰려면 컨텍스트를 함께 제공.
|
|
50
|
-
- 메시지 처리 중 예외는 warn 로그만 남기고 응답하지 않음(레거시 한정 동작).
|
|
7
|
+
V1 소켓 연결을 처리한다. 두 가지 시그니처:
|
|
8
|
+
|
|
9
|
+
- `handleV1Connection(socket, autoUpdateMethods: V1AutoUpdateMethods, clientNameSetter?)` — 자동 업데이트 메서드만 넘기는 단축형.
|
|
10
|
+
- `handleV1Connection(socket, options: V1ConnectionOptions)` — 전체 옵션형.
|
|
11
|
+
|
|
12
|
+
연결 즉시 `{ name: "connected" }` 전송. 메시지 수신 시: ① `clientNameSetter` 호출 → ② 사용자 `handlers` 순회(처리되면 그 응답) → ③ 미처리이고 command 가 `"SdAutoUpdateService.getLastVersion"` 이면 자동 업데이트 메서드 실행 → ④ 그래도 미처리면 `{ message: "앱 업그레이드가 필요합니다.", code: "UPGRADE_REQUIRED" }` 에러 응답. 메시지 파싱 에러는 warn 로그.
|
|
13
|
+
|
|
14
|
+
## V1ConnectionOptions
|
|
15
|
+
|
|
16
|
+
- `serviceContext?: ServiceContext` — 핸들러에 넘길 고정 컨텍스트.
|
|
17
|
+
- `serviceContextFactory?: (request: V1Request) => ServiceContext` — 요청별 컨텍스트 생성(고정 컨텍스트보다 우선 적용). `ServiceServer` 는 이걸로 clientName 만 담은 컨텍스트를 만든다.
|
|
18
|
+
- `handlers?: V1RequestHandler[]` — 사용자 정의 처리기 목록. 하나라도 `handled: true` 면 그 응답으로 종료. 핸들러가 있는데 컨텍스트가 없으면 throw.
|
|
19
|
+
- `autoUpdateMethods?: V1AutoUpdateMethods` — getLastVersion fallback 의 고정 구현.
|
|
20
|
+
- `autoUpdateMethodsFactory?: (ctx: V1RequestHandlerContext) => V1AutoUpdateMethods` — 요청별 fallback 생성(있으면 고정 구현보다 우선). 컨텍스트 없으면 throw.
|
|
21
|
+
- `clientNameSetter?: (clientName: string | undefined) => void` — 매 메시지의 `clientName` 을 외부로 전달하는 콜백.
|
|
22
|
+
|
|
23
|
+
## V1RequestHandler
|
|
24
|
+
|
|
25
|
+
`V1RequestHandler` — `(ctx: V1RequestHandlerContext) => V1RequestHandlerResult | Promise<...>`. 동기/비동기 모두 허용.
|
|
26
|
+
|
|
27
|
+
`V1RequestHandlerContext` — `{ request: V1Request; serviceContext: ServiceContext }`.
|
|
28
|
+
|
|
29
|
+
`V1RequestHandlerResult` — `{ handled: true; state?: "success" | "error"; body: unknown }`(이 핸들러가 처리; `state` 미지정 시 `"success"`) 또는 `{ handled: false }`(다음 핸들러/fallback 으로 위임).
|
|
30
|
+
|
|
31
|
+
## V1Request / V1Response
|
|
32
|
+
|
|
33
|
+
`V1Request` — 클라이언트 요청. `{ uuid: string; command: string; params: unknown[]; clientName?: string }`. `command` 는 `"<service>.<method>"` 형태.
|
|
34
|
+
|
|
35
|
+
`V1Response` — 서버 응답. `{ name: "response"; reqUuid: string; state: "success" | "error"; body: unknown }`. `state` 가 `"success"` 면 정상 결과, `"error"` 면 오류 본문.
|
|
36
|
+
|
|
37
|
+
## V1AutoUpdateMethods
|
|
38
|
+
|
|
39
|
+
`V1AutoUpdateMethods` — `{ getLastVersion: (platform: string) => Promise<unknown> | unknown }`. V1 자동 업데이트 fallback 의 최소 인터페이스. `ServiceServer` 는 등록된 `AutoUpdate` 서비스의 `getLastVersion` 을 여기에 어댑트해 넘긴다.
|