silgi 0.50.1 → 0.50.2
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/adapters/_fetch-adapter.d.mts +4 -2
- package/dist/adapters/_fetch-adapter.mjs +4 -4
- package/dist/client/adapters/websocket/index.d.mts +2 -2
- package/dist/client/adapters/websocket/index.mjs +146 -15
- package/dist/compile.d.mts +0 -2
- package/dist/compile.mjs +0 -1
- package/dist/core/handler.d.mts +7 -1
- package/dist/core/handler.mjs +30 -1
- package/dist/core/serve.d.mts +7 -2
- package/dist/core/serve.mjs +54 -21
- package/dist/index.d.mts +1 -1
- package/dist/plugins/file-upload.d.mts +1 -1
- package/dist/scalar.mjs +2 -2
- package/dist/silgi.d.mts +1 -1
- package/dist/silgi.mjs +23 -23
- package/dist/types.d.mts +0 -2
- package/dist/ws.d.mts +6 -16
- package/dist/ws.mjs +8 -18
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { WrapHandlerOptions } from "../core/handler.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/adapters/_fetch-adapter.d.ts
|
|
2
|
-
interface FetchAdapterConfig<TCtx extends Record<string, unknown>> {
|
|
4
|
+
interface FetchAdapterConfig<TCtx extends Record<string, unknown>> extends WrapHandlerOptions {
|
|
3
5
|
/** Route prefix to strip. Default: "/api/rpc" */
|
|
4
6
|
prefix?: string;
|
|
5
7
|
/** Context factory — receives the Request (or framework event via eventMap). */
|
|
@@ -9,7 +11,7 @@ interface FetchAdapterConfig<TCtx extends Record<string, unknown>> {
|
|
|
9
11
|
* For adapters where the context factory needs access to a framework event
|
|
10
12
|
* (SvelteKit RequestEvent, SolidStart event), use this extended config.
|
|
11
13
|
*/
|
|
12
|
-
interface FetchAdapterConfigWithEvent<TCtx extends Record<string, unknown>, TEvent = any> {
|
|
14
|
+
interface FetchAdapterConfigWithEvent<TCtx extends Record<string, unknown>, TEvent = any> extends WrapHandlerOptions {
|
|
13
15
|
prefix?: string;
|
|
14
16
|
/** Context factory — receives the framework event, not raw Request. */
|
|
15
17
|
context?: (event: TEvent) => TCtx | Promise<TCtx>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFetchHandler } from "../core/handler.mjs";
|
|
1
|
+
import { createFetchHandler, wrapHandler } from "../core/handler.mjs";
|
|
2
2
|
//#region src/adapters/_fetch-adapter.ts
|
|
3
3
|
/**
|
|
4
4
|
* Shared factory for fetch-passthrough adapters.
|
|
@@ -25,7 +25,7 @@ function rewriteRequest(request, prefix) {
|
|
|
25
25
|
*/
|
|
26
26
|
function createFetchAdapter(router, options, defaultPrefix) {
|
|
27
27
|
const prefix = options.prefix ?? defaultPrefix;
|
|
28
|
-
const handler = createFetchHandler(router, options.context ?? (() => ({})));
|
|
28
|
+
const handler = wrapHandler(createFetchHandler(router, options.context ?? (() => ({}))), router, options);
|
|
29
29
|
return (request) => {
|
|
30
30
|
return handler(rewriteRequest(request, prefix));
|
|
31
31
|
};
|
|
@@ -38,11 +38,11 @@ function createFetchAdapter(router, options, defaultPrefix) {
|
|
|
38
38
|
function createEventFetchAdapter(router, options, defaultPrefix, extractRequest) {
|
|
39
39
|
const prefix = options.prefix ?? defaultPrefix;
|
|
40
40
|
const requestEventMap = /* @__PURE__ */ new WeakMap();
|
|
41
|
-
const handler = createFetchHandler(router, (_req) => {
|
|
41
|
+
const handler = wrapHandler(createFetchHandler(router, (_req) => {
|
|
42
42
|
const eventRef = requestEventMap.get(_req);
|
|
43
43
|
if (options.context && eventRef) return options.context(eventRef);
|
|
44
44
|
return {};
|
|
45
|
-
});
|
|
45
|
+
}), router, options);
|
|
46
46
|
return (event) => {
|
|
47
47
|
const rewritten = rewriteRequest(extractRequest(event), prefix);
|
|
48
48
|
requestEventMap.set(rewritten, event);
|
|
@@ -2,7 +2,7 @@ import { ClientContext, ClientLink, ClientOptions } from "../../types.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/client/adapters/websocket/index.d.ts
|
|
4
4
|
interface WSLinkOptions {
|
|
5
|
-
/** WebSocket URL (e.g. 'ws://localhost:3000/
|
|
5
|
+
/** WebSocket URL (e.g. 'ws://localhost:3000/_ws') */
|
|
6
6
|
url: string | URL;
|
|
7
7
|
/** Wire protocol (default: 'json') */
|
|
8
8
|
protocol?: 'json' | 'messagepack';
|
|
@@ -13,7 +13,7 @@ declare class WSLink<TClientContext extends ClientContext = ClientContext> imple
|
|
|
13
13
|
#private;
|
|
14
14
|
constructor(options: WSLinkOptions);
|
|
15
15
|
call(path: readonly string[], input: unknown, options: ClientOptions<TClientContext>): Promise<unknown>;
|
|
16
|
-
/** Close the WebSocket connection and reject all pending calls */
|
|
16
|
+
/** Close the WebSocket connection and reject/terminate all pending calls */
|
|
17
17
|
dispose(): void;
|
|
18
18
|
}
|
|
19
19
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SilgiError } from "../../../core/error.mjs";
|
|
2
|
+
import { decode, encode } from "../../../codec/msgpack.mjs";
|
|
2
3
|
//#region src/client/adapters/websocket/index.ts
|
|
3
4
|
/**
|
|
4
5
|
* WebSocket client link — bidirectional RPC over WebSocket.
|
|
@@ -8,14 +9,91 @@ import { SilgiError } from "../../../core/error.mjs";
|
|
|
8
9
|
* Server → Client: { id: string, result?: unknown, error?: unknown }
|
|
9
10
|
* Server → Client (stream): { id: string, data: unknown, done?: boolean }
|
|
10
11
|
*
|
|
12
|
+
* Single-response calls return a Promise; streaming calls (subscriptions)
|
|
13
|
+
* resolve to an AsyncIterableIterator that yields each `data` message until
|
|
14
|
+
* the terminating `{ done: true }` frame.
|
|
15
|
+
*
|
|
11
16
|
* @example
|
|
12
17
|
* ```ts
|
|
13
18
|
* import { WSLink } from 'silgi/client/ws'
|
|
14
19
|
*
|
|
15
|
-
* const link = new WSLink({ url: 'ws://localhost:3000/
|
|
20
|
+
* const link = new WSLink({ url: 'ws://localhost:3000/_ws' })
|
|
16
21
|
* const client = createClient<AppRouter>(link)
|
|
22
|
+
*
|
|
23
|
+
* // Query/mutation — single response
|
|
24
|
+
* const users = await client.users.list()
|
|
25
|
+
*
|
|
26
|
+
* // Subscription — async iterator
|
|
27
|
+
* const iter = await client.onUserUpdate()
|
|
28
|
+
* for await (const ev of iter) console.log(ev)
|
|
17
29
|
* ```
|
|
18
30
|
*/
|
|
31
|
+
/** Queue-backed async iterator used for subscription streams */
|
|
32
|
+
function createStreamIterator() {
|
|
33
|
+
const values = [];
|
|
34
|
+
const waiters = [];
|
|
35
|
+
let done = false;
|
|
36
|
+
let error = void 0;
|
|
37
|
+
const push = (v) => {
|
|
38
|
+
if (done) return;
|
|
39
|
+
const w = waiters.shift();
|
|
40
|
+
if (w) w.resolve({
|
|
41
|
+
value: v,
|
|
42
|
+
done: false
|
|
43
|
+
});
|
|
44
|
+
else values.push(v);
|
|
45
|
+
};
|
|
46
|
+
const end = () => {
|
|
47
|
+
if (done) return;
|
|
48
|
+
done = true;
|
|
49
|
+
while (waiters.length > 0) waiters.shift().resolve({
|
|
50
|
+
value: void 0,
|
|
51
|
+
done: true
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const fail = (err) => {
|
|
55
|
+
if (done) return;
|
|
56
|
+
done = true;
|
|
57
|
+
error = err;
|
|
58
|
+
while (waiters.length > 0) waiters.shift().reject(err);
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
iter: {
|
|
62
|
+
[Symbol.asyncIterator]() {
|
|
63
|
+
return this;
|
|
64
|
+
},
|
|
65
|
+
next() {
|
|
66
|
+
if (values.length > 0) return Promise.resolve({
|
|
67
|
+
value: values.shift(),
|
|
68
|
+
done: false
|
|
69
|
+
});
|
|
70
|
+
if (done) {
|
|
71
|
+
if (error !== void 0) return Promise.reject(error);
|
|
72
|
+
return Promise.resolve({
|
|
73
|
+
value: void 0,
|
|
74
|
+
done: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
waiters.push({
|
|
79
|
+
resolve,
|
|
80
|
+
reject
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
return() {
|
|
85
|
+
end();
|
|
86
|
+
return Promise.resolve({
|
|
87
|
+
value: void 0,
|
|
88
|
+
done: true
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
push,
|
|
93
|
+
end,
|
|
94
|
+
fail
|
|
95
|
+
};
|
|
96
|
+
}
|
|
19
97
|
var WSLink = class {
|
|
20
98
|
#url;
|
|
21
99
|
#WebSocket;
|
|
@@ -23,30 +101,50 @@ var WSLink = class {
|
|
|
23
101
|
#pending = /* @__PURE__ */ new Map();
|
|
24
102
|
#nextId = 0;
|
|
25
103
|
#connecting;
|
|
104
|
+
#useMsgpack;
|
|
26
105
|
constructor(options) {
|
|
27
106
|
this.#url = typeof options.url === "string" ? options.url : options.url.href;
|
|
28
107
|
this.#WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
108
|
+
this.#useMsgpack = options.protocol === "messagepack";
|
|
29
109
|
}
|
|
30
110
|
async call(path, input, options) {
|
|
31
111
|
await this.#ensureConnected();
|
|
32
112
|
const id = String(this.#nextId++);
|
|
33
113
|
return new Promise((resolve, reject) => {
|
|
34
114
|
this.#pending.set(id, {
|
|
35
|
-
|
|
115
|
+
kind: "single",
|
|
116
|
+
resolve: (value) => {
|
|
117
|
+
resolve(value);
|
|
118
|
+
},
|
|
36
119
|
reject
|
|
37
120
|
});
|
|
38
121
|
options.signal?.addEventListener("abort", () => {
|
|
122
|
+
const p = this.#pending.get(id);
|
|
123
|
+
if (!p) return;
|
|
39
124
|
this.#pending.delete(id);
|
|
40
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
125
|
+
if (p.kind === "single") p.reject(new DOMException("Aborted", "AbortError"));
|
|
126
|
+
else p.fail(new DOMException("Aborted", "AbortError"));
|
|
41
127
|
}, { once: true });
|
|
42
128
|
const msg = {
|
|
43
129
|
id,
|
|
44
130
|
path: path.join("/"),
|
|
45
131
|
input
|
|
46
132
|
};
|
|
47
|
-
this.#
|
|
133
|
+
this.#sendFrame(msg);
|
|
48
134
|
});
|
|
49
135
|
}
|
|
136
|
+
#sendFrame(msg) {
|
|
137
|
+
if (this.#useMsgpack) this.#ws.send(encode(msg));
|
|
138
|
+
else this.#ws.send(JSON.stringify(msg));
|
|
139
|
+
}
|
|
140
|
+
#decodeFrame(data) {
|
|
141
|
+
if (this.#useMsgpack) {
|
|
142
|
+
if (data instanceof ArrayBuffer) return decode(new Uint8Array(data));
|
|
143
|
+
if (typeof data === "string") return JSON.parse(data);
|
|
144
|
+
throw new SilgiError("INTERNAL_SERVER_ERROR", { message: "Unexpected Blob frame" });
|
|
145
|
+
}
|
|
146
|
+
return JSON.parse(typeof data === "string" ? data : new TextDecoder().decode(data));
|
|
147
|
+
}
|
|
50
148
|
#ensureConnected() {
|
|
51
149
|
if (this.#ws?.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
52
150
|
if (this.#connecting) return this.#connecting;
|
|
@@ -63,37 +161,70 @@ var WSLink = class {
|
|
|
63
161
|
reject(new SilgiError("INTERNAL_SERVER_ERROR", { message: "WebSocket connection failed" }));
|
|
64
162
|
});
|
|
65
163
|
ws.addEventListener("message", (event) => {
|
|
66
|
-
|
|
164
|
+
let msg;
|
|
165
|
+
try {
|
|
166
|
+
msg = this.#decodeFrame(event.data);
|
|
167
|
+
} catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
67
170
|
const pending = this.#pending.get(msg.id);
|
|
68
171
|
if (!pending) return;
|
|
69
172
|
if (msg.error) {
|
|
70
173
|
this.#pending.delete(msg.id);
|
|
71
174
|
const err = msg.error;
|
|
72
|
-
|
|
175
|
+
const silgiErr = new SilgiError(err.code ?? "INTERNAL_SERVER_ERROR", {
|
|
73
176
|
status: err.status,
|
|
74
177
|
message: err.message,
|
|
75
178
|
data: err.data
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
179
|
+
});
|
|
180
|
+
if (pending.kind === "single") pending.reject(silgiErr);
|
|
181
|
+
else pending.fail(silgiErr);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if ("result" in msg) {
|
|
81
185
|
this.#pending.delete(msg.id);
|
|
82
|
-
pending.resolve(msg.result);
|
|
186
|
+
if (pending.kind === "single") pending.resolve(msg.result);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if ("data" in msg) {
|
|
190
|
+
if (msg.done === true) {
|
|
191
|
+
this.#pending.delete(msg.id);
|
|
192
|
+
if (pending.kind === "stream") pending.end();
|
|
193
|
+
else pending.resolve(msg.data);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (pending.kind === "single") {
|
|
197
|
+
const { iter, push, end, fail } = createStreamIterator();
|
|
198
|
+
const streamPending = {
|
|
199
|
+
kind: "stream",
|
|
200
|
+
push,
|
|
201
|
+
end,
|
|
202
|
+
fail
|
|
203
|
+
};
|
|
204
|
+
this.#pending.set(msg.id, streamPending);
|
|
205
|
+
pending.resolve(iter);
|
|
206
|
+
push(msg.data);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
pending.push(msg.data);
|
|
83
210
|
}
|
|
84
211
|
});
|
|
85
212
|
ws.addEventListener("close", () => {
|
|
86
213
|
this.#ws = void 0;
|
|
87
|
-
|
|
214
|
+
const err = new SilgiError("INTERNAL_SERVER_ERROR", { message: "WebSocket closed" });
|
|
215
|
+
for (const [, p] of this.#pending) if (p.kind === "single") p.reject(err);
|
|
216
|
+
else p.fail(err);
|
|
88
217
|
this.#pending.clear();
|
|
89
218
|
});
|
|
90
219
|
});
|
|
91
220
|
return this.#connecting;
|
|
92
221
|
}
|
|
93
|
-
/** Close the WebSocket connection and reject all pending calls */
|
|
222
|
+
/** Close the WebSocket connection and reject/terminate all pending calls */
|
|
94
223
|
dispose() {
|
|
95
224
|
this.#ws?.close();
|
|
96
|
-
|
|
225
|
+
const err = new DOMException("Link disposed", "AbortError");
|
|
226
|
+
for (const [, p] of this.#pending) if (p.kind === "single") p.reject(err);
|
|
227
|
+
else p.fail(err);
|
|
97
228
|
this.#pending.clear();
|
|
98
229
|
}
|
|
99
230
|
};
|
package/dist/compile.d.mts
CHANGED
|
@@ -20,8 +20,6 @@ interface CompiledRoute {
|
|
|
20
20
|
handler: CompiledHandler;
|
|
21
21
|
/** Pre-computed Cache-Control header value, or undefined if no caching */
|
|
22
22
|
cacheControl?: string;
|
|
23
|
-
/** Procedure is accessible over WebSocket */
|
|
24
|
-
ws?: boolean;
|
|
25
23
|
/** Skip body parsing — procedure receives raw request (e.g. catch-all proxy) */
|
|
26
24
|
passthrough?: boolean;
|
|
27
25
|
/** HTTP method this route is registered for (uppercase) */
|
package/dist/compile.mjs
CHANGED
package/dist/core/handler.d.mts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { AnalyticsOptions } from "../plugins/analytics/types.mjs";
|
|
2
|
+
import { ScalarOptions } from "../scalar.mjs";
|
|
1
3
|
import { Hookable } from "hookable";
|
|
2
4
|
|
|
3
5
|
//#region src/core/handler.d.ts
|
|
4
6
|
type FetchHandler = (request: Request) => Response | Promise<Response>;
|
|
7
|
+
interface WrapHandlerOptions {
|
|
8
|
+
analytics?: boolean | AnalyticsOptions;
|
|
9
|
+
scalar?: boolean | ScalarOptions;
|
|
10
|
+
}
|
|
5
11
|
//#endregion
|
|
6
|
-
export { FetchHandler };
|
|
12
|
+
export { FetchHandler, WrapHandlerOptions };
|
package/dist/core/handler.mjs
CHANGED
|
@@ -67,6 +67,35 @@ function makeResponse(output, route, format, ctx) {
|
|
|
67
67
|
...cacheHeaders
|
|
68
68
|
} : { "content-type": "application/json" } });
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Lazily wrap a FetchHandler with analytics and/or scalar.
|
|
72
|
+
* Returns a new handler that applies wrappers on first request (async import).
|
|
73
|
+
* If no wrappers are needed, returns the original handler as-is.
|
|
74
|
+
*/
|
|
75
|
+
function wrapHandler(handler, router, options) {
|
|
76
|
+
if (!options?.scalar && !options?.analytics) return handler;
|
|
77
|
+
let wrapped;
|
|
78
|
+
let initPromise;
|
|
79
|
+
async function init() {
|
|
80
|
+
let h = handler;
|
|
81
|
+
if (options.scalar) {
|
|
82
|
+
const { wrapWithScalar } = await import("../scalar.mjs");
|
|
83
|
+
const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
|
|
84
|
+
h = wrapWithScalar(h, router, scalarOpts);
|
|
85
|
+
}
|
|
86
|
+
if (options.analytics) {
|
|
87
|
+
const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
|
|
88
|
+
const analyticsOpts = typeof options.analytics === "object" ? options.analytics : {};
|
|
89
|
+
h = wrapWithAnalytics(h, analyticsOpts);
|
|
90
|
+
}
|
|
91
|
+
wrapped = h;
|
|
92
|
+
}
|
|
93
|
+
return (request) => {
|
|
94
|
+
if (wrapped) return wrapped(request);
|
|
95
|
+
initPromise ??= init();
|
|
96
|
+
return initPromise.then(() => wrapped(request));
|
|
97
|
+
};
|
|
98
|
+
}
|
|
70
99
|
function createFetchHandler(routerDef, contextFactory, hooks) {
|
|
71
100
|
let compiledRouter = routerCache.get(routerDef);
|
|
72
101
|
if (!compiledRouter) {
|
|
@@ -150,4 +179,4 @@ function createFetchHandler(routerDef, contextFactory, hooks) {
|
|
|
150
179
|
};
|
|
151
180
|
}
|
|
152
181
|
//#endregion
|
|
153
|
-
export { createFetchHandler };
|
|
182
|
+
export { createFetchHandler, wrapHandler };
|
package/dist/core/serve.d.mts
CHANGED
|
@@ -26,8 +26,13 @@ interface ServeOptions {
|
|
|
26
26
|
scalar?: boolean | ScalarOptions;
|
|
27
27
|
/** Enable analytics dashboard at /api/analytics */
|
|
28
28
|
analytics?: boolean | AnalyticsOptions;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* WebSocket RPC configuration.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to auto-enabled when the router contains any subscription procedure.
|
|
33
|
+
* Pass `false` to disable, or an options object to fine-tune crossws (compression, keepalive, maxPayload).
|
|
34
|
+
*/
|
|
35
|
+
ws?: false | WSAdapterOptions;
|
|
31
36
|
/** Enable HTTP/2 (requires cert + key for TLS) */
|
|
32
37
|
http2?: {
|
|
33
38
|
cert: string;
|
package/dist/core/serve.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { createFetchHandler } from "./handler.mjs";
|
|
1
|
+
import { createFetchHandler, wrapHandler } from "./handler.mjs";
|
|
2
|
+
import { _createWSHooks } from "../ws.mjs";
|
|
2
3
|
import { serve } from "srvx";
|
|
3
4
|
//#region src/core/serve.ts
|
|
5
|
+
function detectRuntime() {
|
|
6
|
+
if (typeof globalThis.Bun !== "undefined") return "bun";
|
|
7
|
+
if (typeof globalThis.Deno !== "undefined") return "deno";
|
|
8
|
+
return "node";
|
|
9
|
+
}
|
|
10
|
+
function routerHasSubscription(def) {
|
|
11
|
+
if (!def || typeof def !== "object") return false;
|
|
12
|
+
if (def.type === "subscription") return true;
|
|
13
|
+
for (const v of Object.values(def)) if (routerHasSubscription(v)) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
4
16
|
async function createServeHandler(routerDef, contextFactory, hooks, options) {
|
|
5
17
|
const port = options?.port ?? 3e3;
|
|
6
18
|
const hostname = options?.hostname ?? "127.0.0.1";
|
|
7
|
-
|
|
8
|
-
if (options?.scalar) {
|
|
9
|
-
const { wrapWithScalar } = await import("../scalar.mjs");
|
|
10
|
-
const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
|
|
11
|
-
handler = wrapWithScalar(handler, routerDef, scalarOpts);
|
|
12
|
-
}
|
|
13
|
-
if (options?.analytics) {
|
|
14
|
-
const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
|
|
15
|
-
const analyticsOpts = typeof options.analytics === "object" ? options.analytics : {};
|
|
16
|
-
handler = wrapWithAnalytics(handler, analyticsOpts);
|
|
17
|
-
}
|
|
19
|
+
const httpHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks), routerDef, options);
|
|
18
20
|
const shutdownOpt = options?.gracefulShutdown ?? true;
|
|
19
21
|
let gracefulShutdown;
|
|
20
22
|
if (typeof shutdownOpt === "object") gracefulShutdown = {
|
|
@@ -22,34 +24,65 @@ async function createServeHandler(routerDef, contextFactory, hooks, options) {
|
|
|
22
24
|
forceTimeout: shutdownOpt.forceTimeout
|
|
23
25
|
};
|
|
24
26
|
else gracefulShutdown = shutdownOpt;
|
|
27
|
+
const wsExplicitlyDisabled = options?.ws === false;
|
|
28
|
+
const wsOpts = typeof options?.ws === "object" ? options.ws : void 0;
|
|
29
|
+
const wsEnabled = !wsExplicitlyDisabled && routerHasSubscription(routerDef);
|
|
30
|
+
const runtime = detectRuntime();
|
|
31
|
+
let fetchHandler = httpHandler;
|
|
32
|
+
let bunWebsocket;
|
|
33
|
+
const bunServerRef = { current: void 0 };
|
|
34
|
+
let nodeAttach;
|
|
35
|
+
if (wsEnabled) {
|
|
36
|
+
const hooksObj = _createWSHooks(routerDef, wsOpts);
|
|
37
|
+
if (runtime === "bun") {
|
|
38
|
+
const bunAdapter = (await import("crossws/adapters/bun")).default;
|
|
39
|
+
const adapter = bunAdapter({ hooks: hooksObj });
|
|
40
|
+
bunWebsocket = adapter.websocket;
|
|
41
|
+
fetchHandler = (async (req) => {
|
|
42
|
+
if (req.headers.get("upgrade") === "websocket" && bunServerRef.current) {
|
|
43
|
+
const res = await adapter.handleUpgrade(req, bunServerRef.current);
|
|
44
|
+
if (res) return res;
|
|
45
|
+
}
|
|
46
|
+
return httpHandler(req);
|
|
47
|
+
});
|
|
48
|
+
} else if (runtime === "deno") {
|
|
49
|
+
const denoAdapter = (await import("crossws/adapters/deno")).default;
|
|
50
|
+
const adapter = denoAdapter({ hooks: hooksObj });
|
|
51
|
+
fetchHandler = (async (req) => {
|
|
52
|
+
if (req.headers.get("upgrade") === "websocket") return adapter.handleUpgrade(req, {});
|
|
53
|
+
return httpHandler(req);
|
|
54
|
+
});
|
|
55
|
+
} else nodeAttach = async (httpServer) => {
|
|
56
|
+
const { attachWebSocket } = await import("../ws.mjs");
|
|
57
|
+
await attachWebSocket(httpServer, routerDef, wsOpts);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
25
60
|
const server = await serve({
|
|
26
61
|
port,
|
|
27
62
|
hostname,
|
|
28
|
-
fetch:
|
|
63
|
+
fetch: fetchHandler,
|
|
29
64
|
gracefulShutdown,
|
|
30
65
|
silent: true,
|
|
31
66
|
...options?.http2 && { tls: {
|
|
32
67
|
cert: options.http2.cert,
|
|
33
68
|
key: options.http2.key
|
|
34
|
-
} }
|
|
69
|
+
} },
|
|
70
|
+
...bunWebsocket ? { bun: { websocket: bunWebsocket } } : {}
|
|
35
71
|
});
|
|
36
72
|
await server.ready();
|
|
73
|
+
if (runtime === "bun" && server.bun?.server) bunServerRef.current = server.bun.server;
|
|
74
|
+
if (nodeAttach && server.node?.server) await nodeAttach(server.node.server);
|
|
37
75
|
let resolvedPort = port;
|
|
38
76
|
if (server.node?.server) {
|
|
39
77
|
const addr = server.node.server.address();
|
|
40
78
|
if (addr && typeof addr === "object") resolvedPort = addr.port;
|
|
41
|
-
}
|
|
79
|
+
} else if (server.bun?.server) resolvedPort = server.bun.server.port ?? port;
|
|
42
80
|
const protocol = options?.http2 ? "https" : "http";
|
|
43
81
|
const rawUrl = server.url || `${protocol}://${hostname}:${resolvedPort}`;
|
|
44
82
|
const url = rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl;
|
|
45
|
-
if (options?.ws && server.node?.server) {
|
|
46
|
-
const { attachWebSocket } = await import("../ws.mjs");
|
|
47
|
-
const wsOpts = typeof options.ws === "object" ? options.ws : void 0;
|
|
48
|
-
await attachWebSocket(server.node.server, routerDef, wsOpts);
|
|
49
|
-
}
|
|
50
83
|
console.log(`\nSilgi server running at ${url}`);
|
|
51
84
|
if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
|
|
52
|
-
if (
|
|
85
|
+
if (wsEnabled) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}/_ws (${runtime})`);
|
|
53
86
|
if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
|
|
54
87
|
if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
|
|
55
88
|
console.log();
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AnySchema, InferSchemaInput, InferSchemaOutput, Schema, ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
2
|
import { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { ErrorDef, ErrorDefItem, FailFn, GuardDef, GuardFn, InferClient, InferContextFromUse, InferGuardOutput, Meta, MiddlewareDef, ProcedureDef, ProcedureType, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
|
|
4
|
-
import { ProcedureBuilder, ProcedureBuilderWithOutput } from "./builder.mjs";
|
|
5
4
|
import { ScalarOptions, generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
5
|
+
import { ProcedureBuilder, ProcedureBuilderWithOutput } from "./builder.mjs";
|
|
6
6
|
import { ServeOptions, SilgiServer } from "./core/serve.mjs";
|
|
7
7
|
import { Driver, Storage, StorageConfig, StorageValue, initStorage, resetStorage, useStorage } from "./core/storage.mjs";
|
|
8
8
|
import { SilgiConfig, SilgiInstance, silgi } from "./silgi.mjs";
|
|
@@ -25,7 +25,7 @@ interface UploadedFile {
|
|
|
25
25
|
* Adds `ctx.file` (single) or `ctx.files` (multiple) to the context.
|
|
26
26
|
* Validates file size and MIME type before the procedure runs.
|
|
27
27
|
*/
|
|
28
|
-
declare function fileGuard(options?: FileGuardOptions): GuardDef<Record<string, unknown>>;
|
|
28
|
+
declare function fileGuard(options?: FileGuardOptions): GuardDef<Record<string, unknown>, Record<string, unknown>>;
|
|
29
29
|
/**
|
|
30
30
|
* Parse multipart form data from a Request.
|
|
31
31
|
* Returns files and fields separately.
|
package/dist/scalar.mjs
CHANGED
|
@@ -47,8 +47,8 @@ function generateOpenAPI(router, options = {}) {
|
|
|
47
47
|
for (const t of opTags) if (!tags.has(t)) tags.set(t, {});
|
|
48
48
|
}
|
|
49
49
|
let description = route?.description;
|
|
50
|
-
if (
|
|
51
|
-
const wsNote = "
|
|
50
|
+
if (proc.type === "subscription") {
|
|
51
|
+
const wsNote = "Streams over WebSocket (`ws://…/_ws`). Send `{ id, path: \"" + path.join("/") + "\", input }` as JSON.";
|
|
52
52
|
description = description ? `${description}\n\n${wsNote}` : wsNote;
|
|
53
53
|
}
|
|
54
54
|
const operation = {
|
package/dist/silgi.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AnySchema, InferSchemaInput, InferSchemaOutput } from "./core/schema.mjs";
|
|
2
2
|
import { ErrorDef, GuardDef, GuardFn, InferClient, ProcedureDef, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
|
|
3
|
-
import { ProcedureBuilder } from "./builder.mjs";
|
|
4
3
|
import { AnalyticsOptions } from "./plugins/analytics/types.mjs";
|
|
5
4
|
import { ScalarOptions } from "./scalar.mjs";
|
|
5
|
+
import { ProcedureBuilder } from "./builder.mjs";
|
|
6
6
|
import { ServeOptions, SilgiServer } from "./core/serve.mjs";
|
|
7
7
|
import { StorageConfig, useStorage } from "./core/storage.mjs";
|
|
8
8
|
import { Hookable } from "hookable";
|
package/dist/silgi.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createProcedureBuilder } from "./builder.mjs";
|
|
|
3
3
|
import { assignPaths, routerCache } from "./core/router-utils.mjs";
|
|
4
4
|
import { compileRouter } from "./compile.mjs";
|
|
5
5
|
import { createCaller } from "./caller.mjs";
|
|
6
|
-
import { createFetchHandler } from "./core/handler.mjs";
|
|
6
|
+
import { createFetchHandler, wrapHandler } from "./core/handler.mjs";
|
|
7
7
|
import { createHooks } from "hookable";
|
|
8
8
|
//#region src/silgi.ts
|
|
9
9
|
/**
|
|
@@ -113,30 +113,30 @@ function silgi(config) {
|
|
|
113
113
|
return createCaller(routerDef, contextFactory, options);
|
|
114
114
|
},
|
|
115
115
|
handler: (routerDef, options) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
h = wrapWithAnalytics(h, analyticsOpts);
|
|
129
|
-
}
|
|
130
|
-
fetchHandler = h;
|
|
116
|
+
const fetchHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks), routerDef, options);
|
|
117
|
+
if (!(function checkWs(def) {
|
|
118
|
+
if (!def || typeof def !== "object") return false;
|
|
119
|
+
if (def.type === "subscription") return true;
|
|
120
|
+
for (const v of Object.values(def)) if (checkWs(v)) return true;
|
|
121
|
+
return false;
|
|
122
|
+
})(routerDef)) return fetchHandler;
|
|
123
|
+
let wsHooks;
|
|
124
|
+
let wsInitPromise;
|
|
125
|
+
async function initWsHooks() {
|
|
126
|
+
const { _createWSHooks } = await import("./ws.mjs");
|
|
127
|
+
wsHooks = _createWSHooks(routerDef);
|
|
131
128
|
}
|
|
132
|
-
return (request) => {
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
return async (request) => {
|
|
130
|
+
if (new URL(request.url).pathname === "/_ws") {
|
|
131
|
+
if (!wsHooks) {
|
|
132
|
+
wsInitPromise ??= initWsHooks();
|
|
133
|
+
await wsInitPromise;
|
|
134
|
+
}
|
|
135
|
+
const response = new Response(null, { status: 200 });
|
|
136
|
+
response.crossws = wsHooks;
|
|
137
|
+
return response;
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
-
return initPromise.then(() => fetchHandler(request));
|
|
139
|
+
return fetchHandler(request);
|
|
140
140
|
};
|
|
141
141
|
},
|
|
142
142
|
serve: async (routerDef, options) => {
|
package/dist/types.d.mts
CHANGED
|
@@ -43,8 +43,6 @@ interface Route {
|
|
|
43
43
|
* - Only applies to query procedures (mutations and subscriptions are never cached)
|
|
44
44
|
*/
|
|
45
45
|
cache?: number | string;
|
|
46
|
-
/** Enable WebSocket RPC for this procedure */
|
|
47
|
-
ws?: boolean;
|
|
48
46
|
}
|
|
49
47
|
/** Procedure metadata */
|
|
50
48
|
type Meta = Record<string, unknown>;
|
package/dist/ws.d.mts
CHANGED
|
@@ -53,23 +53,13 @@ interface WSAdapterOptions<TCtx extends Record<string, unknown> = Record<string,
|
|
|
53
53
|
keepalive?: number | false;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Internal — build crossws-compatible hooks for Silgi RPC over WebSocket.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```ts
|
|
62
|
-
* // Nitro / Nuxt
|
|
63
|
-
* import { createWSHooks } from "silgi/ws";
|
|
64
|
-
* export default defineWebSocketHandler(createWSHooks(appRouter));
|
|
65
|
-
*
|
|
66
|
-
* // With context
|
|
67
|
-
* export default defineWebSocketHandler(createWSHooks(appRouter, {
|
|
68
|
-
* context: (peer) => ({ userId: peer.request?.headers.get('x-user-id') }),
|
|
69
|
-
* }));
|
|
70
|
-
* ```
|
|
58
|
+
* Used by `attachWebSocket()`, `serve({ ws: true })`, and `handler()` auto-WS.
|
|
59
|
+
* Not part of the public API; callers should use one of those higher-level entry points.
|
|
71
60
|
*/
|
|
72
|
-
|
|
61
|
+
/** @internal — exported only for use by silgi.ts handler() and attachWebSocket(). Not part of the public API. */
|
|
62
|
+
declare function _createWSHooks<TCtx extends Record<string, unknown>>(routerDef: RouterDef, options?: WSAdapterOptions<TCtx>): Partial<Hooks>;
|
|
73
63
|
/**
|
|
74
64
|
* Attach WebSocket RPC handler to an existing Node.js HTTP server.
|
|
75
65
|
*
|
|
@@ -85,4 +75,4 @@ declare function createWSHooks<TCtx extends Record<string, unknown>>(routerDef:
|
|
|
85
75
|
*/
|
|
86
76
|
declare function attachWebSocket<TCtx extends Record<string, unknown>>(server: Server, routerDef: RouterDef, options?: WSAdapterOptions<TCtx>): Promise<void>;
|
|
87
77
|
//#endregion
|
|
88
|
-
export { WSAdapterOptions,
|
|
78
|
+
export { WSAdapterOptions, _createWSHooks, attachWebSocket };
|
package/dist/ws.mjs
CHANGED
|
@@ -17,23 +17,13 @@ import { decode, encode } from "./codec/msgpack.mjs";
|
|
|
17
17
|
const peerAbortControllers = /* @__PURE__ */ new WeakMap();
|
|
18
18
|
const peerKeepaliveTimers = /* @__PURE__ */ new WeakMap();
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Internal — build crossws-compatible hooks for Silgi RPC over WebSocket.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* // Nitro / Nuxt
|
|
27
|
-
* import { createWSHooks } from "silgi/ws";
|
|
28
|
-
* export default defineWebSocketHandler(createWSHooks(appRouter));
|
|
29
|
-
*
|
|
30
|
-
* // With context
|
|
31
|
-
* export default defineWebSocketHandler(createWSHooks(appRouter, {
|
|
32
|
-
* context: (peer) => ({ userId: peer.request?.headers.get('x-user-id') }),
|
|
33
|
-
* }));
|
|
34
|
-
* ```
|
|
22
|
+
* Used by `attachWebSocket()`, `serve({ ws: true })`, and `handler()` auto-WS.
|
|
23
|
+
* Not part of the public API; callers should use one of those higher-level entry points.
|
|
35
24
|
*/
|
|
36
|
-
|
|
25
|
+
/** @internal — exported only for use by silgi.ts handler() and attachWebSocket(). Not part of the public API. */
|
|
26
|
+
function _createWSHooks(routerDef, options = {}) {
|
|
37
27
|
const flat = compileRouter(routerDef);
|
|
38
28
|
const useMsgpack = options.protocol === "messagepack" || options.protocol == null && (options.binary ?? false);
|
|
39
29
|
const contextFactory = options.context;
|
|
@@ -87,7 +77,7 @@ function createWSHooks(routerDef, options = {}) {
|
|
|
87
77
|
}
|
|
88
78
|
const { id, path, input } = req;
|
|
89
79
|
const route = flat("POST", "/" + path)?.data;
|
|
90
|
-
if (!route
|
|
80
|
+
if (!route) {
|
|
91
81
|
send(peer, {
|
|
92
82
|
id,
|
|
93
83
|
error: {
|
|
@@ -194,7 +184,7 @@ async function attachWebSocket(server, routerDef, options = {}) {
|
|
|
194
184
|
if (options.compress) serverOptions.perMessageDeflate = typeof options.compress === "object" ? options.compress : true;
|
|
195
185
|
if (options.maxPayload !== void 0) serverOptions.maxPayload = options.maxPayload;
|
|
196
186
|
const ws = nodeAdapter({
|
|
197
|
-
hooks:
|
|
187
|
+
hooks: _createWSHooks(routerDef, options),
|
|
198
188
|
...Object.keys(serverOptions).length > 0 && { serverOptions }
|
|
199
189
|
});
|
|
200
190
|
server.on("upgrade", (req, socket, head) => {
|
|
@@ -202,4 +192,4 @@ async function attachWebSocket(server, routerDef, options = {}) {
|
|
|
202
192
|
});
|
|
203
193
|
}
|
|
204
194
|
//#endregion
|
|
205
|
-
export {
|
|
195
|
+
export { _createWSHooks, attachWebSocket };
|