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/CLAUDE.md +63 -0
- package/README.md +6 -322
- package/doc/NAMING_RENAMES.md +36 -0
- package/doc/RECOMMENDATIONS.md +56 -0
- package/doc/REPLAY-PLAN.md +85 -0
- package/doc/ROADMAP.md +131 -0
- package/doc/changes/1.0.63.md +8 -0
- package/doc/changes/1.0.64.md +10 -0
- package/doc/changes/1.0.65.md +7 -0
- package/doc/changes/README.md +8 -0
- package/doc/wenay-common2-rare.md +525 -0
- package/doc/wenay-common2.md +338 -0
- package/lib/Common/Observe/store-replay.d.ts +8 -0
- package/lib/Common/Observe/store-replay.js +5 -0
- package/lib/Common/events/replay-index.d.ts +1 -0
- package/lib/Common/events/replay-index.js +1 -0
- package/lib/Common/events/replay-route.d.ts +25 -0
- package/lib/Common/events/replay-route.js +174 -0
- package/package.json +6 -2
- package/rpc.md +293 -0
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
|
+
```
|