@simplysm/service-client 13.0.96 → 13.0.98
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/README.md +53 -283
- package/docs/features.md +142 -0
- package/docs/protocol.md +39 -0
- package/docs/service-client.md +210 -0
- package/docs/transport.md +102 -0
- package/docs/types.md +38 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
# @simplysm/service-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Service module (client) -- WebSocket-based service client for communicating with `@simplysm/service-server`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @simplysm/service-client
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Exports
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
// Main
|
|
16
|
+
ServiceClient,
|
|
17
|
+
createServiceClient,
|
|
18
|
+
type ServiceProxy,
|
|
19
|
+
// Types
|
|
20
|
+
type ServiceConnectionOptions,
|
|
21
|
+
type ServiceProgress,
|
|
22
|
+
type ServiceProgressState,
|
|
23
|
+
// Transport
|
|
24
|
+
type SocketProvider,
|
|
25
|
+
type SocketProviderEvents,
|
|
26
|
+
createSocketProvider,
|
|
27
|
+
type ServiceTransport,
|
|
28
|
+
type ServiceTransportEvents,
|
|
29
|
+
createServiceTransport,
|
|
30
|
+
// Protocol
|
|
31
|
+
type ClientProtocolWrapper,
|
|
32
|
+
createClientProtocolWrapper,
|
|
33
|
+
// Features
|
|
34
|
+
type EventClient,
|
|
35
|
+
createEventClient,
|
|
36
|
+
type FileClient,
|
|
37
|
+
createFileClient,
|
|
38
|
+
type OrmConnectOptions,
|
|
39
|
+
type OrmClientConnector,
|
|
40
|
+
createOrmClientConnector,
|
|
41
|
+
OrmClientDbContextExecutor,
|
|
42
|
+
} from "@simplysm/service-client";
|
|
43
|
+
```
|
|
14
44
|
|
|
15
|
-
|
|
45
|
+
## Quick Start
|
|
16
46
|
|
|
17
47
|
```typescript
|
|
18
48
|
import { createServiceClient } from "@simplysm/service-client";
|
|
@@ -20,293 +50,33 @@ import { createServiceClient } from "@simplysm/service-client";
|
|
|
20
50
|
const client = createServiceClient("my-app", {
|
|
21
51
|
host: "localhost",
|
|
22
52
|
port: 3000,
|
|
23
|
-
ssl: false,
|
|
24
|
-
maxReconnectCount: 10, // 0이면 재연결 비활성화
|
|
25
53
|
});
|
|
26
54
|
|
|
27
55
|
await client.connect();
|
|
28
|
-
// ... 사용 ...
|
|
29
|
-
await client.close();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`ServiceClient` 클래스를 직접 사용할 수도 있다.
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { ServiceClient } from "@simplysm/service-client";
|
|
36
|
-
|
|
37
|
-
const client = new ServiceClient("my-app", {
|
|
38
|
-
host: "localhost",
|
|
39
|
-
port: 3000,
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 서비스 호출 (RPC)
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
// 타입 안전한 서비스 프록시
|
|
47
|
-
const userService = client.getService<UserService>("User");
|
|
48
|
-
const users = await userService.findAll();
|
|
49
|
-
const user = await userService.findById(1);
|
|
50
|
-
|
|
51
|
-
// 직접 호출
|
|
52
|
-
const result = await client.send("User", "findById", [1]);
|
|
53
|
-
|
|
54
|
-
// 진행률 콜백과 함께 호출
|
|
55
|
-
const result = await client.send("User", "exportData", [], {
|
|
56
|
-
request: (state) => {
|
|
57
|
-
// state.completedSize / state.totalSize 로 진행률 계산
|
|
58
|
-
},
|
|
59
|
-
response: (state) => {
|
|
60
|
-
// state.completedSize / state.totalSize 로 진행률 계산
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 인증
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
await client.auth(jwtToken);
|
|
69
|
-
```
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
### 이벤트 구독
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
import { defineEvent } from "@simplysm/service-common";
|
|
77
|
-
|
|
78
|
-
const OrderCreated = defineEvent<{ shopId: string }, { orderId: string; amount: number }>(
|
|
79
|
-
"order-created"
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// 이벤트 구독 (info로 필터 조건 전달)
|
|
83
|
-
const key = await client.addListener(
|
|
84
|
-
OrderCreated,
|
|
85
|
-
{ shopId: "shop-1" },
|
|
86
|
-
async (data) => {
|
|
87
|
-
// data: { orderId: string; amount: number }
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// 이벤트 구독 해제
|
|
92
|
-
await client.removeListener(key);
|
|
93
|
-
|
|
94
|
-
// 이벤트 발행 (서버를 통해 다른 클라이언트에 전달)
|
|
95
|
-
await client.emitEvent(
|
|
96
|
-
OrderCreated,
|
|
97
|
-
(info) => info.shopId === "shop-1",
|
|
98
|
-
{ orderId: "order-123", amount: 50000 }
|
|
99
|
-
);
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
재연결 시 이벤트 리스너가 서버에 자동으로 재등록된다.
|
|
103
|
-
|
|
104
|
-
### 파일 업로드/다운로드
|
|
105
|
-
|
|
106
|
-
파일 업로드는 `auth()`로 인증한 후에만 사용할 수 있다.
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// 업로드 (인증 필수)
|
|
110
|
-
await client.auth(jwtToken);
|
|
111
|
-
const results = await client.uploadFile(fileInput.files);
|
|
112
|
-
// results: ServiceUploadResult[] (path, filename, size)
|
|
113
|
-
|
|
114
|
-
// { name, data } 객체 배열도 지원
|
|
115
|
-
await client.uploadFile([
|
|
116
|
-
{ name: "report.xlsx", data: uint8ArrayData },
|
|
117
|
-
]);
|
|
118
|
-
|
|
119
|
-
// 다운로드 (Uint8Array 반환)
|
|
120
|
-
const buffer = await client.downloadFileBuffer("uploads/report.xlsx");
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 진행률 추적
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// 이벤트 기반 (모든 요청/응답에 대해)
|
|
127
|
-
client.on("request-progress", ({ uuid, totalSize, completedSize }) => {
|
|
128
|
-
// 요청 전송 진행률
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
client.on("response-progress", ({ uuid, totalSize, completedSize }) => {
|
|
132
|
-
// 응답 수신 진행률
|
|
133
|
-
});
|
|
57
|
+
// Authenticate
|
|
58
|
+
await client.auth(token);
|
|
134
59
|
|
|
135
|
-
//
|
|
136
|
-
client.
|
|
137
|
-
|
|
138
|
-
});
|
|
60
|
+
// Call a service method
|
|
61
|
+
const service = client.getService<MyServiceType>("MyService");
|
|
62
|
+
const result = await service.someMethod("arg1", "arg2");
|
|
139
63
|
|
|
140
|
-
//
|
|
141
|
-
client.
|
|
142
|
-
//
|
|
143
|
-
location.reload();
|
|
64
|
+
// Listen for events
|
|
65
|
+
await client.addListener(SomeEvent, { id: 1 }, async (data) => {
|
|
66
|
+
// handle event
|
|
144
67
|
});
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### ORM 클라이언트 연동
|
|
148
|
-
|
|
149
|
-
서버의 ORM 서비스를 통해 클라이언트에서 DbContext를 사용한다.
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
import { createOrmClientConnector } from "@simplysm/service-client";
|
|
153
|
-
|
|
154
|
-
const ormConnector = createOrmClientConnector(client);
|
|
155
|
-
|
|
156
|
-
// 트랜잭션 포함 연결
|
|
157
|
-
const result = await ormConnector.connect(
|
|
158
|
-
{
|
|
159
|
-
dbContextDef: MyDb,
|
|
160
|
-
connOpt: { configName: "main" },
|
|
161
|
-
// 선택적: DB/스키마 오버라이드
|
|
162
|
-
// dbContextOpt: { database: "mydb", schema: "dbo" },
|
|
163
|
-
},
|
|
164
|
-
async (db) => {
|
|
165
|
-
return await db.user().execute();
|
|
166
|
-
}
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
// 트랜잭션 없이 연결
|
|
170
|
-
const result = await ormConnector.connectWithoutTransaction(
|
|
171
|
-
{ dbContextDef: MyDb, connOpt: { configName: "main" } },
|
|
172
|
-
async (db) => {
|
|
173
|
-
return await db.user().execute();
|
|
174
|
-
}
|
|
175
|
-
);
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## API 레퍼런스
|
|
179
|
-
|
|
180
|
-
### `createServiceClient(name, options)`
|
|
181
|
-
|
|
182
|
-
`ServiceClient` 인스턴스를 생성하는 팩토리 함수.
|
|
183
|
-
|
|
184
|
-
| 파라미터 | 타입 | 설명 |
|
|
185
|
-
|---------|------|------|
|
|
186
|
-
| `name` | `string` | 클라이언트 식별 이름 |
|
|
187
|
-
| `options` | `ServiceConnectionOptions` | 연결 설정 |
|
|
188
|
-
|
|
189
|
-
### `ServiceClient`
|
|
190
|
-
|
|
191
|
-
`EventEmitter<ServiceClientEvents>`를 확장한 메인 클라이언트 클래스.
|
|
192
|
-
|
|
193
|
-
**속성:**
|
|
194
|
-
|
|
195
|
-
| 이름 | 타입 | 설명 |
|
|
196
|
-
|------|------|------|
|
|
197
|
-
| `name` | `string` | 클라이언트 이름 (읽기 전용) |
|
|
198
|
-
| `options` | `ServiceConnectionOptions` | 연결 설정 (읽기 전용) |
|
|
199
|
-
| `connected` | `boolean` | 현재 연결 상태 |
|
|
200
|
-
| `hostUrl` | `string` | `http(s)://host:port` 형식 URL |
|
|
201
|
-
|
|
202
|
-
**메서드:**
|
|
203
|
-
|
|
204
|
-
| 메서드 | 반환 | 설명 |
|
|
205
|
-
|--------|------|------|
|
|
206
|
-
| `connect()` | `Promise<void>` | 서버에 연결 |
|
|
207
|
-
| `close()` | `Promise<void>` | 연결 종료 |
|
|
208
|
-
| `auth(token)` | `Promise<void>` | JWT 토큰으로 인증 |
|
|
209
|
-
| `send(serviceName, methodName, params, progress?)` | `Promise<unknown>` | RPC 호출 |
|
|
210
|
-
| `getService<T>(serviceName)` | `ServiceProxy<T>` | 타입 안전 서비스 프록시 생성 |
|
|
211
|
-
| `addListener(eventDef, info, cb)` | `Promise<string>` | 이벤트 구독 (키 반환) |
|
|
212
|
-
| `removeListener(key)` | `Promise<void>` | 이벤트 구독 해제 |
|
|
213
|
-
| `emitEvent(eventDef, infoSelector, data)` | `Promise<void>` | 이벤트 발행 |
|
|
214
|
-
| `uploadFile(files)` | `Promise<ServiceUploadResult[]>` | 파일 업로드 (인증 필수) |
|
|
215
|
-
| `downloadFileBuffer(relPath)` | `Promise<Bytes>` | 파일 다운로드 |
|
|
216
|
-
|
|
217
|
-
**이벤트:**
|
|
218
|
-
|
|
219
|
-
| 이벤트 | 데이터 타입 | 설명 |
|
|
220
|
-
|--------|-----------|------|
|
|
221
|
-
| `request-progress` | `ServiceProgressState` | 요청 전송 진행률 |
|
|
222
|
-
| `response-progress` | `ServiceProgressState` | 응답 수신 진행률 |
|
|
223
|
-
| `state` | `"connected" \| "closed" \| "reconnecting"` | 연결 상태 변경 |
|
|
224
|
-
| `reload` | `Set<string>` | 서버 HMR 리로드 알림 |
|
|
225
|
-
|
|
226
|
-
### `ServiceProxy<TService>`
|
|
227
68
|
|
|
228
|
-
|
|
69
|
+
// Upload files
|
|
70
|
+
const results = await client.uploadFile(fileList);
|
|
229
71
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
[K in keyof TService]: TService[K] extends (...args: infer P) => infer R
|
|
233
|
-
? (...args: P) => Promise<Awaited<R>>
|
|
234
|
-
: never;
|
|
235
|
-
};
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### `ServiceConnectionOptions`
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
interface ServiceConnectionOptions {
|
|
242
|
-
port: number;
|
|
243
|
-
host: string;
|
|
244
|
-
ssl?: boolean; // HTTPS/WSS 사용 (기본: false)
|
|
245
|
-
maxReconnectCount?: number; // 최대 재연결 시도 (기본: 10, 0=비활성화)
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### `ServiceProgress` / `ServiceProgressState`
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
interface ServiceProgress {
|
|
253
|
-
request?: (s: ServiceProgressState) => void;
|
|
254
|
-
response?: (s: ServiceProgressState) => void;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
interface ServiceProgressState {
|
|
258
|
-
uuid: string;
|
|
259
|
-
totalSize: number;
|
|
260
|
-
completedSize: number;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### `createOrmClientConnector(serviceClient)`
|
|
265
|
-
|
|
266
|
-
`OrmClientConnector` 인스턴스를 생성한다.
|
|
267
|
-
|
|
268
|
-
**`OrmClientConnector` 메서드:**
|
|
269
|
-
|
|
270
|
-
| 메서드 | 설명 |
|
|
271
|
-
|--------|------|
|
|
272
|
-
| `connect(config, callback)` | 트랜잭션 포함 DB 연결. 외래 키 제약 위반 시 친화적 에러 메시지 제공 |
|
|
273
|
-
| `connectWithoutTransaction(config, callback)` | 트랜잭션 없이 DB 연결 |
|
|
274
|
-
|
|
275
|
-
### `OrmConnectOptions<TDef>`
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
interface OrmConnectOptions<TDef extends DbContextDef<any, any, any>> {
|
|
279
|
-
dbContextDef: TDef;
|
|
280
|
-
connOpt: DbConnOptions & { configName: string };
|
|
281
|
-
dbContextOpt?: {
|
|
282
|
-
database: string;
|
|
283
|
-
schema: string;
|
|
284
|
-
};
|
|
285
|
-
}
|
|
72
|
+
// Close
|
|
73
|
+
await client.close();
|
|
286
74
|
```
|
|
287
75
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
다음 인터페이스와 팩토리 함수도 export되며, 커스텀 구성이 필요할 때 사용한다.
|
|
291
|
-
|
|
292
|
-
| Export | 설명 |
|
|
293
|
-
|--------|------|
|
|
294
|
-
| `createSocketProvider(url, clientName, maxReconnectCount)` | WebSocket 연결 관리 (하트비트, 재연결) |
|
|
295
|
-
| `SocketProvider` | 소켓 프로바이더 인터페이스 |
|
|
296
|
-
| `createServiceTransport(socket, protocol)` | 요청/응답 매칭 및 메시지 라우팅 |
|
|
297
|
-
| `ServiceTransport` | 트랜스포트 인터페이스 |
|
|
298
|
-
| `createClientProtocolWrapper(protocol)` | Worker 기반 인코딩/디코딩 래퍼 |
|
|
299
|
-
| `ClientProtocolWrapper` | 프로토콜 래퍼 인터페이스 |
|
|
300
|
-
| `createEventClient(transport)` | 이벤트 구독/발행 관리 |
|
|
301
|
-
| `EventClient` | 이벤트 클라이언트 인터페이스 |
|
|
302
|
-
| `createFileClient(hostUrl, clientName)` | HTTP 기반 파일 업로드/다운로드 |
|
|
303
|
-
| `FileClient` | 파일 클라이언트 인터페이스 |
|
|
304
|
-
| `OrmClientDbContextExecutor` | `DbContextExecutor` 구현체 (RPC 경유) |
|
|
305
|
-
|
|
306
|
-
## 내부 동작
|
|
76
|
+
## Documentation
|
|
307
77
|
|
|
308
|
-
-
|
|
309
|
-
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
78
|
+
- [Types](docs/types.md)
|
|
79
|
+
- [Transport](docs/transport.md)
|
|
80
|
+
- [Protocol](docs/protocol.md)
|
|
81
|
+
- [Features](docs/features.md)
|
|
82
|
+
- [ServiceClient](docs/service-client.md)
|
package/docs/features.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Features
|
|
2
|
+
|
|
3
|
+
## EventClient
|
|
4
|
+
|
|
5
|
+
Client-side event subscription manager. Handles adding/removing event listeners and auto-resubscription on reconnect.
|
|
6
|
+
|
|
7
|
+
### `EventClient`
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
interface EventClient {
|
|
11
|
+
addListener<TInfo, TData>(
|
|
12
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
13
|
+
info: TInfo,
|
|
14
|
+
cb: (data: TData) => PromiseLike<void>,
|
|
15
|
+
): Promise<string>;
|
|
16
|
+
|
|
17
|
+
removeListener(key: string): Promise<void>;
|
|
18
|
+
|
|
19
|
+
emit<TInfo, TData>(
|
|
20
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
21
|
+
infoSelector: (item: TInfo) => boolean,
|
|
22
|
+
data: TData,
|
|
23
|
+
): Promise<void>;
|
|
24
|
+
|
|
25
|
+
resubscribeAll(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `createEventClient`
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
function createEventClient(transport: ServiceTransport): EventClient;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Behavior:**
|
|
36
|
+
- `addListener` registers on the server and stores locally for reconnect recovery
|
|
37
|
+
- `removeListener` removes from local map and sends removal request to server
|
|
38
|
+
- `emit` queries the server for matching listener infos, then sends event to matching keys
|
|
39
|
+
- `resubscribeAll` re-registers all local listeners on the server (called on reconnect)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## FileClient
|
|
44
|
+
|
|
45
|
+
HTTP-based file upload/download client.
|
|
46
|
+
|
|
47
|
+
### `FileClient`
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface FileClient {
|
|
51
|
+
download(relPath: string): Promise<Bytes>;
|
|
52
|
+
upload(
|
|
53
|
+
files: File[] | FileList | { name: string; data: BlobPart }[],
|
|
54
|
+
authToken: string,
|
|
55
|
+
): Promise<ServiceUploadResult[]>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `createFileClient`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
function createFileClient(hostUrl: string, clientName: string): FileClient;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Behavior:**
|
|
66
|
+
- `download` fetches a file via HTTP GET and returns it as `Uint8Array`
|
|
67
|
+
- `upload` sends files via multipart form POST to `/upload` with auth token in headers
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ORM Features
|
|
72
|
+
|
|
73
|
+
### `OrmConnectOptions`
|
|
74
|
+
|
|
75
|
+
Configuration for ORM database connections via the service client.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface OrmConnectOptions<TDef extends DbContextDef<any, any, any>> {
|
|
79
|
+
dbContextDef: TDef;
|
|
80
|
+
connOpt: DbConnOptions & { configName: string };
|
|
81
|
+
dbContextOpt?: {
|
|
82
|
+
database: string;
|
|
83
|
+
schema: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `OrmClientConnector`
|
|
89
|
+
|
|
90
|
+
Manages ORM database connections through the service client.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface OrmClientConnector {
|
|
94
|
+
connect<TDef extends DbContextDef<any, any, any>, R>(
|
|
95
|
+
config: OrmConnectOptions<TDef>,
|
|
96
|
+
callback: (db: DbContextInstance<TDef>) => Promise<R> | R,
|
|
97
|
+
): Promise<R>;
|
|
98
|
+
|
|
99
|
+
connectWithoutTransaction<TDef extends DbContextDef<any, any, any>, R>(
|
|
100
|
+
config: OrmConnectOptions<TDef>,
|
|
101
|
+
callback: (db: DbContextInstance<TDef>) => Promise<R> | R,
|
|
102
|
+
): Promise<R>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `createOrmClientConnector`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
function createOrmClientConnector(serviceClient: ServiceClient): OrmClientConnector;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Behavior:**
|
|
113
|
+
- `connect` creates a database context and executes the callback within a transaction
|
|
114
|
+
- `connectWithoutTransaction` creates a database context without transaction wrapping
|
|
115
|
+
- Foreign key constraint violations are caught and re-thrown with a user-friendly message
|
|
116
|
+
|
|
117
|
+
### `OrmClientDbContextExecutor`
|
|
118
|
+
|
|
119
|
+
Implements `DbContextExecutor` by delegating all database operations to the remote `OrmService` via WebSocket.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
class OrmClientDbContextExecutor implements DbContextExecutor {
|
|
123
|
+
constructor(client: ServiceClient, opt: DbConnOptions & { configName: string });
|
|
124
|
+
|
|
125
|
+
async getInfo(): Promise<{ dialect: Dialect; database?: string; schema?: string }>;
|
|
126
|
+
async connect(): Promise<void>;
|
|
127
|
+
async beginTransaction(isolationLevel?: IsolationLevel): Promise<void>;
|
|
128
|
+
async commitTransaction(): Promise<void>;
|
|
129
|
+
async rollbackTransaction(): Promise<void>;
|
|
130
|
+
async close(): Promise<void>;
|
|
131
|
+
async executeDefs<T = Record<string, unknown>>(
|
|
132
|
+
defs: QueryDef[],
|
|
133
|
+
options?: (ResultMeta | undefined)[],
|
|
134
|
+
): Promise<T[][]>;
|
|
135
|
+
async executeParametrized(query: string, params?: unknown[]): Promise<unknown[][]>;
|
|
136
|
+
async bulkInsert(
|
|
137
|
+
tableName: string,
|
|
138
|
+
columnDefs: Record<string, ColumnMeta>,
|
|
139
|
+
records: Record<string, unknown>[],
|
|
140
|
+
): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
```
|
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Protocol
|
|
2
|
+
|
|
3
|
+
Client-side protocol wrapper that handles message encoding/decoding with optional Web Worker offloading for large payloads.
|
|
4
|
+
|
|
5
|
+
## `ClientProtocolWrapper`
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
interface ClientProtocolWrapper {
|
|
9
|
+
encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
|
|
10
|
+
decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## `createClientProtocolWrapper`
|
|
15
|
+
|
|
16
|
+
Create a client protocol wrapper instance.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
function createClientProtocolWrapper(protocol: ServiceProtocol): ClientProtocolWrapper;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Parameters:**
|
|
23
|
+
- `protocol` -- A `ServiceProtocol` instance (from `@simplysm/service-common`)
|
|
24
|
+
|
|
25
|
+
**Behavior:**
|
|
26
|
+
- Messages smaller than 30KB are processed on the main thread
|
|
27
|
+
- Larger messages are offloaded to a Web Worker for encoding/decoding
|
|
28
|
+
- Worker is automatically initialized as a lazy singleton
|
|
29
|
+
- Worker tasks that do not complete within 60s are rejected (prevents memory leaks)
|
|
30
|
+
- Falls back to main-thread processing when `Worker` is not available (e.g., SSR)
|
|
31
|
+
|
|
32
|
+
**Worker delegation heuristics for encoding:**
|
|
33
|
+
- `Uint8Array` body: always use worker
|
|
34
|
+
- String body longer than 30KB: use worker
|
|
35
|
+
- Array body with >100 elements or containing `Uint8Array`: use worker
|
|
36
|
+
|
|
37
|
+
**Worker delegation heuristics for decoding:**
|
|
38
|
+
- Byte size > 30KB: use worker
|
|
39
|
+
- After worker decoding, `transfer.decode()` is applied to restore class instances (e.g., `DateTime`)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# ServiceClient
|
|
2
|
+
|
|
3
|
+
Main client class that orchestrates all service communication modules.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { ServiceClient, createServiceClient, type ServiceProxy } from "@simplysm/service-client";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## `ServiceProxy<TService>`
|
|
10
|
+
|
|
11
|
+
Type transformer that wraps all method return types of `TService` with `Promise`.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
type ServiceProxy<TService> = {
|
|
15
|
+
[K in keyof TService]: TService[K] extends (...args: infer P) => infer R
|
|
16
|
+
? (...args: P) => Promise<Awaited<R>>
|
|
17
|
+
: never;
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Class: `ServiceClient`
|
|
22
|
+
|
|
23
|
+
Extends `EventEmitter<ServiceClientEvents>`.
|
|
24
|
+
|
|
25
|
+
### Events
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface ServiceClientEvents {
|
|
29
|
+
"request-progress": ServiceProgressState;
|
|
30
|
+
"response-progress": ServiceProgressState;
|
|
31
|
+
"state": "connected" | "closed" | "reconnecting";
|
|
32
|
+
"reload": Set<string>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Constructor
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
constructor(name: string, options: ServiceConnectionOptions)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `name` -- Client name (used for identification on the server)
|
|
43
|
+
- `options` -- Connection options
|
|
44
|
+
|
|
45
|
+
### Properties
|
|
46
|
+
|
|
47
|
+
#### `connected`
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
get connected(): boolean;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### `hostUrl`
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
get hostUrl(): string;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Returns the HTTP(S) URL of the server (e.g., `"https://localhost:3000"`).
|
|
60
|
+
|
|
61
|
+
### Methods
|
|
62
|
+
|
|
63
|
+
#### `connect`
|
|
64
|
+
|
|
65
|
+
Connect to the server via WebSocket.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
async connect(): Promise<void>;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### `close`
|
|
72
|
+
|
|
73
|
+
Close the WebSocket connection.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
async close(): Promise<void>;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### `auth`
|
|
80
|
+
|
|
81
|
+
Authenticate with the server using a JWT token.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
async auth(token: string): Promise<void>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### `getService`
|
|
88
|
+
|
|
89
|
+
Create a type-safe proxy for calling remote service methods.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
getService<TService>(serviceName: string): ServiceProxy<TService>;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Example:**
|
|
96
|
+
```typescript
|
|
97
|
+
const userService = client.getService<UserServiceType>("User");
|
|
98
|
+
const profile = await userService.getProfile();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `send`
|
|
102
|
+
|
|
103
|
+
Send a raw service method call.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
async send(
|
|
107
|
+
serviceName: string,
|
|
108
|
+
methodName: string,
|
|
109
|
+
params: unknown[],
|
|
110
|
+
progress?: ServiceProgress,
|
|
111
|
+
): Promise<unknown>;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `addListener`
|
|
115
|
+
|
|
116
|
+
Add a server-side event listener.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
async addListener<TInfo, TData>(
|
|
120
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
121
|
+
info: TInfo,
|
|
122
|
+
cb: (data: TData) => PromiseLike<void>,
|
|
123
|
+
): Promise<string>;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Returns a listener key (UUID) that can be used with `removeListener`.
|
|
127
|
+
|
|
128
|
+
#### `removeListener`
|
|
129
|
+
|
|
130
|
+
Remove a server-side event listener.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
async removeListener(key: string): Promise<void>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### `emitEvent`
|
|
137
|
+
|
|
138
|
+
Emit an event to matching server-side listeners.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
async emitEvent<TInfo, TData>(
|
|
142
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
143
|
+
infoSelector: (item: TInfo) => boolean,
|
|
144
|
+
data: TData,
|
|
145
|
+
): Promise<void>;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `uploadFile`
|
|
149
|
+
|
|
150
|
+
Upload files to the server. Requires prior authentication via `auth()`.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
async uploadFile(
|
|
154
|
+
files: File[] | FileList | { name: string; data: BlobPart }[],
|
|
155
|
+
): Promise<ServiceUploadResult[]>;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### `downloadFileBuffer`
|
|
159
|
+
|
|
160
|
+
Download a file from the server as bytes.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
async downloadFileBuffer(relPath: string): Promise<Bytes>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## `createServiceClient`
|
|
167
|
+
|
|
168
|
+
Factory function.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
function createServiceClient(name: string, options: ServiceConnectionOptions): ServiceClient;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Example
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { createServiceClient, type ServiceProxy } from "@simplysm/service-client";
|
|
178
|
+
import { defineEvent } from "@simplysm/service-common";
|
|
179
|
+
|
|
180
|
+
// Define event
|
|
181
|
+
const OrderUpdated = defineEvent<{ orderId: number }, { status: string }>("OrderUpdated");
|
|
182
|
+
|
|
183
|
+
// Create client
|
|
184
|
+
const client = createServiceClient("my-app", {
|
|
185
|
+
host: "localhost",
|
|
186
|
+
port: 3000,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Connect and authenticate
|
|
190
|
+
await client.connect();
|
|
191
|
+
await client.auth(jwtToken);
|
|
192
|
+
|
|
193
|
+
// Call service
|
|
194
|
+
const orderService = client.getService<OrderServiceType>("Order");
|
|
195
|
+
const orders = await orderService.getAll();
|
|
196
|
+
|
|
197
|
+
// Listen for events
|
|
198
|
+
const key = await client.addListener(OrderUpdated, { orderId: 123 }, async (data) => {
|
|
199
|
+
console.log(data.status);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Track progress
|
|
203
|
+
client.on("request-progress", (state) => {
|
|
204
|
+
console.log(`Upload: ${state.completedSize}/${state.totalSize}`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Cleanup
|
|
208
|
+
await client.removeListener(key);
|
|
209
|
+
await client.close();
|
|
210
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Transport
|
|
2
|
+
|
|
3
|
+
The transport layer handles WebSocket connections and message routing.
|
|
4
|
+
|
|
5
|
+
## SocketProvider
|
|
6
|
+
|
|
7
|
+
Low-level WebSocket connection manager with automatic reconnection and heartbeat keep-alive.
|
|
8
|
+
|
|
9
|
+
### `SocketProviderEvents`
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
interface SocketProviderEvents {
|
|
13
|
+
message: Bytes;
|
|
14
|
+
state: "connected" | "closed" | "reconnecting";
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### `SocketProvider`
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
interface SocketProvider {
|
|
22
|
+
readonly clientName: string;
|
|
23
|
+
readonly connected: boolean;
|
|
24
|
+
on<K extends keyof SocketProviderEvents & string>(
|
|
25
|
+
type: K,
|
|
26
|
+
listener: (data: SocketProviderEvents[K]) => void,
|
|
27
|
+
): void;
|
|
28
|
+
off<K extends keyof SocketProviderEvents & string>(
|
|
29
|
+
type: K,
|
|
30
|
+
listener: (data: SocketProviderEvents[K]) => void,
|
|
31
|
+
): void;
|
|
32
|
+
connect(): Promise<void>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
send(data: Bytes): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `createSocketProvider`
|
|
39
|
+
|
|
40
|
+
Create a SocketProvider instance.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
function createSocketProvider(
|
|
44
|
+
url: string,
|
|
45
|
+
clientName: string,
|
|
46
|
+
maxReconnectCount: number,
|
|
47
|
+
): SocketProvider;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Behavior:**
|
|
51
|
+
- Heartbeat: sends ping every 5s, considers disconnected if no message for 30s
|
|
52
|
+
- Reconnect: retries every 3s up to `maxReconnectCount` times
|
|
53
|
+
- Binary protocol: uses `ArrayBuffer` for data transfer
|
|
54
|
+
- Ping/Pong: `0x01` = ping, `0x02` = pong
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ServiceTransport
|
|
59
|
+
|
|
60
|
+
Higher-level transport that handles message encoding/decoding, request-response correlation, and event dispatching.
|
|
61
|
+
|
|
62
|
+
### `ServiceTransportEvents`
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface ServiceTransportEvents {
|
|
66
|
+
reload: Set<string>;
|
|
67
|
+
event: { keys: string[]; data: unknown };
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `ServiceTransport`
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface ServiceTransport {
|
|
75
|
+
on<K extends keyof ServiceTransportEvents & string>(
|
|
76
|
+
type: K,
|
|
77
|
+
listener: (data: ServiceTransportEvents[K]) => void,
|
|
78
|
+
): void;
|
|
79
|
+
off<K extends keyof ServiceTransportEvents & string>(
|
|
80
|
+
type: K,
|
|
81
|
+
listener: (data: ServiceTransportEvents[K]) => void,
|
|
82
|
+
): void;
|
|
83
|
+
send(message: ServiceClientMessage, progress?: ServiceProgress): Promise<unknown>;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `createServiceTransport`
|
|
88
|
+
|
|
89
|
+
Create a ServiceTransport instance.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
function createServiceTransport(
|
|
93
|
+
socket: SocketProvider,
|
|
94
|
+
protocol: ClientProtocolWrapper,
|
|
95
|
+
): ServiceTransport;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Behavior:**
|
|
99
|
+
- Each `send()` call generates a unique UUID and registers a pending request
|
|
100
|
+
- Incoming messages are correlated by UUID and resolved/rejected accordingly
|
|
101
|
+
- Progress callbacks are invoked for chunked message transfers
|
|
102
|
+
- All pending requests are rejected when the socket disconnects
|
package/docs/types.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Types
|
|
2
|
+
|
|
3
|
+
## ServiceConnectionOptions
|
|
4
|
+
|
|
5
|
+
WebSocket connection configuration.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
interface ServiceConnectionOptions {
|
|
9
|
+
port: number;
|
|
10
|
+
host: string;
|
|
11
|
+
ssl?: boolean;
|
|
12
|
+
/** Set to 0 to disable reconnect; disconnects immediately */
|
|
13
|
+
maxReconnectCount?: number;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## ServiceProgress
|
|
18
|
+
|
|
19
|
+
Progress callback interface for tracking request/response transfer progress.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface ServiceProgress {
|
|
23
|
+
request?: (s: ServiceProgressState) => void;
|
|
24
|
+
response?: (s: ServiceProgressState) => void;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## ServiceProgressState
|
|
29
|
+
|
|
30
|
+
Transfer progress state.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface ServiceProgressState {
|
|
34
|
+
uuid: string;
|
|
35
|
+
totalSize: number;
|
|
36
|
+
completedSize: number;
|
|
37
|
+
}
|
|
38
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/service-client",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.98",
|
|
4
4
|
"description": "Simplysm package - Service module (client)",
|
|
5
5
|
"author": "simplysm",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"sideEffects": false,
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"consola": "^3.4.2",
|
|
23
|
-
"@simplysm/core-common": "13.0.
|
|
24
|
-
"@simplysm/orm-common": "13.0.
|
|
25
|
-
"@simplysm/service-common": "13.0.
|
|
23
|
+
"@simplysm/core-common": "13.0.98",
|
|
24
|
+
"@simplysm/orm-common": "13.0.98",
|
|
25
|
+
"@simplysm/service-common": "13.0.98"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/ws": "^8.18.1",
|