silgi 0.53.0 → 0.53.1
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/dist/builder.mjs +32 -6
- package/dist/caller.mjs +65 -55
- package/dist/compile.d.mts +15 -8
- package/dist/compile.mjs +157 -142
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +69 -73
- package/dist/core/input.mjs +95 -33
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +15 -4
- package/dist/core/task.mjs +160 -76
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -128
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.mjs +160 -117
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +126 -87
- package/package.json +1 -1
package/dist/ws.mjs
CHANGED
|
@@ -1,64 +1,132 @@
|
|
|
1
1
|
import { SilgiError, toSilgiError } from "./core/error.mjs";
|
|
2
|
-
import { compileRouter
|
|
2
|
+
import { compileRouter } from "./compile.mjs";
|
|
3
3
|
import { stringifyJSON } from "./core/utils.mjs";
|
|
4
4
|
import { decode, encode } from "./codec/msgpack.mjs";
|
|
5
5
|
//#region src/ws.ts
|
|
6
6
|
/**
|
|
7
|
-
* WebSocket RPC adapter
|
|
7
|
+
* WebSocket RPC adapter
|
|
8
|
+
* -----------------------
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Exposes silgi procedures over a WebSocket connection using the
|
|
11
|
+
* `crossws` runtime-agnostic adapter underneath. Every procedure
|
|
12
|
+
* registered via `router()` is reachable — no opt-in flag required.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Wire protocol
|
|
15
|
+
* -------------
|
|
16
|
+
*
|
|
17
|
+
* Client → Server: { id, path, input? }
|
|
18
|
+
* Server → Client: { id, result?, error? } (single value)
|
|
19
|
+
* Server → Client: { id, data, done? } (streaming chunk)
|
|
20
|
+
*
|
|
21
|
+
* Requests are correlated by `id`. A subscription (any resolver that returns
|
|
22
|
+
* an async iterable) streams back one `{ id, data }` frame per yielded
|
|
23
|
+
* value, followed by a terminal `{ id, data: null, done: true }`. Clients
|
|
24
|
+
* close a subscription by closing the socket; the peer disconnect aborts
|
|
25
|
+
* every in-flight resolver for that peer.
|
|
26
|
+
*
|
|
27
|
+
* Two encodings are supported: UTF-8 JSON (default) and binary MessagePack.
|
|
28
|
+
* The choice is per-adapter, not per-message.
|
|
16
29
|
*/
|
|
17
30
|
/**
|
|
18
|
-
*
|
|
31
|
+
* Build the crossws hook set that implements silgi's WebSocket RPC.
|
|
19
32
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
33
|
+
* @internal
|
|
34
|
+
*
|
|
35
|
+
* This is not part of the public API — `silgi({...}).handler()`,
|
|
36
|
+
* `serve({ ws: true })`, and `attachWebSocket()` are the three supported
|
|
37
|
+
* entry points. They all go through this builder so protocol behavior
|
|
38
|
+
* stays identical everywhere.
|
|
22
39
|
*/
|
|
23
|
-
/** @internal — exported only for use by silgi.ts handler() and attachWebSocket(). Not part of the public API. */
|
|
24
40
|
function _createWSHooks(routerDef, options = {}) {
|
|
25
|
-
const
|
|
41
|
+
const compiled = compileRouter(routerDef);
|
|
26
42
|
const useMsgpack = options.protocol === "messagepack" || options.protocol == null && (options.binary ?? false);
|
|
27
43
|
const contextFactory = options.context;
|
|
28
44
|
const keepaliveMs = options.keepalive === false ? 0 : options.keepalive ?? 3e4;
|
|
29
45
|
const peerAbortControllers = /* @__PURE__ */ new WeakMap();
|
|
30
46
|
const peerKeepaliveTimers = /* @__PURE__ */ new WeakMap();
|
|
31
|
-
|
|
47
|
+
/** Send a single frame, applying the peer's chosen encoding and compression. */
|
|
48
|
+
const send = (peer, data) => {
|
|
32
49
|
const compress = !!options.compress;
|
|
33
50
|
if (useMsgpack) peer.send(encode(data), { compress });
|
|
34
51
|
else peer.send(stringifyJSON(data), { compress });
|
|
35
|
-
}
|
|
36
|
-
|
|
52
|
+
};
|
|
53
|
+
/** Decode an incoming frame into an `RPCRequest`. Throws on parse error. */
|
|
54
|
+
const parseMessage = (message) => {
|
|
37
55
|
if (useMsgpack) return decode(message.uint8Array());
|
|
38
56
|
return message.json();
|
|
39
|
-
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Build the per-request context.
|
|
60
|
+
*
|
|
61
|
+
* Isolated here so the message handler stays readable; the caller
|
|
62
|
+
* handles send-back-error on failure.
|
|
63
|
+
*/
|
|
64
|
+
const buildContext = async (peer) => {
|
|
65
|
+
const ctx = Object.create(null);
|
|
66
|
+
if (contextFactory) {
|
|
67
|
+
const base = await contextFactory(peer);
|
|
68
|
+
for (const key of Object.keys(base)) ctx[key] = base[key];
|
|
69
|
+
}
|
|
70
|
+
return ctx;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Stream a subscription result back to the peer. Returns when the
|
|
74
|
+
* iterator is exhausted, the peer disconnects, or the resolver throws.
|
|
75
|
+
*
|
|
76
|
+
* `iter.return?.()` is called in `finally` so the resolver's cleanup
|
|
77
|
+
* (database cursors, external watchers, etc.) runs even on disconnect.
|
|
78
|
+
*/
|
|
79
|
+
const streamSubscription = async (peer, id, iter, signal) => {
|
|
80
|
+
try {
|
|
81
|
+
for await (const data of iter) {
|
|
82
|
+
if (signal.aborted) break;
|
|
83
|
+
send(peer, {
|
|
84
|
+
id,
|
|
85
|
+
data
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!signal.aborted) send(peer, {
|
|
89
|
+
id,
|
|
90
|
+
data: null,
|
|
91
|
+
done: true
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (!signal.aborted) send(peer, {
|
|
95
|
+
id,
|
|
96
|
+
error: toClientError(err)
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
await iter.return?.();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Install a keepalive ping loop on a peer, if the runtime gives us
|
|
104
|
+
* access to the underlying `ws` instance. Silently no-ops when it
|
|
105
|
+
* does not — some adapters (e.g. Bun) do not expose that handle.
|
|
106
|
+
*/
|
|
107
|
+
const installKeepalive = (peer) => {
|
|
108
|
+
if (keepaliveMs <= 0) return;
|
|
109
|
+
const ws = peer._internal?.ws;
|
|
110
|
+
if (!ws || typeof ws.ping !== "function" || typeof ws.on !== "function" || typeof ws.terminate !== "function") return;
|
|
111
|
+
let alive = true;
|
|
112
|
+
ws.on("pong", () => {
|
|
113
|
+
alive = true;
|
|
114
|
+
});
|
|
115
|
+
const timer = setInterval(() => {
|
|
116
|
+
if (!alive) {
|
|
117
|
+
clearInterval(timer);
|
|
118
|
+
ws.terminate();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
alive = false;
|
|
122
|
+
ws.ping();
|
|
123
|
+
}, keepaliveMs);
|
|
124
|
+
peerKeepaliveTimers.set(peer, timer);
|
|
125
|
+
};
|
|
40
126
|
return {
|
|
41
127
|
open(peer) {
|
|
42
128
|
peerAbortControllers.set(peer, /* @__PURE__ */ new Set());
|
|
43
|
-
|
|
44
|
-
const ws = peer._internal?.ws;
|
|
45
|
-
if (ws && typeof ws.ping === "function") {
|
|
46
|
-
let alive = true;
|
|
47
|
-
ws.on("pong", () => {
|
|
48
|
-
alive = true;
|
|
49
|
-
});
|
|
50
|
-
const timer = setInterval(() => {
|
|
51
|
-
if (!alive) {
|
|
52
|
-
clearInterval(timer);
|
|
53
|
-
ws.terminate();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
alive = false;
|
|
57
|
-
ws.ping();
|
|
58
|
-
}, keepaliveMs);
|
|
59
|
-
peerKeepaliveTimers.set(peer, timer);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
129
|
+
installKeepalive(peer);
|
|
62
130
|
},
|
|
63
131
|
async message(peer, message) {
|
|
64
132
|
let req;
|
|
@@ -76,7 +144,7 @@ function _createWSHooks(routerDef, options = {}) {
|
|
|
76
144
|
return;
|
|
77
145
|
}
|
|
78
146
|
const { id, path, input } = req;
|
|
79
|
-
const route =
|
|
147
|
+
const route = compiled("POST", "/" + path)?.data;
|
|
80
148
|
if (!route) {
|
|
81
149
|
send(peer, {
|
|
82
150
|
id,
|
|
@@ -88,66 +156,35 @@ function _createWSHooks(routerDef, options = {}) {
|
|
|
88
156
|
});
|
|
89
157
|
return;
|
|
90
158
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const base = baseResult instanceof Promise ? await baseResult : baseResult;
|
|
95
|
-
const keys = Object.keys(base);
|
|
96
|
-
for (let i = 0; i < keys.length; i++) ctx[keys[i]] = base[keys[i]];
|
|
159
|
+
let ctx;
|
|
160
|
+
try {
|
|
161
|
+
ctx = await buildContext(peer);
|
|
97
162
|
} catch (err) {
|
|
98
|
-
releaseContext(ctx);
|
|
99
163
|
send(peer, {
|
|
100
164
|
id,
|
|
101
|
-
error: (err
|
|
165
|
+
error: toClientError(err)
|
|
102
166
|
});
|
|
103
167
|
return;
|
|
104
168
|
}
|
|
105
169
|
const ac = new AbortController();
|
|
106
|
-
|
|
107
|
-
controllers?.add(ac);
|
|
170
|
+
peerAbortControllers.get(peer)?.add(ac);
|
|
108
171
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const iter = output;
|
|
113
|
-
try {
|
|
114
|
-
for await (const data of iter) {
|
|
115
|
-
if (ac.signal.aborted) break;
|
|
116
|
-
send(peer, {
|
|
117
|
-
id,
|
|
118
|
-
data
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if (!ac.signal.aborted) send(peer, {
|
|
122
|
-
id,
|
|
123
|
-
data: null,
|
|
124
|
-
done: true
|
|
125
|
-
});
|
|
126
|
-
} catch (err) {
|
|
127
|
-
if (!ac.signal.aborted) send(peer, {
|
|
128
|
-
id,
|
|
129
|
-
error: (err instanceof SilgiError ? err : toSilgiError(err)).toJSON()
|
|
130
|
-
});
|
|
131
|
-
} finally {
|
|
132
|
-
await iter.return?.();
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
send(peer, {
|
|
172
|
+
const output = await route.handler(ctx, input ?? {}, ac.signal);
|
|
173
|
+
if (output && typeof output === "object" && Symbol.asyncIterator in output) await streamSubscription(peer, id, output, ac.signal);
|
|
174
|
+
else send(peer, {
|
|
137
175
|
id,
|
|
138
176
|
result: output
|
|
139
177
|
});
|
|
140
178
|
} catch (err) {
|
|
141
179
|
send(peer, {
|
|
142
180
|
id,
|
|
143
|
-
error: (err
|
|
181
|
+
error: toClientError(err)
|
|
144
182
|
});
|
|
145
183
|
} finally {
|
|
146
|
-
|
|
147
|
-
releaseContext(ctx);
|
|
184
|
+
peerAbortControllers.get(peer)?.delete(ac);
|
|
148
185
|
}
|
|
149
186
|
},
|
|
150
|
-
close(peer
|
|
187
|
+
close(peer) {
|
|
151
188
|
const timer = peerKeepaliveTimers.get(peer);
|
|
152
189
|
if (timer) {
|
|
153
190
|
clearInterval(timer);
|
|
@@ -165,18 +202,20 @@ function _createWSHooks(routerDef, options = {}) {
|
|
|
165
202
|
}
|
|
166
203
|
};
|
|
167
204
|
}
|
|
205
|
+
/** Normalize any thrown value into the `{ code, status, message, ... }` shape clients expect. */
|
|
206
|
+
function toClientError(err) {
|
|
207
|
+
return (err instanceof SilgiError ? err : toSilgiError(err)).toJSON();
|
|
208
|
+
}
|
|
168
209
|
/**
|
|
169
|
-
* Attach WebSocket RPC
|
|
210
|
+
* Attach silgi's WebSocket RPC to an existing Node.js `http.Server`.
|
|
170
211
|
*
|
|
171
212
|
* @example
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* import { attachWebSocket } from "silgi/ws";
|
|
213
|
+
* import { createServer } from 'node:http'
|
|
214
|
+
* import { attachWebSocket } from 'silgi/ws'
|
|
175
215
|
*
|
|
176
|
-
*
|
|
177
|
-
* attachWebSocket(server, appRouter)
|
|
178
|
-
*
|
|
179
|
-
* ```
|
|
216
|
+
* const server = createServer(httpHandler)
|
|
217
|
+
* await attachWebSocket(server, appRouter)
|
|
218
|
+
* server.listen(3000)
|
|
180
219
|
*/
|
|
181
220
|
async function attachWebSocket(server, routerDef, options = {}) {
|
|
182
221
|
const nodeAdapter = (await import("crossws/adapters/node")).default;
|