@simplysm/service-client 14.0.1 → 14.0.5
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 +127 -0
- package/dist/features/file-client.d.ts +3 -2
- package/dist/features/file-client.d.ts.map +1 -1
- package/dist/features/file-client.js +3 -3
- package/dist/features/file-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/protocol/client-protocol-wrapper.d.ts.map +1 -1
- package/dist/protocol/client-protocol-wrapper.js +6 -5
- package/dist/protocol/client-protocol-wrapper.js.map +1 -1
- package/dist/service-client.d.ts +3 -3
- package/dist/service-client.d.ts.map +1 -1
- package/dist/service-client.js +0 -3
- package/dist/service-client.js.map +1 -1
- package/dist/transport/service-transport.d.ts +0 -1
- package/dist/transport/service-transport.d.ts.map +1 -1
- package/dist/transport/service-transport.js +1 -7
- package/dist/transport/service-transport.js.map +1 -1
- package/dist/transport/socket-provider.d.ts.map +1 -1
- package/dist/transport/socket-provider.js +5 -0
- package/dist/transport/socket-provider.js.map +1 -1
- package/dist/types/browser-compat.d.ts +33 -0
- package/dist/types/browser-compat.d.ts.map +1 -0
- package/dist/types/browser-compat.js +17 -0
- package/dist/types/browser-compat.js.map +1 -0
- package/dist/workers/client-protocol.worker.js +1 -1
- package/dist/workers/client-protocol.worker.js.map +1 -1
- package/docs/browser-compat-types.md +77 -0
- package/docs/client-protocol-wrapper.md +35 -0
- package/docs/connection-options.md +21 -0
- package/docs/features.md +147 -0
- package/docs/progress-types.md +37 -0
- package/docs/service-client.md +89 -0
- package/docs/service-transport.md +60 -0
- package/docs/socket-provider.md +72 -0
- package/package.json +6 -5
- package/src/features/file-client.ts +6 -5
- package/src/index.ts +1 -0
- package/src/protocol/client-protocol-wrapper.ts +13 -9
- package/src/service-client.ts +2 -5
- package/src/transport/service-transport.ts +1 -7
- package/src/transport/socket-provider.ts +6 -0
- package/src/types/browser-compat.ts +47 -0
- package/src/workers/client-protocol.worker.ts +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Client Protocol Wrapper
|
|
2
|
+
|
|
3
|
+
## `ClientProtocolWrapper`
|
|
4
|
+
|
|
5
|
+
Client-side protocol encoder/decoder wrapper. Automatically offloads heavy encode/decode operations to a Web Worker when available, falling back to main-thread processing for small messages or when workers are unsupported.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface ClientProtocolWrapper {
|
|
9
|
+
encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
|
|
10
|
+
decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Method | Parameters | Return | Description |
|
|
16
|
+
|--------|-----------|--------|-------------|
|
|
17
|
+
| `encode` | `uuid: string, message: ServiceMessage` | `Promise<{ chunks: Bytes[]; totalSize: number }>` | Encodes a message into binary chunks. Offloads to worker for large messages |
|
|
18
|
+
| `decode` | `bytes: Bytes` | `Promise<ServiceMessageDecodeResult<ServiceMessage>>` | Decodes received binary data. Uses zero-copy transfer for large payloads |
|
|
19
|
+
| `dispose` | none | `void` | Disposes the underlying protocol and worker resolver resources |
|
|
20
|
+
|
|
21
|
+
## `createClientProtocolWrapper`
|
|
22
|
+
|
|
23
|
+
Creates a `ClientProtocolWrapper` from a `ServiceProtocol` instance.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export function createClientProtocolWrapper(
|
|
27
|
+
protocol: ServiceProtocol,
|
|
28
|
+
): ClientProtocolWrapper;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Parameter | Type | Description |
|
|
32
|
+
|-----------|------|-------------|
|
|
33
|
+
| `protocol` | `ServiceProtocol` | The base protocol encoder/decoder (from `@simplysm/service-common`) |
|
|
34
|
+
|
|
35
|
+
Worker offloading threshold: 30KB. Messages below this size are processed on the main thread. The worker is a shared singleton across all `ClientProtocolWrapper` instances. Worker operations that exceed 60 seconds are automatically timed out and rejected.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Connection Options
|
|
2
|
+
|
|
3
|
+
## `ServiceConnectionOptions`
|
|
4
|
+
|
|
5
|
+
Connection options for the service client.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface ServiceConnectionOptions {
|
|
9
|
+
port: number;
|
|
10
|
+
host: string;
|
|
11
|
+
ssl?: boolean;
|
|
12
|
+
maxReconnectCount?: number;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Field | Type | Description |
|
|
17
|
+
|-------|------|-------------|
|
|
18
|
+
| `port` | `number` | Server port |
|
|
19
|
+
| `host` | `string` | Server hostname |
|
|
20
|
+
| `ssl` | `boolean?` | Enable SSL/TLS (uses `wss://` and `https://` when true) |
|
|
21
|
+
| `maxReconnectCount` | `number?` | Maximum reconnection attempts. Set to `0` to disable reconnection and disconnect immediately |
|
package/docs/features.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Event/File/ORM Client
|
|
2
|
+
|
|
3
|
+
## `EventClient`
|
|
4
|
+
|
|
5
|
+
Event subscription client for the service event system.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface EventClient {
|
|
9
|
+
addListener<TInfo, TData>(
|
|
10
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
11
|
+
info: TInfo,
|
|
12
|
+
cb: (data: TData) => PromiseLike<void>,
|
|
13
|
+
): Promise<string>;
|
|
14
|
+
removeListener(key: string): Promise<void>;
|
|
15
|
+
emit<TInfo, TData>(
|
|
16
|
+
eventDef: ServiceEventDef<TInfo, TData>,
|
|
17
|
+
infoSelector: (item: TInfo) => boolean,
|
|
18
|
+
data: TData,
|
|
19
|
+
): Promise<void>;
|
|
20
|
+
resubscribeAll(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Method | Parameters | Return | Description |
|
|
25
|
+
|--------|-----------|--------|-------------|
|
|
26
|
+
| `addListener` | `eventDef: ServiceEventDef<TInfo, TData>, info: TInfo, cb: (data: TData) => PromiseLike<void>` | `Promise<string>` | Subscribes to an event. Returns a listener key (UUID) for later removal |
|
|
27
|
+
| `removeListener` | `key: string` | `Promise<void>` | Unsubscribes by listener key |
|
|
28
|
+
| `emit` | `eventDef: ServiceEventDef<TInfo, TData>, infoSelector: (item: TInfo) => boolean, data: TData` | `Promise<void>` | Queries matching listeners from the server, then emits the event to them |
|
|
29
|
+
| `resubscribeAll` | none | `Promise<void>` | Re-registers all local listeners on the server (used after reconnection) |
|
|
30
|
+
|
|
31
|
+
## `createEventClient`
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
export function createEventClient(transport: ServiceTransport): EventClient;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Parameter | Type | Description |
|
|
38
|
+
|-----------|------|-------------|
|
|
39
|
+
| `transport` | `ServiceTransport` | The service transport for sending event messages |
|
|
40
|
+
|
|
41
|
+
## `FileClient`
|
|
42
|
+
|
|
43
|
+
File upload and download client using HTTP `fetch`.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export interface FileClient {
|
|
47
|
+
download(relPath: string): Promise<Bytes>;
|
|
48
|
+
upload(
|
|
49
|
+
files: File[] | FileCollection | { name: string; data: BlobInput }[],
|
|
50
|
+
authToken: string,
|
|
51
|
+
): Promise<ServiceUploadResult[]>;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Method | Parameters | Return | Description |
|
|
56
|
+
|--------|-----------|--------|-------------|
|
|
57
|
+
| `download` | `relPath: string` | `Promise<Bytes>` | Downloads a file by relative path as `Uint8Array` |
|
|
58
|
+
| `upload` | `files: File[] \| FileCollection \| { name: string; data: BlobInput }[], authToken: string` | `Promise<ServiceUploadResult[]>` | Uploads files via multipart form data with Bearer token authentication |
|
|
59
|
+
|
|
60
|
+
## `createFileClient`
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export function createFileClient(hostUrl: string, clientName: string): FileClient;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Parameter | Type | Description |
|
|
67
|
+
|-----------|------|-------------|
|
|
68
|
+
| `hostUrl` | `string` | Full host URL (e.g., `http://localhost:3000`) |
|
|
69
|
+
| `clientName` | `string` | Client identifier sent in `x-sd-client-name` header |
|
|
70
|
+
|
|
71
|
+
## `OrmConnectOptions`
|
|
72
|
+
|
|
73
|
+
ORM connection options for client-side database context usage.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export interface OrmConnectOptions<TDef extends DbContextDef<any, any, any>> {
|
|
77
|
+
dbContextDef: TDef;
|
|
78
|
+
connOpt: DbConnOptions & { configName: string };
|
|
79
|
+
dbContextOpt?: {
|
|
80
|
+
database: string;
|
|
81
|
+
schema: string;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Field | Type | Description |
|
|
87
|
+
|-------|------|-------------|
|
|
88
|
+
| `dbContextDef` | `TDef` | Database context definition |
|
|
89
|
+
| `connOpt` | `DbConnOptions & { configName: string }` | Connection options with required config name |
|
|
90
|
+
| `dbContextOpt` | `{ database: string; schema: string }?` | Optional database/schema override |
|
|
91
|
+
|
|
92
|
+
## `OrmClientConnector`
|
|
93
|
+
|
|
94
|
+
ORM client connector that opens a database context over the service RPC layer.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export interface OrmClientConnector {
|
|
98
|
+
connect<TDef extends DbContextDef<any, any, any>, R>(
|
|
99
|
+
config: OrmConnectOptions<TDef>,
|
|
100
|
+
callback: (db: DbContextInstance<TDef>) => Promise<R> | R,
|
|
101
|
+
): Promise<R>;
|
|
102
|
+
connectWithoutTransaction<TDef extends DbContextDef<any, any, any>, R>(
|
|
103
|
+
config: OrmConnectOptions<TDef>,
|
|
104
|
+
callback: (db: DbContextInstance<TDef>) => Promise<R> | R,
|
|
105
|
+
): Promise<R>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Method | Parameters | Return | Description |
|
|
110
|
+
|--------|-----------|--------|-------------|
|
|
111
|
+
| `connect` | `config: OrmConnectOptions<TDef>, callback: (db: DbContextInstance<TDef>) => Promise<R> \| R` | `Promise<R>` | Opens a DB context with automatic transaction (begin/commit/rollback). Wraps foreign key constraint errors with a user-friendly message |
|
|
112
|
+
| `connectWithoutTransaction` | `config: OrmConnectOptions<TDef>, callback: (db: DbContextInstance<TDef>) => Promise<R> \| R` | `Promise<R>` | Opens a DB context without automatic transaction management |
|
|
113
|
+
|
|
114
|
+
## `createOrmClientConnector`
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
export function createOrmClientConnector(serviceClient: ServiceClient): OrmClientConnector;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Parameter | Type | Description |
|
|
121
|
+
|-----------|------|-------------|
|
|
122
|
+
| `serviceClient` | `ServiceClient` | The service client instance for RPC communication |
|
|
123
|
+
|
|
124
|
+
## `OrmClientDbContextExecutor`
|
|
125
|
+
|
|
126
|
+
Implements `DbContextExecutor` (from `@simplysm/orm-common`) for client-side ORM usage over the service RPC layer. Proxies all database operations through the remote `OrmService`.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
export class OrmClientDbContextExecutor implements DbContextExecutor {
|
|
130
|
+
constructor(
|
|
131
|
+
client: ServiceClient,
|
|
132
|
+
opt: DbConnOptions & { configName: string },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Method | Parameters | Return | Description |
|
|
138
|
+
|--------|-----------|--------|-------------|
|
|
139
|
+
| `getInfo` | none | `Promise<{ dialect: Dialect; database?: string; schema?: string }>` | Gets database info via remote ORM service |
|
|
140
|
+
| `connect` | none | `Promise<void>` | Opens a remote connection |
|
|
141
|
+
| `beginTransaction` | `isolationLevel?: IsolationLevel` | `Promise<void>` | Begins a remote transaction |
|
|
142
|
+
| `commitTransaction` | none | `Promise<void>` | Commits the remote transaction |
|
|
143
|
+
| `rollbackTransaction` | none | `Promise<void>` | Rolls back the remote transaction |
|
|
144
|
+
| `close` | none | `Promise<void>` | Closes the remote connection |
|
|
145
|
+
| `executeDefs` | `defs: QueryDef[], options?: (ResultMeta \| undefined)[]` | `Promise<T[][]>` | Executes query definitions via remote ORM service |
|
|
146
|
+
| `executeParametrized` | `query: string, params?: unknown[]` | `Promise<unknown[][]>` | Executes a parameterized query via remote ORM service |
|
|
147
|
+
| `bulkInsert` | `tableName: string, columnDefs: Record<string, ColumnMeta>, records: Record<string, unknown>[]` | `Promise<void>` | Performs bulk insert via remote ORM service |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Progress Types
|
|
2
|
+
|
|
3
|
+
## `ServiceProgress`
|
|
4
|
+
|
|
5
|
+
Callback hooks for monitoring message progress at different stages.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface ServiceProgress {
|
|
9
|
+
request?: (s: ServiceProgressState) => void;
|
|
10
|
+
response?: (s: ServiceProgressState) => void;
|
|
11
|
+
server?: (s: ServiceProgressState) => void;
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Field | Type | Description |
|
|
16
|
+
|-------|------|-------------|
|
|
17
|
+
| `request` | `(s: ServiceProgressState) => void` | Called during request encoding/sending progress |
|
|
18
|
+
| `response` | `(s: ServiceProgressState) => void` | Called during response decoding progress |
|
|
19
|
+
| `server` | `(s: ServiceProgressState) => void` | Called when server reports chunk receive progress |
|
|
20
|
+
|
|
21
|
+
## `ServiceProgressState`
|
|
22
|
+
|
|
23
|
+
Progress state for a single message transfer.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export interface ServiceProgressState {
|
|
27
|
+
uuid: string;
|
|
28
|
+
totalSize: number;
|
|
29
|
+
completedSize: number;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Field | Type | Description |
|
|
34
|
+
|-------|------|-------------|
|
|
35
|
+
| `uuid` | `string` | Message UUID |
|
|
36
|
+
| `totalSize` | `number` | Total message size in bytes |
|
|
37
|
+
| `completedSize` | `number` | Bytes transferred so far |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Main ServiceClient
|
|
2
|
+
|
|
3
|
+
## `ServiceClient`
|
|
4
|
+
|
|
5
|
+
Main service client class. Extends `EventEmitter` with progress and state events. Composes `SocketProvider`, `ServiceTransport`, `EventClient`, `FileClient`, and `ClientProtocolWrapper` internally.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export class ServiceClient extends EventEmitter<{
|
|
9
|
+
"request-progress": ServiceProgressState;
|
|
10
|
+
"response-progress": ServiceProgressState;
|
|
11
|
+
"server-progress": ServiceProgressState;
|
|
12
|
+
"state": "connected" | "closed" | "reconnecting";
|
|
13
|
+
}> {
|
|
14
|
+
constructor(name: string, options: ServiceConnectionOptions);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Events
|
|
19
|
+
|
|
20
|
+
| Event | Data Type | Description |
|
|
21
|
+
|-------|-----------|-------------|
|
|
22
|
+
| `request-progress` | `ServiceProgressState` | Request sending progress (chunked messages) |
|
|
23
|
+
| `response-progress` | `ServiceProgressState` | Response receiving progress (chunked messages) |
|
|
24
|
+
| `server-progress` | `ServiceProgressState` | Server-side chunk reception progress |
|
|
25
|
+
| `state` | `"connected" \| "closed" \| "reconnecting"` | Connection state change |
|
|
26
|
+
|
|
27
|
+
### Properties
|
|
28
|
+
|
|
29
|
+
| Property | Type | Description |
|
|
30
|
+
|----------|------|-------------|
|
|
31
|
+
| `name` | `string` (readonly) | Client name |
|
|
32
|
+
| `options` | `ServiceConnectionOptions` (readonly) | Connection options |
|
|
33
|
+
| `connected` | `boolean` (readonly, getter) | Whether the client is currently connected |
|
|
34
|
+
| `hostUrl` | `string` (readonly, getter) | Full host URL computed from options (e.g., `https://host:port`) |
|
|
35
|
+
|
|
36
|
+
### Methods
|
|
37
|
+
|
|
38
|
+
| Method | Signature | Description |
|
|
39
|
+
|--------|-----------|-------------|
|
|
40
|
+
| `getService` | `getService<TService>(serviceName: string): ServiceProxy<TService>` | Returns a type-safe proxy that maps method calls to RPC requests |
|
|
41
|
+
| `connect` | `connect(): Promise<void>` | Opens the WebSocket connection |
|
|
42
|
+
| `close` | `close(): Promise<void>` | Closes the connection and disposes protocol resources |
|
|
43
|
+
| `send` | `send(serviceName: string, methodName: string, params: unknown[], progress?: ServiceProgress): Promise<unknown>` | Sends an RPC call directly |
|
|
44
|
+
| `auth` | `auth(token: string): Promise<void>` | Authenticates with a JWT token. Token is cached for reconnection |
|
|
45
|
+
| `addListener` | `addListener<TInfo, TData>(eventDef: ServiceEventDef<TInfo, TData>, info: TInfo, cb: (data: TData) => PromiseLike<void>): Promise<string>` | Subscribes to an event. Returns listener key. Throws if not connected |
|
|
46
|
+
| `removeListener` | `removeListener(key: string): Promise<void>` | Unsubscribes from an event by key |
|
|
47
|
+
| `emitEvent` | `emitEvent<TInfo, TData>(eventDef: ServiceEventDef<TInfo, TData>, infoSelector: (item: TInfo) => boolean, data: TData): Promise<void>` | Emits an event to matching listeners |
|
|
48
|
+
| `uploadFile` | `uploadFile(files: File[] \| FileCollection \| { name: string; data: BlobInput }[]): Promise<ServiceUploadResult[]>` | Uploads files. Requires prior `auth()` call |
|
|
49
|
+
| `downloadFileBuffer` | `downloadFileBuffer(relPath: string): Promise<Bytes>` | Downloads a file as `Uint8Array` |
|
|
50
|
+
|
|
51
|
+
### Reconnection Behavior
|
|
52
|
+
|
|
53
|
+
On reconnection (`state === "connected"` after a disconnect):
|
|
54
|
+
1. Re-authenticates with the cached token (if `auth()` was previously called)
|
|
55
|
+
2. Re-subscribes all event listeners via `EventClient.resubscribeAll()`
|
|
56
|
+
|
|
57
|
+
## `ServiceProxy`
|
|
58
|
+
|
|
59
|
+
Type utility that wraps all methods of a service interface to return `Promise<Awaited<R>>`.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export type ServiceProxy<TService> = {
|
|
63
|
+
[K in keyof TService]: TService[K] extends (...args: infer P) => infer R
|
|
64
|
+
? (...args: P) => Promise<Awaited<R>>
|
|
65
|
+
: never;
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Non-function properties are mapped to `never`.
|
|
70
|
+
|
|
71
|
+
## `createServiceClient`
|
|
72
|
+
|
|
73
|
+
Factory function to create a `ServiceClient` instance.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export function createServiceClient(
|
|
77
|
+
name: string,
|
|
78
|
+
options: ServiceConnectionOptions,
|
|
79
|
+
): ServiceClient;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Parameter | Type | Description |
|
|
83
|
+
|-----------|------|-------------|
|
|
84
|
+
| `name` | `string` | Client identifier name |
|
|
85
|
+
| `options` | `ServiceConnectionOptions` | Connection options (host, port, SSL, maxReconnectCount) |
|
|
86
|
+
|
|
87
|
+
**Returns:** `ServiceClient`
|
|
88
|
+
|
|
89
|
+
Default `maxReconnectCount` is `10` if not specified in options.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Service Transport
|
|
2
|
+
|
|
3
|
+
## `ServiceTransportEvents`
|
|
4
|
+
|
|
5
|
+
Event map for the service transport.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface ServiceTransportEvents {
|
|
9
|
+
event: { keys: string[]; data: unknown };
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Event | Data Type | Description |
|
|
14
|
+
|-------|-----------|-------------|
|
|
15
|
+
| `event` | `{ keys: string[]; data: unknown }` | Server-side event broadcast received |
|
|
16
|
+
|
|
17
|
+
## `ServiceTransport`
|
|
18
|
+
|
|
19
|
+
Service-level message transport built on top of `SocketProvider` and `ClientProtocolWrapper`. Manages pending request/response correlation via UUIDs, handles progress notifications, and dispatches events.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
export interface ServiceTransport {
|
|
23
|
+
on<K extends keyof ServiceTransportEvents & string>(
|
|
24
|
+
type: K,
|
|
25
|
+
listener: (data: ServiceTransportEvents[K]) => void,
|
|
26
|
+
): void;
|
|
27
|
+
off<K extends keyof ServiceTransportEvents & string>(
|
|
28
|
+
type: K,
|
|
29
|
+
listener: (data: ServiceTransportEvents[K]) => void,
|
|
30
|
+
): void;
|
|
31
|
+
send(message: ServiceClientMessage, progress?: ServiceProgress): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Method | Parameters | Return | Description |
|
|
36
|
+
|--------|-----------|--------|-------------|
|
|
37
|
+
| `on` | `type: K, listener` | `void` | Subscribe to transport events |
|
|
38
|
+
| `off` | `type: K, listener` | `void` | Unsubscribe from transport events |
|
|
39
|
+
| `send` | `message: ServiceClientMessage, progress?: ServiceProgress` | `Promise<unknown>` | Sends a service message and awaits the server response. Returns the response body |
|
|
40
|
+
|
|
41
|
+
## `createServiceTransport`
|
|
42
|
+
|
|
43
|
+
Creates a `ServiceTransport` instance.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export function createServiceTransport(
|
|
47
|
+
socket: SocketProvider,
|
|
48
|
+
protocol: ClientProtocolWrapper,
|
|
49
|
+
): ServiceTransport;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Parameter | Type | Description |
|
|
53
|
+
|-----------|------|-------------|
|
|
54
|
+
| `socket` | `SocketProvider` | The socket provider for sending/receiving binary data |
|
|
55
|
+
| `protocol` | `ClientProtocolWrapper` | The protocol wrapper for encoding/decoding messages |
|
|
56
|
+
|
|
57
|
+
Behavior:
|
|
58
|
+
- Generates a UUID per `send()` call and registers a pending resolver
|
|
59
|
+
- On socket disconnect (`closed`/`reconnecting`), rejects all pending requests
|
|
60
|
+
- Routes server messages: `response` resolves, `error` rejects, `evt:on` emits event, `progress` invokes progress callbacks
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Socket Provider
|
|
2
|
+
|
|
3
|
+
## `SocketProviderEvents`
|
|
4
|
+
|
|
5
|
+
Event map for the socket provider.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export interface SocketProviderEvents {
|
|
9
|
+
message: Bytes;
|
|
10
|
+
state: "connected" | "closed" | "reconnecting";
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Event | Data Type | Description |
|
|
15
|
+
|-------|-----------|-------------|
|
|
16
|
+
| `message` | `Bytes` | Raw binary message received from server |
|
|
17
|
+
| `state` | `"connected" \| "closed" \| "reconnecting"` | Connection state change |
|
|
18
|
+
|
|
19
|
+
## `SocketProvider`
|
|
20
|
+
|
|
21
|
+
Low-level WebSocket abstraction with automatic reconnection and heartbeat keepalive. Works in both browser and Node.js environments (uses `ws` package as polyfill in Node.js).
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
export interface SocketProvider {
|
|
25
|
+
readonly clientName: string;
|
|
26
|
+
readonly connected: boolean;
|
|
27
|
+
on<K extends keyof SocketProviderEvents & string>(
|
|
28
|
+
type: K,
|
|
29
|
+
listener: (data: SocketProviderEvents[K]) => void,
|
|
30
|
+
): void;
|
|
31
|
+
off<K extends keyof SocketProviderEvents & string>(
|
|
32
|
+
type: K,
|
|
33
|
+
listener: (data: SocketProviderEvents[K]) => void,
|
|
34
|
+
): void;
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
send(data: Bytes): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Member | Kind | Description |
|
|
42
|
+
|--------|------|-------------|
|
|
43
|
+
| `clientName` | property (readonly) | Client identifier name |
|
|
44
|
+
| `connected` | property (readonly) | Whether currently connected |
|
|
45
|
+
| `on` | method | Subscribe to an event |
|
|
46
|
+
| `off` | method | Unsubscribe from an event |
|
|
47
|
+
| `connect` | method | Opens the WebSocket connection. Throws on initial connection failure |
|
|
48
|
+
| `close` | method | Closes the connection gracefully |
|
|
49
|
+
| `send` | method | Sends binary data. Waits for connection if not yet open |
|
|
50
|
+
|
|
51
|
+
## `createSocketProvider`
|
|
52
|
+
|
|
53
|
+
Creates a `SocketProvider` instance with auto-reconnect and heartbeat.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
export function createSocketProvider(
|
|
57
|
+
url: string,
|
|
58
|
+
clientName: string,
|
|
59
|
+
maxReconnectCount: number,
|
|
60
|
+
): SocketProvider;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Parameter | Type | Description |
|
|
64
|
+
|-----------|------|-------------|
|
|
65
|
+
| `url` | `string` | WebSocket URL (e.g., `ws://localhost:3000/ws`) |
|
|
66
|
+
| `clientName` | `string` | Client identifier |
|
|
67
|
+
| `maxReconnectCount` | `number` | Maximum reconnection attempts |
|
|
68
|
+
|
|
69
|
+
Internal constants:
|
|
70
|
+
- Heartbeat timeout: 30 seconds
|
|
71
|
+
- Heartbeat interval: 5 seconds (sends ping `0x01`, expects pong `0x02`)
|
|
72
|
+
- Reconnect delay: 3 seconds between attempts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/service-client",
|
|
3
|
-
"version": "14.0.
|
|
3
|
+
"version": "14.0.5",
|
|
4
4
|
"description": "심플리즘 패키지 - 서비스 (client)",
|
|
5
5
|
"author": "심플리즘",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"src"
|
|
17
|
+
"src",
|
|
18
|
+
"docs"
|
|
18
19
|
],
|
|
19
20
|
"sideEffects": false,
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"consola": "^3.4.2",
|
|
22
|
-
"@simplysm/core-common": "14.0.
|
|
23
|
-
"@simplysm/orm-common": "14.0.
|
|
24
|
-
"@simplysm/service-common": "14.0.
|
|
23
|
+
"@simplysm/core-common": "14.0.5",
|
|
24
|
+
"@simplysm/orm-common": "14.0.5",
|
|
25
|
+
"@simplysm/service-common": "14.0.5"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/ws": "^8.18.1",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Bytes } from "@simplysm/core-common";
|
|
2
2
|
import type { ServiceUploadResult } from "@simplysm/service-common";
|
|
3
|
+
import type { BlobInput, FileCollection } from "../types/browser-compat";
|
|
3
4
|
|
|
4
5
|
export interface FileClient {
|
|
5
6
|
download(relPath: string): Promise<Bytes>;
|
|
6
7
|
upload(
|
|
7
|
-
files: File[] |
|
|
8
|
+
files: File[] | FileCollection | { name: string; data: BlobInput }[],
|
|
8
9
|
authToken: string,
|
|
9
10
|
): Promise<ServiceUploadResult[]>;
|
|
10
11
|
}
|
|
@@ -24,13 +25,13 @@ export function createFileClient(hostUrl: string, clientName: string): FileClien
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
async function upload(
|
|
27
|
-
files: File[] |
|
|
28
|
+
files: File[] | FileCollection | { name: string; data: BlobInput }[],
|
|
28
29
|
authToken: string,
|
|
29
30
|
): Promise<ServiceUploadResult[]> {
|
|
30
31
|
const formData = new FormData();
|
|
31
|
-
const
|
|
32
|
+
const fileArr = Array.isArray(files) ? files : Array.from(files);
|
|
32
33
|
|
|
33
|
-
for (const file of
|
|
34
|
+
for (const file of fileArr) {
|
|
34
35
|
if ("data" in file) {
|
|
35
36
|
// 커스텀 객체 ({ name, data })
|
|
36
37
|
const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
|
|
@@ -54,7 +55,7 @@ export function createFileClient(hostUrl: string, clientName: string): FileClien
|
|
|
54
55
|
throw new Error(`업로드 실패: ${res.statusText}`);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
return res.json();
|
|
58
|
+
return (await res.json()) as ServiceUploadResult[];
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
return {
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
ServiceMessage,
|
|
6
6
|
ServiceProtocol,
|
|
7
7
|
} from "@simplysm/service-common";
|
|
8
|
+
import type { WorkerLike } from "../types/browser-compat";
|
|
9
|
+
import { isWorkerSupported, createBrowserWorker } from "../types/browser-compat";
|
|
8
10
|
|
|
9
11
|
export interface ClientProtocolWrapper {
|
|
10
12
|
encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
|
|
@@ -13,7 +15,7 @@ export interface ClientProtocolWrapper {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
// 공유 worker 상태 (싱글턴 패턴)
|
|
16
|
-
let worker:
|
|
18
|
+
let worker: WorkerLike | undefined;
|
|
17
19
|
const workerResolvers = new LazyGcMap<
|
|
18
20
|
string,
|
|
19
21
|
{ resolve: (res: unknown) => void; reject: (err: Error) => void }
|
|
@@ -30,12 +32,12 @@ let workerAvailable: boolean | undefined;
|
|
|
30
32
|
|
|
31
33
|
function isWorkerAvailable(): boolean {
|
|
32
34
|
if (workerAvailable === undefined) {
|
|
33
|
-
workerAvailable =
|
|
35
|
+
workerAvailable = isWorkerSupported();
|
|
34
36
|
}
|
|
35
37
|
return workerAvailable;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function getWorker():
|
|
40
|
+
function getWorker(): WorkerLike | undefined {
|
|
39
41
|
if (!isWorkerAvailable()) {
|
|
40
42
|
return undefined;
|
|
41
43
|
}
|
|
@@ -43,9 +45,11 @@ function getWorker(): Worker | undefined {
|
|
|
43
45
|
if (!worker) {
|
|
44
46
|
// 모던 번들러 (Vite/Esbuild/Webpack)가 이 구문을 사용하여 Worker를 별도 파일로 분리/로드함
|
|
45
47
|
// 참고: import.meta.resolve 대신 상대 경로 사용 (Vite 호환성)
|
|
46
|
-
worker =
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
worker = createBrowserWorker(
|
|
49
|
+
new URL("../workers/client-protocol.worker.ts", import.meta.url),
|
|
50
|
+
{ type: "module" },
|
|
51
|
+
);
|
|
52
|
+
if (worker == null) return undefined;
|
|
49
53
|
|
|
50
54
|
worker.onmessage = (event: MessageEvent) => {
|
|
51
55
|
const { id, type, result, error } = event.data as {
|
|
@@ -78,14 +82,14 @@ function getWorker(): Worker | undefined {
|
|
|
78
82
|
async function runWorker(
|
|
79
83
|
type: "encode" | "decode",
|
|
80
84
|
data: unknown,
|
|
81
|
-
transferables:
|
|
85
|
+
transferables: ArrayBuffer[] = [],
|
|
82
86
|
): Promise<unknown> {
|
|
83
87
|
return new Promise((resolve, reject) => {
|
|
84
88
|
const id = Uuid.generate().toString();
|
|
85
89
|
|
|
86
90
|
workerResolvers.set(id, { resolve, reject });
|
|
87
91
|
// workerAvailable 확인 후 호출되므로 worker는 항상 존재
|
|
88
|
-
getWorker()!.postMessage({ id, type, data },
|
|
92
|
+
getWorker()!.postMessage({ id, type, data }, transferables);
|
|
89
93
|
});
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -135,7 +139,7 @@ export function createClientProtocolWrapper(protocol: ServiceProtocol): ClientPr
|
|
|
135
139
|
|
|
136
140
|
// [Worker]
|
|
137
141
|
// Zero-copy 전송 (버퍼 소유권이 Worker로 이동)
|
|
138
|
-
const rawResult = await runWorker("decode", bytes, [bytes.buffer]);
|
|
142
|
+
const rawResult = await runWorker("decode", bytes, [bytes.buffer as ArrayBuffer]);
|
|
139
143
|
|
|
140
144
|
// Worker의 plain object 결과에서 클래스 인스턴스 복원 (DateTime 등)
|
|
141
145
|
return transfer.decode(rawResult) as ServiceMessageDecodeResult<ServiceMessage>;
|
package/src/service-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { EventEmitter } from "@simplysm/core-common";
|
|
|
3
3
|
import type { ServiceEventDef } from "@simplysm/service-common";
|
|
4
4
|
import { createServiceProtocol } from "@simplysm/service-common";
|
|
5
5
|
|
|
6
|
+
import type { BlobInput, FileCollection } from "./types/browser-compat";
|
|
6
7
|
import type { ServiceConnectionOptions } from "./types/connection-options";
|
|
7
8
|
import type { ServiceProgress, ServiceProgressState } from "./types/progress.types";
|
|
8
9
|
import { createServiceTransport, type ServiceTransport } from "./transport/service-transport";
|
|
@@ -18,7 +19,6 @@ interface ServiceClientEvents {
|
|
|
18
19
|
"response-progress": ServiceProgressState;
|
|
19
20
|
"server-progress": ServiceProgressState;
|
|
20
21
|
"state": "connected" | "closed" | "reconnecting";
|
|
21
|
-
"reload": Set<string>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
@@ -74,9 +74,6 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
this._transport.on("reload", (changedFiles) => {
|
|
78
|
-
this.emit("reload", changedFiles);
|
|
79
|
-
});
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
// 타입 안전성을 위한 프록시 생성 메서드
|
|
@@ -154,7 +151,7 @@ export class ServiceClient extends EventEmitter<ServiceClientEvents> {
|
|
|
154
151
|
await this._eventClient.emit(eventDef, infoSelector, data);
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
async uploadFile(files: File[] |
|
|
154
|
+
async uploadFile(files: File[] | FileCollection | { name: string; data: BlobInput }[]) {
|
|
158
155
|
if (this._authToken == null) {
|
|
159
156
|
throw new Error(
|
|
160
157
|
"인증 토큰이 없습니다. 파일 업로드 전에 auth()를 호출하여 인증해 주세요.",
|
|
@@ -10,7 +10,6 @@ import type { ServiceProgress } from "../types/progress.types";
|
|
|
10
10
|
import type { SocketProvider } from "./socket-provider";
|
|
11
11
|
|
|
12
12
|
export interface ServiceTransportEvents {
|
|
13
|
-
reload: Set<string>;
|
|
14
13
|
event: { keys: string[]; data: unknown };
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -61,7 +60,7 @@ export function createServiceTransport(
|
|
|
61
60
|
pendingRequests.set(uuid, { resolve, reject, progress });
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
// Promise가 고아가 되었을 때 unhandled rejection 방지 (예:
|
|
63
|
+
// Promise가 고아가 되었을 때 unhandled rejection 방지 (예: 소켓 연결 끊김 시)
|
|
65
64
|
responsePromise.catch(() => {});
|
|
66
65
|
|
|
67
66
|
// 요청 전송
|
|
@@ -139,11 +138,6 @@ export function createServiceTransport(
|
|
|
139
138
|
pendingRequests.delete(decoded.uuid);
|
|
140
139
|
|
|
141
140
|
listenerInfo?.reject(toError(decoded.message.body));
|
|
142
|
-
} else if (decoded.message.name === "reload") {
|
|
143
|
-
const body = decoded.message.body as { clientName: string; changedFileSet: Set<string> };
|
|
144
|
-
if (socket.clientName === body.clientName) {
|
|
145
|
-
emitter.emit("reload", body.changedFileSet);
|
|
146
|
-
}
|
|
147
141
|
} else if (decoded.message.name === "evt:on") {
|
|
148
142
|
const body = decoded.message.body as { keys: string[]; data: unknown };
|
|
149
143
|
emitter.emit("event", { keys: body.keys, data: body.data });
|