@gravito/core 1.6.0 → 1.6.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.
@@ -226,7 +226,14 @@ var AOTRouter = class {
226
226
  dynamicRoutePatterns = /* @__PURE__ */ new Map();
227
227
  middlewareCache = /* @__PURE__ */ new Map();
228
228
  cacheMaxSize = 1e3;
229
- version = 0;
229
+ _version = 0;
230
+ /**
231
+ * Get the current version for cache invalidation
232
+ * Incremented whenever middleware or routes are modified
233
+ */
234
+ get version() {
235
+ return this._version;
236
+ }
230
237
  /**
231
238
  * Register a route
232
239
  *
@@ -298,7 +305,7 @@ var AOTRouter = class {
298
305
  */
299
306
  use(...middleware) {
300
307
  this.globalMiddleware.push(...middleware);
301
- this.version++;
308
+ this._version++;
302
309
  }
303
310
  /**
304
311
  * Add path-based middleware
@@ -315,7 +322,7 @@ var AOTRouter = class {
315
322
  const existing = this.pathMiddleware.get(pattern) ?? [];
316
323
  this.pathMiddleware.set(pattern, [...existing, ...middleware]);
317
324
  }
318
- this.version++;
325
+ this._version++;
319
326
  }
320
327
  /**
321
328
  * Match a request to a route
@@ -377,9 +384,9 @@ var AOTRouter = class {
377
384
  if (this.globalMiddleware.length === 0 && this.pathMiddleware.size === 0 && routeMiddleware.length === 0) {
378
385
  return [];
379
386
  }
380
- const cacheKey = `${path}:${routeMiddleware.length}`;
387
+ const cacheKey = path;
381
388
  const cached = this.middlewareCache.get(cacheKey);
382
- if (cached !== void 0 && cached.version === this.version) {
389
+ if (cached !== void 0 && cached.version === this._version) {
383
390
  return cached.data;
384
391
  }
385
392
  const middleware = [];
@@ -400,7 +407,9 @@ var AOTRouter = class {
400
407
  middleware.push(...routeMiddleware);
401
408
  }
402
409
  if (this.middlewareCache.size < this.cacheMaxSize) {
403
- this.middlewareCache.set(cacheKey, { data: middleware, version: this.version });
410
+ this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
411
+ } else if (this.middlewareCache.has(cacheKey)) {
412
+ this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
404
413
  }
405
414
  return middleware;
406
415
  }
@@ -488,6 +497,172 @@ var HEADERS = {
488
497
  HTML: { "Content-Type": "text/html; charset=utf-8" }
489
498
  };
490
499
 
500
+ // src/Container/RequestScopeMetrics.ts
501
+ var RequestScopeMetrics = class {
502
+ cleanupStartTime = null;
503
+ cleanupDuration = null;
504
+ scopeSize = 0;
505
+ servicesCleaned = 0;
506
+ errorsOccurred = 0;
507
+ /**
508
+ * Record start of cleanup operation
509
+ */
510
+ recordCleanupStart() {
511
+ this.cleanupStartTime = performance.now();
512
+ }
513
+ /**
514
+ * Record end of cleanup operation
515
+ *
516
+ * @param scopeSize - Number of services in the scope
517
+ * @param servicesCleaned - Number of services that had cleanup called
518
+ * @param errorsOccurred - Number of cleanup errors
519
+ */
520
+ recordCleanupEnd(scopeSize, servicesCleaned, errorsOccurred = 0) {
521
+ if (this.cleanupStartTime !== null) {
522
+ this.cleanupDuration = performance.now() - this.cleanupStartTime;
523
+ this.cleanupStartTime = null;
524
+ }
525
+ this.scopeSize = scopeSize;
526
+ this.servicesCleaned = servicesCleaned;
527
+ this.errorsOccurred = errorsOccurred;
528
+ }
529
+ /**
530
+ * Get cleanup duration in milliseconds
531
+ *
532
+ * @returns Duration in ms, or null if cleanup not completed
533
+ */
534
+ getCleanupDuration() {
535
+ return this.cleanupDuration;
536
+ }
537
+ /**
538
+ * Check if cleanup took longer than threshold (default 2ms)
539
+ * Useful for detecting slow cleanups
540
+ *
541
+ * @param thresholdMs - Threshold in milliseconds
542
+ * @returns True if cleanup exceeded threshold
543
+ */
544
+ isSlowCleanup(thresholdMs = 2) {
545
+ if (this.cleanupDuration === null) return false;
546
+ return this.cleanupDuration > thresholdMs;
547
+ }
548
+ /**
549
+ * Export metrics as JSON for logging/monitoring
550
+ */
551
+ toJSON() {
552
+ return {
553
+ cleanupDuration: this.cleanupDuration,
554
+ scopeSize: this.scopeSize,
555
+ servicesCleaned: this.servicesCleaned,
556
+ errorsOccurred: this.errorsOccurred,
557
+ hasErrors: this.errorsOccurred > 0,
558
+ isSlowCleanup: this.isSlowCleanup()
559
+ };
560
+ }
561
+ /**
562
+ * Export metrics as compact string for logging
563
+ */
564
+ toString() {
565
+ const duration = this.cleanupDuration ?? "pending";
566
+ return `cleanup: ${duration}ms, scope: ${this.scopeSize}, cleaned: ${this.servicesCleaned}, errors: ${this.errorsOccurred}`;
567
+ }
568
+ };
569
+
570
+ // src/Container/RequestScopeManager.ts
571
+ var RequestScopeManager = class {
572
+ scoped = /* @__PURE__ */ new Map();
573
+ metadata = /* @__PURE__ */ new Map();
574
+ metrics = new RequestScopeMetrics();
575
+ observer = null;
576
+ constructor(observer) {
577
+ this.observer = observer || null;
578
+ }
579
+ /**
580
+ * Set observer for monitoring scope lifecycle
581
+ */
582
+ setObserver(observer) {
583
+ this.observer = observer;
584
+ }
585
+ /**
586
+ * Get metrics for this scope
587
+ */
588
+ getMetrics() {
589
+ return this.metrics;
590
+ }
591
+ /**
592
+ * Resolve or retrieve a request-scoped service instance.
593
+ *
594
+ * If the service already exists in this scope, returns the cached instance.
595
+ * Otherwise, calls the factory function to create a new instance and caches it.
596
+ *
597
+ * Automatically detects and records services with cleanup methods.
598
+ *
599
+ * @template T - The type of the service.
600
+ * @param key - The service key (for caching).
601
+ * @param factory - Factory function to create the instance if not cached.
602
+ * @returns The cached or newly created instance.
603
+ */
604
+ resolve(key, factory) {
605
+ const keyStr = String(key);
606
+ const isFromCache = this.scoped.has(keyStr);
607
+ if (!isFromCache) {
608
+ const instance = factory();
609
+ this.scoped.set(keyStr, instance);
610
+ if (instance && typeof instance === "object" && "cleanup" in instance) {
611
+ this.metadata.set(keyStr, { hasCleanup: true });
612
+ }
613
+ }
614
+ this.observer?.onServiceResolved?.(key, isFromCache);
615
+ return this.scoped.get(keyStr);
616
+ }
617
+ /**
618
+ * Clean up all request-scoped instances.
619
+ *
620
+ * Calls the cleanup() method on each service that has one.
621
+ * Silently ignores cleanup errors to prevent cascading failures.
622
+ * Called automatically by the Gravito engine in the request finally block.
623
+ *
624
+ * @returns Promise that resolves when all cleanup is complete.
625
+ */
626
+ async cleanup() {
627
+ this.metrics.recordCleanupStart();
628
+ this.observer?.onCleanupStart?.();
629
+ const errors = [];
630
+ let servicesCleaned = 0;
631
+ for (const [, instance] of this.scoped) {
632
+ if (instance && typeof instance === "object" && "cleanup" in instance) {
633
+ const fn = instance.cleanup;
634
+ if (typeof fn === "function") {
635
+ try {
636
+ await fn.call(instance);
637
+ servicesCleaned++;
638
+ } catch (error) {
639
+ errors.push(error);
640
+ this.observer?.onCleanupError?.(
641
+ error instanceof Error ? error : new Error(String(error))
642
+ );
643
+ }
644
+ }
645
+ }
646
+ }
647
+ const scopeSize = this.scoped.size;
648
+ this.scoped.clear();
649
+ this.metadata.clear();
650
+ this.metrics.recordCleanupEnd(scopeSize, servicesCleaned, errors.length);
651
+ this.observer?.onCleanupEnd?.(this.metrics);
652
+ if (errors.length > 0) {
653
+ console.error("RequestScope cleanup errors:", errors);
654
+ }
655
+ }
656
+ /**
657
+ * Get the number of services in this scope (for monitoring).
658
+ *
659
+ * @returns The count of cached services.
660
+ */
661
+ size() {
662
+ return this.scoped.size;
663
+ }
664
+ };
665
+
491
666
  // src/engine/FastContext.ts
