@simplysm/service-client 13.0.97 → 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 ADDED
@@ -0,0 +1,82 @@
1
+ # @simplysm/service-client
2
+
3
+ Service module (client) -- WebSocket-based service client for communicating with `@simplysm/service-server`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @simplysm/service-client
9
+ ```
10
+
11
+ ## Exports
12
+
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
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```typescript
48
+ import { createServiceClient } from "@simplysm/service-client";
49
+
50
+ const client = createServiceClient("my-app", {
51
+ host: "localhost",
52
+ port: 3000,
53
+ });
54
+
55
+ await client.connect();
56
+
57
+ // Authenticate
58
+ await client.auth(token);
59
+
60
+ // Call a service method
61
+ const service = client.getService<MyServiceType>("MyService");
62
+ const result = await service.someMethod("arg1", "arg2");
63
+
64
+ // Listen for events
65
+ await client.addListener(SomeEvent, { id: 1 }, async (data) => {
66
+ // handle event
67
+ });
68
+
69
+ // Upload files
70
+ const results = await client.uploadFile(fileList);
71
+
72
+ // Close
73
+ await client.close();
74
+ ```
75
+
76
+ ## Documentation
77
+
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)
@@ -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
+ ```
@@ -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.97",
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.97",
24
- "@simplysm/orm-common": "13.0.97",
25
- "@simplysm/service-common": "13.0.97"
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",