silgi 0.50.0 → 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.
@@ -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/ws') */
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/ws' })
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
- resolve,
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.#ws.send(JSON.stringify(msg));
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
- const msg = JSON.parse(typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data));
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
- pending.reject(new SilgiError(err.code ?? "INTERNAL_SERVER_ERROR", {
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
- } else if (msg.done) {
78
- this.#pending.delete(msg.id);
79
- pending.resolve(msg.data);
80
- } else if ("result" in msg) {
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
- for (const [, p] of this.#pending) p.reject(new SilgiError("INTERNAL_SERVER_ERROR", { message: "WebSocket closed" }));
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
- for (const [, p] of this.#pending) p.reject(new DOMException("Link disposed", "AbortError"));
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
  };
@@ -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
@@ -302,7 +302,6 @@ function compileRouter(def) {
302
302
  const compiled = {
303
303
  handler: compileProcedure(proc),
304
304
  cacheControl,
305
- ws: route?.ws ?? void 0,
306
305
  passthrough: routePath.includes("**") || void 0,
307
306
  method
308
307
  };
@@ -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 };
@@ -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 };
@@ -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
- /** Enable WebSocket RPC (requires crossws) */
30
- ws?: boolean | WSAdapterOptions;
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;
@@ -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
- let handler = createFetchHandler(routerDef, contextFactory, hooks);
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: handler,
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 (options?.ws) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}`);
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 (route?.ws) {
51
- const wsNote = "Also available over WebSocket (`ws://`). Send `{ id, path: \"" + path.join("/") + "\", input }` as JSON.";
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
- let fetchHandler;
117
- let initPromise;
118
- async function init() {
119
- let h = createFetchHandler(routerDef, contextFactory, hooks);
120
- if (options?.scalar) {
121
- const { wrapWithScalar } = await import("./scalar.mjs");
122
- const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
123
- h = wrapWithScalar(h, routerDef, scalarOpts);
124
- }
125
- if (options?.analytics) {
126
- const { wrapWithAnalytics } = await import("./plugins/analytics.mjs");
127
- const analyticsOpts = typeof options.analytics === "object" ? options.analytics : {};
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 (fetchHandler) return fetchHandler(request);
134
- if (!options?.scalar && !options?.analytics) {
135
- fetchHandler = createFetchHandler(routerDef, contextFactory, hooks);
136
- return fetchHandler(request);
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
- initPromise ??= init();
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
- * Create crossws-compatible hooks for Silgi RPC over WebSocket.
56
+ * Internal — build crossws-compatible hooks for Silgi RPC over WebSocket.
57
57
  *
58
- * Works with any crossws integration Nitro, Deno, Bun, Cloudflare, etc.
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
- declare function createWSHooks<TCtx extends Record<string, unknown>>(routerDef: RouterDef, options?: WSAdapterOptions<TCtx>): Partial<Hooks>;
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, attachWebSocket, createWSHooks };
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
- * Create crossws-compatible hooks for Silgi RPC over WebSocket.
20
+ * Internal — build crossws-compatible hooks for Silgi RPC over WebSocket.
21
21
  *
22
- * Works with any crossws integration Nitro, Deno, Bun, Cloudflare, etc.
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
- function createWSHooks(routerDef, options = {}) {
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 || !route.ws) {
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: createWSHooks(routerDef, options),
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 { attachWebSocket, createWSHooks };
195
+ export { _createWSHooks, attachWebSocket };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silgi",
3
- "version": "0.50.0",
3
+ "version": "0.50.2",
4
4
  "private": false,
5
5
  "description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
6
6
  "keywords": [
@@ -268,7 +268,6 @@
268
268
  "ocache": "^0.1.4",
269
269
  "ofetch": "2.0.0-alpha.3",
270
270
  "ohash": "^2.0.11",
271
- "silgi": "link:",
272
271
  "srvx": "^0.11.13",
273
272
  "unstorage": "^2.0.0-alpha.7"
274
273
  },