492
667
  var FastRequestImpl = class {
493
668
  _request;
@@ -499,6 +674,11 @@ var FastRequestImpl = class {
499
674
  _headers = null;
500
675
  _cachedJson = void 0;
501
676
  _jsonParsed = false;
677
+ _cachedText = void 0;
678
+ _textParsed = false;
679
+ _cachedFormData = void 0;
680
+ _formDataParsed = false;
681
+ _cachedQueries = null;
502
682
  // Back-reference for release check optimization
503
683
  _ctx;
504
684
  constructor(ctx) {
@@ -517,6 +697,11 @@ var FastRequestImpl = class {
517
697
  this._headers = null;
518
698
  this._cachedJson = void 0;
519
699
  this._jsonParsed = false;
700
+ this._cachedText = void 0;
701
+ this._textParsed = false;
702
+ this._cachedFormData = void 0;
703
+ this._formDataParsed = false;
704
+ this._cachedQueries = null;
520
705
  return this;
521
706
  }
522
707
  /**
@@ -530,6 +715,11 @@ var FastRequestImpl = class {
530
715
  this._headers = null;
531
716
  this._cachedJson = void 0;
532
717
  this._jsonParsed = false;
718
+ this._cachedText = void 0;
719
+ this._textParsed = false;
720
+ this._cachedFormData = void 0;
721
+ this._formDataParsed = false;
722
+ this._cachedQueries = null;
533
723
  }
534
724
  checkReleased() {
535
725
  if (this._ctx._isReleased) {
@@ -577,6 +767,9 @@ var FastRequestImpl = class {
577
767
  }
578
768
  queries() {
579
769
  this.checkReleased();
770
+ if (this._cachedQueries !== null) {
771
+ return this._cachedQueries;
772
+ }
580
773
  if (!this._query) {
581
774
  this._query = this.getUrl().searchParams;
582
775
  }
@@ -591,6 +784,7 @@ var FastRequestImpl = class {
591
784
  result[key] = [existing, value];
592
785
  }
593
786
  }
787
+ this._cachedQueries = result;
594
788
  return result;
595
789
  }
596
790
  header(name) {
@@ -617,11 +811,19 @@ var FastRequestImpl = class {
617
811
  }
618
812
  async text() {
619
813
  this.checkReleased();
620
- return this._request.text();
814
+ if (!this._textParsed) {
815
+ this._cachedText = await this._request.text();
816
+ this._textParsed = true;
817
+ }
818
+ return this._cachedText;
621
819
  }
622
820
  async formData() {
623
821
  this.checkReleased();
624
- return this._request.formData();
822
+ if (!this._formDataParsed) {
823
+ this._cachedFormData = await this._request.formData();
824
+ this._formDataParsed = true;
825
+ }
826
+ return this._cachedFormData;
625
827
  }
626
828
  get raw() {
627
829
  this.checkReleased();
@@ -635,6 +837,8 @@ var FastContext = class {
635
837
  // Reuse this object
636
838
  _isReleased = false;
637
839
  // Made public for internal check access
840
+ _requestScope = null;
841
+ // Request-scoped services
638
842
  /**
639
843
  * Initialize context for a new request
640
844
  *
@@ -644,6 +848,7 @@ var FastContext = class {
644
848
  this._isReleased = false;
645
849
  this.req.init(request, params, path, routePattern);
646
850
  this._headers = new Headers();
851
+ this._requestScope = new RequestScopeManager();
647
852
  return this;
648
853
  }
649
854
  /**
@@ -769,6 +974,32 @@ var FastContext = class {
769
974
  this._store.set(key, value);
770
975
  }
771
976
  // ─────────────────────────────────────────────────────────────────────────
977
+ // Request Scope Management
978
+ // ─────────────────────────────────────────────────────────────────────────
979
+ /**
980
+ * Get the request-scoped service manager for this request.
981
+ *
982
+ * @returns The RequestScopeManager for this request.
983
+ * @throws Error if called before init() or after reset().
984
+ */
985
+ requestScope() {
986
+ if (!this._requestScope) {
987
+ throw new Error("RequestScope not initialized. Call init() first.");
988
+ }
989
+ return this._requestScope;
990
+ }
991
+ /**
992
+ * Resolve a request-scoped service (convenience method).
993
+ *
994
+ * @template T - The service type.
995
+ * @param key - The service key for caching.
996
+ * @param factory - Factory function to create the service.
997
+ * @returns The cached or newly created service instance.
998
+ */
999
+ scoped(key, factory) {
1000
+ return this.requestScope().resolve(key, factory);
1001
+ }
1002
+ // ─────────────────────────────────────────────────────────────────────────
772
1003
  // Lifecycle helpers
773
1004
  // ─────────────────────────────────────────────────────────────────────────
774
1005
  route = () => "";
@@ -786,6 +1017,10 @@ var MinimalRequest = class {
786
1017
  this._routePattern = _routePattern;
787
1018
  }
788
1019
  _searchParams = null;
1020
+ _cachedQueries = null;
1021
+ _cachedJsonPromise = null;
1022
+ _cachedTextPromise = null;
1023
+ _cachedFormDataPromise = null;
789
1024
  get url() {
790
1025
  return this._request.url;
791
1026
  }
@@ -825,6 +1060,9 @@ var MinimalRequest = class {
825
1060
  return this.getSearchParams().get(name) ?? void 0;
826
1061
  }
827
1062
  queries() {
1063
+ if (this._cachedQueries !== null) {
1064
+ return this._cachedQueries;
1065
+ }
828
1066
  const params = this.getSearchParams();
829
1067
  const result = {};
830
1068
  for (const [key, value] of params.entries()) {
@@ -837,6 +1075,7 @@ var MinimalRequest = class {
837
1075
  result[key] = [existing, value];
838
1076
  }
839
1077
  }
1078
+ this._cachedQueries = result;
840
1079
  return result;
841
1080
  }
842
1081
  header(name) {
@@ -850,13 +1089,22 @@ var MinimalRequest = class {
850
1089
  return result;
851
1090
  }
852
1091
  async json() {
853
- return this._request.json();
1092
+ if (this._cachedJsonPromise === null) {
1093
+ this._cachedJsonPromise = this._request.json();
1094
+ }
1095
+ return this._cachedJsonPromise;
854
1096
  }
855
1097
  async text() {
856
- return this._request.text();
1098
+ if (this._cachedTextPromise === null) {
1099
+ this._cachedTextPromise = this._request.text();
1100
+ }
1101
+ return this._cachedTextPromise;
857
1102
  }
858
1103
  async formData() {
859
- return this._request.formData();
1104
+ if (this._cachedFormDataPromise === null) {
1105
+ this._cachedFormDataPromise = this._request.formData();
1106
+ }
1107
+ return this._cachedFormDataPromise;
860
1108
  }
861
1109
  get raw() {
862
1110
  return this._request;
@@ -865,18 +1113,19 @@ var MinimalRequest = class {
865
1113
  var MinimalContext = class {
866
1114
  req;
867
1115
  _resHeaders = {};
1116
+ _requestScope;
868
1117
  constructor(request, params, path, routePattern) {
869
1118
  this.req = new MinimalRequest(request, params, path, routePattern);
1119
+ this._requestScope = new RequestScopeManager();
870
1120
  }
871
1121
  // get req(): FastRequest {
872
1122
  // return this._req
873
1123
  // }
874
1124
  // Response helpers - merge custom headers with defaults
1125
+ // Optimized: use Object.assign instead of spread to avoid shallow copy overhead
875
1126
  getHeaders(contentType) {
876
- return {
877
- ...this._resHeaders,
878
- "Content-Type": contentType
879
- };
1127
+ const headers = Object.assign({ "Content-Type": contentType }, this._resHeaders);
1128
+ return headers;
880
1129
  }
881
1130
  json(data, status = 200) {
882
1131
  return new Response(JSON.stringify(data), {
@@ -950,6 +1199,25 @@ var MinimalContext = class {
950
1199
  }
951
1200
  set(_key, _value) {
952
1201
  }
1202
+ /**
1203
+ * Get the request-scoped service manager for this request.
1204
+ *
1205
+ * @returns The RequestScopeManager for this request.
1206
+ */
1207
+ requestScope() {
1208
+ return this._requestScope;
1209
+ }
1210
+ /**
1211
+ * Resolve a request-scoped service (convenience method).
1212
+ *
1213
+ * @template T - The service type.
1214
+ * @param key - The service key for caching.
1215
+ * @param factory - Factory function to create the service.
1216
+ * @returns The cached or newly created service instance.
1217
+ */
1218
+ scoped(key, factory) {
1219
+ return this._requestScope.resolve(key, factory);
1220
+ }
953
1221
  route = () => "";
954
1222
  get native() {
955
1223
  return this;
@@ -1067,18 +1335,40 @@ function compileMiddlewareChain(middleware, handler) {
1067
1335
  if (middleware.length === 0) {
1068
1336
  return handler;
1069
1337
  }
1338
+ if (middleware.length === 1) {
1339
+ const mw = middleware[0];
1340
+ return async (ctx) => {
1341
+ let nextCalled = false;
1342
+ const result = await mw(ctx, async () => {
1343
+ nextCalled = true;
1344
+ return void 0;
1345
+ });
1346
+ if (result instanceof Response) {
1347
+ return result;
1348
+ }
1349
+ if (nextCalled) {
1350
+ return await handler(ctx);
1351
+ }
1352
+ return ctx.json({ error: "Middleware did not call next or return response" }, 500);
1353
+ };
1354
+ }
1070
1355
  let compiled = handler;
1071
1356
  for (let i = middleware.length - 1; i >= 0; i--) {
1072
1357
  const mw = middleware[i];
1073
1358
  const nextHandler = compiled;
1074
1359
  compiled = async (ctx) => {
1075
- let nextResult;
1076
- const next = async () => {
1077
- nextResult = await nextHandler(ctx);
1078
- return nextResult;
1079
- };
1080
- const result = await mw(ctx, next);
1081
- return result ?? nextResult;
1360
+ let nextCalled = false;
1361
+ const result = await mw(ctx, async () => {
1362
+ nextCalled = true;
1363
+ return void 0;
1364
+ });
1365
+ if (result instanceof Response) {
1366
+ return result;
1367
+ }
1368
+ if (nextCalled) {
1369
+ return await nextHandler(ctx);
1370
+ }
1371
+ return ctx.json({ error: "Middleware did not call next or return response" }, 500);
1082
1372
  };
1083
1373
  }
1084
1374
  return compiled;
@@ -1095,8 +1385,6 @@ var Gravito = class {
1095
1385
  isPureStaticApp = true;
1096
1386
  // Cache for precompiled dynamic routes
1097
1387
  compiledDynamicRoutes = /* @__PURE__ */ new Map();
1098
- // Version tracking for cache invalidation
1099
- middlewareVersion = 0;
1100
1388
  /**
1101
1389
  * Create a new Gravito instance
1102
1390
  *
@@ -1184,7 +1472,6 @@ var Gravito = class {
1184
1472
  } else {
1185
1473
  this.router.use(pathOrMiddleware, ...middleware);
1186
1474
  }
1187
- this.middlewareVersion++;
1188
1475
  this.compileRoutes();
1189
1476
  return this;
1190
1477
  }
@@ -1277,6 +1564,11 @@ var Gravito = class {
1277
1564
  } catch (error) {
1278
1565
  return await this.handleError(error, ctx);
1279
1566
  } finally {
1567
+ try {
1568
+ await ctx.requestScope().cleanup();
1569
+ } catch (cleanupError) {
1570
+ console.error("RequestScope cleanup failed:", cleanupError);
1571
+ }
1280
1572
  this.contextPool.release(ctx);
1281
1573
  }
1282
1574
  }
@@ -1290,12 +1582,12 @@ var Gravito = class {
1290
1582
  }
1291
1583
  const cacheKey = `${method}:${match.routePattern ?? path}`;
1292
1584
  let entry = this.compiledDynamicRoutes.get(cacheKey);
1293
- if (!entry || entry.version !== this.middlewareVersion) {
1585
+ if (!entry || entry.version !== this.router.version) {
1294
1586
  const compiled = compileMiddlewareChain(match.middleware, match.handler);
1295
1587
  if (this.compiledDynamicRoutes.size > 1e3) {
1296
1588
  this.compiledDynamicRoutes.clear();
1297
1589
  }
1298
- entry = { compiled, version: this.middlewareVersion };
1590
+ entry = { compiled, version: this.router.version };
1299
1591
  this.compiledDynamicRoutes.set(cacheKey, entry);
1300
1592
  }
1301
1593
  const ctx = this.contextPool.acquire();
@@ -1306,6 +1598,11 @@ var Gravito = class {
1306
1598
  } catch (error) {
1307
1599
  return await this.handleError(error, ctx);
1308
1600
  } finally {
1601
+ try {
1602
+ await ctx.requestScope().cleanup();
1603
+ } catch (cleanupError) {
1604
+ console.error("RequestScope cleanup failed:", cleanupError);
1605
+ }
1309
1606
  this.contextPool.release(ctx);
1310
1607
  }
1311
1608
  };
@@ -1364,7 +1661,7 @@ var Gravito = class {
1364
1661
  const hasPathMiddleware = this.router.pathMiddleware.size > 0;
1365
1662
  this.isPureStaticApp = !hasGlobalMiddleware && !hasPathMiddleware;
1366
1663
  for (const [key, route] of this.staticRoutes) {
1367
- if (route.compiledVersion === this.middlewareVersion) {
1664
+ if (route.compiledVersion === this.router.version) {
1368
1665
  continue;
1369
1666
  }
1370
1667
  const analysis = analyzeHandler(route.handler);
@@ -1374,7 +1671,7 @@ var Gravito = class {
1374
1671
  const allMiddleware = this.collectMiddlewareForPath(key.split(":")[1], route.middleware);
1375
1672
  route.compiled = compileMiddlewareChain(allMiddleware, route.handler);
1376
1673
  }
1377
- route.compiledVersion = this.middlewareVersion;
1674
+ route.compiledVersion = this.router.version;
1378
1675
  }
1379
1676
  }
1380
1677
  /**