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.
@@ -1,19 +1,104 @@
1
1
  import { validateSchema } from "./schema.mjs";
2
2
  //#region src/core/task.ts
3
3
  /**
4
- * Task API — type-safe background tasks built on the procedure builder.
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
- let _onTaskComplete = null;
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
- _onTaskComplete = cb;
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
- const taskResolve = async (opts) => {
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
- let reqTrace = null;
30
- try {
31
- const { RequestTrace } = await import("../plugins/analytics.mjs");
32
- reqTrace = new RequestTrace();
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
- let output;
45
- if (rootWraps && rootWraps.length > 0) {
46
- let execute = () => Promise.resolve(runResolver());
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: Math.round((performance.now() - t0) * 100) / 100,
137
+ durationMs: round2(performance.now() - t0),
66
138
  status: "success",
67
139
  input,
68
140
  output,
69
- spans: reqTrace?.spans
141
+ spans: selfTrace?.spans
70
142
  });
71
143
  return output;
72
144
  } catch (err) {
73
- if (parentTrace) parentTrace.spans.push({
74
- name: `task:${name}`,
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: Math.round((performance.now() - t0) * 100) / 100,
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: reqTrace?.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: taskResolve,
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
- const _running = /* @__PURE__ */ new Map();
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 = _running.get(task);
186
+ const existing = running.get(task);
115
187
  if (existing) return existing;
116
188
  const promise = task.dispatch(args[0]);
117
- _running.set(task, promise);
189
+ running.set(task, promise);
118
190
  try {
119
191
  return await promise;
120
192
  } finally {
121
- _running.delete(task);
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)) if (value && typeof value === "object") {
127
- if ("_tag" in value && value._tag === "task" && value.cron) result.push({
128
- cron: value.cron,
129
- task: value
130
- });
131
- else if (!("_tag" in value)) result.push(...collectCronTasks(value));
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. Each silgi instance owns one, so
137
- * `server.close()` on instance A never stops instance B's jobs and
138
- * `list()` never returns jobs from another instance.
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 e of entries) e.job.stop();
251
+ for (const entry of entries) entry.job.stop();
169
252
  entries.length = 0;
170
253
  },
171
254
  list() {
172
- return entries.map((e) => ({
173
- name: e.name,
174
- cron: e.cron,
175
- description: e.description,
176
- nextRun: e.job.nextRun()?.getTime() ?? null,
177
- lastRun: e.lastRun,
178
- runs: e.runs,
179
- errors: e.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. Shared state — use {@link createCronRegistry}
186
- * when a silgi instance should own its own jobs.
268
+ * Process-default cron registry.
187
269
  *
188
- * @deprecated Prefer per-instance registries. The module-default is
189
- * retained so existing imports of `startCronJobs` / `stopCronJobs` /
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 _defaultRegistry = createCronRegistry();
277
+ const defaultRegistry = createCronRegistry();
194
278
  /** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
195
- const startCronJobs = _defaultRegistry.start;
279
+ const startCronJobs = defaultRegistry.start;
196
280
  /** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
197
- const stopCronJobs = _defaultRegistry.stop;
281
+ const stopCronJobs = defaultRegistry.stop;
198
282
  /** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
199
- const getScheduledTasks = _defaultRegistry.list;
283
+ const getScheduledTasks = defaultRegistry.list;
200
284
  //#endregion
201
285
  export { collectCronTasks, createCronRegistry, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
@@ -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 (default: 60) */
26
+ /** Cache TTL in seconds. @default 60 */
7
27
  maxAge?: number;
8
- /** Enable stale-while-revalidate (default: true) */
28
+ /** Enable stale-while-revalidate. @default true */
9
29
  swr?: boolean;
10
- /** Max seconds to serve stale while revalidating (default: maxAge) */
30
+ /** Max seconds to serve stale while revalidating. @default maxAge */
11
31
  staleMaxAge?: number;
12
- /** Custom cache key generator from input */
32
+ /** Custom cache-key generator from the request input. */
13
33
  getKey?: (input: unknown) => string;
14
- /** Cache key name prefix (default: procedure path, set automatically) */
34
+ /** Human-readable cache name. Defaults to the procedure path. */
15
35
  name?: string;
16
36
  /**
17
- * When returns `true`, skip cache entirely and call resolver directly.
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
- * When returns `true`, invalidate cache for this key and re-resolve.
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 by default.
78
- * When integrity changes (e.g. after redeploy), stale cache is invalidated.
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 results.
62
+ * Wrap middleware that caches a query procedure's output.
86
63
  *
87
- * Uses ocache under the hood: TTL, SWR, dedup, integrity, bypass, invalidation.
88
- * Default: 60s TTL, SWR enabled.
64
+ * Defaults: 60-second TTL, SWR on. Every other knob comes from
65
+ * {@link CacheQueryOptions}.
89
66
  *
90
67
  * @example
91
- * ```ts
92
- * const listUsers = k
93
- * .$use(cacheQuery({ maxAge: 60 }))
94
- * .$resolve(({ ctx }) => ctx.db.users.findMany())
95
- *
96
- * // Advanced: bypass cache for admin, custom validation
97
- * const listPosts = k
98
- * .$use(cacheQuery({
99
- * maxAge: 300,
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
- * Invalidate cached entries for a procedure by name.
80
+ * Wipe every cached entry for a procedure by its cache name.
113
81
  *
114
- * Call this after mutations to clear related query caches.
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
- * ```ts
118
- * const createUser = k.$resolve(async ({ input, ctx }) => {
119
- * const user = await ctx.db.users.create(input)
120
- * await invalidateQueryCache('users_list')
121
- * return user
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
- * Set the cache storage backend.
128
- *
129
- * Default: in-memory Map with TTL.
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
- * ```ts
134
- * import { setCacheStorage, createUnstorageAdapter } from 'silgi/cache'
135
- * import { createStorage } from 'silgi/unstorage'
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 };