wenay-common2 1.0.12 → 1.0.15
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 +240 -1
- package/lib/Common/rcp/rpc-clientHub.d.ts +31 -8
- package/lib/Common/rcp/rpc-clientHub.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,4 +69,243 @@ A set of common utilities and components for TypeScript/Node.js projects.
|
|
|
69
69
|
### Installation
|
|
70
70
|
```bash
|
|
71
71
|
npm install wenay-common2
|
|
72
|
-
```
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Here is a comprehensive guide in a concise style. I've taken into account the architecture, backend, frontend (with the new `Hub` pattern), serialization nuances, limits, and hooks.
|
|
75
|
+
|
|
76
|
+
# wenay-common2 RPC: Complete Guide
|
|
77
|
+
|
|
78
|
+
Bidirectional, strongly-typed RPC protocol over sockets (Socket.IO or similar).
|
|
79
|
+
**Essence:** Server exposes a nested JS object $\to$ Client receives a typed proxy.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 1. Architecture and Limitations
|
|
84
|
+
|
|
85
|
+
* **Multiplexing:** A single physical socket hosts independent channels (`socketKey`), each with its own API object.
|
|
86
|
+
* **Data Types:** Works with JSON-compatible data. `Date`, `Map`, `Set`, `Class` **cannot** be transmitted.
|
|
87
|
+
* **Security (RpcLimits):** Server is protected from DDoS attacks. Strict limits on: `maxDepth`, `maxKeys`, `maxArrayLen`, `maxStringLen`, `maxCallbacks`. Exceeding throws `PayloadLimitError`.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 2. Server (Backend)
|
|
92
|
+
|
|
93
|
+
### 2.1 Socket Connection
|
|
94
|
+
```typescript
|
|
95
|
+
import { createRpcServerAuto, UseListen } from "wenay-common2";
|
|
96
|
+
|
|
97
|
+
io.sockets.on('connection', (socket) => {
|
|
98
|
+
// 1. Create unsubscribe trigger for memory cleanup
|
|
99
|
+
const [stop, listenStop] = UseListen<[]>();
|
|
100
|
+
socket.on('disconnect', stop);
|
|
101
|
+
|
|
102
|
+
// 2. Initialize RPC channel on this socket
|
|
103
|
+
createRpcServerAuto({
|
|
104
|
+
socket,
|
|
105
|
+
socketKey: "mainAPI", // Channel identifier
|
|
106
|
+
object: buildFacade(client), // Target API object
|
|
107
|
+
disconnectListen: listenStop, // Auto-unsubscribe from Listen on disconnect
|
|
108
|
+
debug: process.env.DEV, // Packet logging
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2.2 Building API Object (Facade)
|
|
114
|
+
The object is traversed by the server to build a "Schema" that is sent to the client.
|
|
115
|
+
```typescript
|
|
116
|
+
import { noStrict, UseListen } from "wenay-common2";
|
|
117
|
+
|
|
118
|
+
// Create pub/sub event system
|
|
119
|
+
const [sendEvent, listenEvent] = UseListen<[string]>();
|
|
120
|
+
|
|
121
|
+
export function buildFacade(client) {
|
|
122
|
+
const role = (...roles) => hasRole(client, roles) ? true : null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
// 1. Regular method
|
|
126
|
+
ping: () => "pong",
|
|
127
|
+
|
|
128
|
+
// 2. Nested namespaces + Role model
|
|
129
|
+
// If role() returns null, the method won't be sent to the client (returns null in schema)
|
|
130
|
+
admin: {
|
|
131
|
+
deleteUser: role("admin") && ((id) => db.delete(id)),
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// 3. Dynamic objects (Proxy, ORM)
|
|
135
|
+
// Wrap in noStrict so the server doesn't try to read keys.
|
|
136
|
+
// Client will work with it in "blind" mode (without schema).
|
|
137
|
+
dbRef: noStrict(getProxyDb()),
|
|
138
|
+
|
|
139
|
+
// 4. Events
|
|
140
|
+
// Client will receive an object with .callback() and .removeCallback() methods
|
|
141
|
+
events: { listenEvent },
|
|
142
|
+
|
|
143
|
+
// 5. Method with callback in arguments
|
|
144
|
+
// Callback lives ONLY while await is executing! After return, client deletes it.
|
|
145
|
+
stream: async (cb: (chunk: number) => void) => {
|
|
146
|
+
for(let i=0; i<10; i++) { cb(i); await sleep(50); }
|
|
147
|
+
return "done";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2.3 Server Hooks (Interceptors)
|
|
154
|
+
Use hooks to validate incoming packets.
|
|
155
|
+
```typescript
|
|
156
|
+
createRpcServerAuto({
|
|
157
|
+
/*...*/
|
|
158
|
+
hooks: {
|
|
159
|
+
onRequest: async ({ key, request, fnName, fn }) => {
|
|
160
|
+
// Return false to block the call
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
onInvalid: ({ reason, key, request }) => {
|
|
164
|
+
console.warn(`RPC Attack/Error [${reason}]:`, key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 3. Client (Frontend)
|
|
173
|
+
|
|
174
|
+
**Hub Pattern:** The frontend library doesn't depend on `socket.io-client`. Developer injects the socket factory into `createRpcClientHub`.
|
|
175
|
+
|
|
176
|
+
### 3.1 Hub Initialization
|
|
177
|
+
```typescript
|
|
178
|
+
import { io } from "socket.io-client";
|
|
179
|
+
import { createRpcClientHub, rpc } from "wenay-common2";
|
|
180
|
+
import type { MainFacade } from "../server/facade";
|
|
181
|
+
|
|
182
|
+
export const Api = createRpcClientHub(
|
|
183
|
+
// 1. Socket factory (DI)
|
|
184
|
+
(token) => io("http://localhost:4021", {
|
|
185
|
+
transports: ["websocket"],
|
|
186
|
+
query: token ? { token } : {}
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
// 2. Channel registration
|
|
190
|
+
// rpc() accepts Facade type. Property name ("mainAPI") becomes socketKey.
|
|
191
|
+
(rpc) => ({
|
|
192
|
+
mainAPI: rpc<MainFacade>(),
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 3.2 Connection Lifecycle
|
|
198
|
+
```typescript
|
|
199
|
+
// Listen to statuses
|
|
200
|
+
Api.onConnect((count) => console.log(`Socket connected (attempt ${count})`));
|
|
201
|
+
|
|
202
|
+
// Initiate connect. Creates socket, all channels automatically start.
|
|
203
|
+
Api.setToken("USER_SECRET_TOKEN");
|
|
204
|
+
|
|
205
|
+
// Disconnect
|
|
206
|
+
// Api.setToken(null);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3.3 Call Modes
|
|
210
|
+
Access API channel: `Api.facade.mainAPI`. Hub automatically calls `initStrict()`, so schema loads automatically.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const api = Api.facade.mainAPI;
|
|
214
|
+
|
|
215
|
+
// Wait for schema (REQUIRED before UI render)
|
|
216
|
+
await api.readyStrict();
|
|
217
|
+
|
|
218
|
+
// --- 1. STRICT (Recommended) ---
|
|
219
|
+
// Safe call. Use `?.` since method may be `null` (closed by roles).
|
|
220
|
+
// If method is closed, returns `undefined` without sending network request.
|
|
221
|
+
const res = await api.strict.admin?.deleteUser?.(5);
|
|
222
|
+
|
|
223
|
+
// --- 2. FUNC (Standard) ---
|
|
224
|
+
const res2 = await api.func.ping();
|
|
225
|
+
|
|
226
|
+
// --- 3. PIPE (Pipeline) ---
|
|
227
|
+
// Entire chain goes to server in ONE network packet.
|
|
228
|
+
const data = await api.pipe.dbRef.users.find(1).getName();
|
|
229
|
+
|
|
230
|
+
// --- 4. SPACE (Fire-and-Forget) ---
|
|
231
|
+
// Doesn't wait for response, Promise resolves immediately.
|
|
232
|
+
api.space.admin.logAction("clicked");
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 3.4 Client Subscriptions (Listen)
|
|
236
|
+
Hub automatically wraps methods in `createRpcClientAuto`, providing convenient API for events:
|
|
237
|
+
```typescript
|
|
238
|
+
// Subscribe
|
|
239
|
+
api.events.listenEvent.callback((msg) => {
|
|
240
|
+
console.log("Push from server:", msg);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Unsubscribe (sends `___STOP` packet to server)
|
|
244
|
+
api.events.listenEvent.removeCallback();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
### 3.5 Request Management and Debug
|
|
249
|
+
Each facade has a system object `api` for low-level control, as well as a couple of methods in the root:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const { api, abortAll, schema } = Api.facade.mainAPI;
|
|
253
|
+
|
|
254
|
+
// --- 1. Monitoring and Debug ---
|
|
255
|
+
api.pending(); // Current number of pending responses (Promises)
|
|
256
|
+
api.callbacks(); // Current number of live callbacks in memory
|
|
257
|
+
api.log(true); // Enable logging of all incoming/outgoing packets to console
|
|
258
|
+
|
|
259
|
+
// --- 2. Targeted cleanup (inside .api) ---
|
|
260
|
+
api.clearPromises(true); // Reject (cancel) all current requests
|
|
261
|
+
api.clearCallbacks(); // Force clear all callbacks
|
|
262
|
+
api.remove(myFunc); // Free memory from specific callback (alias: .end)
|
|
263
|
+
|
|
264
|
+
// --- 3. Global facade methods ---
|
|
265
|
+
abortAll("User logout"); // Hard reset: reject all promises with RPC_ABORT error + clear all callbacks
|
|
266
|
+
const map = schema(); // Get raw schema tree (MAP) sent by server
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 4. Advanced Features
|
|
273
|
+
|
|
274
|
+
### 4.1 Listen Argument Interception Modes
|
|
275
|
+
When using events, the client auto-handler (`mode: "smart"`) by default flexibly adapts arguments. If server sends one argument — it comes as a value, if multiple — as an array.
|
|
276
|
+
If you create a client manually without Hub, you can set a strict mode:
|
|
277
|
+
```typescript
|
|
278
|
+
// "first" — callback always receives only the first argument
|
|
279
|
+
// "all" — callback always receives all arguments
|
|
280
|
+
// "smart" — (default) auto-detection
|
|
281
|
+
const autoApi = createRpcClientAuto(rpc.func, { mode: "first" });
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
### 4.2 Manual Callback Termination from Server
|
|
286
|
+
If you passed a callback in function arguments, the server can forcibly terminate it and clear memory on the client before the function completes, by sending a special packet:
|
|
287
|
+
```typescript
|
|
288
|
+
import { rpcEndCallback } from "wenay-common2";
|
|
289
|
+
|
|
290
|
+
async function myMethod(cb: (data: any) => void) {
|
|
291
|
+
cb("chunk 1");
|
|
292
|
+
rpcEndCallback(cb); // Sends "___STOP" to client
|
|
293
|
+
// Client callback is deleted, subsequent cb() calls won't go anywhere
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
### 4.3 Call / Apply Support on Client
|
|
299
|
+
The client proxy can transparently handle standard JS `call` and `apply` calls. Server correctly normalizes them ("collapses" the path):
|
|
300
|
+
```typescript
|
|
301
|
+
// Both variants correctly call `api.users.create("Ivan")` on server
|
|
302
|
+
await api.func.users.create.call(null, "Ivan");
|
|
303
|
+
await api.func.users.create.apply(null, ["Ivan"]);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
### 4.4 Transparent PIPE Request Transit
|
|
308
|
+
For microservice architecture. If your Node server itself is a client of another RPC node (via `pipe`), it can "pass through" the remainder of the pipe chain further using `__executeRemainingPipe`, without waiting for an intermediate response.
|
|
309
|
+
```
|
|
310
|
+
This covers absolutely all library mechanics (Listen modes, `rpcEndCallback`, `call/apply`, `pipe-transit`), while maintaining readability!
|
|
311
|
+
```
|
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import { SocketTmpl } from "./rpc-protocol";
|
|
2
|
+
import { DeepSocketListenSmart } from "./listen-deep";
|
|
2
3
|
export interface RpcHubSocket extends SocketTmpl {
|
|
3
4
|
disconnect?: () => void;
|
|
4
5
|
}
|
|
5
|
-
export
|
|
6
|
-
socketKey
|
|
6
|
+
export type RpcDescriptor<T extends object> = {
|
|
7
|
+
socketKey?: string;
|
|
8
|
+
__type?: T;
|
|
7
9
|
};
|
|
8
|
-
export declare function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export declare function rpc<T extends object>(socketKey?: string): RpcDescriptor<T>;
|
|
11
|
+
export declare function createRpcClientHub<T extends Record<string, RpcDescriptor<any>>>(createSocket: (token: string | null) => RpcHubSocket, schemaBuilder: (helper: typeof rpc) => T): {
|
|
12
|
+
facade: { [K_1 in keyof { [K in keyof T]: T[K] extends RpcDescriptor<infer U extends object> ? {
|
|
13
|
+
func: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
14
|
+
pipe: import("./rpc-client").PipeAPI<DeepSocketListenSmart<U>>;
|
|
15
|
+
pipeStrict: import("./rpc-client").PipeAPI<DeepSocketListenSmart<U>>;
|
|
16
|
+
space: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
17
|
+
all: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
18
|
+
strict: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
19
|
+
api: import("./rpc-client").ClientApiHandle;
|
|
20
|
+
abortAll: (reason: string) => void;
|
|
21
|
+
schema: () => any;
|
|
22
|
+
readyStrict: () => Promise<void>;
|
|
23
|
+
initStrict: (obj?: object) => Promise<void>;
|
|
24
|
+
} : never; }]: { [K in keyof T]: T[K] extends RpcDescriptor<infer U extends object> ? {
|
|
25
|
+
func: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
26
|
+
pipe: import("./rpc-client").PipeAPI<DeepSocketListenSmart<U>>;
|
|
27
|
+
pipeStrict: import("./rpc-client").PipeAPI<DeepSocketListenSmart<U>>;
|
|
28
|
+
space: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
29
|
+
all: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
30
|
+
strict: import("./rpc-client").ClientAPI<DeepSocketListenSmart<U>>;
|
|
31
|
+
api: import("./rpc-client").ClientApiHandle;
|
|
32
|
+
abortAll: (reason: string) => void;
|
|
33
|
+
schema: () => any;
|
|
34
|
+
readyStrict: () => Promise<void>;
|
|
35
|
+
initStrict: (obj?: object) => Promise<void>;
|
|
36
|
+
} : never; }[K_1] | null; };
|
|
14
37
|
setToken: (token: string | null) => void;
|
|
15
38
|
onConnect: (func?: ((count: number) => void) | null) => void;
|
|
16
39
|
connectCount: () => number;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rpc = rpc;
|
|
4
4
|
exports.createRpcClientHub = createRpcClientHub;
|
|
5
|
-
const
|
|
5
|
+
const rpc_client_1 = require("./rpc-client");
|
|
6
6
|
function rpc(socketKey) {
|
|
7
7
|
return { socketKey };
|
|
8
8
|
}
|
|
@@ -20,13 +20,13 @@ function createRpcClientHub(createSocket, schemaBuilder) {
|
|
|
20
20
|
socket = createSocket(token);
|
|
21
21
|
for (const key in schema) {
|
|
22
22
|
const targetSocketKey = schema[key].socketKey || key;
|
|
23
|
-
const client = (0,
|
|
23
|
+
const client = (0, rpc_client_1.createRpcClient)({ socketKey: targetSocketKey, socket });
|
|
24
24
|
facade[key] = client;
|
|
25
|
-
if (client && typeof client
|
|
26
|
-
client
|
|
25
|
+
if (client && typeof client.initStrict === "function") {
|
|
26
|
+
client.initStrict();
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
socket.on(
|
|
29
|
+
socket.on("connect", () => {
|
|
30
30
|
connectCount++;
|
|
31
31
|
onConnectCb?.(connectCount);
|
|
32
32
|
});
|