@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.
@@ -257,7 +257,14 @@ var AOTRouter = class {
257
257
  dynamicRoutePatterns = /* @__PURE__ */ new Map();
258
258
  middlewareCache = /* @__PURE__ */ new Map();
259
259
  cacheMaxSize = 1e3;
260
- version = 0;
260
+ _version = 0;
261
+ /**
262
+ * Get the current version for cache invalidation
263
+ * Incremented whenever middleware or routes are modified
264
+ */
265
+ get version() {
266
+ return this._version;
267
+ }
261
268
  /**
262
269
  * Register a route
263
270
  *
@@ -329,7 +336,7 @@ var AOTRouter = class {
329
336
  */
330
337
  use(...middleware) {
331
338
  this.globalMiddleware.push(...middleware);
332
- this.version++;
339
+ this._version++;
333
340
  }
334
341
  /**
335
342
  * Add path-based middleware
@@ -346,7 +353,7 @@ var AOTRouter = class {
346
353
  const existing = this.pathMiddleware.get(pattern) ?? [];
347
354
  this.pathMiddleware.set(pattern, [...existing, ...middleware]);
348
355
  }
349
- this.version++;
356
+ this._version++;
350
357
  }
351
358
  /**
352
359
  * Match a request to a route
@@ -408,9 +415,9 @@ var AOTRouter = class {
408
415
  if (this.globalMiddleware.length === 0 && this.pathMiddleware.size === 0 && routeMiddleware.length === 0) {
409
416
  return [];
410
417
  }
411
- const cacheKey = `${path}:${routeMiddleware.length}`;
418
+ const cacheKey = path;
412
419
  const cached = this.middlewareCache.get(cacheKey);
413
- if (cached !== void 0 && cached.version === this.version) {
420
+ if (cached !== void 0 && cached.version === this._version) {
414
421
  return cached.data;
415
422
  }
416
423
  const middleware = [];
@@ -431,7 +438,9 @@ var AOTRouter = class {
431
438
  middleware.push(...routeMiddleware);
432
439
  }
433
440
  if (this.middlewareCache.size < this.cacheMaxSize) {
434
- this.middlewareCache.set(cacheKey, { data: middleware, version: this.version });
441
+ this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
442
+ } else if (this.middlewareCache.has(cacheKey)) {
443
+ this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
435
444
  }
436
445
  return middleware;
437
446
  }
@@ -519,6 +528,172 @@ var HEADERS = {
519
528
  HTML: { "Content-Type": "text/html; charset=utf-8" }
520
529
  };
521
530
 
531
+ // src/Container/RequestScopeMetrics.ts
532
+ var RequestScopeMetrics = class {
533
+ cleanupStartTime = null;
534
+ cleanupDuration = null;
535
+ scopeSize = 0;
536
+ servicesCleaned = 0;
537
+ errorsOccurred = 0;
538
+ /**
539
+ * Record start of cleanup operation
540
+ */
541
+ recordCleanupStart() {
542
+ this.cleanupStartTime = performance.now();
543
+ }
544
+ /**
545
+ * Record end of cleanup operation
546
+ *
547
+ * @param scopeSize - Number of services in the scope
548
+ * @param servicesCleaned - Number of services that had cleanup called
549
+ * @param errorsOccurred - Number of cleanup errors
550
+ */
551
+ recordCleanupEnd(scopeSize, servicesCleaned, errorsOccurred = 0) {
552
+ if (this.cleanupStartTime !== null) {
553
+ this.cleanupDuration = performance.now() - this.cleanupStartTime;
554
+ this.cleanupStartTime = null;
555
+ }
556
+ this.scopeSize = scopeSize;
557
+ this.servicesCleaned = servicesCleaned;
558
+ this.errorsOccurred = errorsOccurred;
559
+ }
560
+ /**
561
+ * Get cleanup duration in milliseconds
562
+ *
563
+ * @returns Duration in ms, or null if cleanup not completed
564
+ */
565
+ getCleanupDuration() {
566
+ return this.cleanupDuration;
567
+ }
568
+ /**
569
+ * Check if cleanup took longer than threshold (default 2ms)
570
+ * Useful for detecting slow cleanups
571
+ *
572
+ * @param thresholdMs - Threshold in milliseconds
573
+ * @returns True if cleanup exceeded threshold
574
+ */
575
+ isSlowCleanup(thresholdMs = 2) {
576
+ if (this.cleanupDuration === null) return false;
577
+ return this.cleanupDuration > thresholdMs;
578
+ }
579
+ /**
580
+ * Export metrics as JSON for logging/monitoring
581
+ */
582
+ toJSON() {
583
+ return {
584
+ cleanupDuration: this.cleanupDuration,
585
+ scopeSize: this.scopeSize,
586
+ servicesCleaned: this.servicesCleaned,
587
+ errorsOccurred: this.errorsOccurred,
588
+ hasErrors: this.errorsOccurred > 0,
589
+ isSlowCleanup: this.isSlowCleanup()
590
+ };
591
+ }
592
+ /**
593
+ * Export metrics as compact string for logging
594
+ */
595
+ toString() {
596
+ const duration = this.cleanupDuration ?? "pending";
597
+ return `cleanup: ${duration}ms, scope: ${this.scopeSize}, cleaned: ${this.servicesCleaned}, errors: ${this.errorsOccurred}`;
598
+ }
599
+ };
600
+
601
+ // src/Container/RequestScopeManager.ts
602
+ var RequestScopeManager = class {
603
+ scoped = /* @__PURE__ */ new Map();
604
+ metadata = /* @__PURE__ */ new Map();
605
+ metrics = new RequestScopeMetrics();
606
+ observer = null;
607
+ constructor(observer) {
608
+ this.observer = observer || null;
609
+ }
610
+ /**
611
+ * Set observer for monitoring scope lifecycle
612
+ */
613
+ setObserver(observer) {
614
+ this.observer = observer;
615
+ }
616
+ /**
617
+ * Get metrics for this scope
618
+ */
619
+ getMetrics() {
620
+ return this.metrics;
621
+ }
622
+ /**
623
+ * Resolve or retrieve a request-scoped service instance.
624
+ *
625
+ * If the service already exists in this scope, returns the cached instance.
626
+ * Otherwise, calls the factory function to create a new instance and caches it.
627
+ *
628
+ * Automatically detects and records services with cleanup methods.
629
+ *
630
+ * @template T - The type of the service.
631
+ * @param key - The service key (for caching).
632
+ * @param factory - Factory function to create the instance if not cached.
633
+ * @returns The cached or newly created instance.
634
+ */
635
+ resolve(key, factory) {
636
+ const keyStr = String(key);
637
+ const isFromCache = this.scoped.has(keyStr);
638
+ if (!isFromCache) {
639
+ const instance = factory();
640
+ this.scoped.set(keyStr, instance);
641
+ if (instance && typeof instance === "object" && "cleanup" in instance) {
642
+ this.metadata.set(keyStr, { hasCleanup: true });
643
+ }
644
+ }
645
+ this.observer?.onServiceResolved?.(key, isFromCache);
646
+ return this.scoped.get(keyStr);
647
+ }
648
+ /**
649
+ * Clean up all request-scoped instances.
650
+ *
651
+ * Calls the cleanup() method on each service that has one.
652
+ * Silently ignores cleanup errors to prevent cascading failures.
653
+ * Called automatically by the Gravito engine in the request finally block.
654
+ *
655
+ * @returns Promise that resolves when all cleanup is complete.
656
+ */
657
+ async cleanup() {
658
+ this.metrics.recordCleanupStart();
659
+ this.observer?.onCleanupStart?.();
660
+ const errors = [];
661
+ let servicesCleaned = 0;
662
+ for (const [, instance] of this.scoped) {
663
+ if (instance && typeof instance === "object" && "cleanup" in instance) {
664
+ const fn = instance.cleanup;
665
+ if (typeof fn === "function") {
666
+ try {
667
+ await fn.call(instance);
668
+ servicesCleaned++;
669
+ } catch (error) {
670
+ errors.push(error);
671
+ this.observer?.onCleanupError?.(
672
+ error instanceof Error ? error : new Error(String(error))
673
+ );
674
+ }
675
+ }
676
+ }
677
+ }
678
+ const scopeSize = this.scoped.size;
679
+ this.scoped.clear();
680
+ this.metadata.clear();
681
+ this.metrics.recordCleanupEnd(scopeSize, servicesCleaned, errors.length);
682
+ this.observer?.onCleanupEnd?.(this.metrics);
683
+ if (errors.length > 0) {
684
+ console.error("RequestScope cleanup errors:", errors);
685
+ }
686
+ }
687
+ /**
688
+ * Get the number of services in this scope (for monitoring).
689
+ *
690
+ * @returns The count of cached services.
691
+ */
692
+ size() {
693
+ return this.scoped.size;
694
+ }
695
+ };
696
+
522
697
  // src/engine/FastContext.ts
