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.
@@ -48,6 +48,14 @@ function runWithCtx(ctx, fn) {
48
48
  * Keyed on `Request` identity. GC-friendly: entry auto-released when the
49
49
  * `Request` is collected.
50
50
  *
51
+ * @remarks
52
+ * This is a documented exception to ARCHITECTURE §3 ("no WeakMap keyed on
53
+ * Request identity"). Kept only because `setRequestContext` is a public
54
+ * API consumed by user code outside silgi's control — removing it is a
55
+ * breaking change reserved for the next major. New integrations must
56
+ * use `silgi.runInContext(ctx, fn)` or the per-plugin closure pattern
57
+ * (see `requestMetas` inside `tracing()` below).
58
+ *
51
59
  * @internal
52
60
  */
53
61
  const _requestContextMap = /* @__PURE__ */ new WeakMap();
@@ -186,7 +194,6 @@ function extractUserData(returned) {
186
194
  }
187
195
  return result;
188
196
  }
189
- const requestMetas = /* @__PURE__ */ new WeakMap();
190
197
  /**
191
198
  * Creates a Better Auth plugin that auto-traces all auth operations
192
199
  * into silgi analytics spans.
@@ -197,6 +204,8 @@ const requestMetas = /* @__PURE__ */ new WeakMap();
197
204
  function tracing(config) {
198
205
  const captureInput = config?.captureInput ?? true;
199
206
  const captureOutput = config?.captureOutput ?? true;
207
+ const wrapMiddleware = config?.createAuthMiddleware ?? ((fn) => fn);
208
+ const requestMetas = /* @__PURE__ */ new WeakMap();
200
209
  return {
201
210
  id: "silgi-tracing",
202
211
  onRequest: async (request, _ctx) => {
@@ -215,7 +224,7 @@ function tracing(config) {
215
224
  },
216
225
  hooks: { after: [{
217
226
  matcher: () => true,
218
- handler: (config?.createAuthMiddleware ?? ((fn) => fn))(async (ctx) => {
227
+ handler: wrapMiddleware(async (ctx) => {
219
228
  try {
220
229
  const request = ctx.request;
221
230
  if (!request) return;
package/dist/lazy.mjs CHANGED
@@ -62,6 +62,9 @@ async function resolveLazy(value) {
62
62
  resolved.set(value, mod.default);
63
63
  loading.delete(value);
64
64
  return mod.default;
65
+ }, (err) => {
66
+ loading.delete(value);
67
+ throw err;
65
68
  });
66
69
  loading.set(value, pending);
67
70
  }
@@ -4,13 +4,15 @@ import { WrapDef } from "./types.mjs";
4
4
  /**
5
5
  * Create a wrap that transforms the procedure input before execution.
6
6
  *
7
- * The mapper function receives the raw input and returns the transformed input.
8
- * The mapped input is set on the context as `__mappedInput` and picked up
9
- * by the pipeline.
7
+ * @remarks
8
+ * The mapper receives the raw input and returns the transformed input.
9
+ * Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
10
+ * symbol slot on `ctx`; the resolver reads the same slot to get the
11
+ * rewritten input. Users never interact with the slot directly — it's
12
+ * framework-internal (see `src/core/ctx-symbols.ts`).
10
13
  *
11
- * Note: Since Silgi's pipeline receives input as a separate argument
12
- * (not on ctx), mapInput works as a wrap that intercepts and transforms
13
- * before calling next().
14
+ * Must run inside the wrap onion (not as a guard) so it can observe and
15
+ * mutate the already-parsed input after schema validation.
14
16
  */
15
17
  declare function mapInput<TIn = unknown, TOut = unknown>(mapper: (input: TIn) => TOut | Promise<TOut>): WrapDef;
16
18
  //#endregion
@@ -25,13 +25,15 @@ import { RAW_INPUT } from "./core/ctx-symbols.mjs";
25
25
  /**
26
26
  * Create a wrap that transforms the procedure input before execution.
27
27
  *
28
- * The mapper function receives the raw input and returns the transformed input.
29
- * The mapped input is set on the context as `__mappedInput` and picked up
30
- * by the pipeline.
28
+ * @remarks
29
+ * The mapper receives the raw input and returns the transformed input.
30
+ * Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
31
+ * symbol slot on `ctx`; the resolver reads the same slot to get the
32
+ * rewritten input. Users never interact with the slot directly — it's
33
+ * framework-internal (see `src/core/ctx-symbols.ts`).
31
34
  *
32
- * Note: Since Silgi's pipeline receives input as a separate argument
33
- * (not on ctx), mapInput works as a wrap that intercepts and transforms
34
- * before calling next().
35
+ * Must run inside the wrap onion (not as a guard) so it can observe and
36
+ * mutate the already-parsed input after schema validation.
35
37
  */
36
38
  function mapInput(mapper) {
37
39
  return {
@@ -107,6 +107,22 @@ function parseAnalyticsDetailPath(pathname, prefix) {
107
107
  function jsonResponse(data, headers) {
108
108
  return new Response(JSON.stringify(data), { headers });
109
109
  }
110
+ /**
111
+ * Parse a request body as JSON without throwing. Returns `null` on any
112
+ * failure (empty body, malformed JSON, non-JSON content). Used by the
113
+ * analytics hidden-paths endpoint so a bad request produces a 400 instead
114
+ * of an uncaught exception that surfaces as a 500 to the client.
115
+ */
116
+ async function parseJsonBody(request) {
117
+ try {
118
+ const text = await request.text();
119
+ if (!text) return null;
120
+ const parsed = JSON.parse(text);
121
+ return parsed && typeof parsed === "object" ? parsed : null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
110
126
  /** Serve analytics dashboard and API routes. */
111
127
  async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml) {
112
128
  const jsonCacheHeaders = {
@@ -122,24 +138,20 @@ async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml)
122
138
  if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
123
139
  if (pathname === "api/analytics/hidden") {
124
140
  if (request.method === "GET") return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
125
- if (request.method === "POST") {
126
- const body = await request.json();
127
- if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
128
- status: 400,
129
- headers: jsonCacheHeaders
130
- });
131
- collector.addHiddenPath(body.path);
132
- return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
133
- }
134
- if (request.method === "DELETE") {
135
- const body = await request.json();
136
- if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
141
+ if (request.method === "POST" || request.method === "DELETE") {
142
+ const body = await parseJsonBody(request);
143
+ if (!body || typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
137
144
  status: 400,
138
145
  headers: jsonCacheHeaders
139
146
  });
140
- collector.removeHiddenPath(body.path);
147
+ if (request.method === "POST") collector.addHiddenPath(body.path);
148
+ else collector.removeHiddenPath(body.path);
141
149
  return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
142
150
  }
151
+ return new Response("{\"error\":\"method not allowed\"}", {
152
+ status: 405,
153
+ headers: jsonCacheHeaders
154
+ });
143
155
  }
144
156
  if (pathname === "api/analytics/errors") return jsonResponse(queryErrors((await collector.getErrors()).filter((e) => !collector.isHidden(e.procedure)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
145
157
  if (pathname === "api/analytics/requests") return jsonResponse(queryRequests((await collector.getRequests()).filter((r) => !collector.isHidden(r.path)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
@@ -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 };