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 CHANGED
@@ -12,6 +12,25 @@ import { createTaskFromProcedure } from "./core/task.mjs";
12
12
  * // ← autocomplete suggests id, title, body
13
13
  * ```
14
14
  */
15
+ /**
16
+ * A `ProcBuilder` plays two roles in its lifetime:
17
+ *
18
+ * 1. While the user is chaining methods it is a *builder* — each `$x()`
19
+ * mutates a slot and returns `this`.
20
+ * 2. Once `$resolve()` is called, the *same instance* is returned typed
21
+ * as `ProcedureDef`. This works because `isProcedureDef` (see
22
+ * `core/router-utils.ts`) only checks for `type` and a callable
23
+ * `resolve`, both of which we now have.
24
+ *
25
+ * Using one object for both roles avoids copying the slots into a fresh
26
+ * frozen record at the end — callers hold the object directly, so the
27
+ * shape they see is the same object we wrote into.
28
+ *
29
+ * All slots default to `null` rather than an empty array / object so
30
+ * that downstream code can branch on presence without caring about
31
+ * length, and so that we do not allocate sentinels the user never ends
32
+ * up needing.
33
+ */
15
34
  var ProcBuilder = class {
16
35
  type;
17
36
  input = null;
@@ -21,14 +40,21 @@ var ProcBuilder = class {
21
40
  resolve = null;
22
41
  route = null;
23
42
  meta = null;
43
+ /**
44
+ * Underscore-prefixed slots are framework-internal. They are set by
45
+ * `createProcedureBuilder` when the builder is constructed through a
46
+ * `silgi({...})` instance and are threaded through to `$task()` so
47
+ * that background tasks share the instance's context factory and
48
+ * root wraps.
49
+ */
24
50
  _contextFactory = null;
25
51
  _rootWrapsGetter = null;
26
52
  constructor(type) {
27
53
  this.type = type;
28
54
  }
29
55
  $use(...middleware) {
30
- if (this.use) this.use.push(...middleware);
31
- else this.use = [...middleware];
56
+ this.use ??= [];
57
+ this.use.push(...middleware);
32
58
  return this;
33
59
  }
34
60
  $input(schema) {
@@ -63,10 +89,10 @@ var ProcBuilder = class {
63
89
  }
64
90
  };
65
91
  function createProcedureBuilder(type, contextFactory, rootWrapsGetter) {
66
- const b = new ProcBuilder(type);
67
- if (contextFactory) b._contextFactory = contextFactory;
68
- if (rootWrapsGetter) b._rootWrapsGetter = rootWrapsGetter;
69
- return b;
92
+ const builder = new ProcBuilder(type);
93
+ if (contextFactory) builder._contextFactory = contextFactory;
94
+ if (rootWrapsGetter) builder._rootWrapsGetter = rootWrapsGetter;
95
+ return builder;
70
96
  }
71
97
  //#endregion
72
98
  export { createProcedureBuilder };
package/dist/caller.mjs CHANGED
@@ -1,96 +1,106 @@
1
1
  import { routerCache } from "./core/router-utils.mjs";
2
- import { compileRouter, createContext, releaseContext } from "./compile.mjs";
2
+ import { compileRouter } from "./compile.mjs";
3
3
  import { applyContext } from "./core/dispatch.mjs";
4
4
  //#region src/caller.ts
5
5
  /**
6
- * createCaller — call procedures directly without HTTP.
6
+ * Direct caller
7
+ * --------------
7
8
  *
8
- * Compiles the router, creates context, and runs the pipeline
9
- * for each procedure call. Perfect for testing and server-side usage.
9
+ * `createCaller` returns a proxy that mirrors the router's nested shape.
10
+ * Calling a leaf procedure invokes the compiled pipeline directly no
11
+ * HTTP, no body serialization, no response encoding. This is what tests
12
+ * and server-side orchestration code use.
10
13
  *
11
14
  * @example
12
- * ```ts
13
- * const caller = s.createCaller(appRouter)
15
+ * const caller = s.createCaller(appRouter)
16
+ * const users = await caller.users.list({ limit: 10 })
17
+ * const user = await caller.users.get({ id: 1 })
14
18
  *
