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 +32 -6
- package/dist/caller.mjs +65 -55
- package/dist/compile.d.mts +15 -8
- package/dist/compile.mjs +157 -142
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +69 -73
- package/dist/core/input.mjs +95 -33
- 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 +15 -4
- package/dist/core/task.mjs +160 -76
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -128
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.mjs +160 -117
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +126 -87
- package/package.json +1 -1
package/dist/core/task.mjs
CHANGED
|
@@ -1,19 +1,104 @@
|
|
|
1
1
|
import { validateSchema } from "./schema.mjs";
|
|
2
2
|
//#region src/core/task.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Background tasks
|
|
5
|
+
* ------------------
|
|
6
|
+
*
|
|
7
|
+
* A *task* is a procedure with two extra capabilities:
|
|
8
|
+
*
|
|
9
|
+
* 1. Programmatic `dispatch(input)` — call it directly, outside the
|
|
10
|
+
* HTTP pipeline. Used for async work (send-email, rebuild-index)
|
|
11
|
+
* and as the callback target for cron schedules.
|
|
12
|
+
* 2. Optional `cron` spec — when set, the task is auto-registered
|
|
13
|
+
* with a `croner` job at `serve()` time.
|
|
14
|
+
*
|
|
15
|
+
* Tasks are built via the procedure builder:
|
|
5
16
|
*
|
|
6
|
-
* Tasks are procedures with dispatch + cron capabilities:
|
|
7
17
|
* s.$use(auth).$input(schema).$task({ name: 'send-email', resolve })
|
|
18
|
+
*
|
|
19
|
+
* Dispatch runs through the same root-wrap onion as the HTTP pipeline
|
|
20
|
+
* so tenant scoping, trace propagation, and similar cross-cutting
|
|
21
|
+
* concerns apply uniformly. When no root wraps are configured the
|
|
22
|
+
* dispatch path reduces to a direct `await resolveFn(…)`.
|
|
8
23
|
*/
|
|
9
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Module-global sink for task completion events. This is shared across
|
|
26
|
+
* every silgi instance in the process — analytics is currently wired
|
|
27
|
+
* through the process-default cron registry for the same reason.
|
|
28
|
+
* Per-instance analytics is a future refactor; `setTaskAnalytics(null)`
|
|
29
|
+
* detaches the sink.
|
|
30
|
+
*/
|
|
31
|
+
let onTaskComplete = null;
|
|
10
32
|
function setTaskAnalytics(cb) {
|
|
11
|
-
|
|
33
|
+
onTaskComplete = cb;
|
|
12
34
|
}
|
|
35
|
+
/** Round a millisecond duration to two decimal places — matches dashboard display. */
|
|
36
|
+
const round2 = (ms) => Math.round(ms * 100) / 100;
|
|
37
|
+
/**
|
|
38
|
+
* Wrap `run` in the root-wrap onion. Root wraps are outermost-first, so
|
|
39
|
+
* we fold from the end of the list: the last wrap wraps `run`, the one
|
|
40
|
+
* before wraps that, and so on. When there are no wraps we just return
|
|
41
|
+
* `run` unchanged — zero onion overhead.
|
|
42
|
+
*
|
|
43
|
+
* Same shape as `composeWraps` in `compile.ts`; kept here privately to
|
|
44
|
+
* avoid a core → compile import cycle.
|
|
45
|
+
*/
|
|
46
|
+
function applyRootWraps(ctx, wraps, run) {
|
|
47
|
+
let chain = run;
|
|
48
|
+
for (let i = wraps.length - 1; i >= 0; i--) {
|
|
49
|
+
const wrap = wraps[i];
|
|
50
|
+
const next = chain;
|
|
51
|
+
chain = () => Promise.resolve(wrap.fn(ctx, next));
|
|
52
|
+
}
|
|
53
|
+
return chain();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Lazy-loaded `RequestTrace` constructor. The analytics module is
|
|
57
|
+
* optional — missing it must not break task dispatch — so we attempt
|
|
58
|
+
* the import once and cache the outcome. `null` means "we've tried;
|
|
59
|
+
* analytics is not available in this build".
|
|
60
|
+
*/
|
|
61
|
+
let requestTraceCtor;
|
|
62
|
+
async function getRequestTrace() {
|
|
63
|
+
if (requestTraceCtor !== void 0) return requestTraceCtor;
|
|
64
|
+
try {
|
|
65
|
+
requestTraceCtor = (await import("../plugins/analytics.mjs")).RequestTrace;
|
|
66
|
+
} catch {
|
|
67
|
+
requestTraceCtor = null;
|
|
68
|
+
}
|
|
69
|
+
return requestTraceCtor;
|
|
70
|
+
}
|
|
71
|
+
/** Record this dispatch as a span on the parent request's trace, if one exists. */
|
|
72
|
+
function recordParentSpan(parentTrace, name, spanStart, err) {
|
|
73
|
+
if (!parentTrace) return;
|
|
74
|
+
const span = {
|
|
75
|
+
name: `task:${name}`,
|
|
76
|
+
kind: "queue",
|
|
77
|
+
durationMs: round2(performance.now() - spanStart),
|
|
78
|
+
startOffsetMs: round2(spanStart - parentTrace.t0),
|
|
79
|
+
detail: `dispatch ${name}`
|
|
80
|
+
};
|
|
81
|
+
if (err !== void 0) span.error = err instanceof Error ? err.message : String(err);
|
|
82
|
+
parentTrace.spans.push(span);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a `TaskDef` from the procedure builder's `.$task()` configuration.
|
|
86
|
+
*
|
|
87
|
+
* @param rootWrapsGetter A *live* getter so a task constructed before
|
|
88
|
+
* `s.router()` stamps wraps still picks them up at dispatch time. The
|
|
89
|
+
* silgi instance threads a closure over its own rootWraps reference.
|
|
90
|
+
* Pass `null` when no wraps are configured — dispatch then skips the
|
|
91
|
+
* onion entirely.
|
|
92
|
+
*/
|
|
13
93
|
function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFactory, rootWrapsGetter = null) {
|
|
14
94
|
const { name, cron = null, description } = config;
|
|
15
95
|
if (!name) throw new TypeError("Task name is required");
|
|
16
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Resolver called by the *HTTP* pipeline when the task is reachable
|
|
98
|
+
* through the router tree. `ctx` already carries everything the
|
|
99
|
+
* pipeline set up (base context, guards, trace), so we just forward.
|
|
100
|
+
*/
|
|
101
|
+
const pipelineResolve = async (opts) => {
|
|
17
102
|
return resolveFn({
|
|
18
103
|
input: opts.input,
|
|
19
104
|
ctx: opts.ctx,
|
|
@@ -21,72 +106,52 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
21
106
|
scheduledTime: void 0
|
|
22
107
|
});
|
|
23
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Programmatic dispatch — the one users call from another procedure
|
|
111
|
+
* or a cron callback. Validates input, builds its own context, runs
|
|
112
|
+
* the root-wrap onion around the resolver, records analytics.
|
|
113
|
+
*/
|
|
24
114
|
const dispatch = async (rawInput, parentCtx) => {
|
|
25
115
|
const input = inputSchema ? await validateSchema(inputSchema, rawInput) : rawInput;
|
|
26
116
|
const ctx = contextFactory ? await contextFactory() : {};
|
|
27
117
|
const parentTrace = parentCtx?.trace;
|
|
28
118
|
const spanStart = parentTrace ? performance.now() : 0;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ctx.trace = reqTrace;
|
|
34
|
-
} catch {}
|
|
35
|
-
const rootWraps = rootWrapsGetter ? rootWrapsGetter() : null;
|
|
36
|
-
const runResolver = () => resolveFn({
|
|
119
|
+
const RequestTrace = await getRequestTrace();
|
|
120
|
+
const selfTrace = RequestTrace ? new RequestTrace() : null;
|
|
121
|
+
if (selfTrace) ctx.trace = selfTrace;
|
|
122
|
+
const runResolver = () => Promise.resolve(resolveFn({
|
|
37
123
|
input,
|
|
38
124
|
ctx,
|
|
39
125
|
name,
|
|
40
126
|
scheduledTime: void 0
|
|
41
|
-
});
|
|
127
|
+
}));
|
|
128
|
+
const wraps = rootWrapsGetter?.() ?? null;
|
|
42
129
|
const t0 = performance.now();
|
|
43
130
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (let i = rootWraps.length - 1; i >= 0; i--) {
|
|
48
|
-
const wrapFn = rootWraps[i].fn;
|
|
49
|
-
const next = execute;
|
|
50
|
-
execute = () => wrapFn(ctx, next);
|
|
51
|
-
}
|
|
52
|
-
output = await execute();
|
|
53
|
-
} else output = await runResolver();
|
|
54
|
-
if (parentTrace) parentTrace.spans.push({
|
|
55
|
-
name: `task:${name}`,
|
|
56
|
-
kind: "queue",
|
|
57
|
-
durationMs: Math.round((performance.now() - spanStart) * 100) / 100,
|
|
58
|
-
startOffsetMs: Math.round((spanStart - parentTrace.t0) * 100) / 100,
|
|
59
|
-
detail: `dispatch ${name}`
|
|
60
|
-
});
|
|
61
|
-
if (_onTaskComplete) _onTaskComplete({
|
|
131
|
+
const output = wraps && wraps.length > 0 ? await applyRootWraps(ctx, wraps, runResolver) : await runResolver();
|
|
132
|
+
recordParentSpan(parentTrace, name, spanStart);
|
|
133
|
+
onTaskComplete?.({
|
|
62
134
|
taskName: name,
|
|
63
135
|
trigger: "dispatch",
|
|
64
136
|
timestamp: Date.now(),
|
|
65
|
-
durationMs:
|
|
137
|
+
durationMs: round2(performance.now() - t0),
|
|
66
138
|
status: "success",
|
|
67
139
|
input,
|
|
68
140
|
output,
|
|
69
|
-
spans:
|
|
141
|
+
spans: selfTrace?.spans
|
|
70
142
|
});
|
|
71
143
|
return output;
|
|
72
144
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
kind: "queue",
|
|
76
|
-
durationMs: Math.round((performance.now() - spanStart) * 100) / 100,
|
|
77
|
-
startOffsetMs: Math.round((spanStart - parentTrace.t0) * 100) / 100,
|
|
78
|
-
detail: `dispatch ${name}`,
|
|
79
|
-
error: err instanceof Error ? err.message : String(err)
|
|
80
|
-
});
|
|
81
|
-
if (_onTaskComplete) _onTaskComplete({
|
|
145
|
+
recordParentSpan(parentTrace, name, spanStart, err);
|
|
146
|
+
onTaskComplete?.({
|
|
82
147
|
taskName: name,
|
|
83
148
|
trigger: "dispatch",
|
|
84
149
|
timestamp: Date.now(),
|
|
85
|
-
durationMs:
|
|
150
|
+
durationMs: round2(performance.now() - t0),
|
|
86
151
|
status: "error",
|
|
87
152
|
error: err instanceof Error ? err.message : String(err),
|
|
88
153
|
input,
|
|
89
|
-
spans:
|
|
154
|
+
spans: selfTrace?.spans
|
|
90
155
|
});
|
|
91
156
|
throw err;
|
|
92
157
|
}
|
|
@@ -99,7 +164,7 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
99
164
|
output: null,
|
|
100
165
|
errors: null,
|
|
101
166
|
use,
|
|
102
|
-
resolve:
|
|
167
|
+
resolve: pipelineResolve,
|
|
103
168
|
route: description ? {
|
|
104
169
|
summary: description,
|
|
105
170
|
tags: ["Tasks"]
|
|
@@ -109,33 +174,51 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
109
174
|
dispatch
|
|
110
175
|
};
|
|
111
176
|
}
|
|
112
|
-
|
|
177
|
+
/**
|
|
178
|
+
* In-flight dispatches keyed by task object.
|
|
179
|
+
*
|
|
180
|
+
* `runTask` coalesces concurrent calls so two callers asking for the
|
|
181
|
+
* same task at once share a single execution. Useful for idempotent
|
|
182
|
+
* background jobs that can be kicked off from multiple places.
|
|
183
|
+
*/
|
|
184
|
+
const running = /* @__PURE__ */ new Map();
|
|
113
185
|
async function runTask(task, ...args) {
|
|
114
|
-
const existing =
|
|
186
|
+
const existing = running.get(task);
|
|
115
187
|
if (existing) return existing;
|
|
116
188
|
const promise = task.dispatch(args[0]);
|
|
117
|
-
|
|
189
|
+
running.set(task, promise);
|
|
118
190
|
try {
|
|
119
191
|
return await promise;
|
|
120
192
|
} finally {
|
|
121
|
-
|
|
193
|
+
running.delete(task);
|
|
122
194
|
}
|
|
123
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Walk a router tree and collect every task that has a `cron` field set.
|
|
198
|
+
* Nested namespaces are recursed into; we stop at any node already
|
|
199
|
+
* tagged as a task so a task's internal structure is never inspected.
|
|
200
|
+
*/
|
|
124
201
|
function collectCronTasks(def) {
|
|
125
202
|
const result = [];
|
|
126
|
-
for (const value of Object.values(def))
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
task
|
|
130
|
-
|
|
131
|
-
|
|
203
|
+
for (const value of Object.values(def)) {
|
|
204
|
+
if (!value || typeof value !== "object") continue;
|
|
205
|
+
if ("_tag" in value && value._tag === "task") {
|
|
206
|
+
const task = value;
|
|
207
|
+
if (task.cron) result.push({
|
|
208
|
+
cron: task.cron,
|
|
209
|
+
task
|
|
210
|
+
});
|
|
211
|
+
} else if (!("_tag" in value)) result.push(...collectCronTasks(value));
|
|
132
212
|
}
|
|
133
213
|
return result;
|
|
134
214
|
}
|
|
135
215
|
/**
|
|
136
|
-
* Create an isolated cron registry.
|
|
137
|
-
*
|
|
138
|
-
* `
|
|
216
|
+
* Create an isolated cron registry.
|
|
217
|
+
*
|
|
218
|
+
* Each silgi instance owns one, so `server.close()` on instance A
|
|
219
|
+
* never stops instance B's jobs and `list()` never returns jobs from
|
|
220
|
+
* another instance. The module-default registry below keeps the
|
|
221
|
+
* legacy top-level exports working.
|
|
139
222
|
*/
|
|
140
223
|
function createCronRegistry() {
|
|
141
224
|
const entries = [];
|
|
@@ -165,37 +248,38 @@ function createCronRegistry() {
|
|
|
165
248
|
}
|
|
166
249
|
},
|
|
167
250
|
stop() {
|
|
168
|
-
for (const
|
|
251
|
+
for (const entry of entries) entry.job.stop();
|
|
169
252
|
entries.length = 0;
|
|
170
253
|
},
|
|
171
254
|
list() {
|
|
172
|
-
return entries.map((
|
|
173
|
-
name:
|
|
174
|
-
cron:
|
|
175
|
-
description:
|
|
176
|
-
nextRun:
|
|
177
|
-
lastRun:
|
|
178
|
-
runs:
|
|
179
|
-
errors:
|
|
255
|
+
return entries.map((entry) => ({
|
|
256
|
+
name: entry.name,
|
|
257
|
+
cron: entry.cron,
|
|
258
|
+
description: entry.description,
|
|
259
|
+
nextRun: entry.job.nextRun()?.getTime() ?? null,
|
|
260
|
+
lastRun: entry.lastRun,
|
|
261
|
+
runs: entry.runs,
|
|
262
|
+
errors: entry.errors
|
|
180
263
|
}));
|
|
181
264
|
}
|
|
182
265
|
};
|
|
183
266
|
}
|
|
184
267
|
/**
|
|
185
|
-
* Process-default cron registry.
|
|
186
|
-
* when a silgi instance should own its own jobs.
|
|
268
|
+
* Process-default cron registry.
|
|
187
269
|
*
|
|
188
|
-
* @deprecated
|
|
189
|
-
*
|
|
270
|
+
* @deprecated
|
|
271
|
+
* Shared state. Prefer {@link createCronRegistry} and give each silgi
|
|
272
|
+
* instance its own. The module-default registry is retained so
|
|
273
|
+
* existing imports of `startCronJobs` / `stopCronJobs` /
|
|
190
274
|
* `getScheduledTasks` keep working; a future major will remove these
|
|
191
275
|
* top-level re-exports.
|
|
192
276
|
*/
|
|
193
|
-
const
|
|
277
|
+
const defaultRegistry = createCronRegistry();
|
|
194
278
|
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
195
|
-
const startCronJobs =
|
|
279
|
+
const startCronJobs = defaultRegistry.start;
|
|
196
280
|
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
197
|
-
const stopCronJobs =
|
|
281
|
+
const stopCronJobs = defaultRegistry.stop;
|
|
198
282
|
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
199
|
-
const getScheduledTasks =
|
|
283
|
+
const getScheduledTasks = defaultRegistry.list;
|
|
200
284
|
//#endregion
|
|
201
285
|
export { collectCronTasks, createCronRegistry, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
package/dist/plugins/cache.d.mts
CHANGED
|
@@ -2,169 +2,105 @@ import { WrapDef } from "../types.mjs";
|
|
|
2
2
|
import { CacheEntry, CacheOptions, StorageInterface } from "ocache";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/cache.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimal interface matching an unstorage `Storage`. We describe it
|
|
7
|
+
* locally so the cache plugin does not take a hard dependency on the
|
|
8
|
+
* unstorage package — users bring their own.
|
|
9
|
+
*/
|
|
10
|
+
interface UnstorageCompatible {
|
|
11
|
+
getItem<T = unknown>(key: string): Promise<T | null> | T | null;
|
|
12
|
+
setItem(key: string, value: unknown, opts?: {
|
|
13
|
+
ttl?: number;
|
|
14
|
+
}): Promise<void> | void;
|
|
15
|
+
removeItem(key: string): Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Public helper for users who already have an unstorage instance.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const storage = createStorage({ driver: redisDriver({ ... }) })
|
|
22
|
+
* setCacheStorage(createUnstorageAdapter(storage))
|
|
23
|
+
*/
|
|
24
|
+
declare function createUnstorageAdapter(storage: UnstorageCompatible): StorageInterface;
|
|
5
25
|
interface CacheQueryOptions<T = unknown> {
|
|
6
|
-
/** Cache TTL in seconds
|
|
26
|
+
/** Cache TTL in seconds. @default 60 */
|
|
7
27
|
maxAge?: number;
|
|
8
|
-
/** Enable stale-while-revalidate
|
|
28
|
+
/** Enable stale-while-revalidate. @default true */
|
|
9
29
|
swr?: boolean;
|
|
10
|
-
/** Max seconds to serve stale while revalidating
|
|
30
|
+
/** Max seconds to serve stale while revalidating. @default maxAge */
|
|
11
31
|
staleMaxAge?: number;
|
|
12
|
-
/** Custom cache
|
|
32
|
+
/** Custom cache-key generator from the request input. */
|
|
13
33
|
getKey?: (input: unknown) => string;
|
|
14
|
-
/**
|
|
34
|
+
/** Human-readable cache name. Defaults to the procedure path. */
|
|
15
35
|
name?: string;
|
|
16
36
|
/**
|
|
17
|
-
*
|
|
18
|
-
* Useful for admin users or debug modes.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* cacheQuery({
|
|
23
|
-
* shouldBypassCache: (input) => input?.noCache === true,
|
|
24
|
-
* })
|
|
25
|
-
* ```
|
|
37
|
+
* Return `true` to skip cache entirely and call the resolver
|
|
38
|
+
* directly. Useful for admin users or debug modes.
|
|
26
39
|
*/
|
|
27
40
|
shouldBypassCache?: (input: unknown) => boolean | Promise<boolean>;
|
|
28
41
|
/**
|
|
29
|
-
*
|
|
42
|
+
* Return `true` to invalidate cache for this key and re-resolve.
|
|
30
43
|
* The new result is cached normally.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* cacheQuery({
|
|
35
|
-
* shouldInvalidateCache: (input) => input?.refresh === true,
|
|
36
|
-
* })
|
|
37
|
-
* ```
|
|
38
44
|
*/
|
|
39
45
|
shouldInvalidateCache?: (input: unknown) => boolean | Promise<boolean>;
|
|
40
|
-
/**
|
|
41
|
-
* Validate a cache entry before returning it.
|
|
42
|
-
* Return `false` to treat as cache miss and re-resolve.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```ts
|
|
46
|
-
* cacheQuery({
|
|
47
|
-
* validate: (entry) => entry.value !== null && entry.value !== undefined,
|
|
48
|
-
* })
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
46
|
+
/** Validate a cache entry before returning it. `false` = treat as miss. */
|
|
51
47
|
validate?: (entry: CacheEntry<T>) => boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Transform a cache entry before returning.
|
|
54
|
-
* Runs on both cache hits and fresh resolves.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```ts
|
|
58
|
-
* cacheQuery({
|
|
59
|
-
* transform: (entry) => ({ ...entry.value, cachedAt: entry.mtime }),
|
|
60
|
-
* })
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
48
|
+
/** Transform a cache entry before returning. Runs on both hits and fresh resolves. */
|
|
63
49
|
transform?: (entry: CacheEntry<T>) => T;
|
|
64
|
-
/**
|
|
65
|
-
* Storage base prefix for cache keys.
|
|
66
|
-
* Defaults to `'/cache'`.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```ts
|
|
70
|
-
* cacheQuery({
|
|
71
|
-
* base: '/my-app-cache',
|
|
72
|
-
* })
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
50
|
+
/** Storage key prefix. @default '/cache' */
|
|
75
51
|
base?: string;
|
|
76
52
|
/**
|
|
77
|
-
* Custom integrity value. Auto-generated from the resolver + options
|
|
78
|
-
*
|
|
53
|
+
* Custom integrity value. Auto-generated from the resolver + options
|
|
54
|
+
* by default; when it changes (e.g. after a redeploy) stale cache is
|
|
55
|
+
* invalidated.
|
|
79
56
|
*/
|
|
80
57
|
integrity?: string;
|
|
81
|
-
/** Error handler for cache read/write/SWR failures */
|
|
58
|
+
/** Error handler for cache read / write / SWR failures. */
|
|
82
59
|
onError?: (error: unknown) => void;
|
|
83
60
|
}
|
|
84
61
|
/**
|
|
85
|
-
* Wrap middleware that caches query
|
|
62
|
+
* Wrap middleware that caches a query procedure's output.
|
|
86
63
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
64
|
+
* Defaults: 60-second TTL, SWR on. Every other knob comes from
|
|
65
|
+
* {@link CacheQueryOptions}.
|
|
89
66
|
*
|
|
90
67
|
* @example
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* swr: true,
|
|
101
|
-
* staleMaxAge: 600,
|
|
102
|
-
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
103
|
-
* shouldInvalidateCache: (input) => (input as any)?.refresh,
|
|
104
|
-
* validate: (entry) => Array.isArray(entry.value),
|
|
105
|
-
* onError: (err) => console.error('[cache]', err),
|
|
106
|
-
* }))
|
|
107
|
-
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
108
|
-
* ```
|
|
68
|
+
* const listPosts = k
|
|
69
|
+
* .$use(cacheQuery({
|
|
70
|
+
* maxAge: 300,
|
|
71
|
+
* staleMaxAge: 600,
|
|
72
|
+
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
73
|
+
* validate: (entry) => Array.isArray(entry.value),
|
|
74
|
+
* onError: (err) => console.error('[cache]', err),
|
|
75
|
+
* }))
|
|
76
|
+
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
109
77
|
*/
|
|
110
78
|
declare function cacheQuery<T = unknown>(options?: CacheQueryOptions<T>): WrapDef;
|
|
111
79
|
/**
|
|
112
|
-
*
|
|
80
|
+
* Wipe every cached entry for a procedure by its cache name.
|
|
113
81
|
*
|
|
114
|
-
*
|
|
82
|
+
* Typically called after a mutation that makes related queries stale.
|
|
83
|
+
* Names default to the procedure's auto-generated path, or whatever
|
|
84
|
+
* was passed via `cacheQuery({ name })`.
|
|
115
85
|
*
|
|
116
86
|
* @example
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* })
|
|
123
|
-
* ```
|
|
87
|
+
* const createUser = k.$resolve(async ({ input, ctx }) => {
|
|
88
|
+
* const user = await ctx.db.users.create(input)
|
|
89
|
+
* await invalidateQueryCache('users_list')
|
|
90
|
+
* return user
|
|
91
|
+
* })
|
|
124
92
|
*/
|
|
125
93
|
declare function invalidateQueryCache(name: string): Promise<void>;
|
|
126
94
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* For production, use `createUnstorageAdapter()` with Redis, Cloudflare KV, etc.
|
|
95
|
+
* Replace ocache's storage backend. Default is an in-memory map with
|
|
96
|
+
* TTL; production deployments usually want Redis or Cloudflare KV via
|
|
97
|
+
* `createUnstorageAdapter()`.
|
|
131
98
|
*
|
|
132
99
|
* @example
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
137
|
-
*
|
|
138
|
-
* setCacheStorage(createUnstorageAdapter(
|
|
139
|
-
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
140
|
-
* ))
|
|
141
|
-
* ```
|
|
100
|
+
* setCacheStorage(createUnstorageAdapter(
|
|
101
|
+
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
102
|
+
* ))
|
|
142
103
|
*/
|
|
143
104
|
declare function setCacheStorage(storage: StorageInterface): void;
|
|
144
|
-
/**
|
|
145
|
-
* Minimal interface matching unstorage's Storage.
|
|
146
|
-
* Avoids hard dependency on unstorage — users bring their own.
|
|
147
|
-
*/
|
|
148
|
-
interface UnstorageCompatible {
|
|
149
|
-
getItem<T = unknown>(key: string): Promise<T | null> | T | null;
|
|
150
|
-
setItem(key: string, value: unknown, opts?: {
|
|
151
|
-
ttl?: number;
|
|
152
|
-
}): Promise<void> | void;
|
|
153
|
-
removeItem(key: string): Promise<void> | void;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Create an ocache-compatible storage adapter from an unstorage instance.
|
|
157
|
-
*
|
|
158
|
-
* @example
|
|
159
|
-
* ```ts
|
|
160
|
-
* import { createStorage } from 'silgi/unstorage'
|
|
161
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
162
|
-
*
|
|
163
|
-
* const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
164
|
-
* const adapter = createUnstorageAdapter(storage)
|
|
165
|
-
* setCacheStorage(adapter)
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
|
-
declare function createUnstorageAdapter(storage: UnstorageCompatible): StorageInterface;
|
|
169
105
|
//#endregion
|
|
170
106
|
export { type CacheEntry, type CacheOptions, CacheQueryOptions, type StorageInterface, UnstorageCompatible, cacheQuery, createUnstorageAdapter, invalidateQueryCache, setCacheStorage };
|