@simplysm/service-server 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/docs/server.md ADDED
@@ -0,0 +1,206 @@
1
+ # Server
2
+
3
+ ## ServiceServerOptions
4
+
5
+ Server configuration options.
6
+
7
+ ```typescript
8
+ interface ServiceServerOptions {
9
+ rootPath: string;
10
+ port: number;
11
+ ssl?: {
12
+ pfxBytes: Uint8Array;
13
+ passphrase: string;
14
+ };
15
+ auth?: {
16
+ jwtSecret: string;
17
+ };
18
+ services: ServiceDefinition[];
19
+ }
20
+ ```
21
+
22
+ ## ServerProtocolWrapper
23
+
24
+ Server-side protocol wrapper. Automatically offloads heavy message encoding/decoding to a worker thread while using main thread for lightweight operations.
25
+
26
+ ```typescript
27
+ interface ServerProtocolWrapper {
28
+ encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
29
+ decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
30
+ dispose(): void;
31
+ }
32
+ ```
33
+
34
+ ### `createServerProtocolWrapper`
35
+
36
+ ```typescript
37
+ function createServerProtocolWrapper(): ServerProtocolWrapper;
38
+ ```
39
+
40
+ **Behavior:**
41
+ - Messages with `Uint8Array` body or arrays containing `Uint8Array` are encoded via worker thread
42
+ - Messages larger than 30KB are decoded via worker thread
43
+ - Worker is a lazy singleton shared across all protocol wrappers (4GB memory limit)
44
+ - Small messages are processed on the main thread using `createServiceProtocol()`
45
+
46
+ ---
47
+
48
+ ## Config Utilities
49
+
50
+ ### `getConfig`
51
+
52
+ Read and cache a JSON config file with automatic live-reload via file watcher.
53
+
54
+ ```typescript
55
+ async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
56
+ ```
57
+
58
+ **Behavior:**
59
+ - Returns `undefined` if file does not exist
60
+ - Caches loaded config in a `LazyGcMap` (expires after 1 hour, GC runs every 10 minutes)
61
+ - Registers a file watcher that live-reloads config on changes
62
+ - Watcher is cleaned up when the cache entry expires
63
+
64
+ ---
65
+
66
+ ## Legacy
67
+
68
+ ### `handleV1Connection`
69
+
70
+ V1 legacy client handler. Only auto-update is supported; all other requests return an upgrade-required error.
71
+
72
+ ```typescript
73
+ function handleV1Connection(
74
+ socket: WebSocket,
75
+ autoUpdateMethods: { getLastVersion: (platform: string) => Promise<any> },
76
+ clientNameSetter?: (clientName: string | undefined) => void,
77
+ ): void;
78
+ ```
79
+
80
+ ---
81
+
82
+ ## ServiceServer
83
+
84
+ Main server class. Extends `EventEmitter<{ ready: void; close: void }>`.
85
+
86
+ ```typescript
87
+ class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }> {
88
+ isOpen: boolean;
89
+ readonly fastify: FastifyInstance;
90
+ readonly options: ServiceServerOptions;
91
+
92
+ constructor(options: ServiceServerOptions);
93
+
94
+ async listen(): Promise<void>;
95
+ async close(): Promise<void>;
96
+ async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
97
+ async emitEvent<TInfo, TData>(
98
+ eventDef: ServiceEventDef<TInfo, TData>,
99
+ infoSelector: (item: TInfo) => boolean,
100
+ data: TData,
101
+ ): Promise<void>;
102
+ async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
103
+ async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
104
+ }
105
+ ```
106
+
107
+ ### `listen`
108
+
109
+ Start the server. Registers all Fastify plugins and routes:
110
+
111
+ - `@fastify/websocket` -- WebSocket support
112
+ - `@fastify/helmet` -- Security headers
113
+ - `@fastify/multipart` -- File upload support
114
+ - `@fastify/static` -- Static file serving
115
+ - `@fastify/cors` -- Cross-origin resource sharing
116
+
117
+ **Routes:**
118
+ - `GET/POST /api/:service/:method` -- HTTP API endpoint
119
+ - `POST /upload` -- File upload endpoint
120
+ - `GET /ws` or `GET /` (WebSocket) -- WebSocket connection (V2 protocol with V1 legacy fallback)
121
+ - `GET/POST/PUT/DELETE/PATCH/HEAD /*` -- Static file wildcard handler
122
+
123
+ Registers graceful shutdown handlers for `SIGINT` and `SIGTERM` (10s timeout before force exit).
124
+
125
+ ### `close`
126
+
127
+ Close all WebSocket connections and shut down the Fastify server.
128
+
129
+ ### `broadcastReload`
130
+
131
+ Broadcast a reload message to all connected WebSocket clients.
132
+
133
+ ```typescript
134
+ async broadcastReload(
135
+ clientName: string | undefined,
136
+ changedFileSet: Set<string>,
137
+ ): Promise<void>;
138
+ ```
139
+
140
+ ### `emitEvent`
141
+
142
+ Emit an event to matching WebSocket clients.
143
+
144
+ ```typescript
145
+ async emitEvent<TInfo, TData>(
146
+ eventDef: ServiceEventDef<TInfo, TData>,
147
+ infoSelector: (item: TInfo) => boolean,
148
+ data: TData,
149
+ ): Promise<void>;
150
+ ```
151
+
152
+ ### `signAuthToken`
153
+
154
+ Sign a JWT auth token.
155
+
156
+ ```typescript
157
+ async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
158
+ ```
159
+
160
+ ### `verifyAuthToken`
161
+
162
+ Verify a JWT auth token.
163
+
164
+ ```typescript
165
+ async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
166
+ ```
167
+
168
+ ## `createServiceServer`
169
+
170
+ Factory function.
171
+
172
+ ```typescript
173
+ function createServiceServer<TAuthInfo = unknown>(
174
+ options: ServiceServerOptions,
175
+ ): ServiceServer<TAuthInfo>;
176
+ ```
177
+
178
+ ## Example
179
+
180
+ ```typescript
181
+ import {
182
+ createServiceServer,
183
+ defineService,
184
+ auth,
185
+ OrmService,
186
+ AutoUpdateService,
187
+ SmtpClientService,
188
+ } from "@simplysm/service-server";
189
+
190
+ const MyService = defineService("My", auth((ctx) => ({
191
+ hello: (name: string) => `Hello, ${name}!`,
192
+ })));
193
+
194
+ const server = createServiceServer({
195
+ rootPath: "/app",
196
+ port: 3000,
197
+ auth: { jwtSecret: process.env.JWT_SECRET! },
198
+ services: [MyService, OrmService, AutoUpdateService, SmtpClientService],
199
+ });
200
+
201
+ server.on("ready", () => {
202
+ console.log("Server is ready");
203
+ });
204
+
205
+ await server.listen();
206
+ ```
@@ -0,0 +1,176 @@
1
+ # Built-in Services
2
+
3
+ Pre-defined service definitions ready to use with `ServiceServer`.
4
+
5
+ ## OrmService
6
+
7
+ Database ORM service. Requires authentication (wrapped with `auth()`). Requires WebSocket connection (cannot be used over HTTP).
8
+
9
+ ```typescript
10
+ import { OrmService, type OrmServiceType } from "@simplysm/service-server";
11
+ ```
12
+
13
+ ### Definition
14
+
15
+ ```typescript
16
+ const OrmService: ServiceDefinition;
17
+ ```
18
+
19
+ Service name: `"Orm"`
20
+
21
+ ### Methods
22
+
23
+ ```typescript
24
+ interface OrmServiceType {
25
+ getInfo(opt: DbConnOptions & { configName: string }): Promise<{
26
+ dialect: Dialect;
27
+ database?: string;
28
+ schema?: string;
29
+ }>;
30
+ connect(opt: DbConnOptions & { configName: string }): Promise<number>;
31
+ close(connId: number): Promise<void>;
32
+ beginTransaction(connId: number, isolationLevel?: IsolationLevel): Promise<void>;
33
+ commitTransaction(connId: number): Promise<void>;
34
+ rollbackTransaction(connId: number): Promise<void>;
35
+ executeParametrized(connId: number, query: string, params?: unknown[]): Promise<unknown[][]>;
36
+ executeDefs(
37
+ connId: number,
38
+ defs: QueryDef[],
39
+ options?: (ResultMeta | undefined)[],
40
+ ): Promise<unknown[][]>;
41
+ bulkInsert(
42
+ connId: number,
43
+ tableName: string,
44
+ columnDefs: Record<string, ColumnMeta>,
45
+ records: Record<string, unknown>[],
46
+ ): Promise<void>;
47
+ }
48
+ ```
49
+
50
+ **Behavior:**
51
+ - Database connections are tracked per WebSocket socket using a `WeakMap`
52
+ - Connections are automatically cleaned up when the socket closes
53
+ - Configuration is loaded from `.config.json` under the `"orm"` section
54
+ - Supports `mssql-azure` dialect (mapped to `mssql` for query building)
55
+
56
+ **Configuration example** (`.config.json`):
57
+ ```json
58
+ {
59
+ "orm": {
60
+ "myDb": {
61
+ "dialect": "mysql",
62
+ "host": "localhost",
63
+ "port": 3306,
64
+ "database": "mydb",
65
+ "user": "root",
66
+ "password": "secret"
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ---
73
+
74
+ ## AutoUpdateService
75
+
76
+ Auto-update service for client applications. No authentication required.
77
+
78
+ ```typescript
79
+ import { AutoUpdateService, type AutoUpdateServiceType } from "@simplysm/service-server";
80
+ ```
81
+
82
+ ### Definition
83
+
84
+ ```typescript
85
+ const AutoUpdateService: ServiceDefinition;
86
+ ```
87
+
88
+ Service name: `"AutoUpdate"`
89
+
90
+ ### Methods
91
+
92
+ ```typescript
93
+ interface AutoUpdateServiceType {
94
+ getLastVersion(platform: string): Promise<
95
+ | { version: string; downloadPath: string }
96
+ | undefined
97
+ >;
98
+ }
99
+ ```
100
+
101
+ **Behavior:**
102
+ - Scans `{clientPath}/{platform}/updates/` for version files
103
+ - Android: looks for `.apk` files
104
+ - Other platforms: looks for `.exe` files
105
+ - Returns the highest semver version found
106
+ - Returns `undefined` if no updates directory or no valid versions exist
107
+
108
+ ---
109
+
110
+ ## SmtpClientService
111
+
112
+ SMTP email sending service. No authentication required.
113
+
114
+ ```typescript
115
+ import { SmtpClientService, type SmtpClientServiceType } from "@simplysm/service-server";
116
+ ```
117
+
118
+ ### Definition
119
+
120
+ ```typescript
121
+ const SmtpClientService: ServiceDefinition;
122
+ ```
123
+
124
+ Service name: `"SmtpClient"`
125
+
126
+ ### Methods
127
+
128
+ ```typescript
129
+ interface SmtpClientServiceType {
130
+ send(options: SmtpClientSendOption): Promise<string>;
131
+ sendByConfig(configName: string, options: SmtpClientSendByDefaultOption): Promise<string>;
132
+ }
133
+ ```
134
+
135
+ **`send`** -- Send email with explicit SMTP configuration. Returns the message ID.
136
+
137
+ **`sendByConfig`** -- Send email using server-side SMTP configuration. Configuration is loaded from `.config.json` under the `"smtp"` section.
138
+
139
+ **Configuration example** (`.config.json`):
140
+ ```json
141
+ {
142
+ "smtp": {
143
+ "default": {
144
+ "senderName": "My App",
145
+ "senderEmail": "noreply@example.com",
146
+ "user": "smtp-user",
147
+ "pass": "smtp-pass",
148
+ "host": "smtp.example.com",
149
+ "port": 587,
150
+ "secure": false
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Usage
159
+
160
+ Register built-in services when creating the server:
161
+
162
+ ```typescript
163
+ import {
164
+ createServiceServer,
165
+ OrmService,
166
+ AutoUpdateService,
167
+ SmtpClientService,
168
+ } from "@simplysm/service-server";
169
+
170
+ const server = createServiceServer({
171
+ rootPath: "/app",
172
+ port: 3000,
173
+ auth: { jwtSecret: "secret" },
174
+ services: [OrmService, AutoUpdateService, SmtpClientService],
175
+ });
176
+ ```
@@ -0,0 +1,152 @@
1
+ # Transport
2
+
3
+ ## WebSocket Transport
4
+
5
+ ### `WebSocketHandler`
6
+
7
+ Manages multiple WebSocket connections, routes messages to services, and handles event broadcasting.
8
+
9
+ ```typescript
10
+ interface WebSocketHandler {
11
+ addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
12
+ closeAll(): void;
13
+ broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
14
+ emit<TInfo, TData>(
15
+ eventDef: ServiceEventDef<TInfo, TData>,
16
+ infoSelector: (item: TInfo) => boolean,
17
+ data: TData,
18
+ ): Promise<void>;
19
+ }
20
+ ```
21
+
22
+ ### `createWebSocketHandler`
23
+
24
+ ```typescript
25
+ function createWebSocketHandler(
26
+ runMethod: (def: {
27
+ serviceName: string;
28
+ methodName: string;
29
+ params: unknown[];
30
+ socket?: ServiceSocket;
31
+ }) => Promise<unknown>,
32
+ jwtSecret: string | undefined,
33
+ ): WebSocketHandler;
34
+ ```
35
+
36
+ **Behavior:**
37
+ - Routes incoming messages to service methods, auth, and event operations
38
+ - Manages a map of connected `ServiceSocket` instances by client ID
39
+ - Replaces existing connections for the same client ID
40
+ - Handles `auth`, `evt:add`, `evt:remove`, `evt:gets`, `evt:emit`, and service method requests
41
+
42
+ ---
43
+
44
+ ### `ServiceSocket`
45
+
46
+ Manages a single WebSocket connection with protocol encoding/decoding, ping/pong keep-alive, and event listener tracking.
47
+
48
+ ```typescript
49
+ interface ServiceSocket {
50
+ readonly connectedAtDateTime: DateTime;
51
+ readonly clientName: string;
52
+ readonly connReq: FastifyRequest;
53
+ authTokenPayload?: AuthTokenPayload;
54
+
55
+ close(): void;
56
+ send(uuid: string, msg: ServiceServerMessage): Promise<number>;
57
+ addListener(key: string, eventName: string, info: unknown): void;
58
+ removeListener(key: string): void;
59
+ getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
60
+ filterEventTargetKeys(targetKeys: string[]): string[];
61
+
62
+ on(event: "error", handler: (err: Error) => void): void;
63
+ on(event: "close", handler: (code: number) => void): void;
64
+ on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
65
+ }
66
+ ```
67
+
68
+ ### `createServiceSocket`
69
+
70
+ ```typescript
71
+ function createServiceSocket(
72
+ socket: WebSocket,
73
+ clientId: string,
74
+ clientName: string,
75
+ connReq: FastifyRequest,
76
+ ): ServiceSocket;
77
+ ```
78
+
79
+ **Behavior:**
80
+ - Wraps raw WebSocket with protocol encoding/decoding via `ServerProtocolWrapper`
81
+ - Sends ping every 5s; terminates if pong not received
82
+ - Handles raw ping/pong packets (`0x01` ping, `0x02` pong)
83
+ - Tracks event listeners per socket for event broadcasting
84
+ - Sends progress notifications for chunked message reception
85
+
86
+ ---
87
+
88
+ ## HTTP Transport
89
+
90
+ ### `handleHttpRequest`
91
+
92
+ Handle HTTP API requests (GET/POST) to `/api/:service/:method`.
93
+
94
+ ```typescript
95
+ async function handleHttpRequest<TAuthInfo = unknown>(
96
+ req: FastifyRequest,
97
+ reply: FastifyReply,
98
+ jwtSecret: string | undefined,
99
+ runMethod: (def: {
100
+ serviceName: string;
101
+ methodName: string;
102
+ params: unknown[];
103
+ http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
104
+ }) => Promise<unknown>,
105
+ ): Promise<void>;
106
+ ```
107
+
108
+ **Behavior:**
109
+ - Requires `x-sd-client-name` header
110
+ - GET: reads params from `?json=...` query parameter
111
+ - POST: reads params from request body (must be an array)
112
+ - Parses `Authorization: Bearer <token>` header if present
113
+
114
+ ### `handleUpload`
115
+
116
+ Handle multipart file upload to `/upload`.
117
+
118
+ ```typescript
119
+ async function handleUpload(
120
+ req: FastifyRequest,
121
+ reply: FastifyReply,
122
+ rootPath: string,
123
+ jwtSecret: string | undefined,
124
+ ): Promise<void>;
125
+ ```
126
+
127
+ **Behavior:**
128
+ - Requires authentication (JWT token in Authorization header)
129
+ - Saves files to `{rootPath}/www/uploads/` with UUID filenames
130
+ - Returns `ServiceUploadResult[]` with path, filename, and size
131
+ - Cleans up incomplete files on error
132
+
133
+ ### `handleStaticFile`
134
+
135
+ Handle static file serving.
136
+
137
+ ```typescript
138
+ async function handleStaticFile(
139
+ req: FastifyRequest,
140
+ reply: FastifyReply,
141
+ rootPath: string,
142
+ urlPath: string,
143
+ ): Promise<void>;
144
+ ```
145
+
146
+ **Behavior:**
147
+ - Serves files from `{rootPath}/www/`
148
+ - Path traversal protection (rejects paths outside allowed root)
149
+ - Redirects directories to trailing-slash URLs
150
+ - Returns `index.html` for directory requests
151
+ - Returns 403 for hidden files (starting with `.`)
152
+ - Returns 404 for missing files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "13.0.96",
3
+ "version": "13.0.98",
4
4
  "description": "Simplysm package - service module (server)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -30,17 +30,17 @@
30
30
  "bufferutil": "^4.1.0",
31
31
  "consola": "^3.4.2",
32
32
  "fastify": "^5.8.2",
33
- "jose": "^6.2.1",
33
+ "jose": "^6.2.2",
34
34
  "mime": "^4.1.0",
35
- "nodemailer": "^8.0.2",
35
+ "nodemailer": "^8.0.3",
36
36
  "semver": "^7.7.4",
37
37
  "utf-8-validate": "^6.0.6",
38
38
  "ws": "^8.19.0",
39
- "@simplysm/core-node": "13.0.96",
40
- "@simplysm/core-common": "13.0.96",
41
- "@simplysm/orm-node": "13.0.96",
42
- "@simplysm/orm-common": "13.0.96",
43
- "@simplysm/service-common": "13.0.96"
39
+ "@simplysm/core-node": "13.0.98",
40
+ "@simplysm/core-common": "13.0.98",
41
+ "@simplysm/orm-common": "13.0.98",
42
+ "@simplysm/service-common": "13.0.98",
43
+ "@simplysm/orm-node": "13.0.98"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/nodemailer": "^7.0.11",