silgi 0.53.0 → 0.53.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/silgi.mjs CHANGED
@@ -21,36 +21,44 @@ import { createHooks } from "hookable";
21
21
  * const k = silgi({ context: (req) => ({ db, headers }) })
22
22
  * // k.$input(), k.$resolve(), k.$use(), k.guard(), k.router(), k.handler()
23
23
  */
24
- function createProcedure(type, ...args) {
25
- if (args.length === 0) return createProcedureBuilder(type);
26
- if (args.length === 1 && typeof args[0] === "function") return {
27
- type,
28
- input: null,
29
- output: null,
30
- errors: null,
31
- use: null,
32
- resolve: args[0],
33
- route: null,
34
- meta: null
35
- };
36
- if (args.length === 2 && typeof args[1] === "function") return {
24
+ /**
25
+ * Build a `ProcedureDef` with all optional slots set to `null`.
26
+ *
27
+ * Procedures carry eight slots — `type`, `input`, `output`, `errors`,
28
+ * `use`, `resolve`, `route`, `meta`. Most call sites set only a couple;
29
+ * funnelling construction through this helper keeps the shape in one
30
+ * place so new slots do not have to be added to every short form.
31
+ */
32
+ function makeProcedureDef(type, input, resolve) {
33
+ return {
37
34
  type,
38
- input: args[0],
35
+ input,
39
36
  output: null,
40
37
  errors: null,
41
38
  use: null,
42
- resolve: args[1],
39
+ resolve,
43
40
  route: null,
44
41
  meta: null
45
42
  };
43
+ }
44
+ /**
45
+ * Dispatch on the call shape of `$resolve` / `subscription`.
46
+ *
47
+ * createProcedure(type) → chainable builder
48
+ * createProcedure(type, resolve) → single-shot procedure
49
+ * createProcedure(type, input, resolve) → single-shot with input schema
50
+ */
51
+ function createProcedure(type, ...args) {
52
+ if (args.length === 0) return createProcedureBuilder(type);
53
+ if (args.length === 1 && typeof args[0] === "function") return makeProcedureDef(type, null, args[0]);
54
+ if (args.length === 2 && typeof args[1] === "function") return makeProcedureDef(type, args[0], args[1]);
46
55
  throw new TypeError(`Invalid arguments for ${type}()`);
47
56
  }
48
57
  /**
49
- * Stamp root wraps onto a router def as a non-enumerable Symbol-keyed
50
- * property. Idempotent if the def is already branded with the same
51
- * reference, this is a no-op. Multiple silgi instances registering the
52
- * same def throws, because a single compiled router cannot serve two
53
- * context shapes.
58
+ * Stamp root wraps onto a router def via a non-enumerable Symbol-keyed
59
+ * property. Idempotent for the same wrap reference; throws when a
60
+ * different silgi instance has already registered the def, because a
61
+ * single compiled router cannot serve two different context shapes.
54
62
  *
55
63
  * @internal
56
64
  */
@@ -66,6 +74,55 @@ function stampRootWraps(def, wraps) {
66
74
  });
67
75
  }
