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/plugins/cache.mjs
CHANGED
|
@@ -4,83 +4,128 @@ import { defineCachedFunction, setStorage, useStorage } from "ocache";
|
|
|
4
4
|
import { hash } from "ohash";
|
|
5
5
|
//#region src/plugins/cache.ts
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
7
|
+
* Response cache plugin
|
|
8
|
+
* -----------------------
|
|
9
|
+
*
|
|
10
|
+
* Caches resolver output using `ocache` underneath. Drop the wrap on
|
|
11
|
+
* a query procedure and its return value is memoized, deduplicated,
|
|
12
|
+
* and (by default) served stale-while-revalidate.
|
|
13
|
+
*
|
|
14
|
+
* The storage backend is pluggable. If the parent silgi instance has
|
|
15
|
+
* a `'cache'` mount configured via `silgi({ storage })`, we wire ocache
|
|
16
|
+
* to that mount the first time `cacheQuery()` runs. Otherwise ocache
|
|
17
|
+
* falls back to its built-in in-memory map. Users who want a different
|
|
18
|
+
* backend without the silgi storage mount can call `setCacheStorage()`
|
|
19
|
+
* directly, or wrap an unstorage instance via `createUnstorageAdapter()`.
|
|
20
|
+
*
|
|
21
|
+
* Every ocache feature is exposed through the options:
|
|
22
|
+
*
|
|
23
|
+
* TTL + stale-while-revalidate — `maxAge`, `swr`, `staleMaxAge`
|
|
24
|
+
* Request deduplication — built-in, concurrent calls share
|
|
25
|
+
* one in-flight promise
|
|
26
|
+
* Integrity — redeploy invalidates stale cache
|
|
27
|
+
* when the resolver's hash changes
|
|
28
|
+
* Per-request bypass / invalidate — `shouldBypassCache`,
|
|
29
|
+
* `shouldInvalidateCache`
|
|
30
|
+
* Entry validation / transform — `validate`, `transform`
|
|
31
|
+
* Multi-tier storage — read cascade, write to all
|
|
32
|
+
* Manual invalidation — `invalidateQueryCache(name)`
|
|
19
33
|
*
|
|
20
34
|
* @example
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* import { createStorage } from 'silgi/unstorage'
|
|
32
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
33
|
-
*
|
|
34
|
-
* const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
35
|
-
* setCacheStorage(createUnstorageAdapter(storage))
|
|
36
|
-
* ```
|
|
35
|
+
* import { cacheQuery, setCacheStorage, createUnstorageAdapter } from 'silgi/cache'
|
|
36
|
+
* import { createStorage } from 'silgi/unstorage'
|
|
37
|
+
* import redisDriver from 'unstorage/drivers/redis'
|
|
38
|
+
*
|
|
39
|
+
* const listUsers = k
|
|
40
|
+
* .$use(cacheQuery({ maxAge: 60 }))
|
|
41
|
+
* .$resolve(({ ctx }) => ctx.db.users.findMany())
|
|
42
|
+
*
|
|
43
|
+
* const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
44
|
+
* setCacheStorage(createUnstorageAdapter(storage))
|
|
37
45
|
*/
|
|
38
46
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
47
|
+
* Build an ocache `StorageInterface` from anything that quacks like
|
|
48
|
+
* an unstorage store. The plugin uses it twice — once internally to
|
|
49
|
+
* bridge silgi's configured storage mount, and once as a public helper
|
|
50
|
+
* for users who bring their own unstorage instance.
|
|
51
|
+
*
|
|
52
|
+
* Setting `value` to `null` / `undefined` intentionally triggers a
|
|
53
|
+
* delete so ocache's "mark as stale" signalling survives every
|
|
54
|
+
* backend uniformly.
|
|
41
55
|
*/
|
|
42
|
-
|
|
56
|
+
function adaptUnstorage(storage) {
|
|
57
|
+
return {
|
|
58
|
+
get: (key) => storage.getItem(key),
|
|
59
|
+
set: (key, value, opts) => {
|
|
60
|
+
if (value === null || value === void 0) return storage.removeItem(key);
|
|
61
|
+
return storage.setItem(key, value, opts);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Public helper for users who already have an unstorage instance.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const storage = createStorage({ driver: redisDriver({ ... }) })
|
|
70
|
+
* setCacheStorage(createUnstorageAdapter(storage))
|
|
71
|
+
*/
|
|
72
|
+
function createUnstorageAdapter(storage) {
|
|
73
|
+
return adaptUnstorage(storage);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Connect ocache to the silgi instance's `'cache'` storage mount, if
|
|
77
|
+
* one is configured. Idempotent — the first `cacheQuery()` call wins
|
|
78
|
+
* and every subsequent one is a cheap boolean check.
|
|
79
|
+
*
|
|
80
|
+
* When silgi has no storage mount the call is a silent no-op: ocache
|
|
81
|
+
* keeps its built-in in-memory backend, which is still correct (just
|
|
82
|
+
* not shared across processes).
|
|
83
|
+
*/
|
|
84
|
+
let storageConnected = false;
|
|
43
85
|
function ensureStorageConnected() {
|
|
44
|
-
if (
|
|
45
|
-
|
|
86
|
+
if (storageConnected) return;
|
|
87
|
+
storageConnected = true;
|
|
46
88
|
try {
|
|
47
|
-
|
|
48
|
-
setStorage({
|
|
49
|
-
get: (key) => storage.getItem(key),
|
|
50
|
-
set: (key, value, opts) => {
|
|
51
|
-
if (value === null || value === void 0) return storage.removeItem(key);
|
|
52
|
-
return storage.setItem(key, value, opts?.ttl ? { ttl: opts.ttl } : void 0);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
89
|
+
setStorage(adaptUnstorage(useStorage$1("cache")));
|
|
55
90
|
} catch {}
|
|
56
91
|
}
|
|
57
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Per-procedure set of cache keys, keyed by the procedure's cache name.
|
|
94
|
+
* `invalidateQueryCache(name)` uses this to wipe every key that a given
|
|
95
|
+
* procedure has written to backing storage.
|
|
96
|
+
*
|
|
97
|
+
* Module-global by design: different silgi instances in the same
|
|
98
|
+
* process calling the same procedure name end up sharing the same
|
|
99
|
+
* ocache backend anyway, so a unified registry is correct.
|
|
100
|
+
*/
|
|
58
101
|
const cacheKeyRegistry = /* @__PURE__ */ new Map();
|
|
59
102
|
/**
|
|
60
|
-
*
|
|
103
|
+
* Build the cache-key generator from user options. When the user
|
|
104
|
+
* passes `getKey`, we use it verbatim. Otherwise we hash the input
|
|
105
|
+
* with ohash, falling back to an empty string when there is no input
|
|
106
|
+
* (shared-cache-for-parameterless-queries is almost always what
|
|
107
|
+
* people want).
|
|
108
|
+
*/
|
|
109
|
+
function buildKeyFn(custom) {
|
|
110
|
+
if (custom) return custom;
|
|
111
|
+
return (input) => input !== void 0 && input !== null ? hash(input) : "";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap middleware that caches a query procedure's output.
|
|
61
115
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
116
|
+
* Defaults: 60-second TTL, SWR on. Every other knob comes from
|
|
117
|
+
* {@link CacheQueryOptions}.
|
|
64
118
|
*
|
|
65
119
|
* @example
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* swr: true,
|
|
76
|
-
* staleMaxAge: 600,
|
|
77
|
-
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
78
|
-
* shouldInvalidateCache: (input) => (input as any)?.refresh,
|
|
79
|
-
* validate: (entry) => Array.isArray(entry.value),
|
|
80
|
-
* onError: (err) => console.error('[cache]', err),
|
|
81
|
-
* }))
|
|
82
|
-
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
83
|
-
* ```
|
|
120
|
+
* const listPosts = k
|
|
121
|
+
* .$use(cacheQuery({
|
|
122
|
+
* maxAge: 300,
|
|
123
|
+
* staleMaxAge: 600,
|
|
124
|
+
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
125
|
+
* validate: (entry) => Array.isArray(entry.value),
|
|
126
|
+
* onError: (err) => console.error('[cache]', err),
|
|
127
|
+
* }))
|
|
128
|
+
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
84
129
|
*/
|
|
85
130
|
function cacheQuery(options = {}) {
|
|
86
131
|
const maxAge = options.maxAge ?? 60;
|
|
@@ -88,29 +133,32 @@ function cacheQuery(options = {}) {
|
|
|
88
133
|
const staleMaxAge = options.staleMaxAge ?? maxAge;
|
|
89
134
|
const customGetKey = options.getKey;
|
|
90
135
|
let cacheName = options.name;
|
|
91
|
-
const pendingNextMap = /* @__PURE__ */ new Map();
|
|
92
|
-
let requestCounter = 0;
|
|
93
136
|
let cachedFn = null;
|
|
94
|
-
let keyFn;
|
|
137
|
+
let keyFn = buildKeyFn(customGetKey);
|
|
138
|
+
/**
|
|
139
|
+
* Two concurrent requests can race through the same cache entry:
|
|
140
|
+
* ocache calls our inner `_resolve` for each one, and they must not
|
|
141
|
+
* share a closure-scoped `next`. We keep a per-request map keyed
|
|
142
|
+
* by a monotonic counter so each call resolves its *own* `next()`.
|
|
143
|
+
*/
|
|
144
|
+
const pendingNext = /* @__PURE__ */ new Map();
|
|
145
|
+
let requestCounter = 0;
|
|
95
146
|
return {
|
|
96
147
|
kind: "wrap",
|
|
97
148
|
fn: async (ctx, next) => {
|
|
98
149
|
ensureStorageConnected();
|
|
99
150
|
if (!cachedFn) {
|
|
100
|
-
|
|
151
|
+
cacheName ??= ctx.__procedurePath ?? `proc_${hash(next.toString()).slice(0, 8)}`;
|
|
101
152
|
const resolvedName = cacheName;
|
|
153
|
+
const resolvedBase = options.base ?? "/cache";
|
|
102
154
|
const keySet = /* @__PURE__ */ new Set();
|
|
103
155
|
cacheKeyRegistry.set(resolvedName, keySet);
|
|
104
|
-
keyFn = customGetKey ? (input) => customGetKey(input) : (input) => input !== void 0 && input !== null ? hash(input) : "";
|
|
105
|
-
const resolvedBase = options.base ?? "/cache";
|
|
106
156
|
cachedFn = defineCachedFunction(async (_input, requestId) => {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
throw new Error("[silgi/cache] Missing next() for cache resolve");
|
|
157
|
+
const key = requestId ?? keyFn(_input);
|
|
158
|
+
const fn = pendingNext.get(key);
|
|
159
|
+
if (!fn) throw new Error("[silgi/cache] Missing next() for cache resolve");
|
|
160
|
+
pendingNext.delete(key);
|
|
161
|
+
return fn();
|
|
114
162
|
}, {
|
|
115
163
|
name: resolvedName,
|
|
116
164
|
group: "silgi",
|
|
@@ -122,8 +170,8 @@ function cacheQuery(options = {}) {
|
|
|
122
170
|
onError: options.onError,
|
|
123
171
|
validate: options.validate,
|
|
124
172
|
transform: options.transform,
|
|
125
|
-
shouldBypassCache: options.shouldBypassCache
|
|
126
|
-
shouldInvalidateCache: options.shouldInvalidateCache
|
|
173
|
+
shouldBypassCache: options.shouldBypassCache,
|
|
174
|
+
shouldInvalidateCache: options.shouldInvalidateCache,
|
|
127
175
|
getKey: (input) => {
|
|
128
176
|
const key = keyFn(input);
|
|
129
177
|
keySet.add(`${resolvedBase}:silgi:${resolvedName}:${key}.json`);
|
|
@@ -133,74 +181,44 @@ function cacheQuery(options = {}) {
|
|
|
133
181
|
}
|
|
134
182
|
const input = ctx[RAW_INPUT];
|
|
135
183
|
const requestId = `__req_${++requestCounter}`;
|
|
136
|
-
|
|
184
|
+
pendingNext.set(requestId, next);
|
|
137
185
|
return cachedFn(input, requestId);
|
|
138
186
|
}
|
|
139
187
|
};
|
|
140
188
|
}
|
|
141
189
|
/**
|
|
142
|
-
*
|
|
190
|
+
* Wipe every cached entry for a procedure by its cache name.
|
|
143
191
|
*
|
|
144
|
-
*
|
|
192
|
+
* Typically called after a mutation that makes related queries stale.
|
|
193
|
+
* Names default to the procedure's auto-generated path, or whatever
|
|
194
|
+
* was passed via `cacheQuery({ name })`.
|
|
145
195
|
*
|
|
146
196
|
* @example
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* })
|
|
153
|
-
* ```
|
|
197
|
+
* const createUser = k.$resolve(async ({ input, ctx }) => {
|
|
198
|
+
* const user = await ctx.db.users.create(input)
|
|
199
|
+
* await invalidateQueryCache('users_list')
|
|
200
|
+
* return user
|
|
201
|
+
* })
|
|
154
202
|
*/
|
|
155
203
|
async function invalidateQueryCache(name) {
|
|
156
204
|
const keys = cacheKeyRegistry.get(name);
|
|
157
|
-
if (keys)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
205
|
+
if (!keys) return;
|
|
206
|
+
const storage = useStorage();
|
|
207
|
+
await Promise.all([...keys].map((key) => storage.set(key, null)));
|
|
208
|
+
keys.clear();
|
|
162
209
|
}
|
|
163
210
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* For production, use `createUnstorageAdapter()` with Redis, Cloudflare KV, etc.
|
|
211
|
+
* Replace ocache's storage backend. Default is an in-memory map with
|
|
212
|
+
* TTL; production deployments usually want Redis or Cloudflare KV via
|
|
213
|
+
* `createUnstorageAdapter()`.
|
|
168
214
|
*
|
|
169
215
|
* @example
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
174
|
-
*
|
|
175
|
-
* setCacheStorage(createUnstorageAdapter(
|
|
176
|
-
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
177
|
-
* ))
|
|
178
|
-
* ```
|
|
216
|
+
* setCacheStorage(createUnstorageAdapter(
|
|
217
|
+
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
218
|
+
* ))
|
|
179
219
|
*/
|
|
180
220
|
function setCacheStorage(storage) {
|
|
181
221
|
setStorage(storage);
|
|
182
222
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Create an ocache-compatible storage adapter from an unstorage instance.
|
|
185
|
-
*
|
|
186
|
-
* @example
|
|
187
|
-
* ```ts
|
|
188
|
-
* import { createStorage } from 'silgi/unstorage'
|
|
189
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
190
|
-
*
|
|
191
|
-
* const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
192
|
-
* const adapter = createUnstorageAdapter(storage)
|
|
193
|
-
* setCacheStorage(adapter)
|
|
194
|
-
* ```
|
|
195
|
-
*/
|
|
196
|
-
function createUnstorageAdapter(storage) {
|
|
197
|
-
return {
|
|
198
|
-
get: (key) => storage.getItem(key),
|
|
199
|
-
set: (key, value, opts) => {
|
|
200
|
-
if (value === null || value === void 0) return storage.removeItem(key);
|
|
201
|
-
return storage.setItem(key, value, opts);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
223
|
//#endregion
|
|
206
224
|
export { cacheQuery, createUnstorageAdapter, invalidateQueryCache, setCacheStorage };
|
package/dist/scalar.d.mts
CHANGED
|
@@ -9,42 +9,53 @@ interface ScalarOptions {
|
|
|
9
9
|
url: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
}[];
|
|
12
|
-
/** Security scheme (e.g. Bearer token) */
|
|
12
|
+
/** Security scheme (e.g. Bearer token, API key header). */
|
|
13
13
|
security?: {
|
|
14
|
-
type: 'http' | 'apiKey';
|
|
15
|
-
scheme?: string;
|
|
16
|
-
bearerFormat?: string;
|
|
17
|
-
in?: 'header' | 'query';
|
|
14
|
+
type: 'http' | 'apiKey'; /** For `type: 'http'`, e.g. `'bearer'`. */
|
|
15
|
+
scheme?: string; /** For `type: 'http'` + bearer, e.g. `'JWT'`. */
|
|
16
|
+
bearerFormat?: string; /** For `type: 'apiKey'`, which side of the request carries the key. */
|
|
17
|
+
in?: 'header' | 'query'; /** For `type: 'apiKey'`, the header / query-param name. */
|
|
18
18
|
name?: string;
|
|
19
19
|
description?: string;
|
|
20
20
|
};
|
|
21
|
-
/** Contact info */
|
|
22
21
|
contact?: {
|
|
23
22
|
name?: string;
|
|
24
23
|
url?: string;
|
|
25
24
|
email?: string;
|
|
26
25
|
};
|
|
27
|
-
/** License */
|
|
28
26
|
license?: {
|
|
29
27
|
name: string;
|
|
30
28
|
url?: string;
|
|
31
29
|
};
|
|
32
|
-
/** External docs */
|
|
33
30
|
externalDocs?: {
|
|
34
31
|
url: string;
|
|
35
32
|
description?: string;
|
|
36
33
|
};
|
|
37
34
|
/**
|
|
38
|
-
* Scalar UI script
|
|
35
|
+
* Where to load the Scalar UI script from.
|
|
39
36
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
37
|
+
* `'cdn'` — `cdn.jsdelivr.net` (default).
|
|
38
|
+
* `'unpkg'` — `unpkg.com`.
|
|
39
|
+
* `'local'` — serve from `node_modules` (offline; requires the
|
|
40
|
+
* `@scalar/api-reference` package installed).
|
|
41
|
+
* string — any custom URL, e.g. a self-hosted asset path.
|
|
44
42
|
*/
|
|
45
43
|
cdn?: 'cdn' | 'unpkg' | 'local' | (string & {});
|
|
46
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate an OpenAPI 3.1.0 document for a `RouterDef`.
|
|
47
|
+
*
|
|
48
|
+
* The document is a plain object and can be re-serialized, cached,
|
|
49
|
+
* piped into any OpenAPI consumer (codegen, docs site, validators). We
|
|
50
|
+
* do not return a typed `OpenAPIV3_1.Document` because the typings in
|
|
51
|
+
* the ecosystem are noisy; downstream consumers usually only care
|
|
52
|
+
* about a handful of keys anyway.
|
|
53
|
+
*/
|
|
47
54
|
declare function generateOpenAPI(router: RouterDef, options?: ScalarOptions, basePath?: string, registry?: SchemaRegistry): Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Render the minimal HTML shell the Scalar UI needs. The UI itself is
|
|
57
|
+
* a single script that reads the `data-url` attribute to pull the spec.
|
|
58
|
+
*/
|
|
48
59
|
declare function scalarHTML(specUrl: string, options?: ScalarOptions): string;
|
|
49
60
|
//#endregion
|
|
50
61
|
export { ScalarOptions, generateOpenAPI, scalarHTML };
|