wenay-common2 1.0.63 → 1.0.65

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/rpc.md ADDED
@@ -0,0 +1,293 @@
1
+
2
+ 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.
3
+
4
+ # wenay-common2 RPC: Complete Guide
5
+
6
+ Bidirectional, strongly-typed RPC protocol over sockets (Socket.IO or similar).
7
+ **Essence:** Server exposes a nested JS object $\to$ Client receives a typed proxy.
8
+
9
+ ---
10
+
11
+ ## 1. Architecture and Limitations
12
+
13
+ * **Multiplexing:** A single physical socket hosts independent channels (`socketKey`), each with its own API object.
14
+ * **Data Types:** Works with JSON-compatible data plus `Date`, `Map`, `Set`, `RegExp`, and `BigInt`. Class instances are sent as plain enumerable object data; methods/prototypes are not preserved.
15
+ * **Security (RpcLimits):** Server is protected from DDoS attacks. Strict limits on: `maxDepth`, `maxKeys`, `maxArrayLen`, `maxStringLen`, `maxCallbacks`. Exceeding throws `PayloadLimitError`.
16
+
17
+ ---
18
+
19
+ ## 2. Server (Backend)
20
+
21
+ ### 2.1 Socket Connection
22
+ ```typescript
23
+ import { createRpcServerAuto, listen } from "wenay-common2";
24
+
25
+ io.sockets.on('connection', (socket) => {
26
+ // 1. Create unsubscribe trigger for memory cleanup
27
+ const [stop, listenStop] = listen<[]>();
28
+ socket.on('disconnect', stop);
29
+
30
+ // 2. Initialize RPC channel on this socket
31
+ createRpcServerAuto({
32
+ socket,
33
+ socketKey: "mainAPI", // Channel identifier
34
+ object: buildFacade(client), // Target API object
35
+ disconnectListen: listenStop, // Auto-unsubscribe from Listen on disconnect
36
+ debug: process.env.DEV, // Packet logging
37
+ });
38
+ });
39
+ ```
40
+
41
+ ### 2.2 Building API Object (Facade)
42
+ The object is traversed by the server to build a "Schema" that is sent to the client.
43
+ ```typescript
44
+ import { noStrict, listen } from "wenay-common2";
45
+
46
+ // Create pub/sub event system
47
+ const [sendEvent, listenEvent] = listen<[string]>();
48
+
49
+ export function buildFacade(client) {
50
+ const role = (...roles) => hasRole(client, roles) ? true : null;
51
+
52
+ return {
53
+ // 1. Regular method
54
+ ping: () => "pong",
55
+
56
+ // 2. Nested namespaces + Role model
57
+ // If role() returns null, the method won't be sent to the client (returns null in schema)
58
+ admin: {
59
+ deleteUser: role("admin") && ((id) => db.delete(id)),
60
+ },
61
+
62
+ // 3. Dynamic objects (Proxy, ORM)
63
+ // Wrap in noStrict so the server doesn't try to read keys.
64
+ // Client will work with it in "blind" mode (without schema).
65
+ dbRef: noStrict(getProxyDb()),
66
+
67
+ // 4. Events
68
+ // Client will receive a Listen surface: .on(cb), .once(cb), .close()
69
+ events: { listenEvent },
70
+
71
+ // 5. Method with callback in arguments
72
+ // Callback lives ONLY while await is executing! After return, client deletes it.
73
+ stream: async (cb: (chunk: number) => void) => {
74
+ for(let i=0; i<10; i++) { cb(i); await sleep(50); }
75
+ return "done";
76
+ }
77
+ };
78
+ }
79
+ ```
80
+
81
+ ### 2.3 Server Hooks (Interceptors)
82
+ Use hooks to validate incoming packets.
83
+ ```typescript
84
+ createRpcServerAuto({
85
+ /*...*/
86
+ hooks: {
87
+ onRequest: async ({ key, request, fnName, fn }) => {
88
+ // Return false to block the call
89
+ return true;
90
+ },
91
+ onInvalid: ({ reason, key, request }) => {
92
+ console.warn(`RPC Attack/Error [${reason}]:`, key);
93
+ }
94
+ }
95
+ })
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 3. Client (Frontend)
101
+
102
+ **Hub Pattern:** The frontend library doesn't depend on `socket.io-client`. Developer injects the socket factory into `createRpcClientHub`.
103
+
104
+ ### 3.1 Hub Initialization
105
+ ```typescript
106
+ import { io } from "socket.io-client";
107
+ import { createRpcClientHub, rpc } from "wenay-common2";
108
+ import type { MainFacade } from "../server/facade";
109
+
110
+ export const Api = createRpcClientHub(
111
+ // 1. Socket factory (DI)
112
+ (token) => io("http://localhost:4021", {
113
+ transports: ["websocket"],
114
+ query: token ? { token } : {}
115
+ }),
116
+
117
+ // 2. Channel registration
118
+ // rpc() accepts Facade type. Property name ("mainAPI") becomes socketKey.
119
+ (rpc) => ({
120
+ mainAPI: rpc<MainFacade>(),
121
+ })
122
+ );
123
+ ```
124
+
125
+ ### 3.2 Connection Lifecycle
126
+ ```typescript
127
+ // Listen to statuses
128
+ Api.onConnect((count) => console.log(`Socket connected (attempt ${count})`));
129
+
130
+ // Initiate connect. Creates socket, all channels automatically start.
131
+ await Api.connect("USER_SECRET_TOKEN");
132
+
133
+ // Disconnect
134
+ // await Api.connect(null);
135
+ ```
136
+
137
+ ### 3.3 Call Modes
138
+ Access API channel: `Api.facade.mainAPI`. Hub initializes all channels, so schema loads automatically.
139
+
140
+ ```typescript
141
+ const api = Api.facade.mainAPI;
142
+
143
+ // Wait for schema (REQUIRED before UI render)
144
+ await api.ready();
145
+
146
+ // --- 1. STRICT (Recommended) ---
147
+ // Safe call. Use `?.` since method may be `null` (closed by roles).
148
+ // If method is closed, returns `undefined` without sending network request.
149
+ const res = await api.strict.admin?.deleteUser?.(5);
150
+
151
+ // **TIP:** You can save .strict reference to avoid repetitive code:
152
+ // Works in ALL modes (strict, func, pipe, space, all)
153
+ const orchestrator = Api.facade.orchestrator?.strict;
154
+ await orchestrator?.repositories?.getAll?.();
155
+
156
+ // **IMPORTANT: Optional chaining for methods**
157
+ // - Use `?.` ONLY if method can be filtered by roles on backend
158
+ // - If backend has NO role filtering, method is ALWAYS available - skip `?.`
159
+ // Example: if backend has no roles, use .all mode instead:
160
+ const orchestrator = Api.facade.orchestrator?.all;
161
+ await orchestrator.repositories.getAll(); // No `?.` needed!
162
+ await orchestrator.deployments.getAll(); // Cleaner code!
163
+
164
+ // --- 2. FUNC (Standard) ---
165
+ const res2 = await api.func.ping();
166
+
167
+ // --- 3. PIPE (Pipeline) ---
168
+ // Entire chain goes to server in ONE network packet.
169
+ const data = await api.pipe.dbRef.users.find(1).getName();
170
+
171
+ // --- 4. SPACE (Fire-and-Forget) ---
172
+ // Doesn't wait for response, Promise resolves immediately.
173
+ api.space.admin.logAction("clicked");
174
+ ```
175
+
176
+ ### 3.4 Client Subscriptions (Listen)
177
+ `createRpcServerAuto` exposes server `listen` / `createListen` values as RPC Listen nodes. New code uses `on`/`once` and keeps the returned `off` handle. For TypeScript, project `client.func` to `DeepSocketListen<ServerFacade>`; this mirrors the runtime shape and keeps event argument types.
178
+ ```typescript
179
+ import type { DeepSocketListen } from "wenay-common2";
180
+
181
+ function webListen<T extends object>(client: { func: unknown }) {
182
+ return client.func as DeepSocketListen<T>;
183
+ }
184
+
185
+ const events = webListen<MainFacade>(api).events;
186
+
187
+ // Subscribe
188
+ const off = events.listenEvent.on((msg) => {
189
+ console.log("Push from server:", msg);
190
+ });
191
+
192
+ // Unsubscribe. The handle is callable and also thenable.
193
+ off();
194
+ // await off; // waits until the stream ends
195
+
196
+ // One event, then automatic unsubscribe
197
+ const done = events.listenEvent.once((msg) => {
198
+ console.log("First push:", msg);
199
+ });
200
+ await done;
201
+ ```
202
+
203
+ Compatibility names `.callback(cb)`, `.removeCallback()`, and `.unsubscribe()` still exist for old clients, but they are not the recommended API.
204
+
205
+
206
+ ### 3.5 Request Management and Debug
207
+ Each facade has a system object `api` for low-level control, as well as a couple of methods in the root:
208
+
209
+ ```typescript
210
+ const { api, abortAll, schema } = Api.facade.mainAPI;
211
+
212
+ // --- 1. Monitoring and Debug ---
213
+ api.pending(); // Current number of pending responses (Promises)
214
+ api.callbacks(); // Current number of live callback ids in memory
215
+ api.log(true); // Enable logging of all incoming/outgoing packets to console
216
+
217
+ // --- 2. Targeted cleanup (inside .api) ---
218
+ api.clearPromises(true); // Reject (cancel) all current requests
219
+ api.clearCallbacks(); // Force clear all callbacks
220
+ api.remove(myFunc); // Force-release a specific callback id (alias: .end)
221
+
222
+ // --- 3. Global facade methods ---
223
+ abortAll("User logout"); // Hard reset: reject all promises with RPC_ABORT error + clear all callback ids
224
+ const map = schema(); // Get raw schema tree (MAP) sent by server
225
+ ```
226
+
227
+
228
+ ---
229
+
230
+ ## 4. Advanced Features
231
+
232
+ ### 4.1 Listen Argument Interception Modes
233
+ 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.
234
+ If you create a client manually without Hub, you can set a strict mode:
235
+ ```typescript
236
+ // "first" — listener always receives only the first argument
237
+ // "all" — listener always receives all arguments
238
+ // "smart" — (default) auto-detection
239
+ const autoApi = createRpcClientAuto(api.func, { mode: "first" });
240
+ ```
241
+
242
+
243
+ ### 4.2 Manual Callback Termination from Server
244
+ This is a low-level escape hatch for callbacks passed as ordinary function arguments. For Listen subscriptions prefer `off()`/`.once()`.
245
+ ```typescript
246
+ import { endCallback } from "wenay-common2";
247
+
248
+ async function myMethod(cb: (data: any) => void) {
249
+ cb("chunk 1");
250
+ endCallback(cb); // Alias: rpcEndCallback. Sends "___STOP" to client.
251
+ // Client callback id is deleted, subsequent cb() calls won't go anywhere.
252
+ }
253
+ ```
254
+
255
+ ### 4.3 Call / Apply Support on Client
256
+ The client proxy can transparently handle standard JS `call` and `apply` calls. Server correctly normalizes them ("collapses" the path):
257
+ ```typescript
258
+ // Both variants correctly call `api.users.create("Ivan")` on server
259
+ await api.func.users.create.call(null, "Ivan");
260
+ await api.func.users.create.apply(null, ["Ivan"]);
261
+ ```
262
+
263
+
264
+ ### 4.4 Transparent PIPE Request Transit
265
+ 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.
266
+ This covers the important RPC mechanics (Listen `on/once/off`, `endCallback`, `call/apply`, `pipe-transit`) while maintaining readability.
267
+ ```typescript
268
+
269
+ /**
270
+ * Extract orchestrator type from Api facade
271
+ */
272
+ export type OrchestratorFacade = NonNullable<typeof Api.facade.orchestrator>['strict'];
273
+
274
+ /**
275
+ * Hook for RPC initialization and facade access
276
+ */
277
+ export function useOrchestrator() {
278
+ const [rpcInitialized, setRpcInitialized] = useState(false);
279
+
280
+ useEffect(() => {
281
+ Api.connect(null); // Connect without token
282
+ Api.facade.orchestrator?.ready().then(() => {
283
+ setRpcInitialized(true);
284
+ });
285
+ }, []);
286
+
287
+ // Use .all mode for simpler code without optional chaining when no role filtering
288
+ const orchestrator = Api.facade.orchestrator?.all;
289
+
290
+ return { orchestrator, rpcInitialized };
291
+ }
292
+
293
+ ```