silgi 0.1.0-beta.7 → 0.1.0-beta.8

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,6 +1,7 @@
1
1
  import { routerCache } from "./router-utils.mjs";
2
2
  import { compileRouter, createContext, releaseContext } from "../compile.mjs";
3
3
  import { applyContext } from "./dispatch.mjs";
4
+ import { analyticsTraceMap } from "../plugins/analytics.mjs";
4
5
  import { detectResponseFormat, encodeResponse, makeErrorResponse } from "./codec.mjs";
5
6
  import { parseInput } from "./input.mjs";
6
7
  import { iteratorToEventStream } from "./sse.mjs";
@@ -116,6 +117,8 @@ function createFetchHandler(routerDef, contextFactory, hooks) {
116
117
  const baseCtxResult = contextFactory(request);
117
118
  applyContext(ctx, baseCtxResult instanceof Promise ? await baseCtxResult : baseCtxResult);
118
119
  if (match.params) ctx.params = match.params;
120
+ const reqTrace = analyticsTraceMap.get(request);
121
+ if (reqTrace) ctx.__analyticsTrace = reqTrace;
119
122
  if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
120
123
  callHook("request", {
121
124
  path: pathname,
@@ -3,16 +3,15 @@ import { Driver, Storage, StorageValue } from "unstorage";
3
3
  //#region src/core/storage.d.ts
4
4
  /** Storage config — map of mount path → driver instance, or a pre-built Storage */
5
5
  type StorageConfig = Storage | Record<string, Driver>;
6
- /**
7
- * Initialize storage from config — called once at startup.
8
- * Accepts either a pre-built Storage or a map of mount paths → drivers.
9
- */
10
- declare function initStorage(config?: StorageConfig): Storage;
11
6
  /**
12
7
  * Get the storage instance with optional prefix.
13
- * Creates default in-memory if not initialized.
8
+ * Creates default storage with `data` and `cache` mounts on first call.
14
9
  */
15
10
  declare function useStorage<T extends StorageValue = StorageValue>(base?: string): Storage<T>;
11
+ /**
12
+ * Initialize storage from config — call once at startup.
13
+ */
14
+ declare function initStorage(config?: StorageConfig): Storage;
16
15
  /**
17
16
  * Reset storage — for testing.
18
17
  */
@@ -1,63 +1,61 @@
1
1
  import { createStorage, prefixStorage } from "unstorage";
2
+ import memoryDriver from "unstorage/drivers/memory";
2
3
  //#region src/core/storage.ts
3
4
  /**
4
- * Storage integration unstorage with type-safe driver config.
5
+ * Storage — Nitro-style global storage with unstorage.
5
6
  *
6
- * Two usage modes:
7
+ * Two default mounts are created automatically:
8
+ * - `data` — persistent data (analytics, sessions, etc.)
9
+ * - `cache` — ephemeral cache (query results, SWR, etc.)
10
+ *
11
+ * Both use in-memory drivers by default. Override with custom drivers:
7
12
  *
8
- * 1. Declarative config (type-safe driver options):
9
13
  * ```ts
10
14
  * import redisDriver from 'unstorage/drivers/redis'
11
- * import memoryDriver from 'unstorage/drivers/memory'
15
+ * import fsDriver from 'unstorage/drivers/fs'
12
16
  *
13
17
  * const s = silgi({
14
18
  * context: () => ({}),
15
19
  * storage: {
20
+ * data: fsDriver({ base: '.data' }),
16
21
  * cache: redisDriver({ url: 'redis://localhost' }),
17
- * sessions: memoryDriver(),
18
22
  * },
19
23
  * })
20
- * ```
21
- *
22
- * 2. Bring your own storage instance:
23
- * ```ts
24
- * const storage = createStorage({})
25
- * storage.mount('cache', redisDriver({ ... }))
26
24
  *
27
- * const s = silgi({
28
- * context: () => ({}),
29
- * storage,
30
- * })
25
+ * // In procedures:
26
+ * const data = useStorage('data')
27
+ * const cache = useStorage('cache')
31
28
  * ```
32
29
  */
33
- let _storage;
34
- /**
35
- * Initialize storage from config — called once at startup.
36
- * Accepts either a pre-built Storage or a map of mount paths → drivers.
37
- */
38
- function initStorage(config) {
39
- if (_storage) return _storage;
40
- if (config && "getItem" in config) {
41
- _storage = config;
42
- return _storage;
43
- }
44
- _storage = createStorage({});
45
- if (config) for (const [path, driver] of Object.entries(config)) _storage.mount(path, driver);
46
- return _storage;
30
+ function _initStorage(config) {
31
+ if (config && "getItem" in config) return config;
32
+ const storage = createStorage({});
33
+ storage.mount("data", memoryDriver());
34
+ storage.mount("cache", memoryDriver());
35
+ if (config) for (const [path, driver] of Object.entries(config)) storage.mount(path, driver);
36
+ return storage;
47
37
  }
48
38
  /**
49
39
  * Get the storage instance with optional prefix.
50
- * Creates default in-memory if not initialized.
40
+ * Creates default storage with `data` and `cache` mounts on first call.
51
41
  */
52
42
  function useStorage(base = "") {
53
- const storage = _storage ?? initStorage();
43
+ const storage = useStorage._storage ??= _initStorage();
54
44
  return base ? prefixStorage(storage, base) : storage;
55
45
  }
56
46
  /**
47
+ * Initialize storage from config — call once at startup.
48
+ */
49
+ function initStorage(config) {
50
+ const storage = _initStorage(config);
51
+ useStorage._storage = storage;
52
+ return storage;
53
+ }
54
+ /**
57
55
  * Reset storage — for testing.
58
56
  */
59
57
  function resetStorage() {
60
- _storage = void 0;
58
+ useStorage._storage = void 0;
61
59
  }
62
60
  //#endregion
63
61
  export { initStorage, resetStorage, useStorage };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SilgiError, isDefinedError, toSilgiError } from "./core/error.mjs";
2
2
  import { ValidationError, type, validateSchema } from "./core/schema.mjs";
3
3
  import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
4
+ import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
4
5
  import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
5
6
  import { getEventMeta, withEventMeta } from "./core/sse.mjs";
6
7
  import { silgi } from "./silgi.mjs";
@@ -8,6 +9,5 @@ import { callable } from "./callable.mjs";
8
9
  import { lifecycleWrap } from "./lifecycle.mjs";
9
10
  import { mapInput } from "./map-input.mjs";
10
11
  import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
11
- import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
12
12
  import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
13
13
  export { AsyncIteratorClass, SilgiError, ValidationError, callable, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, scalarHTML, silgi, toSilgiError, type, useStorage, validateSchema, withEventMeta };
@@ -91,6 +91,8 @@ interface AnalyticsOptions {
91
91
  * - `undefined` — no auth (open access, NOT recommended in production)
92
92
  */
93
93
  auth?: string | ((req: Request) => boolean | Promise<boolean>);
94
+ /** Interval in ms between storage flushes (default: 5000) */
95
+ flushInterval?: number;
94
96
  }
95
97
  interface ProcedureSnapshot {
96
98
  count: number;
@@ -165,8 +167,9 @@ declare class AnalyticsCollector {
165
167
  recordError(path: string, durationMs: number, errorMsg: string): void;
166
168
  recordDetailedError(entry: Omit<ErrorEntry, 'id'>): void;
167
169
  recordDetailedRequest(entry: Omit<RequestEntry, 'id'>): void;
168
- getErrors(): ErrorEntry[];
169
- getRequests(): RequestEntry[];
170
+ getErrors(): Promise<ErrorEntry[]>;
171
+ getRequests(): Promise<RequestEntry[]>;
172
+ dispose(): Promise<void>;
170
173
  toJSON(): AnalyticsSnapshot;
171
174
  }
172
175
  declare class RequestAccumulator {
@@ -193,7 +196,7 @@ declare function sanitizeHeaders(headers: Headers): Record<string, string>;
193
196
  /** Return auth-failure response for analytics routes. */
194
197
  declare function analyticsAuthResponse(pathname: string): Response;
195
198
  /** Serve analytics dashboard and API routes. */
196
- declare function serveAnalyticsRoute(pathname: string, collector: AnalyticsCollector, dashboardHtml: string | undefined): Response;
199
+ declare function serveAnalyticsRoute(pathname: string, collector: AnalyticsCollector, dashboardHtml: string | undefined): Promise<Response>;
197
200
  /**
198
201
  * Wrap a fetch handler with analytics collection.
199
202
  * Intercepts analytics dashboard routes and instruments every request.
@@ -1,9 +1,10 @@
1
1
  import { SilgiError, toSilgiError } from "../core/error.mjs";
2
2
  import { ValidationError } from "../core/schema.mjs";
3
- import { parse } from "cookie-es";
3
+ import { useStorage } from "../core/storage.mjs";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { parse } from "cookie-es";
7
8
  //#region src/plugins/analytics.ts
8
9
  /**
9
10
  * Built-in analytics plugin — zero-dependency monitoring with deep error tracing.
@@ -142,6 +143,86 @@ async function trace(ctx, name, fn, opts) {
142
143
  if (reqTrace) return reqTrace.trace(name, fn, opts);
143
144
  return fn();
144
145
  }
146
+ var AnalyticsStore = class {
147
+ #storage;
148
+ #pendingRequests = [];
149
+ #pendingErrors = [];
150
+ #maxRequests;
151
+ #maxErrors;
152
+ #timer = null;
153
+ #flushing = false;
154
+ constructor(maxRequests, maxErrors, flushInterval) {
155
+ this.#storage = useStorage("data");
156
+ this.#maxRequests = maxRequests;
157
+ this.#maxErrors = maxErrors;
158
+ this.#timer = setInterval(() => this.flush(), flushInterval);
159
+ if (typeof this.#timer === "object" && "unref" in this.#timer) this.#timer.unref();
160
+ }
161
+ enqueueRequest(entry) {
162
+ this.#pendingRequests.push(entry);
163
+ }
164
+ enqueueError(entry) {
165
+ this.#pendingErrors.push(entry);
166
+ }
167
+ async flush() {
168
+ if (this.#flushing) return;
169
+ const requests = this.#pendingRequests.splice(0);
170
+ const errors = this.#pendingErrors.splice(0);
171
+ if (requests.length === 0 && errors.length === 0) return;
172
+ this.#flushing = true;
173
+ try {
174
+ if (requests.length > 0) {
175
+ const merged = [...await this.#storage.getItem("analytics:requests") ?? [], ...requests].slice(-this.#maxRequests);
176
+ await this.#storage.setItem("analytics:requests", merged);
177
+ }
178
+ if (errors.length > 0) {
179
+ const merged = [...await this.#storage.getItem("analytics:errors") ?? [], ...errors].slice(-this.#maxErrors);
180
+ await this.#storage.setItem("analytics:errors", merged);
181
+ }
182
+ } catch {
183
+ this.#pendingRequests.unshift(...requests);
184
+ this.#pendingErrors.unshift(...errors);
185
+ } finally {
186
+ this.#flushing = false;
187
+ }
188
+ }
189
+ async getRequests() {
190
+ const stored = await this.#storage.getItem("analytics:requests") ?? [];
191
+ if (this.#pendingRequests.length === 0) return stored;
192
+ return [...stored, ...this.#pendingRequests].slice(-this.#maxRequests);
193
+ }
194
+ async getErrors() {
195
+ const stored = await this.#storage.getItem("analytics:errors") ?? [];
196
+ if (this.#pendingErrors.length === 0) return stored;
197
+ return [...stored, ...this.#pendingErrors].slice(-this.#maxErrors);
198
+ }
199
+ async hydrate() {
200
+ try {
201
+ return await this.#storage.getItem("analytics:counters") ?? {
202
+ totalRequests: 0,
203
+ totalErrors: 0
204
+ };
205
+ } catch {
206
+ return {
207
+ totalRequests: 0,
208
+ totalErrors: 0
209
+ };
210
+ }
211
+ }
212
+ async saveCounters(totalRequests, totalErrors) {
213
+ try {
214
+ await this.#storage.setItem("analytics:counters", {
215
+ totalRequests,
216
+ totalErrors
217
+ });
218
+ } catch {}
219
+ }
220
+ async dispose() {
221
+ if (this.#timer) clearInterval(this.#timer);
222
+ this.#timer = null;
223
+ await this.flush();
224
+ }
225
+ };
145
226
  var AnalyticsCollector = class {
146
227
  #procedures = /* @__PURE__ */ new Map();
147
228
  #startTime = Date.now();
@@ -157,6 +238,8 @@ var AnalyticsCollector = class {
157
238
  #nextErrorId = 1;
158
239
  #requests = [];
159
240
  #nextRequestId = 1;
241
+ #store;
242
+ #counterFlushCounter = 0;
160
243
  constructor(options = {}) {
161
244
  this.#bufferSize = options.bufferSize ?? 1024;
162
245
  this.#historySeconds = options.historySeconds ?? 120;
@@ -167,6 +250,11 @@ var AnalyticsCollector = class {
167
250
  count: 0,
168
251
  errors: 0
169
252
  };
253
+ this.#store = new AnalyticsStore(this.#maxRequests, this.#maxErrors, options.flushInterval ?? 5e3);
254
+ this.#store.hydrate().then((c) => {
255
+ this.#totalRequests += c.totalRequests;
256
+ this.#totalErrors += c.totalErrors;
257
+ });
170
258
  }
171
259
  record(path, durationMs) {
172
260
  this.#totalRequests++;
@@ -187,18 +275,23 @@ var AnalyticsCollector = class {
187
275
  this.#tick(true);
188
276
  }
189
277
  recordDetailedError(entry) {
190
- this.#errors.push({
278
+ const full = {
191
279
  ...entry,
192
280
  id: this.#nextErrorId++
193
- });
281
+ };
282
+ this.#errors.push(full);
194
283
  if (this.#errors.length > this.#maxErrors) this.#errors.shift();
284
+ this.#store.enqueueError(full);
195
285
  }
196
286
  recordDetailedRequest(entry) {
197
- this.#requests.push({
287
+ const full = {
198
288
  ...entry,
199
289
  id: this.#nextRequestId++
200
- });
290
+ };
291
+ this.#requests.push(full);
201
292
  if (this.#requests.length > this.#maxRequests) this.#requests.shift();
293
+ this.#store.enqueueRequest(full);
294
+ this.#flushCountersIfNeeded();
202
295
  }
203
296
  #getOrCreate(path) {
204
297
  let entry = this.#procedures.get(path);
@@ -231,10 +324,17 @@ var AnalyticsCollector = class {
231
324
  if (isError) this.#currentWindow.errors++;
232
325
  }
233
326
  getErrors() {
234
- return this.#errors;
327
+ return this.#store.getErrors();
235
328
  }
236
329
  getRequests() {
237
- return this.#requests;
330
+ return this.#store.getRequests();
331
+ }
332
+ #flushCountersIfNeeded() {
333
+ if (++this.#counterFlushCounter % 50 === 0) this.#store.saveCounters(this.#totalRequests, this.#totalErrors);
334
+ }
335
+ async dispose() {
336
+ await this.#store.saveCounters(this.#totalRequests, this.#totalErrors);
337
+ await this.#store.dispose();
238
338
  }
239
339
  toJSON() {
240
340
  const uptimeSeconds = (Date.now() - this.#startTime) / 1e3;
@@ -539,7 +639,7 @@ function analyticsAuthResponse(pathname) {
539
639
  });
540
640
  }
541
641
  /** Serve analytics dashboard and API routes. */
542
- function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
642
+ async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
543
643
  const jsonCacheHeaders = {
544
644
  "content-type": "application/json",
545
645
  "cache-control": "no-cache"
@@ -549,22 +649,28 @@ function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
549
649
  "cache-control": "no-cache"
550
650
  };
551
651
  if (pathname === "analytics/_api/stats") return new Response(JSON.stringify(collector.toJSON()), { headers: jsonCacheHeaders });
552
- if (pathname === "analytics/_api/errors") return new Response(JSON.stringify(collector.getErrors()), { headers: jsonCacheHeaders });
553
- if (pathname === "analytics/_api/requests") return new Response(JSON.stringify(collector.getRequests()), { headers: jsonCacheHeaders });
652
+ if (pathname === "analytics/_api/errors") {
653
+ const errors = await collector.getErrors();
654
+ return new Response(JSON.stringify(errors), { headers: jsonCacheHeaders });
655
+ }
656
+ if (pathname === "analytics/_api/requests") {
657
+ const requests = await collector.getRequests();
658
+ return new Response(JSON.stringify(requests), { headers: jsonCacheHeaders });
659
+ }
554
660
  if (pathname.startsWith("analytics/_api/requests/") && pathname.endsWith("/md")) {
555
661
  const id = Number(pathname.slice(24, -3));
556
- const entry = collector.getRequests().find((r) => r.id === id);
662
+ const entry = (await collector.getRequests()).find((r) => r.id === id);
557
663
  if (entry) return new Response(requestToMarkdown(entry), { headers: mdHeaders });
558
664
  return new Response("not found", { status: 404 });
559
665
  }
560
666
  if (pathname.startsWith("analytics/_api/errors/") && pathname.endsWith("/md")) {
561
667
  const id = Number(pathname.slice(22, -3));
562
- const entry = collector.getErrors().find((e) => e.id === id);
668
+ const entry = (await collector.getErrors()).find((e) => e.id === id);
563
669
  if (entry) return new Response(errorToMarkdown(entry), { headers: mdHeaders });
564
670
  return new Response("not found", { status: 404 });
565
671
  }
566
672
  if (pathname === "analytics/_api/errors/md") {
567
- const errors = collector.getErrors();
673
+ const errors = await collector.getErrors();
568
674
  const md = errors.length === 0 ? "No errors.\n" : `# Errors (${errors.length})\n\n` + errors.map((e) => errorToMarkdown(e)).join("\n\n---\n\n");
569
675
  return new Response(md, { headers: mdHeaders });
570
676
  }
@@ -1,3 +1,4 @@
1
+ import { AnalyticsCollector, RequestTrace, analyticsHTML, errorToMarkdown, trace } from "./analytics.mjs";
1
2
  import { cors, corsHeaders } from "./cors.mjs";
2
3
  import { otelWrap } from "./otel.mjs";
3
4
  import { loggingHooks } from "./pino.mjs";
@@ -10,5 +11,4 @@ import { coerceGuard, coerceObject, coerceValue } from "./coerce.mjs";
10
11
  import { createBatchHandler } from "./batch-server.mjs";
11
12
  import { MemoryPubSub, createPublisher } from "./pubsub.mjs";
12
13
  import { fileGuard, parseMultipart } from "./file-upload.mjs";
13
- import { AnalyticsCollector, RequestTrace, analyticsHTML, errorToMarkdown, trace } from "./analytics.mjs";
14
14
  export { AnalyticsCollector, MemoryPubSub, MemoryRateLimiter, RequestTrace, analyticsHTML, bodyLimitGuard, coerceGuard, coerceObject, coerceValue, cors, corsHeaders, createBatchHandler, createPublisher, decrypt, deleteCookie, encrypt, errorToMarkdown, fileGuard, getCookie, loggingHooks, otelWrap, parseCookies, parseMultipart, rateLimitGuard, setCookie, sign, strictGetGuard, trace, unsign };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silgi",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "private": false,
5
5
  "description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
6
6
  "keywords": [