silgi 0.52.2 → 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/adapters/aws-lambda.mjs +1 -1
- package/dist/broker/redis.mjs +1 -2
- package/dist/builder.mjs +35 -7
- package/dist/caller.mjs +67 -50
- package/dist/client/plugins/retry.mjs +9 -4
- package/dist/compile.d.mts +17 -10
- package/dist/compile.mjs +161 -144
- package/dist/core/ctx-symbols.mjs +18 -1
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +94 -83
- package/dist/core/input.mjs +116 -37
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +36 -8
- package/dist/core/task.mjs +210 -90
- package/dist/core/url.mjs +19 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/integrations/better-auth/index.mjs +11 -2
- package/dist/lazy.mjs +3 -0
- package/dist/map-input.d.mts +8 -6
- package/dist/map-input.mjs +8 -6
- package/dist/plugins/analytics/routes.mjs +25 -13
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -134
- package/dist/plugins/coerce.d.mts +3 -2
- package/dist/plugins/coerce.mjs +25 -8
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.d.mts +35 -0
- package/dist/silgi.mjs +177 -103
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +128 -89
- package/package.json +2 -4
package/dist/compile.mjs
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { validateSchema } from "./core/schema.mjs";
|
|
2
2
|
import { SilgiError } from "./core/error.mjs";
|
|
3
3
|
import { isProcedureDef } from "./core/router-utils.mjs";
|
|
4
|
-
import { RAW_INPUT } from "./core/ctx-symbols.mjs";
|
|
4
|
+
import { RAW_INPUT, ROOT_WRAPS } from "./core/ctx-symbols.mjs";
|
|
5
5
|
import { addRoute, createRouter, findRoute } from "rou3";
|
|
6
6
|
//#region src/compile.ts
|
|
7
7
|
/**
|
|
8
|
-
* Pipeline Compiler
|
|
8
|
+
* Pipeline Compiler
|
|
9
|
+
* ------------------
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Turns a user-authored procedure (input schema, guards, wraps,
|
|
12
|
+
* resolver, output schema) into a single handler that the adapters
|
|
13
|
+
* call once per request:
|
|
14
|
+
*
|
|
15
|
+
* (ctx, rawInput, signal) => output | Promise<output>
|
|
16
|
+
*
|
|
17
|
+
* The pipeline order is:
|
|
18
|
+
*
|
|
19
|
+
* 1. Guards — pre-steps that may mutate `ctx` or throw.
|
|
20
|
+
* 2. Input validation — via Standard Schema if `input` is set.
|
|
21
|
+
* 3. Wraps — onion middleware around the resolver (root wraps first).
|
|
22
|
+
* 4. Resolver — user's business logic.
|
|
23
|
+
* 5. Output validation — via Standard Schema if `output` is set.
|
|
24
|
+
*
|
|
25
|
+
* Everything that can be decided up-front (the merged error map, the
|
|
26
|
+
* guard/wrap lists, whether validation exists) is closed over at
|
|
27
|
+
* compile time so the per-request path stays small.
|
|
28
|
+
*
|
|
29
|
+
* Router compilation lives in `compileRouter`. It walks the nested
|
|
30
|
+
* router def, compiles each procedure, and registers it in a rou3
|
|
31
|
+
* radix tree.
|
|
13
32
|
*/
|
|
14
33
|
function isThenable(value) {
|
|
15
34
|
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
@@ -31,14 +50,30 @@ function noopFail(code, data) {
|
|
|
31
50
|
defined: false
|
|
32
51
|
});
|
|
33
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Keys forbidden anywhere in a guard's return value. Blocking them at
|
|
55
|
+
* every level keeps `ctx` (a plain object) safe from attacker-supplied
|
|
56
|
+
* payloads that could otherwise reach `Object.prototype`.
|
|
57
|
+
*/
|
|
34
58
|
const UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
35
59
|
"__proto__",
|
|
36
60
|
"constructor",
|
|
37
61
|
"prototype"
|
|
38
62
|
]);
|
|
39
|
-
/**
|
|
63
|
+
/** Shared frozen empty params object. Read only, never mutated. */
|
|
40
64
|
const EMPTY_PARAMS = /* @__PURE__ */ Object.freeze(Object.create(null));
|
|
41
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Recursively scrub a value produced by a guard so it cannot reach
|
|
67
|
+
* `Object.prototype` through nested `__proto__` / `constructor` /
|
|
68
|
+
* `prototype` keys.
|
|
69
|
+
*
|
|
70
|
+
* Arrays are scrubbed in place (they cannot be prototype-polluted
|
|
71
|
+
* themselves, but their elements might). Class instances are left
|
|
72
|
+
* alone — they already have a non-literal prototype, so merging them
|
|
73
|
+
* into `ctx` does not mutate `Object.prototype`. Plain objects get a
|
|
74
|
+
* shallow rebuild when they carry a forbidden key, otherwise their
|
|
75
|
+
* values are scrubbed in place.
|
|
76
|
+
*/
|
|
42
77
|
function sanitizeValue(value) {
|
|
43
78
|
if (typeof value !== "object" || value === null) return value;
|
|
44
79
|
if (Array.isArray(value)) {
|
|
@@ -50,131 +85,71 @@ function sanitizeValue(value) {
|
|
|
50
85
|
const obj = value;
|
|
51
86
|
if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
|
|
52
87
|
const clean = Object.create(null);
|
|
53
|
-
const
|
|
54
|
-
for (let i = 0; i < keys.length; i++) {
|
|
55
|
-
const k = keys[i];
|
|
56
|
-
if (!UNSAFE_KEYS.has(k)) clean[k] = sanitizeValue(obj[k]);
|
|
57
|
-
}
|
|
88
|
+
for (const key of Object.keys(obj)) if (!UNSAFE_KEYS.has(key)) clean[key] = sanitizeValue(obj[key]);
|
|
58
89
|
return clean;
|
|
59
90
|
}
|
|
60
|
-
const
|
|
61
|
-
for (let i = 0; i < keys.length; i++) {
|
|
62
|
-
const k = keys[i];
|
|
63
|
-
obj[k] = sanitizeValue(obj[k]);
|
|
64
|
-
}
|
|
91
|
+
for (const key of Object.keys(obj)) obj[key] = sanitizeValue(obj[key]);
|
|
65
92
|
return value;
|
|
66
93
|
}
|
|
67
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Merge a single guard's return value into the live context. Guards
|
|
96
|
+
* typically return a partial patch (e.g. `{ user }`); returning
|
|
97
|
+
* nothing is fine and is how guards that only validate are expressed.
|
|
98
|
+
*/
|
|
68
99
|
function applyGuardResult(ctx, result) {
|
|
69
100
|
if (result === null || result === void 0 || typeof result !== "object") return;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (UNSAFE_KEYS.has(k)) continue;
|
|
74
|
-
ctx[k] = sanitizeValue(result[k]);
|
|
101
|
+
for (const key of Object.keys(result)) {
|
|
102
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
103
|
+
ctx[key] = sanitizeValue(result[key]);
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const r1 = g1.fn(ctx);
|
|
96
|
-
if (isThenable(r1)) return r1.then((v) => applyGuardResult(ctx, v));
|
|
97
|
-
applyGuardResult(ctx, r1);
|
|
98
|
-
}
|
|
99
|
-
function runGuards3(ctx, g0, g1, g2) {
|
|
100
|
-
const r0 = g0.fn(ctx);
|
|
101
|
-
if (isThenable(r0)) return r0.then(async (v) => {
|
|
102
|
-
applyGuardResult(ctx, v);
|
|
103
|
-
await applyGuard(ctx, g1);
|
|
104
|
-
await applyGuard(ctx, g2);
|
|
105
|
-
});
|
|
106
|
-
applyGuardResult(ctx, r0);
|
|
107
|
-
const r1 = g1.fn(ctx);
|
|
108
|
-
if (isThenable(r1)) return r1.then(async (v) => {
|
|
109
|
-
applyGuardResult(ctx, v);
|
|
110
|
-
await applyGuard(ctx, g2);
|
|
111
|
-
});
|
|
112
|
-
applyGuardResult(ctx, r1);
|
|
113
|
-
const r2 = g2.fn(ctx);
|
|
114
|
-
if (isThenable(r2)) return r2.then((v) => applyGuardResult(ctx, v));
|
|
115
|
-
applyGuardResult(ctx, r2);
|
|
116
|
-
}
|
|
117
|
-
function runGuards4(ctx, g0, g1, g2, g3) {
|
|
118
|
-
const r0 = g0.fn(ctx);
|
|
119
|
-
if (isThenable(r0)) return r0.then(async (v) => {
|
|
120
|
-
applyGuardResult(ctx, v);
|
|
121
|
-
await applyGuard(ctx, g1);
|
|
122
|
-
await applyGuard(ctx, g2);
|
|
123
|
-
await applyGuard(ctx, g3);
|
|
124
|
-
});
|
|
125
|
-
applyGuardResult(ctx, r0);
|
|
126
|
-
const r1 = g1.fn(ctx);
|
|
127
|
-
if (isThenable(r1)) return r1.then(async (v) => {
|
|
128
|
-
applyGuardResult(ctx, v);
|
|
129
|
-
await applyGuard(ctx, g2);
|
|
130
|
-
await applyGuard(ctx, g3);
|
|
131
|
-
});
|
|
132
|
-
applyGuardResult(ctx, r1);
|
|
133
|
-
const r2 = g2.fn(ctx);
|
|
134
|
-
if (isThenable(r2)) return r2.then(async (v) => {
|
|
135
|
-
applyGuardResult(ctx, v);
|
|
136
|
-
await applyGuard(ctx, g3);
|
|
137
|
-
});
|
|
138
|
-
applyGuardResult(ctx, r2);
|
|
139
|
-
const r3 = g3.fn(ctx);
|
|
140
|
-
if (isThenable(r3)) return r3.then((v) => applyGuardResult(ctx, v));
|
|
141
|
-
applyGuardResult(ctx, r3);
|
|
106
|
+
/**
|
|
107
|
+
* Run every guard in order, applying each result to `ctx` before the
|
|
108
|
+
* next guard runs.
|
|
109
|
+
*
|
|
110
|
+
* Sync-first: when a guard returns synchronously we stay on the sync
|
|
111
|
+
* path — only the first guard that returns a `Promise` forces us onto
|
|
112
|
+
* the async branch. That keeps the common case of all-sync guards from
|
|
113
|
+
* allocating a Promise at all.
|
|
114
|
+
*
|
|
115
|
+
* Empty-guards path is short-circuited at the call site (the returned
|
|
116
|
+
* runner is `undefined` when `guards.length === 0`).
|
|
117
|
+
*/
|
|
118
|
+
function runGuardsSequential(ctx, guards) {
|
|
119
|
+
for (let i = 0; i < guards.length; i++) {
|
|
120
|
+
const result = guards[i].fn(ctx);
|
|
121
|
+
if (isThenable(result)) return finishGuardsAsync(ctx, guards, i, result);
|
|
122
|
+
applyGuardResult(ctx, result);
|
|
123
|
+
}
|
|
142
124
|
}
|
|
143
|
-
/**
|
|
144
|
-
async
|
|
145
|
-
|
|
146
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Complete the guard chain on the async branch once a guard returned a
|
|
127
|
+
* `Promise`. The remaining guards are awaited in order so their results
|
|
128
|
+
* land on `ctx` in the same order a sync run would have produced.
|
|
129
|
+
*/
|
|
130
|
+
async function finishGuardsAsync(ctx, guards, resumeIndex, firstPromise) {
|
|
131
|
+
applyGuardResult(ctx, await firstPromise);
|
|
132
|
+
for (let i = resumeIndex + 1; i < guards.length; i++) {
|
|
133
|
+
const result = guards[i].fn(ctx);
|
|
147
134
|
applyGuardResult(ctx, isThenable(result) ? await result : result);
|
|
148
135
|
}
|
|
149
136
|
}
|
|
150
137
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
138
|
+
* Pre-bind the guard list to a runner. Returning `undefined` for the
|
|
139
|
+
* zero-guard case means the call site can skip the call entirely with
|
|
140
|
+
* a cheap null check.
|
|
153
141
|
*/
|
|
154
142
|
function selectGuardRunner(guards) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
case 1: {
|
|
158
|
-
const g0 = guards[0];
|
|
159
|
-
return (ctx) => runGuards1(ctx, g0);
|
|
160
|
-
}
|
|
161
|
-
case 2: {
|
|
162
|
-
const [g0, g1] = guards;
|
|
163
|
-
return (ctx) => runGuards2(ctx, g0, g1);
|
|
164
|
-
}
|
|
165
|
-
case 3: {
|
|
166
|
-
const [g0, g1, g2] = guards;
|
|
167
|
-
return (ctx) => runGuards3(ctx, g0, g1, g2);
|
|
168
|
-
}
|
|
169
|
-
case 4: {
|
|
170
|
-
const [g0, g1, g2, g3] = guards;
|
|
171
|
-
return (ctx) => runGuards4(ctx, g0, g1, g2, g3);
|
|
172
|
-
}
|
|
173
|
-
default: return (ctx) => runGuardsN(ctx, guards);
|
|
174
|
-
}
|
|
143
|
+
if (guards.length === 0) return void 0;
|
|
144
|
+
return (ctx) => runGuardsSequential(ctx, guards);
|
|
175
145
|
}
|
|
176
146
|
/** Call resolve, then validate output (sync-first, async fallback) */
|
|
177
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Call the resolver, then validate the output. Stays sync when the
|
|
149
|
+
* resolver is sync and there is no output schema; switches to
|
|
150
|
+
* `.then()` chaining only once an async boundary appears.
|
|
151
|
+
*/
|
|
152
|
+
function resolveWithOutput(resolveFn, input, ctx, failFn, signal, outputSchema) {
|
|
178
153
|
const output = resolveFn({
|
|
179
154
|
input,
|
|
180
155
|
ctx,
|
|
@@ -186,14 +161,19 @@ function _resolveWithOutput(resolveFn, input, ctx, failFn, signal, outputSchema)
|
|
|
186
161
|
if (isThenable(output)) return output.then((o) => validateSchema(outputSchema, o));
|
|
187
162
|
return validateSchema(outputSchema, output);
|
|
188
163
|
}
|
|
189
|
-
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Validate input, call the resolver, validate output.
|
|
166
|
+
*
|
|
167
|
+
* Everything that throws synchronously (input validation errors,
|
|
168
|
+
* `fail()` calls inside the resolver, the resolver itself) is turned
|
|
169
|
+
* into a rejected `Promise` so callers can rely on a single
|
|
170
|
+
* `.then().catch()` chain no matter which branch the pipeline took.
|
|
171
|
+
*/
|
|
172
|
+
function validateAndResolve(inputSchema, outputSchema, resolveFn, rawInput, ctx, failFn, signal) {
|
|
193
173
|
try {
|
|
194
174
|
const input = inputSchema ? validateSchema(inputSchema, rawInput ?? {}) : rawInput;
|
|
195
|
-
if (isThenable(input)) return input.then((resolvedInput) =>
|
|
196
|
-
return
|
|
175
|
+
if (isThenable(input)) return input.then((resolvedInput) => resolveWithOutput(resolveFn, resolvedInput, ctx, failFn, signal, outputSchema));
|
|
176
|
+
return resolveWithOutput(resolveFn, input, ctx, failFn, signal, outputSchema);
|
|
197
177
|
} catch (e) {
|
|
198
178
|
return Promise.reject(e);
|
|
199
179
|
}
|
|
@@ -207,12 +187,14 @@ function _validateAndResolve(inputSchema, outputSchema, resolveFn, rawInput, ctx
|
|
|
207
187
|
* - Pre-computed fail function (singleton per procedure)
|
|
208
188
|
* - Sync fast path when all guards are sync
|
|
209
189
|
*/
|
|
210
|
-
function compileProcedure(procedure) {
|
|
190
|
+
function compileProcedure(procedure, rootWraps) {
|
|
211
191
|
const middlewares = procedure.use ?? [];
|
|
212
192
|
const guards = [];
|
|
213
|
-
const
|
|
193
|
+
const procedureWraps = [];
|
|
214
194
|
for (const mw of middlewares) if (mw.kind === "guard") guards.push(mw);
|
|
215
|
-
else
|
|
195
|
+
else procedureWraps.push(mw);
|
|
196
|
+
const rootWrapList = rootWraps && rootWraps.length > 0 ? rootWraps : EMPTY_WRAPS;
|
|
197
|
+
const hasRootWraps = rootWrapList.length > 0;
|
|
216
198
|
const inputSchema = procedure.input;
|
|
217
199
|
const outputSchema = procedure.output;
|
|
218
200
|
const resolveFn = procedure.resolve;
|
|
@@ -223,9 +205,10 @@ function compileProcedure(procedure) {
|
|
|
223
205
|
} : guard.errors;
|
|
224
206
|
const failFn = mergedErrors ? createFail(mergedErrors) : noopFail;
|
|
225
207
|
const runGuards = selectGuardRunner(guards);
|
|
226
|
-
|
|
208
|
+
let innerHandler;
|
|
209
|
+
if (procedureWraps.length === 0 && !inputSchema && !outputSchema) innerHandler = (ctx, rawInput, signal) => {
|
|
227
210
|
try {
|
|
228
|
-
const guardResult = runGuards(ctx);
|
|
211
|
+
const guardResult = runGuards?.(ctx);
|
|
229
212
|
if (guardResult && isThenable(guardResult)) return guardResult.then(() => resolveFn({
|
|
230
213
|
input: rawInput,
|
|
231
214
|
ctx,
|
|
@@ -244,17 +227,17 @@ function compileProcedure(procedure) {
|
|
|
244
227
|
return Promise.reject(e);
|
|
245
228
|
}
|
|
246
229
|
};
|
|
247
|
-
if (
|
|
230
|
+
else if (procedureWraps.length === 0) innerHandler = (ctx, rawInput, signal) => {
|
|
248
231
|
try {
|
|
249
|
-
const guardResult = runGuards(ctx);
|
|
250
|
-
if (guardResult && isThenable(guardResult)) return guardResult.then(() =>
|
|
251
|
-
return
|
|
232
|
+
const guardResult = runGuards?.(ctx);
|
|
233
|
+
if (guardResult && isThenable(guardResult)) return guardResult.then(() => validateAndResolve(inputSchema, outputSchema, resolveFn, rawInput, ctx, failFn, signal));
|
|
234
|
+
return validateAndResolve(inputSchema, outputSchema, resolveFn, rawInput, ctx, failFn, signal);
|
|
252
235
|
} catch (e) {
|
|
253
236
|
return Promise.reject(e);
|
|
254
237
|
}
|
|
255
238
|
};
|
|
256
|
-
|
|
257
|
-
const guardResult = runGuards(ctx);
|
|
239
|
+
else innerHandler = async (ctx, rawInput, signal) => {
|
|
240
|
+
const guardResult = runGuards?.(ctx);
|
|
258
241
|
if (guardResult && isThenable(guardResult)) await guardResult;
|
|
259
242
|
let input;
|
|
260
243
|
if (inputSchema) {
|
|
@@ -272,8 +255,8 @@ function compileProcedure(procedure) {
|
|
|
272
255
|
params: ctx.params ?? EMPTY_PARAMS
|
|
273
256
|
}));
|
|
274
257
|
};
|
|
275
|
-
for (let i =
|
|
276
|
-
const wrapFn =
|
|
258
|
+
for (let i = procedureWraps.length - 1; i >= 0; i--) {
|
|
259
|
+
const wrapFn = procedureWraps[i].fn;
|
|
277
260
|
const next = execute;
|
|
278
261
|
execute = () => wrapFn(ctx, next);
|
|
279
262
|
}
|
|
@@ -282,7 +265,19 @@ function compileProcedure(procedure) {
|
|
|
282
265
|
const validated = validateSchema(outputSchema, output);
|
|
283
266
|
return isThenable(validated) ? await validated : validated;
|
|
284
267
|
};
|
|
268
|
+
if (!hasRootWraps) return innerHandler;
|
|
269
|
+
return async (ctx, rawInput, signal) => {
|
|
270
|
+
let execute = async () => innerHandler(ctx, rawInput, signal);
|
|
271
|
+
for (let i = rootWrapList.length - 1; i >= 0; i--) {
|
|
272
|
+
const wrapFn = rootWrapList[i].fn;
|
|
273
|
+
const next = execute;
|
|
274
|
+
execute = () => Promise.resolve(wrapFn(ctx, next));
|
|
275
|
+
}
|
|
276
|
+
return execute();
|
|
277
|
+
};
|
|
285
278
|
}
|
|
279
|
+
/** Shared empty array for the "no root wraps" case — avoids per-call allocation. */
|
|
280
|
+
const EMPTY_WRAPS = /* @__PURE__ */ Object.freeze([]);
|
|
286
281
|
/**
|
|
287
282
|
* Compile a router tree into a rou3 radix router.
|
|
288
283
|
*
|
|
@@ -290,6 +285,7 @@ function compileProcedure(procedure) {
|
|
|
290
285
|
*/
|
|
291
286
|
function compileRouter(def) {
|
|
292
287
|
const router = createRouter();
|
|
288
|
+
const rootWraps = def[ROOT_WRAPS];
|
|
293
289
|
function walk(node, path) {
|
|
294
290
|
if (isProcedureDef(node)) {
|
|
295
291
|
const proc = node;
|
|
@@ -299,7 +295,7 @@ function compileRouter(def) {
|
|
|
299
295
|
let cacheControl;
|
|
300
296
|
if (route?.cache != null) cacheControl = typeof route.cache === "number" ? `public, max-age=${route.cache}` : route.cache;
|
|
301
297
|
const compiled = {
|
|
302
|
-
handler: compileProcedure(proc),
|
|
298
|
+
handler: compileProcedure(proc, rootWraps),
|
|
303
299
|
cacheControl,
|
|
304
300
|
passthrough: routePath.includes("**") || void 0,
|
|
305
301
|
method
|
|
@@ -313,28 +309,49 @@ function compileRouter(def) {
|
|
|
313
309
|
walk(def, []);
|
|
314
310
|
return (method, path) => findRoute(router, method, path);
|
|
315
311
|
}
|
|
316
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* Small pool of recyclable context objects.
|
|
314
|
+
*
|
|
315
|
+
* Each request allocates a context; rather than let every one become
|
|
316
|
+
* GC pressure, released contexts with their properties wiped are
|
|
317
|
+
* parked here and re-used on the next `createContext()` call. Capped
|
|
318
|
+
* to prevent unbounded growth under burst traffic.
|
|
319
|
+
*
|
|
320
|
+
* Externally-visible: `test/core/context-release.test.ts` relies on
|
|
321
|
+
* the recycling behaviour to verify that `releaseContext` runs
|
|
322
|
+
* exactly once on every request exit path.
|
|
323
|
+
*/
|
|
317
324
|
const CTX_POOL = [];
|
|
318
325
|
const CTX_POOL_MAX = 128;
|
|
319
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Acquire a context object — from the pool when one is available,
|
|
328
|
+
* otherwise a fresh null-prototype object. Null-prototype keeps user
|
|
329
|
+
* keys from colliding with `Object.prototype` members and avoids a
|
|
330
|
+
* prototype-chain walk on every property lookup.
|
|
331
|
+
*/
|
|
320
332
|
function createContext() {
|
|
321
333
|
const ctx = CTX_POOL.length > 0 ? CTX_POOL.pop() : Object.create(null);
|
|
322
334
|
ctx[Symbol.dispose] = disposeContext;
|
|
323
335
|
return ctx;
|
|
324
336
|
}
|
|
325
|
-
/** Mark the context as owned elsewhere so `using` won't release it. */
|
|
326
|
-
function detachContext(ctx) {
|
|
327
|
-
ctx[Symbol.dispose] = noopDispose;
|
|
328
|
-
}
|
|
329
337
|
function disposeContext() {
|
|
330
338
|
releaseContext(this);
|
|
331
339
|
}
|
|
332
|
-
|
|
333
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Release a context. Called automatically at `using` scope exit and
|
|
342
|
+
* explicitly by stream handlers when their stream ends.
|
|
343
|
+
*
|
|
344
|
+
* With the pool gone the object itself will be GC'd as soon as its
|
|
345
|
+
* last reference drops, but we still wipe its properties here.
|
|
346
|
+
* Callers (and tests) use "properties were cleared" as the observable
|
|
347
|
+
* signal that release ran exactly once — notably
|
|
348
|
+
* `test/core/context-release.test.ts` tags a context before handing
|
|
349
|
+
* it off and checks the tag is gone once the request completes.
|
|
350
|
+
*/
|
|
334
351
|
function releaseContext(ctx) {
|
|
335
352
|
for (const key of Object.keys(ctx)) delete ctx[key];
|
|
336
353
|
for (const sym of Object.getOwnPropertySymbols(ctx)) delete ctx[sym];
|
|
337
354
|
if (CTX_POOL.length < CTX_POOL_MAX) CTX_POOL.push(ctx);
|
|
338
355
|
}
|
|
339
356
|
//#endregion
|
|
340
|
-
export { compileProcedure, compileRouter, createContext
|
|
357
|
+
export { compileProcedure, compileRouter, createContext };
|
|
@@ -17,5 +17,22 @@
|
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
19
|
const RAW_INPUT = Symbol.for("silgi.rawInput");
|
|
20
|
+
/**
|
|
21
|
+
* Brand stamped on a `RouterDef` by `silgi({ wraps }).router(def)` to carry
|
|
22
|
+
* the instance's root wraps along with the def itself.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Every compile site (`silgi.router`, `createCaller`, `createFetchHandler`,
|
|
26
|
+
* WS hooks, adapter `createHandler` variants) calls `compileRouter(def)`.
|
|
27
|
+
* Reading the brand off `def` inside `compileRouter` means root wraps
|
|
28
|
+
* reach every adapter without any per-adapter plumbing, without relying
|
|
29
|
+
* on `routerCache`, and without a second tree walk.
|
|
30
|
+
*
|
|
31
|
+
* The brand is a non-enumerable own property on the user's def (Symbol
|
|
32
|
+
* keys are skipped by `Object.entries`, so router traversal is unaffected).
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const ROOT_WRAPS = Symbol.for("silgi.rootWraps");
|
|
20
37
|
//#endregion
|
|
21
|
-
export { RAW_INPUT };
|
|
38
|
+
export { RAW_INPUT, ROOT_WRAPS };
|
package/dist/core/handler.d.mts
CHANGED
|
@@ -9,7 +9,7 @@ type FetchHandler = (request: Request) => Response | Promise<Response>;
|
|
|
9
9
|
interface WrapHandlerOptions {
|
|
10
10
|
analytics?: AnalyticsOptions;
|
|
11
11
|
scalar?: boolean | ScalarOptions;
|
|
12
|
-
/** URL path prefix for the handler (e.g.
|
|
12
|
+
/** URL path prefix for the handler (e.g. `/api`). Requests outside the prefix return 404. */
|
|
13
13
|
basePath?: string;
|
|
14
14
|
/**
|
|
15
15
|
* Schema registry for OpenAPI / analytics schema conversion. Built from
|
|
@@ -18,8 +18,8 @@ interface WrapHandlerOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
schemaRegistry?: SchemaRegistry;
|
|
20
20
|
/**
|
|
21
|
-
* Hookable instance
|
|
22
|
-
*
|
|
21
|
+
* Hookable instance threaded through so `wrapWithAnalytics` can register
|
|
22
|
+
* listeners on `request:prepare` / `response:finalize`.
|
|
23
23
|
* @internal
|
|
24
24
|
*/
|
|
25
25
|
hooks?: Hookable<SilgiHooks>;
|