523
698
  var FastRequestImpl = class {
524
699
  _request;
@@ -530,6 +705,11 @@ var FastRequestImpl = class {
530
705
  _headers = null;
531
706
  _cachedJson = void 0;
532
707
  _jsonParsed = false;
708
+ _cachedText = void 0;
709
+ _textParsed = false;
710
+ _cachedFormData = void 0;
711
+ _formDataParsed = false;
712
+ _cachedQueries = null;
533
713
  // Back-reference for release check optimization
534
714
  _ctx;
535
715
  constructor(ctx) {
@@ -548,6 +728,11 @@ var FastRequestImpl = class {
548
728
  this._headers = null;
549
729
  this._cachedJson = void 0;
550
730
  this._jsonParsed = false;
731
+ this._cachedText = void 0;
732
+ this._textParsed = false;
733
+ this._cachedFormData = void 0;
734
+ this._formDataParsed = false;
735
+ this._cachedQueries = null;
551
736
  return this;
552
737
  }
553
738
  /**
@@ -561,6 +746,11 @@ var FastRequestImpl = class {
561
746
  this._headers = null;
562
747
  this._cachedJson = void 0;
563
748
  this._jsonParsed = false;
749
+ this._cachedText = void 0;
750
+ this._textParsed = false;
751
+ this._cachedFormData = void 0;
752
+ this._formDataParsed = false;
753
+ this._cachedQueries = null;
564
754
  }
565
755
  checkReleased() {
566
756
  if (this._ctx._isReleased) {
@@ -608,6 +798,9 @@ var FastRequestImpl = class {
608
798
  }
609
799
  queries() {
610
800
  this.checkReleased();
801
+ if (this._cachedQueries !== null) {
802
+ return this._cachedQueries;
803
+ }
611
804
  if (!this._query) {
612
805
  this._query = this.getUrl().searchParams;
613
806
  }
@@ -622,6 +815,7 @@ var FastRequestImpl = class {
622
815
  result[key] = [existing, value];
623
816
  }
624
817
  }
818
+ this._cachedQueries = result;
625
819
  return result;
626
820
  }
627
821
  header(name) {
@@ -648,11 +842,19 @@ var FastRequestImpl = class {
648
842
  }
649
843
  async text() {
650
844
  this.checkReleased();
651
- return this._request.text();
845
+ if (!this._textParsed) {
846
+ this._cachedText = await this._request.text();
847
+ this._textParsed = true;
848
+ }
849
+ return this._cachedText;
652
850
  }
653
851
  async formData() {
654
852
  this.checkReleased();
655
- return this._request.formData();
853
+ if (!this._formDataParsed) {
854
+ this._cachedFormData = await this._request.formData();
855
+ this._formDataParsed = true;
856
+ }
857
+ return this._cachedFormData;
656
858
  }
657
859
  get raw() {
658
860
  this.checkReleased();
@@ -666,6 +868,8 @@ var FastContext = class {
666
868
  // Reuse this object
667
869
  _isReleased = false;
668
870
  // Made public for internal check access
871
+ _requestScope = null;
872
+ // Request-scoped services
669
873
  /**
670
874
  * Initialize context for a new request
671
875
  *
@@ -675,6 +879,7 @@ var FastContext = class {
675
879
  this._isReleased = false;
676
880
  this.req.init(request, params, path, routePattern);
677
881
  this._headers = new Headers();
882
+ this._requestScope = new RequestScopeManager();
678
883
  return this;
679
884
  }
680
885
  /**
@@ -800,6 +1005,32 @@ var FastContext = class {
800
1005
  this._store.set(key, value);
801
1006
  }
802
1007
  // ─────────────────────────────────────────────────────────────────────────
1008
+ // Request Scope Management
1009
+ // ─────────────────────────────────────────────────────────────────────────
1010
+ /**
1011
+ * Get the request-scoped service manager for this request.
1012
+ *
1013
+ * @returns The RequestScopeManager for this request.
1014
+ * @throws Error if called before init() or after reset().
1015
+ */
1016
+ requestScope() {
1017
+ if (!this._requestScope) {
1018
+ throw new Error("RequestScope not initialized. Call init() first.");
1019
+ }
1020
+ return this._requestScope;
1021
+ }
1022
+ /**
1023
+ * Resolve a request-scoped service (convenience method).
1024
+ *
1025
+ * @template T - The service type.
1026
+ * @param key - The service key for caching.
1027
+ * @param factory - Factory function to create the service.
1028
+ * @returns The cached or newly created service instance.
1029
+ */
1030
+ scoped(key, factory) {
1031
+ return this.requestScope().resolve(key, factory);
1032
+ }
1033
+ // ─────────────────────────────────────────────────────────────────────────
803
1034
  // Lifecycle helpers
804
1035
  // ─────────────────────────────────────────────────────────────────────────
805
1036
  route = () => "";
@@ -817,6 +1048,10 @@ var MinimalRequest = class {
817
1048
  this._routePattern = _routePattern;
818
1049
  }
819
1050
  _searchParams = null;
1051
+ _cachedQueries = null;
1052
+ _cachedJsonPromise = null;
1053
+ _cachedTextPromise = null;
1054
+ _cachedFormDataPromise = null;
820
1055
  get url() {
821
1056
  return this._request.url;
822
1057
  }
@@ -856,6 +1091,9 @@ var MinimalRequest = class {
856
1091
  return this.getSearchParams().get(name) ?? void 0;
857
1092
  }
858
1093
  queries() {
1094
+ if (this._cachedQueries !== null) {
1095
+ return this._cachedQueries;
1096
+ }
859
1097
  const params = this.getSearchParams();
860
1098
  const result = {};
861
1099
  for (const [key, value] of params.entries()) {
@@ -868,6 +1106,7 @@ var MinimalRequest = class {
868
1106
  result[key] = [existing, value];
869
1107
  }
870
1108
  }
1109
+ this._cachedQueries = result;
871
1110
  return result;
872
1111
  }
873
1112
  header(name) {
@@ -881,13 +1120,22 @@ var MinimalRequest = class {
881
1120
  return result;
882
1121
  }
883
1122
  async json() {
884
- return this._request.json();
1123
+ if (this._cachedJsonPromise === null) {
1124
+ this._cachedJsonPromise = this._request.json();
1125
+ }
1126
+ return this._cachedJsonPromise;
885
1127
  }
886
1128
  async text() {
887
- return this._request.text();
1129
+ if (this._cachedTextPromise === null) {
1130
+ this._cachedTextPromise = this._request.text();
1131
+ }
1132
+ return this._cachedTextPromise;
888
1133
  }
889
1134
  async formData() {
890
- return this._request.formData();
1135
+ if (this._cachedFormDataPromise === null) {
1136
+ this._cachedFormDataPromise = this._request.formData();
1137
+ }
1138
+ return this._cachedFormDataPromise;
891
1139
  }
892
1140
  get raw() {
893
1141
  return this._request;
@@ -896,18 +1144,19 @@ var MinimalRequest = class {
896
1144
  var MinimalContext = class {
897
1145
  req;
898
1146
  _resHeaders = {};
1147
+ _requestScope;
899
1148
  constructor(request, params, path, routePattern) {
900
1149
  this.req = new MinimalRequest(request, params, path, routePattern);
1150
+ this._requestScope = new RequestScopeManager();
901
1151
  }
902
1152
  // get req(): FastRequest {
903
1153
  // return this._req
904
1154
  // }
905
1155
  // Response helpers - merge custom headers with defaults
1156
+ // Optimized: use Object.assign instead of spread to avoid shallow copy overhead
906
1157
  getHeaders(contentType) {
907
- return {
908
- ...this._resHeaders,
909
- "Content-Type": contentType
910
- };
1158
+ const headers = Object.assign({ "Content-Type": contentType }, this._resHeaders);
1159
+ return headers;
911
1160
  }
912
1161
  json(data, status = 200) {
913
1162
  return new Response(JSON.stringify(data), {
@@ -981,6 +1230,25 @@ var MinimalContext = class {
981
1230
  }
982
1231
  set(_key, _value) {
983
1232
  }
1233
+ /**
1234
+ * Get the request-scoped service manager for this request.
1235
+ *
1236
+ * @returns The RequestScopeManager for this request.
1237
+ */
1238
+ requestScope() {
1239
+ return this._requestScope;
1240
+ }
1241
+ /**
1242
+ * Resolve a request-scoped service (convenience method).
1243
+ *
1244
+ * @template T - The service type.
1245
+ * @param key - The service key for caching.
1246
+ * @param factory - Factory function to create the service.
1247
+ * @returns The cached or newly created service instance.
1248
+ */
1249
+ scoped(key, factory) {
1250
+ return this._requestScope.resolve(key, factory);
1251
+ }
984
1252
  route = () => "";
985
1253
  get native() {
986
1254
  return this;
@@ -1098,18 +1366,40 @@ function compileMiddlewareChain(middleware, handler) {
1098
1366
  if (middleware.length === 0) {
1099
1367
  return handler;
1100
1368
  }
1369
+ if (middleware.length === 1) {
1370
+ const mw = middleware[0];
1371
+ return async (ctx) => {
1372
+ let nextCalled = false;
1373
+ const result = await mw(ctx, async () => {
1374
+ nextCalled = true;
1375
+ return void 0;
1376
+ });
1377
+ if (result instanceof Response) {
1378
+ return result;
1379
+ }
1380
+ if (nextCalled) {
1381
+ return await handler(ctx);
1382
+ }
1383
+ return ctx.json({ error: "Middleware did not call next or return response" }, 500);
1384
+ };
1385
+ }
1101
1386
  let compiled = handler;
1102
1387
  for (let i = middleware.length - 1; i >= 0; i--) {
1103
1388
  const mw = middleware[i];
1104
1389
  const nextHandler = compiled;
1105
1390
  compiled = async (ctx) => {
1106
- let nextResult;
1107
- const next = async () => {
1108
- nextResult = await nextHandler(ctx);
1109
- return nextResult;
1110
- };
1111
- const result = await mw(ctx, next);
1112
- return result ?? nextResult;
1391
+ let nextCalled = false;
1392
+ const result = await mw(ctx, async () => {
1393
+ nextCalled = true;
1394
+ return void 0;
1395
+ });
1396
+ if (result instanceof Response) {
1397
+ return result;
1398
+ }
1399
+ if (nextCalled) {
1400
+ return await nextHandler(ctx);
1401
+ }
1402
+ return ctx.json({ error: "Middleware did not call next or return response" }, 500);
1113
1403
  };
1114
1404
  }
1115
1405
  return compiled;
@@ -1126,8 +1416,6 @@ var Gravito = class {
1126
1416
  isPureStaticApp = true;
1127
1417
  // Cache for precompiled dynamic routes
1128
1418
  compiledDynamicRoutes = /* @__PURE__ */ new Map();
1129
- // Version tracking for cache invalidation
1130
- middlewareVersion = 0;
1131
1419
  /**
1132
1420
  * Create a new Gravito instance
1133
1421
  *
@@ -1215,7 +1503,6 @@ var Gravito = class {
1215
1503
  } else {
1216
1504
  this.router.use(pathOrMiddleware, ...middleware);
1217
1505
  }
1218
- this.middlewareVersion++;
1219
1506
  this.compileRoutes();
1220
1507
  return this;
1221
1508
  }
@@ -1308,6 +1595,11 @@ var Gravito = class {
1308
1595
  } catch (error) {
1309
1596
  return await this.handleError(error, ctx);
1310
1597
  } finally {
1598
+ try {
1599
+ await ctx.requestScope().cleanup();
1600
+ } catch (cleanupError) {
1601
+ console.error("RequestScope cleanup failed:", cleanupError);
1602
+ }
1311
1603
  this.contextPool.release(ctx);
1312
1604
  }
1313
1605
  }
@@ -1321,12 +1613,12 @@ var Gravito = class {
1321
1613
  }
1322
1614
  const cacheKey = `${method}:${match.routePattern ?? path}`;
1323
1615
  let entry = this.compiledDynamicRoutes.get(cacheKey);
1324
- if (!entry || entry.version !== this.middlewareVersion) {
1616
+ if (!entry || entry.version !== this.router.version) {
1325
1617
  const compiled = compileMiddlewareChain(match.middleware, match.handler);
1326
1618
  if (this.compiledDynamicRoutes.size > 1e3) {
1327
1619
  this.compiledDynamicRoutes.clear();
1328
1620
  }
1329
- entry = { compiled, version: this.middlewareVersion };
1621
+ entry = { compiled, version: this.router.version };
1330
1622
  this.compiledDynamicRoutes.set(cacheKey, entry);
1331
1623
  }
1332
1624
  const ctx = this.contextPool.acquire();
@@ -1337,6 +1629,11 @@ var Gravito = class {
1337
1629
  } catch (error) {
1338
1630
  return await this.handleError(error, ctx);
1339
1631
  } finally {
1632
+ try {
1633
+ await ctx.requestScope().cleanup();
1634
+ } catch (cleanupError) {
1635
+ console.error("RequestScope cleanup failed:", cleanupError);
1636
+ }
1340
1637
  this.contextPool.release(ctx);
1341
1638
  }
1342
1639
  };
@@ -1395,7 +1692,7 @@ var Gravito = class {
1395
1692
  const hasPathMiddleware = this.router.pathMiddleware.size > 0;
1396
1693
  this.isPureStaticApp = !hasGlobalMiddleware && !hasPathMiddleware;
1397
1694
  for (const [key, route] of this.staticRoutes) {
1398
- if (route.compiledVersion === this.middlewareVersion) {
1695
+ if (route.compiledVersion === this.router.version) {
1399
1696
  continue;
1400
1697
  }
1401
1698
  const analysis = analyzeHandler(route.handler);
@@ -1405,7 +1702,7 @@ var Gravito = class {
1405
1702
  const allMiddleware = this.collectMiddlewareForPath(key.split(":")[1], route.middleware);
1406
1703
  route.compiled = compileMiddlewareChain(allMiddleware, route.handler);
1407
1704
  }
1408
- route.compiledVersion = this.middlewareVersion;
1705
+ route.compiledVersion = this.router.version;
1409
1706
  }
1410
1707
  }
1411
1708
  /**