68
76
  /**
77
+ * Validate + freeze the `wraps` config array.
78
+ *
79
+ * Guards are rejected at the instance level because a guard's return
80
+ * type must flow into every procedure's context, and the instance-level
81
+ * config cannot express that across an unknown router shape. Guards
82
+ * must be attached via route-level `.$use()` where the context
83
+ * enrichment can be typed.
84
+ */
85
+ function prepareRootWraps(wraps) {
86
+ if (!wraps || wraps.length === 0) return null;
87
+ for (const w of wraps) if (!w || w.kind !== "wrap") throw new TypeError("silgi({ wraps }) only accepts wrap middleware — use route-level .$use() for guards.");
88
+ return Object.freeze([...wraps]);
89
+ }
90
+ /**
91
+ * Register the user-provided hook listeners on a fresh `Hookable`.
92
+ * Accepts either a single function or an array of functions per hook,
93
+ * matching the shape documented on `SilgiConfig['hooks']`.
94
+ */
95
+ function registerHooks(hooks, config) {
96
+ if (!config) return;
97
+ for (const [name, fn] of Object.entries(config)) {
98
+ const key = name;
99
+ if (Array.isArray(fn)) for (const f of fn) hooks.hook(key, f);
100
+ else if (fn) hooks.hook(key, fn);
101
+ }
102
+ }
103
+ /**
104
+ * Build the storage-ready promise. Resolves immediately when no storage
105
+ * is configured — and crucially, never triggers the dynamic import in
106
+ * that case, so tree-shakers can drop the driver code entirely.
107
+ */
108
+ function makeStorageReady(storage) {
109
+ if (!storage) return Promise.resolve();
110
+ return import("./core/storage.mjs").then((m) => {
111
+ m.initStorage(storage);
112
+ });
113
+ }
114
+ /**
115
+ * Recursively search a router def for any `subscription` procedure.
116
+ * Used to decide whether `handler()` should bother to lazy-load the
117
+ * crossws hooks for the `/_ws` mount.
118
+ */
119
+ function routerHasSubscriptions(def) {
120
+ if (!def || typeof def !== "object") return false;
121
+ if (def.type === "subscription") return true;
122
+ for (const child of Object.values(def)) if (routerHasSubscriptions(child)) return true;
123
+ return false;
124
+ }
125
+ /**
69
126
  * Create a Silgi RPC instance with typed context.
70
127
  *
71
128
  * @remarks
@@ -101,20 +158,80 @@ function silgi(config) {
101
158
  const schemaRegistry = createSchemaRegistry(config.schemaConverters ?? []);
102
159
  const bridge = createContextBridge();
103
160
  const hooks = createHooks();
104
- if (config.hooks) {
105
- for (const [name, fn] of Object.entries(config.hooks)) if (Array.isArray(fn)) for (const f of fn) hooks.hook(name, f);
106
- else if (fn) hooks.hook(name, fn);
107
- }
108
- const readyPromise = config.storage ? import("./core/storage.mjs").then((m) => {
109
- m.initStorage(config.storage);
110
- }) : Promise.resolve();
161
+ registerHooks(hooks, config.hooks);
162
+ const readyPromise = makeStorageReady(config.storage);
163
+ const rootWraps = prepareRootWraps(config.wraps);
111
164
  const ctxFactory = () => contextFactory(new Request("http://localhost"));
112
- let rootWraps = null;
113
- if (config.wraps && config.wraps.length > 0) {
114
- for (const w of config.wraps) if (!w || w.kind !== "wrap") throw new TypeError("silgi({ wraps }) only accepts wrap middleware — use route-level .$use() for guards.");
115
- rootWraps = Object.freeze([...config.wraps]);
116
- }
117
165
  const rootWrapsGetter = rootWraps ? () => rootWraps : null;
166
+ const startBuilder = () => createProcedureBuilder("query", ctxFactory, rootWrapsGetter);
167
+ const registerRouter = (def) => {
168
+ const assigned = assignPaths(def);
169
+ if (rootWraps) {
170
+ stampRootWraps(def, rootWraps);
171
+ stampRootWraps(assigned, rootWraps);
172
+ }
173
+ const compiled = compileRouter(assigned);
174
+ routerCache.set(def, compiled);
175
+ routerCache.set(assigned, compiled);
176
+ return def;
177
+ };
178
+ const buildHandler = (routerDef, options) => {
179
+ const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
180
+ const fetchHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
181
+ ...options,
182
+ schemaRegistry,
183
+ hooks
184
+ } : {
185
+ schemaRegistry,
186
+ hooks
187
+ }, prefix);
188
+ if (!routerHasSubscriptions(routerDef)) return fetchHandler;
189
+ let wsHooks;
190
+ let wsInitPromise;
191
+ const initWsHooks = async () => {
192
+ const { _createWSHooks } = await import("./ws.mjs");
193
+ wsHooks = _createWSHooks(routerDef, { context: (peer) => {
194
+ return contextFactory(peer?.request instanceof Request ? peer.request : peer);
195
+ } });
196
+ };
197
+ const wsPath = prefix ? `${prefix}/_ws` : "/_ws";
198
+ return async (request) => {
199
+ if (new URL(request.url).pathname === wsPath) {
200
+ if (!wsHooks) {
201
+ wsInitPromise ??= initWsHooks();
202
+ await wsInitPromise;
203
+ }
204
+ const response = new Response(null, { status: 200 });
205
+ response.crossws = wsHooks;
206
+ return response;
207
+ }
208
+ return fetchHandler(request);
209
+ };
210
+ };
211
+ const buildServe = async (routerDef, options) => {
212
+ const { createServeHandler } = await import("./core/serve.mjs");
213
+ const server = await createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge);
214
+ const { collectCronTasks, startCronJobs, stopCronJobs } = await import("./core/task.mjs");
215
+ const cronTasks = collectCronTasks(routerDef);
216
+ if (cronTasks.length > 0) {
217
+ await startCronJobs(cronTasks);
218
+ console.log(` ${cronTasks.length} cron task(s) scheduled`);
219
+ }
220
+ const originalClose = server.close.bind(server);
221
+ const wrappedClose = async (force) => {
222
+ stopCronJobs();
223
+ return originalClose(force);
224
+ };
225
+ const silgiServer = Object.assign(Object.create(Object.getPrototypeOf(server)), server, { close: wrappedClose });
226
+ if (options?.handleSignals) {
227
+ const onSignal = () => {
228
+ wrappedClose().catch(() => {});
229
+ };
230
+ process.once("SIGINT", onSignal);
231
+ process.once("SIGTERM", onSignal);
232
+ }
233
+ return silgiServer;
234
+ };
118
235
  return {
119
236
  hook: hooks.hook.bind(hooks),
120
237
  removeHook: hooks.removeHook.bind(hooks),
@@ -140,96 +257,22 @@ function silgi(config) {
140
257
  fn
141
258
  }),
142
259
  $resolve: ((fn) => createProcedure("query", fn)),
143
- $input: ((schema) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$input(schema)),
260
+ $input: ((schema) => startBuilder().$input(schema)),
144
261
  $use: ((...middleware) => {
145
- const b = createProcedureBuilder("query", ctxFactory, rootWrapsGetter);
262
+ const b = startBuilder();
146
263
  for (const m of middleware) b.$use(m);
147
264
  return b;
148
265
  }),
149
- $output: ((schema) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$output(schema)),
150
- $errors: ((errors) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$errors(errors)),
151
- $route: ((route) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$route(route)),
152
- $meta: ((meta) => createProcedureBuilder("query", ctxFactory, rootWrapsGetter).$meta(meta)),
266
+ $output: ((schema) => startBuilder().$output(schema)),
267
+ $errors: ((errors) => startBuilder().$errors(errors)),
268
+ $route: ((route) => startBuilder().$route(route)),
269
+ $meta: ((meta) => startBuilder().$meta(meta)),
153
270
  subscription: ((...args) => createProcedure("subscription", ...args)),
154
- $task: ((config) => {
155
- return createTaskFromProcedure(config, config.resolve, null, null, ctxFactory, rootWrapsGetter);
156
- }),
157
- router: (def) => {
158
- const assigned = assignPaths(def);
159
- if (rootWraps) {
160
- stampRootWraps(def, rootWraps);
161
- stampRootWraps(assigned, rootWraps);
162
- }
163
- const flat = compileRouter(assigned);
164
- routerCache.set(def, flat);
165
- routerCache.set(assigned, flat);
166
- return def;
167
- },
168
- createCaller: (routerDef, options) => {
169
- return createCaller(routerDef, contextFactory, options);
170
- },
171
- handler: (routerDef, options) => {
172
- const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
173
- const fetchHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
174
- ...options,
175
- schemaRegistry,
176
- hooks
177
- } : {
178
- schemaRegistry,
179
- hooks
180
- }, prefix);
181
- if (!(function checkWs(def) {
182
- if (!def || typeof def !== "object") return false;
183
- if (def.type === "subscription") return true;
184
- for (const v of Object.values(def)) if (checkWs(v)) return true;
185
- return false;
186
- })(routerDef)) return fetchHandler;
187
- let wsHooks;
188
- let wsInitPromise;
189
- async function initWsHooks() {
190
- const { _createWSHooks } = await import("./ws.mjs");
191
- wsHooks = _createWSHooks(routerDef, { context: (peer) => {
192
- return contextFactory(peer?.request instanceof Request ? peer.request : peer);
193
- } });
194
- }
195
- const wsPath = prefix ? `${prefix}/_ws` : "/_ws";
196
- return async (request) => {
197
- if (new URL(request.url).pathname === wsPath) {
198
- if (!wsHooks) {
199
- wsInitPromise ??= initWsHooks();
200
- await wsInitPromise;
201
- }
202
- const response = new Response(null, { status: 200 });
203
- response.crossws = wsHooks;
204
- return response;
205
- }
206
- return fetchHandler(request);
207
- };
208
- },
209
- serve: async (routerDef, options) => {
210
- const { createServeHandler } = await import("./core/serve.mjs");
211
- const server = await createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge);
212
- const { collectCronTasks, startCronJobs, stopCronJobs } = await import("./core/task.mjs");
213
- const cronTasks = collectCronTasks(routerDef);
214
- if (cronTasks.length > 0) {
215
- await startCronJobs(cronTasks);
216
- console.log(` ${cronTasks.length} cron task(s) scheduled`);
217
- }
218
- const originalClose = server.close.bind(server);
219
- const wrappedClose = async (force) => {
220
- stopCronJobs();
221
- return originalClose(force);
222
- };
223
- const silgiServer = Object.assign(Object.create(Object.getPrototypeOf(server)), server, { close: wrappedClose });
224
- if (options?.handleSignals) {
225
- const onSignal = () => {
226
- wrappedClose().catch(() => {});
227
- };
228
- process.once("SIGINT", onSignal);
229
- process.once("SIGTERM", onSignal);
230
- }
231
- return silgiServer;
232
- }
271
+ $task: ((cfg) => createTaskFromProcedure(cfg, cfg.resolve, null, null, ctxFactory, rootWrapsGetter)),
272
+ router: registerRouter,
273
+ createCaller: (routerDef, options) => createCaller(routerDef, contextFactory, options),
274
+ handler: buildHandler,
275
+ serve: buildServe
233
276
  };
234
277
  }
235
278
  //#endregion
package/dist/ws.d.mts CHANGED
@@ -7,23 +7,21 @@ interface WSAdapterOptions<TCtx extends Record<string, unknown> = Record<string,
7
7
  /**
8
8
  * Wire protocol for WebSocket message encoding.
9
9
  *
10
- * - `'json'` — default, text frames with JSON
11
- * - `'messagepack'` — binary frames with MessagePack
10
+ * - `'json'` — default, text frames with JSON.
11
+ * - `'messagepack'` — binary frames with MessagePack.
12
12
  *
13
13
  * @default 'json'
14
14
  */