15
- * // Call procedures directly
16
- * const users = await caller.users.list({ limit: 10 })
17
- * const user = await caller.users.get({ id: 1 })
18
- *
19
- * // With custom context override
20
- * const adminCaller = s.createCaller(appRouter, {
21
- * contextOverride: { user: { id: 1, role: 'admin' } },
22
- * })
23
- * ```
19
+ * // Override the context for this caller (e.g. for admin tests):
20
+ * const admin = s.createCaller(appRouter, {
21
+ * contextOverride: { user: { id: 1, role: 'admin' } },
22
+ * })
24
23
  */
25
24
  /**
26
- * Never-aborting signal used when `timeout: null` is opted into and the
27
- * caller passes no per-call signal. Allocated once at module load; compiled
28
- * handlers can still `.addEventListener('abort', …)` safely — the listener
29
- * simply never fires.
25
+ * Placeholder signal for callers that opt out of timeouts (`timeout: null`)
26
+ * and do not pass their own signal. We still hand the pipeline a real
27
+ * `AbortSignal` so that user code doing `signal.addEventListener('abort', …)`
28
+ * does not crash on `undefined` — this signal just never fires.
30
29
  */
31
- const NEVER = new AbortController().signal;
30
+ const NEVER_ABORTS = new AbortController().signal;
32
31
  /**
33
- * Create a direct caller for a router — no HTTP, no serialization.
32
+ * Build a direct caller for a router.
34
33
  *
35
- * Returns a proxy that mirrors the router's nested structure.
36
- * Calling a leaf procedure invokes the compiled pipeline directly.
34
+ * The returned value is a proxy that mirrors the router tree: accessing
35
+ * `caller.users.list` returns another proxy; calling it at the leaf
36
+ * dispatches to the compiled pipeline for that procedure.
37
37
  */
38
38
  function createCaller(routerDef, contextFactory, options) {
39
- let compiledRouter = routerCache.get(routerDef);
40
- if (!compiledRouter) {
41
- compiledRouter = compileRouter(routerDef);
42
- routerCache.set(routerDef, compiledRouter);
39
+ let compiled = routerCache.get(routerDef);
40
+ if (!compiled) {
41
+ compiled = compileRouter(routerDef);
42
+ routerCache.set(routerDef, compiled);
43
43
  }
44
- const router = compiledRouter;
45
- const defaultTimeout = options?.timeout !== void 0 ? options.timeout : 3e4;
46
- function createMockRequest(extraHeaders) {
44
+ const defaultTimeoutMs = options?.timeout !== void 0 ? options.timeout : 3e4;
45
+ /**
46
+ * Construct a mock `Request` to feed into the user's context factory.
47
+ * Tests rarely care about the URL — only the headers matter, because
48
+ * that is the surface most factories actually read.
49
+ */
50
+ const mockRequest = (extraHeaders) => {
47
51
  const headers = new Headers(options?.headers);
48
- if (extraHeaders) for (const [k, v] of Object.entries(extraHeaders)) headers.set(k, v);
52
+ if (extraHeaders) for (const [key, value] of Object.entries(extraHeaders)) headers.set(key, value);
49
53
  return new Request("http://localhost/__caller", { headers });
50
- }
51
- async function resolveContext(perCallContext) {
52
- const ctx = createContext();
53
- if (contextFactory) {
54
- const result = contextFactory(createMockRequest());
55
- applyContext(ctx, result instanceof Promise ? await result : result);
56
- }
54
+ };
55
+ /**
56
+ * Build a fresh context for a single call. Layers are applied in the
57
+ * order they would be on the HTTP path: context factory → caller-level
58
+ * `contextOverride` per-call override.
59
+ */
60
+ const buildContext = async (perCallContext) => {
61
+ const ctx = Object.create(null);
62
+ if (contextFactory) applyContext(ctx, await contextFactory(mockRequest()));
57
63
  if (options?.contextOverride) applyContext(ctx, options.contextOverride);
58
64
  if (perCallContext) applyContext(ctx, perCallContext);
59
65
  return ctx;
60
- }
61
- function createProxy(segments) {
66
+ };
67
+ /**
68
+ * Build a proxy node for the current `segments` path. Each property
69
+ * access descends one level; each function call dispatches to the
70
+ * compiled pipeline.
71
+ *
72
+ * The `cache` map ensures repeated property access (e.g.
73
+ * `caller.users.list`) returns the same proxy object so equality
74
+ * checks and `.bind` in user tests stay stable.
75
+ */
76
+ const createProxy = (segments) => {
62
77
  const cache = /* @__PURE__ */ new Map();
63
78
  return new Proxy(() => {}, {
64
79
  get(_target, prop) {
65
80
  if (typeof prop === "symbol") return void 0;
66
81
  if (prop === "then" || prop === "toJSON" || prop === "toString" || prop === "$$typeof") return;
67
- let sub = cache.get(prop);
68
- if (!sub) {
69
- sub = createProxy([...segments, prop]);
70
- cache.set(prop, sub);
82
+ let child = cache.get(prop);
83
+ if (!child) {
84
+ child = createProxy([...segments, prop]);
85
+ cache.set(prop, child);
71
86
  }
72
- return sub;
87
+ return child;
73
88
  },
74
89
  apply(_target, _thisArg, args) {
75
90
  const path = "/" + segments.join("/");
76
91
  const input = args[0];
77
92
  const callOptions = args[1];
78
93
  return (async () => {
79
- const match = router("", path);
94
+ const match = compiled("", path);
80
95
  if (!match) throw new Error(`Procedure not found: ${path}`);
81
- const ctx = await resolveContext(callOptions?.context);
96
+ const ctx = await buildContext(callOptions?.context);
82
97
  if (match.params) ctx.params = match.params;
83
- const signal = callOptions?.signal ?? (defaultTimeout !== null ? AbortSignal.timeout(defaultTimeout) : NEVER);
84
- try {
85
- const result = match.data.handler(ctx, input, signal);
86
- return result instanceof Promise ? await result : result;
87
- } finally {
88
- releaseContext(ctx);
89
- }
98
+ const signal = callOptions?.signal ?? (defaultTimeoutMs !== null ? AbortSignal.timeout(defaultTimeoutMs) : NEVER_ABORTS);
99
+ return await match.data.handler(ctx, input, signal);
90
100
  })();
91
101
  }
92
102
  });
93
- }
103
+ };
94
104
  return createProxy([]);
95
105
  }
96
106
  //#endregion
@@ -2,8 +2,9 @@ import { ProcedureDef, WrapDef } from "./types.mjs";
2
2
 
3
3
  //#region src/compile.d.ts
4
4
  /**
5
- * Compiled pipeline called per request.
6
- * May return sync value OR Promise caller uses instanceof check.
5
+ * Compiled request handler. May return a sync value or a `Promise` —
6
+ * adapters branch on `instanceof Promise` for the fast path when the
7
+ * resolver and guards were all synchronous.
7
8
  */
8
9
  type CompiledHandler = (ctx: Record<string, unknown>, rawInput: unknown, signal: AbortSignal) => unknown | Promise<unknown>;
9
10
  /**
@@ -39,15 +40,21 @@ type CompiledRouterFn = (method: string, path: string) => MatchedRoute<CompiledR
39
40
  */
40
41
  declare function compileRouter(def: Record<string, unknown>): CompiledRouterFn;
41
42
  /**
42
- * Pooled context with built-in `using` support. `Symbol.dispose` calls
43
- * `releaseContext` unless ownership has been transferred elsewhere
44
- * (e.g. to a streaming Response) — in that case the new owner is
45
- * expected to call `releaseContext` when the stream ends.
43
+ * Disposable wrapper around the pipeline context.
46
44
  *
47
- * Transfer ownership by calling `detachContext(ctx)` before returning.
45
+ * Adapters use `using ctx = createContext()` so the context is released
46
+ * automatically at scope exit — unless ownership has been transferred
47
+ * elsewhere (e.g. to a streaming `Response` that keeps reading from
48
+ * `ctx` after the handler returns). In that case the handler calls
49
+ * `detachContext(ctx)` and the new owner is responsible for cleanup.
48
50
  */
49
51
  type PooledContext = Record<string, unknown> & Disposable;
50
- /** Acquire a context object from the pool (or create one). */
52
+ /**
53
+ * Acquire a context object — from the pool when one is available,
54
+ * otherwise a fresh null-prototype object. Null-prototype keeps user
55
+ * keys from colliding with `Object.prototype` members and avoids a
56
+ * prototype-chain walk on every property lookup.
57
+ */
51
58
  declare function createContext(): PooledContext;
52
59
  //#endregion
53
60
  export { compileProcedure, compileRouter, createContext };