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 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 declare function rpc<T extends object>(socketKey?: string): {
6
- socketKey: string | undefined;
6
+ export type RpcDescriptor<T extends object> = {
7
+ socketKey?: string;
8
+ __type?: T;
7
9
  };
8
- export declare function createRpcClientHub<T extends Record<string, ReturnType<typeof rpc<any>>>>(createSocket: (token: string | null) => RpcHubSocket, schemaBuilder: (helper: typeof rpc) => T): {
9
- facade: { [K_1 in keyof { [K in keyof T]: T[K] extends {
10
- socketKey: string | undefined;
11
- } ? import("./listen-deep").DeepSocketListen<U> : never; }]: { [K in keyof T]: T[K] extends {
12
- socketKey: string | undefined;
13
- } ? import("./listen-deep").DeepSocketListen<U> : never; }[K_1] | null; };
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 rpc_client_auto_1 = require("./rpc-client-auto");
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, rpc_client_auto_1.createRpcClientAuto)({ socketKey: targetSocketKey, socket });
23
+ const client = (0, rpc_client_1.createRpcClient)({ socketKey: targetSocketKey, socket });
24
24
  facade[key] = client;
25
- if (client && typeof client['initStrict'] === 'function') {
26
- client['initStrict']();
25
+ if (client && typeof client.initStrict === "function") {
26
+ client.initStrict();
27
27
  }
28
28
  }
29
- socket.on('connect', () => {
29
+ socket.on("connect", () => {
30
30
  connectCount++;
31
31
  onConnectCb?.(connectCount);
32
32
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-common2",
3
- "version": "1.0.12",
3
+ "version": "1.0.15",
4
4
  "description": "Common library",
5
5
  "strict": true,
6
6
  "main": "lib/index.js",