15
15
  protocol?: 'json' | 'messagepack';
16
- /**
17
- * @deprecated Use `protocol: 'messagepack'` instead.
18
- */
16
+ /** @deprecated Use `protocol: 'messagepack'` instead. */
19
17
  binary?: boolean;
20
- /** Context factory — receives the peer on each message */
18
+ /** Context factory — invoked for every incoming peer message. */
21
19
  context?: (peer: Peer) => TCtx | Promise<TCtx>;
22
20
  /**
23
21
  * Enable per-message-deflate compression.
24
22
  *
25
- * - `true`: enable with defaults
26
- * - `object`: fine-tune zlib options (passed to ws `perMessageDeflate`)
23
+ * - `true` — enable with library defaults.
24
+ * - object zlib tuning, forwarded to `ws.perMessageDeflate`.
27
25
  *
28
26
  * @default false
29
27
  */
@@ -35,43 +33,44 @@ interface WSAdapterOptions<TCtx extends Record<string, unknown> = Record<string,
35
33
  clientMaxWindowBits?: number;
36
34
  };
37
35
  /**
38
- * Maximum allowed message size in bytes.
39
- * Messages exceeding this limit will cause the connection to be closed.
36
+ * Maximum allowed message size in bytes. Exceeding the limit closes
37
+ * the connection.
40
38
  *
41
39
  * @default 1_048_576 (1 MB)
42
40
  */
