@simplysm/service-server 13.0.69 → 13.0.70
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 +48 -249
- package/dist/auth/jwt-manager.js +2 -2
- package/dist/auth/jwt-manager.js.map +1 -1
- package/dist/core/define-service.js +2 -2
- package/dist/core/define-service.js.map +1 -1
- package/dist/core/service-executor.js +5 -5
- package/dist/core/service-executor.js.map +1 -1
- package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
- package/dist/legacy/v1-auto-update-handler.js +2 -2
- package/dist/legacy/v1-auto-update-handler.js.map +1 -1
- package/dist/service-server.js +11 -11
- package/dist/service-server.js.map +1 -1
- package/dist/services/auto-update-service.js +1 -1
- package/dist/services/auto-update-service.js.map +1 -1
- package/dist/services/orm-service.js +6 -6
- package/dist/services/orm-service.js.map +1 -1
- package/dist/transport/http/http-request-handler.js +1 -1
- package/dist/transport/http/http-request-handler.js.map +1 -1
- package/dist/transport/http/static-file-handler.js +3 -3
- package/dist/transport/http/upload-handler.js +2 -2
- package/dist/transport/http/upload-handler.js.map +1 -1
- package/dist/transport/socket/service-socket.js +2 -2
- package/dist/transport/socket/service-socket.js.map +1 -1
- package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
- package/dist/transport/socket/websocket-handler.js +11 -9
- package/dist/transport/socket/websocket-handler.js.map +1 -1
- package/dist/utils/config-manager.js +7 -7
- package/dist/utils/config-manager.js.map +1 -1
- package/package.json +9 -9
- package/src/auth/jwt-manager.ts +2 -2
- package/src/core/define-service.ts +2 -2
- package/src/core/service-executor.ts +13 -13
- package/src/legacy/v1-auto-update-handler.ts +8 -8
- package/src/service-server.ts +28 -28
- package/src/services/auto-update-service.ts +1 -1
- package/src/services/orm-service.ts +6 -6
- package/src/transport/http/http-request-handler.ts +5 -5
- package/src/transport/http/static-file-handler.ts +7 -7
- package/src/transport/http/upload-handler.ts +3 -3
- package/src/transport/socket/service-socket.ts +4 -4
- package/src/transport/socket/websocket-handler.ts +12 -10
- package/src/utils/config-manager.ts +11 -11
- package/tests/define-service.spec.ts +85 -0
- package/tests/orm-service.spec.ts +83 -0
- package/tests/service-executor.spec.ts +114 -0
- package/docs/authentication.md +0 -114
- package/docs/built-in-services.md +0 -100
- package/docs/server.md +0 -374
- package/docs/transport.md +0 -273
package/docs/server.md
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
# ServiceServer
|
|
2
|
-
|
|
3
|
-
## ServiceServer
|
|
4
|
-
|
|
5
|
-
`ServiceServer<TAuthInfo>` extends `EventEmitter` and is the main entry point for creating a server.
|
|
6
|
-
|
|
7
|
-
**Properties:**
|
|
8
|
-
|
|
9
|
-
| Property | Type | Description |
|
|
10
|
-
|----------|------|------|
|
|
11
|
-
| `options` | `ServiceServerOptions` | Server configuration (read-only, passed via constructor) |
|
|
12
|
-
| `isOpen` | `boolean` | Whether the server is currently listening |
|
|
13
|
-
| `fastify` | `FastifyInstance` | Underlying Fastify instance (read-only, for advanced use) |
|
|
14
|
-
|
|
15
|
-
**Methods:**
|
|
16
|
-
|
|
17
|
-
| Method | Returns | Description |
|
|
18
|
-
|--------|---------|------|
|
|
19
|
-
| `listen()` | `Promise<void>` | Register all plugins/routes and start listening |
|
|
20
|
-
| `close()` | `Promise<void>` | Close all WebSocket connections and shut down the server |
|
|
21
|
-
| `generateAuthToken(payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
|
|
22
|
-
| `verifyAuthToken(token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify and decode a JWT token |
|
|
23
|
-
| `emitEvent(eventDef, infoSelector, data)` | `Promise<void>` | Publish an event to matching WebSocket clients |
|
|
24
|
-
| `broadcastReload(clientName, changedFileSet)` | `Promise<void>` | Send a reload command to all connected clients |
|
|
25
|
-
|
|
26
|
-
**Events:**
|
|
27
|
-
|
|
28
|
-
| Event | Payload | Description |
|
|
29
|
-
|-------|---------|------|
|
|
30
|
-
| `ready` | `void` | Emitted when the server starts listening |
|
|
31
|
-
| `close` | `void` | Emitted when the server is closed |
|
|
32
|
-
|
|
33
|
-
## createServiceServer
|
|
34
|
-
|
|
35
|
-
Factory function for creating a `ServiceServer` instance:
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { createServiceServer } from "@simplysm/service-server";
|
|
39
|
-
|
|
40
|
-
const server = createServiceServer({
|
|
41
|
-
port: 8080,
|
|
42
|
-
rootPath: "/app/data",
|
|
43
|
-
auth: { jwtSecret: "my-secret-key" },
|
|
44
|
-
services: [MyService],
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Server Options (`ServiceServerOptions`)
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
import type { ServiceServerOptions } from "@simplysm/service-server";
|
|
52
|
-
|
|
53
|
-
interface ServiceServerOptions {
|
|
54
|
-
/** Server root path (base directory for static files and config files) */
|
|
55
|
-
rootPath: string;
|
|
56
|
-
/** Listen port */
|
|
57
|
-
port: number;
|
|
58
|
-
/** SSL/TLS config (enables HTTPS) */
|
|
59
|
-
ssl?: {
|
|
60
|
-
pfxBytes: Uint8Array;
|
|
61
|
-
passphrase: string;
|
|
62
|
-
};
|
|
63
|
-
/** JWT authentication config */
|
|
64
|
-
auth?: {
|
|
65
|
-
jwtSecret: string;
|
|
66
|
-
};
|
|
67
|
-
/** List of service definitions to register */
|
|
68
|
-
services: ServiceDefinition[];
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
The following structure is expected under `rootPath`:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
rootPath/
|
|
76
|
-
.config.json # Root config file
|
|
77
|
-
www/ # Static file root
|
|
78
|
-
uploads/ # Upload file storage directory
|
|
79
|
-
{clientName}/ # Per-client directory
|
|
80
|
-
.config.json # Per-client config file
|
|
81
|
-
index.html
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## SSL/HTTPS Server
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { createServiceServer } from "@simplysm/service-server";
|
|
88
|
-
import { fsReadFile } from "@simplysm/core-node";
|
|
89
|
-
|
|
90
|
-
const pfxBytes = await fsReadFile("/path/to/cert.pfx");
|
|
91
|
-
|
|
92
|
-
const server = createServiceServer({
|
|
93
|
-
port: 443,
|
|
94
|
-
rootPath: "/app/data",
|
|
95
|
-
ssl: {
|
|
96
|
-
pfxBytes,
|
|
97
|
-
passphrase: "certificate-password",
|
|
98
|
-
},
|
|
99
|
-
auth: { jwtSecret: "my-secret-key" },
|
|
100
|
-
services: [],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
await server.listen();
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Custom Service Definition
|
|
107
|
-
|
|
108
|
-
Define services using the `defineService` function. Service methods are called via RPC from the client.
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
import { defineService } from "@simplysm/service-server";
|
|
112
|
-
|
|
113
|
-
export const MyService = defineService("My", (ctx) => ({
|
|
114
|
-
hello: async (name: string): Promise<string> => {
|
|
115
|
-
return `Hello, ${name}!`;
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
getServerTime: async (): Promise<Date> => {
|
|
119
|
-
return new Date();
|
|
120
|
-
},
|
|
121
|
-
}));
|
|
122
|
-
|
|
123
|
-
// Export type for client-side type sharing
|
|
124
|
-
export type MyServiceMethods = import("@simplysm/service-server").ServiceMethods<typeof MyService>;
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### ServiceContext
|
|
128
|
-
|
|
129
|
-
The `ctx` parameter provides access to server resources within service methods.
|
|
130
|
-
|
|
131
|
-
| Property | Type | Description |
|
|
132
|
-
|----------|------|------|
|
|
133
|
-
| `ctx.server` | `ServiceServer<TAuthInfo>` | Server instance reference |
|
|
134
|
-
| `ctx.socket` | `ServiceSocket \| undefined` | WebSocket connection (`undefined` for HTTP calls) |
|
|
135
|
-
| `ctx.http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> } \| undefined` | HTTP request context |
|
|
136
|
-
| `ctx.legacy` | `{ clientName?: string } \| undefined` | V1 legacy context (auto-update only) |
|
|
137
|
-
| `ctx.authInfo` | `TAuthInfo \| undefined` | Authenticated user's custom data (from JWT `data` field) |
|
|
138
|
-
| `ctx.clientName` | `string \| undefined` | Client app name (validated against path traversal) |
|
|
139
|
-
| `ctx.clientPath` | `string \| undefined` | Resolved per-client directory path (`rootPath/www/{clientName}`) |
|
|
140
|
-
|
|
141
|
-
### ServiceContext Methods
|
|
142
|
-
|
|
143
|
-
| Method | Returns | Description |
|
|
144
|
-
|--------|---------|------|
|
|
145
|
-
| `ctx.getConfig<T>(section)` | `Promise<T>` | Read a section from `.config.json` (root + client configs merged) |
|
|
146
|
-
|
|
147
|
-
### createServiceContext
|
|
148
|
-
|
|
149
|
-
Factory function that creates a `ServiceContext` object. Used internally by the server and exported for advanced use cases (e.g., calling services programmatically without an active connection).
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
import { createServiceContext } from "@simplysm/service-server";
|
|
153
|
-
|
|
154
|
-
const ctx = createServiceContext(server, socket, httpContext, legacyContext);
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
| Parameter | Type | Description |
|
|
158
|
-
|-----------|------|------|
|
|
159
|
-
| `server` | `ServiceServer<TAuthInfo>` | The server instance |
|
|
160
|
-
| `socket` | `ServiceSocket \| undefined` | WebSocket connection, or `undefined` for HTTP/legacy |
|
|
161
|
-
| `http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> } \| undefined` | HTTP request context |
|
|
162
|
-
| `legacy` | `{ clientName?: string } \| undefined` | V1 legacy context (auto-update only) |
|
|
163
|
-
|
|
164
|
-
## ServiceDefinition
|
|
165
|
-
|
|
166
|
-
Type describing a registered service. Created by `defineService()`.
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
import type { ServiceDefinition } from "@simplysm/service-server";
|
|
170
|
-
|
|
171
|
-
interface ServiceDefinition<TMethods = Record<string, (...args: any[]) => any>> {
|
|
172
|
-
/** Service name (used as RPC route prefix, e.g., "User" → "User.getProfile") */
|
|
173
|
-
name: string;
|
|
174
|
-
/** Factory function that returns the service's method object */
|
|
175
|
-
factory: (ctx: ServiceContext) => TMethods;
|
|
176
|
-
/** Auth permissions required at service level (set by wrapping factory with auth()) */
|
|
177
|
-
authPermissions?: string[];
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Pass `ServiceDefinition[]` to `ServiceServerOptions.services` when creating the server.
|
|
182
|
-
|
|
183
|
-
## ServiceMethods
|
|
184
|
-
|
|
185
|
-
Type utility that extracts method signatures from a `ServiceDefinition`. Use this to share method types with the client without exposing implementation details.
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import type { ServiceMethods } from "@simplysm/service-server";
|
|
189
|
-
|
|
190
|
-
export const MyService = defineService("My", (ctx) => ({
|
|
191
|
-
hello: async (name: string): Promise<string> => `Hello, ${name}!`,
|
|
192
|
-
}));
|
|
193
|
-
|
|
194
|
-
// Export type for client-side type sharing
|
|
195
|
-
export type MyServiceType = ServiceMethods<typeof MyService>;
|
|
196
|
-
// Equivalent to: { hello: (name: string) => Promise<string> }
|
|
197
|
-
|
|
198
|
-
// Client usage:
|
|
199
|
-
// const service = client.getService<MyServiceType>("My");
|
|
200
|
-
// const result = await service.hello("World");
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## runServiceMethod
|
|
204
|
-
|
|
205
|
-
Dispatches a single service method call. Performs auth checks and executes the method. Used internally by `ServiceServer` for both WebSocket and HTTP transports, but exported for advanced use cases such as custom transports or testing.
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
import { runServiceMethod } from "@simplysm/service-server";
|
|
209
|
-
|
|
210
|
-
const result = await runServiceMethod(server, {
|
|
211
|
-
serviceName: "My",
|
|
212
|
-
methodName: "hello",
|
|
213
|
-
params: ["World"],
|
|
214
|
-
socket: serviceSocket, // or undefined for HTTP
|
|
215
|
-
http: undefined, // or { clientName, authTokenPayload } for HTTP
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
| Parameter | Type | Description |
|
|
220
|
-
|-----------|------|------|
|
|
221
|
-
| `server` | `ServiceServer` | The server instance |
|
|
222
|
-
| `def.serviceName` | `string` | Name of the service to dispatch to |
|
|
223
|
-
| `def.methodName` | `string` | Name of the method to invoke |
|
|
224
|
-
| `def.params` | `unknown[]` | Positional arguments for the method |
|
|
225
|
-
| `def.socket` | `ServiceSocket \| undefined` | WebSocket context (for WebSocket requests) |
|
|
226
|
-
| `def.http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload } \| undefined` | HTTP context (for HTTP requests) |
|
|
227
|
-
|
|
228
|
-
Returns: `Promise<unknown>` — the method's return value.
|
|
229
|
-
|
|
230
|
-
Throws if the service or method is not found, or if the caller lacks required auth permissions.
|
|
231
|
-
|
|
232
|
-
## Config File Reference
|
|
233
|
-
|
|
234
|
-
Read sections from `.config.json` files using `ctx.getConfig()`. Root and per-client configs are automatically merged.
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
import { defineService } from "@simplysm/service-server";
|
|
238
|
-
|
|
239
|
-
export const MyService = defineService("My", (ctx) => ({
|
|
240
|
-
getDbHost: async (): Promise<string> => {
|
|
241
|
-
// Read "mySection" key from rootPath/.config.json or clientPath/.config.json
|
|
242
|
-
const config = await ctx.getConfig<{ host: string }>("mySection");
|
|
243
|
-
return config.host;
|
|
244
|
-
},
|
|
245
|
-
}));
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
`.config.json` example:
|
|
249
|
-
|
|
250
|
-
```json
|
|
251
|
-
{
|
|
252
|
-
"mySection": {
|
|
253
|
-
"host": "localhost"
|
|
254
|
-
},
|
|
255
|
-
"orm": {
|
|
256
|
-
"default": {
|
|
257
|
-
"dialect": "mysql",
|
|
258
|
-
"host": "localhost",
|
|
259
|
-
"port": 3306,
|
|
260
|
-
"database": "mydb",
|
|
261
|
-
"user": "root",
|
|
262
|
-
"password": "password"
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Config files are cached and automatically refreshed on file changes (LazyGcMap-based, auto expires after 1 hour).
|
|
269
|
-
|
|
270
|
-
## getConfig
|
|
271
|
-
|
|
272
|
-
A standalone function that loads, caches, and watches JSON config files. Used internally by `ctx.getConfig()`.
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { getConfig } from "@simplysm/service-server";
|
|
276
|
-
|
|
277
|
-
// Returns undefined if the file does not exist
|
|
278
|
-
const config = await getConfig<MyConfig>("/path/to/.config.json");
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
| Signature | Returns | Description |
|
|
282
|
-
|-----------|---------|------|
|
|
283
|
-
| `getConfig<T>(filePath: string)` | `Promise<T \| undefined>` | Load and cache a JSON config file. Returns `undefined` if file not found |
|
|
284
|
-
|
|
285
|
-
Behavior:
|
|
286
|
-
- Caches file in `LazyGcMap` on first load.
|
|
287
|
-
- Registers file change watch (`FsWatcher`) to auto-refresh cache on changes.
|
|
288
|
-
- Cache auto-expires after 1 hour of no access, and associated watcher is released.
|
|
289
|
-
- GC runs every 10 minutes to check for expired entries.
|
|
290
|
-
|
|
291
|
-
## Server Route Structure
|
|
292
|
-
|
|
293
|
-
The following routes are automatically registered when `ServiceServer.listen()` is called:
|
|
294
|
-
|
|
295
|
-
| Route | Method | Description |
|
|
296
|
-
|--------|--------|------|
|
|
297
|
-
| `/api/:service/:method` | GET, POST | Service method call via HTTP |
|
|
298
|
-
| `/upload` | POST | Multipart file upload (auth required) |
|
|
299
|
-
| `/` | WebSocket | WebSocket connection endpoint |
|
|
300
|
-
| `/ws` | WebSocket | WebSocket connection endpoint (alias) |
|
|
301
|
-
| `/*` | GET, etc. | Static file serving (based on `rootPath/www/`) |
|
|
302
|
-
|
|
303
|
-
## Full Server Example
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
import { createServiceServer, defineService, auth, OrmService } from "@simplysm/service-server";
|
|
307
|
-
import { defineEvent } from "@simplysm/service-common";
|
|
308
|
-
|
|
309
|
-
// Define a custom service with auth
|
|
310
|
-
export const UserService = defineService("User", auth((ctx) => ({
|
|
311
|
-
getProfile: async (): Promise<{ name: string }> => {
|
|
312
|
-
const userId = (ctx.authInfo as { userId: number; role: string })?.userId;
|
|
313
|
-
// Use ctx.getConfig(), ctx.socket, ctx.server, etc.
|
|
314
|
-
return { name: "John" };
|
|
315
|
-
},
|
|
316
|
-
|
|
317
|
-
deleteUser: auth(["admin"], async (targetId: number): Promise<void> => {
|
|
318
|
-
// Admin-only operation
|
|
319
|
-
}),
|
|
320
|
-
})));
|
|
321
|
-
|
|
322
|
-
export const PublicService = defineService("Public", (ctx) => ({
|
|
323
|
-
healthCheck: async (): Promise<string> => {
|
|
324
|
-
return "OK";
|
|
325
|
-
},
|
|
326
|
-
}));
|
|
327
|
-
|
|
328
|
-
// Create and start server
|
|
329
|
-
const server = createServiceServer({
|
|
330
|
-
port: 8080,
|
|
331
|
-
rootPath: "/app/data",
|
|
332
|
-
auth: { jwtSecret: "my-secret-key" },
|
|
333
|
-
services: [UserService, PublicService, OrmService],
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
server.on("ready", () => {
|
|
337
|
-
console.log("Server is ready on port 8080");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
await server.listen();
|
|
341
|
-
|
|
342
|
-
// Generate auth token for a user
|
|
343
|
-
const token = await server.generateAuthToken({
|
|
344
|
-
roles: ["admin"],
|
|
345
|
-
data: { userId: 1, role: "admin" },
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Emit events to connected clients
|
|
349
|
-
const UserUpdatedEvent = defineEvent<
|
|
350
|
-
{ userId: number },
|
|
351
|
-
{ action: string }
|
|
352
|
-
>("UserUpdatedEvent");
|
|
353
|
-
|
|
354
|
-
await server.emitEvent(
|
|
355
|
-
UserUpdatedEvent,
|
|
356
|
-
(info) => info.userId === 1,
|
|
357
|
-
{ action: "profile-updated" },
|
|
358
|
-
);
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
## Security
|
|
362
|
-
|
|
363
|
-
- **Helmet**: `@fastify/helmet` plugin automatically sets security headers like CSP, HSTS.
|
|
364
|
-
- **CORS**: `@fastify/cors` plugin configures CORS.
|
|
365
|
-
- **Path Traversal Prevention**: Static file handler and client name validation block `..`, `/`, `\` characters.
|
|
366
|
-
- **Hidden File Blocking**: Files starting with `.` return a 403 response.
|
|
367
|
-
- **Graceful Shutdown**: Detects `SIGINT`/`SIGTERM` signals to safely close open WebSocket connections and server (10-second timeout).
|
|
368
|
-
|
|
369
|
-
## Caveats
|
|
370
|
-
|
|
371
|
-
- `OrmService` is WebSocket-only. Cannot be used via HTTP requests.
|
|
372
|
-
- Config files (`.config.json`) contain sensitive information (DB passwords, JWT secrets, etc.), so hidden files (starting with `.`) are automatically blocked by the static file handler.
|
|
373
|
-
- WebSocket connection requires query parameters `ver=2`, `clientId`, `clientName`. Without these parameters, it operates in V1 legacy mode.
|
|
374
|
-
- If SSL is not configured, the `upgrade-insecure-requests` CSP directive is disabled.
|
package/docs/transport.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
# Transport Layer
|
|
2
|
-
|
|
3
|
-
## WebSocketHandler
|
|
4
|
-
|
|
5
|
-
`WebSocketHandler` is an interface that manages multiple WebSocket connections, routes messages to services, and handles event broadcasting. Create an instance with `createWebSocketHandler`.
|
|
6
|
-
|
|
7
|
-
**Methods:**
|
|
8
|
-
|
|
9
|
-
| Method | Returns | Description |
|
|
10
|
-
|--------|---------|------|
|
|
11
|
-
| `addSocket(socket, clientId, clientName, connReq)` | `void` | Add a new WebSocket connection. If a connection with the same `clientId` already exists, it is closed first |
|
|
12
|
-
| `closeAll()` | `void` | Close all active connections |
|
|
13
|
-
| `broadcastReload(clientName, changedFileSet)` | `Promise<void>` | Send a reload message to all connected clients |
|
|
14
|
-
| `emitToServer(eventDef, infoSelector, data)` | `Promise<void>` | Emit an event to clients whose registered event listeners match the `infoSelector` |
|
|
15
|
-
|
|
16
|
-
`WebSocketHandler` is created and managed internally by `ServiceServer`. Access its functionality through `ServiceServer` methods (`emitEvent`, `broadcastReload`).
|
|
17
|
-
|
|
18
|
-
### createWebSocketHandler
|
|
19
|
-
|
|
20
|
-
Factory function that creates a `WebSocketHandler` instance.
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
import { createWebSocketHandler } from "@simplysm/service-server";
|
|
24
|
-
|
|
25
|
-
const handler = createWebSocketHandler(runMethod, jwtSecret);
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
| Parameter | Type | Description |
|
|
29
|
-
|-----------|------|------|
|
|
30
|
-
| `runMethod` | `(def: { serviceName: string; methodName: string; params: unknown[]; socket?: ServiceSocket }) => Promise<unknown>` | Function to execute service method calls |
|
|
31
|
-
| `jwtSecret` | `string \| undefined` | JWT secret for authentication (`undefined` disables auth) |
|
|
32
|
-
|
|
33
|
-
**Returns:** `WebSocketHandler`
|
|
34
|
-
|
|
35
|
-
## ServiceSocket
|
|
36
|
-
|
|
37
|
-
`ServiceSocket` is an interface wrapping a single WebSocket connection. It is available in service methods as `ctx.socket` when the request comes via WebSocket. Create an instance with `createServiceSocket`.
|
|
38
|
-
|
|
39
|
-
**Properties:**
|
|
40
|
-
|
|
41
|
-
| Property | Type | Description |
|
|
42
|
-
|----------|------|------|
|
|
43
|
-
| `clientName` | `string` | Client app name (from WebSocket query parameter) |
|
|
44
|
-
| `connectedAtDateTime` | `DateTime` | Connection timestamp |
|
|
45
|
-
| `authTokenPayload` | `AuthTokenPayload \| undefined` | Authenticated token payload (set after `auth` message) |
|
|
46
|
-
| `connReq` | `FastifyRequest` | Original Fastify request that initiated the WebSocket upgrade |
|
|
47
|
-
|
|
48
|
-
**Methods:**
|
|
49
|
-
|
|
50
|
-
| Method | Returns | Description |
|
|
51
|
-
|--------|---------|------|
|
|
52
|
-
| `send(uuid, msg)` | `Promise<number>` | Send a message to this client. Returns total bytes sent |
|
|
53
|
-
| `close()` | `void` | Terminate the WebSocket connection |
|
|
54
|
-
| `addEventListener(key, eventName, info)` | `void` | Register an event listener for this socket |
|
|
55
|
-
| `removeEventListener(key)` | `void` | Remove an event listener by key |
|
|
56
|
-
| `getEventListeners(eventName)` | `{ key, info }[]` | Get all event listeners for a given event name |
|
|
57
|
-
| `filterEventTargetKeys(targetKeys)` | `string[]` | Filter the given keys to only those registered on this socket |
|
|
58
|
-
| `on(event, handler)` | `void` | Register an event handler (`error`, `close`, or `message`) |
|
|
59
|
-
|
|
60
|
-
**Events:**
|
|
61
|
-
|
|
62
|
-
| Event | Payload | Description |
|
|
63
|
-
|-------|---------|------|
|
|
64
|
-
| `error` | `Error` | WebSocket error occurred |
|
|
65
|
-
| `close` | `number` | Connection closed (payload is the close code) |
|
|
66
|
-
| `message` | `{ uuid: string; msg: ServiceClientMessage }` | Decoded message received from client |
|
|
67
|
-
|
|
68
|
-
### createServiceSocket
|
|
69
|
-
|
|
70
|
-
Factory function that creates a `ServiceSocket` instance wrapping a raw WebSocket.
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { createServiceSocket } from "@simplysm/service-server";
|
|
74
|
-
|
|
75
|
-
const serviceSocket = createServiceSocket(socket, clientId, clientName, connReq);
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
| Parameter | Type | Description |
|
|
79
|
-
|-----------|------|------|
|
|
80
|
-
| `socket` | `WebSocket` | Raw WebSocket connection |
|
|
81
|
-
| `clientId` | `string` | Unique client identifier |
|
|
82
|
-
| `clientName` | `string` | Client app name |
|
|
83
|
-
| `connReq` | `FastifyRequest` | Original Fastify request that initiated the WebSocket upgrade |
|
|
84
|
-
|
|
85
|
-
**Returns:** `ServiceSocket`
|
|
86
|
-
|
|
87
|
-
## HTTP API Call
|
|
88
|
-
|
|
89
|
-
Service methods can also be called via HTTP through the `/api/:service/:method` path.
|
|
90
|
-
|
|
91
|
-
**GET Request:**
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
GET /api/MyService/hello?json=["World"]
|
|
95
|
-
Header: x-sd-client-name: my-app
|
|
96
|
-
Header: Authorization: Bearer <token> (optional)
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**POST Request:**
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
POST /api/MyService/hello
|
|
103
|
-
Header: Content-Type: application/json
|
|
104
|
-
Header: x-sd-client-name: my-app
|
|
105
|
-
Header: Authorization: Bearer <token> (optional)
|
|
106
|
-
Body: ["World"]
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
- The `x-sd-client-name` header is required.
|
|
110
|
-
- Parameters are passed in array form (in the order of method arguments).
|
|
111
|
-
- For GET requests, pass a JSON-serialized array in the `json` query parameter.
|
|
112
|
-
|
|
113
|
-
### handleHttpRequest
|
|
114
|
-
|
|
115
|
-
The internal handler function registered at `/api/:service/:method`. Exported for advanced use cases.
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import { handleHttpRequest } from "@simplysm/service-server";
|
|
119
|
-
|
|
120
|
-
await handleHttpRequest(req, reply, jwtSecret, runMethod);
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
| Parameter | Type | Description |
|
|
124
|
-
|-----------|------|------|
|
|
125
|
-
| `req` | `FastifyRequest` | Fastify request object |
|
|
126
|
-
| `reply` | `FastifyReply` | Fastify reply object |
|
|
127
|
-
| `jwtSecret` | `string \| undefined` | JWT secret for authentication |
|
|
128
|
-
| `runMethod` | `(def: { serviceName: string; methodName: string; params: unknown[]; http: { clientName: string; authTokenPayload?: AuthTokenPayload } }) => Promise<unknown>` | Function to execute service method calls |
|
|
129
|
-
|
|
130
|
-
## File Upload
|
|
131
|
-
|
|
132
|
-
Upload files via multipart request to the `/upload` endpoint. Auth token is required.
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
// Client-side example
|
|
136
|
-
const formData = new FormData();
|
|
137
|
-
formData.append("file", file);
|
|
138
|
-
|
|
139
|
-
const response = await fetch("/upload", {
|
|
140
|
-
method: "POST",
|
|
141
|
-
headers: {
|
|
142
|
-
Authorization: `Bearer ${token}`,
|
|
143
|
-
},
|
|
144
|
-
body: formData,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Response: ServiceUploadResult[]
|
|
148
|
-
const results = await response.json();
|
|
149
|
-
// [{ path: "uploads/uuid.ext", filename: "original-filename.ext", size: 12345 }]
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Uploaded files are stored in the `rootPath/www/uploads/` directory with UUID-based filenames.
|
|
153
|
-
|
|
154
|
-
### handleUpload
|
|
155
|
-
|
|
156
|
-
The internal handler function registered at `/upload`. Exported for advanced use cases.
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
import { handleUpload } from "@simplysm/service-server";
|
|
160
|
-
|
|
161
|
-
await handleUpload(req, reply, rootPath, jwtSecret);
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
| Parameter | Type | Description |
|
|
165
|
-
|-----------|------|------|
|
|
166
|
-
| `req` | `FastifyRequest` | Fastify request object (must be multipart) |
|
|
167
|
-
| `reply` | `FastifyReply` | Fastify reply object |
|
|
168
|
-
| `rootPath` | `string` | Server root path (files stored under `rootPath/www/uploads/`) |
|
|
169
|
-
| `jwtSecret` | `string \| undefined` | JWT secret for authentication |
|
|
170
|
-
|
|
171
|
-
### handleStaticFile
|
|
172
|
-
|
|
173
|
-
The internal handler function for static file serving. Exported for advanced use cases.
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
import { handleStaticFile } from "@simplysm/service-server";
|
|
177
|
-
|
|
178
|
-
await handleStaticFile(req, reply, rootPath, urlPath);
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
| Parameter | Type | Description |
|
|
182
|
-
|-----------|------|------|
|
|
183
|
-
| `req` | `FastifyRequest` | Fastify request object |
|
|
184
|
-
| `reply` | `FastifyReply` | Fastify reply object |
|
|
185
|
-
| `rootPath` | `string` | Server root path (serves files from `rootPath/www/`) |
|
|
186
|
-
| `urlPath` | `string` | URL path relative to the root |
|
|
187
|
-
|
|
188
|
-
Security behavior:
|
|
189
|
-
- Blocks path traversal (`..`, outside `rootPath/www/`)
|
|
190
|
-
- Returns 403 for files starting with `.` (hidden files)
|
|
191
|
-
- Redirects directory paths without trailing slash to path with trailing slash
|
|
192
|
-
|
|
193
|
-
## Real-time Event Publishing
|
|
194
|
-
|
|
195
|
-
Publish events to connected clients from the server using `defineEvent` from `@simplysm/service-common`.
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
import { createServiceServer } from "@simplysm/service-server";
|
|
199
|
-
import { defineEvent } from "@simplysm/service-common";
|
|
200
|
-
|
|
201
|
-
// Event definition
|
|
202
|
-
const OrderUpdatedEvent = defineEvent<
|
|
203
|
-
{ orderId: number },
|
|
204
|
-
{ status: string }
|
|
205
|
-
>("OrderUpdatedEvent");
|
|
206
|
-
|
|
207
|
-
// Publish event from server
|
|
208
|
-
await server.emitEvent(
|
|
209
|
-
OrderUpdatedEvent,
|
|
210
|
-
(info) => info.orderId === 123, // Target filter
|
|
211
|
-
{ status: "completed" }, // Data to send
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// Send reload command to all clients
|
|
215
|
-
await server.broadcastReload("my-app", new Set(["main.js"]));
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## ProtocolWrapper
|
|
219
|
-
|
|
220
|
-
`ProtocolWrapper` is an interface for encoding/decoding WebSocket messages. Create an instance with `createProtocolWrapper`. Automatically branches between main thread and worker thread based on message size. Uses a shared worker singleton internally.
|
|
221
|
-
|
|
222
|
-
### createProtocolWrapper
|
|
223
|
-
|
|
224
|
-
Factory function that creates a `ProtocolWrapper` instance.
|
|
225
|
-
|
|
226
|
-
**Returns:** `ProtocolWrapper` (no parameters)
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
import { createProtocolWrapper } from "@simplysm/service-server";
|
|
230
|
-
|
|
231
|
-
const protocol = createProtocolWrapper();
|
|
232
|
-
|
|
233
|
-
// Encode a message into chunks
|
|
234
|
-
const { chunks, totalSize } = await protocol.encode(uuid, message);
|
|
235
|
-
|
|
236
|
-
// Decode received bytes
|
|
237
|
-
const result = await protocol.decode(bytes);
|
|
238
|
-
|
|
239
|
-
// Clean up
|
|
240
|
-
protocol.dispose();
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
| Method | Returns | Description |
|
|
244
|
-
|--------|---------|------|
|
|
245
|
-
| `encode(uuid, message)` | `Promise<{ chunks: Uint8Array[]; totalSize: number }>` | Encode a message into transmittable chunks |
|
|
246
|
-
| `decode(bytes)` | `Promise<ServiceMessageDecodeResult>` | Decode received bytes into a message |
|
|
247
|
-
| `dispose()` | `void` | Clean up internal protocol resources |
|
|
248
|
-
|
|
249
|
-
Worker thread branching:
|
|
250
|
-
|
|
251
|
-
| Condition | Processing Method |
|
|
252
|
-
|------|-----------|
|
|
253
|
-
| 30KB or less | Processed directly in main thread |
|
|
254
|
-
| Over 30KB | Processed in worker thread (max 4GB memory allocation) |
|
|
255
|
-
|
|
256
|
-
Messages containing large binary data (Uint8Array) also branch to worker thread regardless of size.
|
|
257
|
-
|
|
258
|
-
## Legacy: handleV1Connection
|
|
259
|
-
|
|
260
|
-
Handles V1 protocol WebSocket clients. Only supports the `SdAutoUpdateService.getLastVersion` command. All other requests return an upgrade-required error.
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
import { handleV1Connection, AutoUpdateService } from "@simplysm/service-server";
|
|
264
|
-
|
|
265
|
-
// Used internally by ServiceServer for WebSocket connections without ver=2 query parameter
|
|
266
|
-
handleV1Connection(webSocket, autoUpdateMethods, clientNameSetter);
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
| Parameter | Type | Description |
|
|
270
|
-
|-----------|------|------|
|
|
271
|
-
| `socket` | `WebSocket` | The raw WebSocket connection |
|
|
272
|
-
| `autoUpdateMethods` | `{ getLastVersion: (platform: string) => Promise<any> }` | Auto-update service methods |
|
|
273
|
-
| `clientNameSetter` | `((clientName: string \| undefined) => void) \| undefined` | Optional callback to set the client name from the V1 request |
|