43
41
  maxPayload?: number;
44
42
  /**
45
- * Keepalive ping interval in milliseconds.
46
- * Server sends a ping frame at this interval; if the client
47
- * does not respond with a pong before the next ping, the connection is terminated.
43
+ * Keepalive ping interval in milliseconds. The server sends a ping
44
+ * every `keepalive` ms; if the client does not pong before the next
45
+ * ping, the socket is terminated.
48
46
  *
49
- * Set to `0` or `false` to disable.
47
+ * Set to `0` or `false` to disable keepalive entirely.
50
48
  *
51
- * @default 30_000 (30 seconds)
49
+ * @default 30_000
52
50
  */
53
51
  keepalive?: number | false;
54
52
  }
55
53
  /**
56
- * Internal build crossws-compatible hooks for Silgi RPC over WebSocket.
54
+ * Build the crossws hook set that implements silgi's WebSocket RPC.
55
+ *
56
+ * @internal
57
57
  *
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.
58
+ * This is not part of the public API `silgi({...}).handler()`,
59
+ * `serve({ ws: true })`, and `attachWebSocket()` are the three supported
60
+ * entry points. They all go through this builder so protocol behavior
61
+ * stays identical everywhere.
60
62
  */
61
- /** @internal — exported only for use by silgi.ts handler() and attachWebSocket(). Not part of the public API. */
62
63
  declare function _createWSHooks<TCtx extends Record<string, unknown>>(routerDef: RouterDef, options?: WSAdapterOptions<TCtx>): Partial<Hooks>;
63
64
  /**
64
- * Attach WebSocket RPC handler to an existing Node.js HTTP server.
65
+ * Attach silgi's WebSocket RPC to an existing Node.js `http.Server`.
65
66
  *
66
67
  * @example
67
- * ```ts
68
- * import { createServer } from "node:http";
69
- * import { attachWebSocket } from "silgi/ws";
68
+ * import { createServer } from 'node:http'
69
+ * import { attachWebSocket } from 'silgi/ws'
70
70
  *
71
- * const server = createServer(httpHandler);
72
- * attachWebSocket(server, appRouter);
73
- * server.listen(3000);
74
- * ```
71
+ * const server = createServer(httpHandler)
72
+ * await attachWebSocket(server, appRouter)
73
+ * server.listen(3000)
75
74
  */
76
75
  declare function attachWebSocket<TCtx extends Record<string, unknown>>(server: Server, routerDef: RouterDef, options?: WSAdapterOptions<TCtx>): Promise<void>;
77
76
  //#endregion