@fragno-dev/core 0.2.0 → 0.2.2

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.
Files changed (146) hide show
  1. package/.turbo/turbo-build.log +72 -62
  2. package/CHANGELOG.md +28 -0
  3. package/dist/api/api.d.ts +3 -2
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js +2 -1
  6. package/dist/api/api.js.map +1 -1
  7. package/dist/api/bind-services.d.ts +0 -1
  8. package/dist/api/bind-services.d.ts.map +1 -1
  9. package/dist/api/bind-services.js.map +1 -1
  10. package/dist/api/error.d.ts.map +1 -1
  11. package/dist/api/error.js.map +1 -1
  12. package/dist/api/fragment-definition-builder.d.ts +26 -44
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -22
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -37
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +74 -69
  19. package/dist/api/fragment-instantiator.js.map +1 -1
  20. package/dist/api/request-context-storage.d.ts +4 -0
  21. package/dist/api/request-context-storage.d.ts.map +1 -1
  22. package/dist/api/request-context-storage.js +6 -0
  23. package/dist/api/request-context-storage.js.map +1 -1
  24. package/dist/api/request-input-context.d.ts.map +1 -1
  25. package/dist/api/request-input-context.js.map +1 -1
  26. package/dist/api/request-middleware.d.ts +1 -1
  27. package/dist/api/request-middleware.d.ts.map +1 -1
  28. package/dist/api/request-middleware.js.map +1 -1
  29. package/dist/api/request-output-context.d.ts +1 -1
  30. package/dist/api/request-output-context.d.ts.map +1 -1
  31. package/dist/api/request-output-context.js.map +1 -1
  32. package/dist/api/route-caller.d.ts +30 -0
  33. package/dist/api/route-caller.d.ts.map +1 -0
  34. package/dist/api/route-caller.js +63 -0
  35. package/dist/api/route-caller.js.map +1 -0
  36. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  37. package/dist/api/route.d.ts +1 -1
  38. package/dist/api/route.d.ts.map +1 -1
  39. package/dist/api/route.js.map +1 -1
  40. package/dist/api/shared-types.d.ts.map +1 -1
  41. package/dist/client/client-error.d.ts.map +1 -1
  42. package/dist/client/client-error.js.map +1 -1
  43. package/dist/client/client.d.ts +91 -52
  44. package/dist/client/client.d.ts.map +1 -1
  45. package/dist/client/client.js +25 -9
  46. package/dist/client/client.js.map +1 -1
  47. package/dist/client/client.svelte.d.ts +6 -5
  48. package/dist/client/client.svelte.d.ts.map +1 -1
  49. package/dist/client/client.svelte.js +10 -2
  50. package/dist/client/client.svelte.js.map +1 -1
  51. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  52. package/dist/client/react.d.ts +5 -4
  53. package/dist/client/react.d.ts.map +1 -1
  54. package/dist/client/react.js +104 -12
  55. package/dist/client/react.js.map +1 -1
  56. package/dist/client/solid.d.ts +7 -5
  57. package/dist/client/solid.d.ts.map +1 -1
  58. package/dist/client/solid.js +23 -9
  59. package/dist/client/solid.js.map +1 -1
  60. package/dist/client/vanilla.d.ts +16 -4
  61. package/dist/client/vanilla.d.ts.map +1 -1
  62. package/dist/client/vanilla.js +21 -1
  63. package/dist/client/vanilla.js.map +1 -1
  64. package/dist/client/vue.d.ts +7 -5
  65. package/dist/client/vue.d.ts.map +1 -1
  66. package/dist/client/vue.js +18 -10
  67. package/dist/client/vue.js.map +1 -1
  68. package/dist/id.d.ts +2 -0
  69. package/dist/id.js +3 -0
  70. package/dist/internal/cuid.d.ts +16 -0
  71. package/dist/internal/cuid.d.ts.map +1 -0
  72. package/dist/internal/cuid.js +82 -0
  73. package/dist/internal/cuid.js.map +1 -0
  74. package/dist/mod-client.d.ts +5 -4
  75. package/dist/mod-client.d.ts.map +1 -1
  76. package/dist/mod-client.js +7 -5
  77. package/dist/mod-client.js.map +1 -1
  78. package/dist/mod.d.ts +6 -5
  79. package/dist/mod.js +2 -1
  80. package/dist/runtime.js +1 -1
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/test/test.d.ts +6 -6
  83. package/dist/test/test.d.ts.map +1 -1
  84. package/dist/test/test.js.map +1 -1
  85. package/dist/util/ssr.js.map +1 -1
  86. package/package.json +24 -40
  87. package/src/api/api.test.ts +3 -1
  88. package/src/api/api.ts +6 -0
  89. package/src/api/bind-services.ts +0 -5
  90. package/src/api/error.ts +1 -0
  91. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  92. package/src/api/fragment-definition-builder.test.ts +2 -1
  93. package/src/api/fragment-definition-builder.ts +49 -124
  94. package/src/api/fragment-instantiator.test.ts +92 -233
  95. package/src/api/fragment-instantiator.ts +228 -196
  96. package/src/api/fragment-services.test.ts +1 -0
  97. package/src/api/internal/path-runtime.test.ts +1 -0
  98. package/src/api/internal/path-type.test.ts +3 -1
  99. package/src/api/internal/route.test.ts +1 -0
  100. package/src/api/request-context-storage.ts +7 -0
  101. package/src/api/request-input-context.test.ts +4 -2
  102. package/src/api/request-input-context.ts +2 -1
  103. package/src/api/request-middleware.test.ts +9 -14
  104. package/src/api/request-middleware.ts +3 -2
  105. package/src/api/request-output-context.test.ts +3 -1
  106. package/src/api/request-output-context.ts +2 -1
  107. package/src/api/route-caller.test.ts +195 -0
  108. package/src/api/route-caller.ts +167 -0
  109. package/src/api/route-handler-input-options.ts +2 -1
  110. package/src/api/route.test.ts +4 -2
  111. package/src/api/route.ts +2 -1
  112. package/src/api/shared-types.ts +2 -1
  113. package/src/client/client-builder.test.ts +4 -2
  114. package/src/client/client-error.test.ts +2 -1
  115. package/src/client/client-error.ts +1 -1
  116. package/src/client/client-types.test.ts +19 -5
  117. package/src/client/client.ssr.test.ts +6 -4
  118. package/src/client/client.svelte.test.ts +18 -9
  119. package/src/client/client.svelte.ts +38 -13
  120. package/src/client/client.test.ts +49 -10
  121. package/src/client/client.ts +291 -141
  122. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  123. package/src/client/internal/ndjson-streaming.ts +1 -0
  124. package/src/client/react.test.ts +176 -6
  125. package/src/client/react.ts +226 -31
  126. package/src/client/solid.test.ts +29 -5
  127. package/src/client/solid.ts +60 -22
  128. package/src/client/vanilla.test.ts +148 -6
  129. package/src/client/vanilla.ts +63 -9
  130. package/src/client/vue.test.ts +223 -84
  131. package/src/client/vue.ts +57 -30
  132. package/src/id.ts +1 -0
  133. package/src/internal/cuid.test.ts +164 -0
  134. package/src/internal/cuid.ts +133 -0
  135. package/src/mod-client.ts +4 -2
  136. package/src/mod.ts +3 -2
  137. package/src/runtime.ts +1 -1
  138. package/src/test/test.test.ts +4 -2
  139. package/src/test/test.ts +7 -9
  140. package/src/util/async.test.ts +1 -0
  141. package/src/util/content-type.test.ts +1 -0
  142. package/src/util/nanostores.test.ts +3 -1
  143. package/src/util/ssr.ts +1 -0
  144. package/tsconfig.json +1 -1
  145. package/tsdown.config.ts +1 -0
  146. package/vitest.config.ts +2 -1
@@ -1,10 +1,26 @@
1
+ import { addRoute, createRouter, findRoute } from "rou3";
2
+
1
3
  import type { StandardSchemaV1 } from "@standard-schema/spec";
4
+
5
+ import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
6
+ import { instantiatedFragmentFakeSymbol } from "../internal/symbols";
7
+ import { recordTraceEvent } from "../internal/trace-context";
8
+ import type { InferOrUnknown } from "../util/types-util";
2
9
  import { type FragnoRouteConfig, type HTTPMethod, type RequestThisContext } from "./api";
10
+ import { bindServicesToContext, type BoundServices } from "./bind-services";
3
11
  import { FragnoApiError } from "./error";
12
+ import type { FragmentDefinition } from "./fragment-definition-builder";
13
+ import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
14
+ import type { ExtractPathParams } from "./internal/path";
4
15
  import { getMountRoute } from "./internal/route";
5
- import { addRoute, createRouter, findRoute } from "rou3";
16
+ import { MutableRequestState } from "./mutable-request-state";
17
+ import { RequestContextStorage } from "./request-context-storage";
6
18
  import { RequestInputContext, type RequestBodyType } from "./request-input-context";
7
- import type { ExtractPathParams } from "./internal/path";
19
+ import {
20
+ RequestMiddlewareInputContext,
21
+ RequestMiddlewareOutputContext,
22
+ type FragnoMiddlewareCallback,
23
+ } from "./request-middleware";
8
24
  import { RequestOutputContext } from "./request-output-context";
9
25
  import {
10
26
  type AnyFragnoRouteConfig,
@@ -12,26 +28,42 @@ import {
12
28
  type FlattenRouteFactories,
13
29
  resolveRouteFactories,
14
30
  } from "./route";
15
- import {
16
- RequestMiddlewareInputContext,
17
- RequestMiddlewareOutputContext,
18
- type FragnoMiddlewareCallback,
19
- } from "./request-middleware";
20
- import { MutableRequestState } from "./mutable-request-state";
21
31
  import type { RouteHandlerInputOptions } from "./route-handler-input-options";
22
- import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
23
- import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
24
- import type { InferOrUnknown } from "../util/types-util";
25
- import type { FragmentDefinition } from "./fragment-definition-builder";
26
32
  import type { FragnoPublicConfig } from "./shared-types";
27
- import { RequestContextStorage } from "./request-context-storage";
28
- import { bindServicesToContext, type BoundServices } from "./bind-services";
29
- import { instantiatedFragmentFakeSymbol } from "../internal/symbols";
30
- import { recordTraceEvent } from "../internal/trace-context";
31
33
 
32
34
  // Re-export types needed by consumers
33
35
  export type { BoundServices };
34
36
 
37
+ type CallRoutePath<TRoutes extends readonly AnyFragnoRouteConfig[], TMethod extends HTTPMethod> = [
38
+ ExtractRoutePath<TRoutes, TMethod>,
39
+ ] extends [never]
40
+ ? string
41
+ : ExtractRoutePath<TRoutes, TMethod>;
42
+
43
+ type CallRouteMatch<
44
+ TRoutes extends readonly AnyFragnoRouteConfig[],
45
+ TMethod extends HTTPMethod,
46
+ TPath extends string,
47
+ > = [ExtractRouteByPath<TRoutes, TPath, TMethod>] extends [never]
48
+ ? AnyFragnoRouteConfig
49
+ : ExtractRouteByPath<TRoutes, TPath, TMethod>;
50
+
51
+ const requestSourceSymbol = Symbol.for("fragno-request-source");
52
+ const requestRouteSymbol = Symbol.for("fragno-request-route");
53
+ const requestWaitUntilSymbol = Symbol.for("fragno-request-wait-until");
54
+
55
+ type RequestRouteInfo = {
56
+ method: HTTPMethod;
57
+ path: string;
58
+ mountRoute?: string;
59
+ fullPath?: string;
60
+ };
61
+ type RequestSource = "route" | "context";
62
+
63
+ export type FragnoRequestLifecycleContext = {
64
+ waitUntil?: (promise: Promise<unknown>) => void;
65
+ };
66
+
35
67
  type InternalRoutePrefix = "/_internal";
36
68
 
37
69
  type JoinInternalRoutePath<TPath extends string> = TPath extends "" | "/"
@@ -66,33 +98,27 @@ type PrefixInternalRoutes<TRoutes extends readonly AnyFragnoRouteConfig[]> =
66
98
  ? { [K in keyof TRoutesTuple]: PrefixInternalRoute<TRoutesTuple[K]> }
67
99
  : readonly AnyFragnoRouteConfig[];
68
100
 
69
- type ExtractRoutesFromFragment<T> =
70
- T extends FragnoInstantiatedFragment<
71
- infer TRoutes,
72
- infer _TDeps,
73
- infer _TServices,
74
- infer _TServiceThisContext,
75
- infer _THandlerThisContext,
76
- infer _TRequestStorage,
77
- infer _TOptions,
78
- infer _TLinkedFragments
79
- >
80
- ? TRoutes
81
- : never;
82
-
83
- type InternalLinkedRoutes<TLinkedFragments> =
84
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment>
85
- ? TLinkedFragments extends { _fragno_internal: infer TInternal }
86
- ? ExtractRoutesFromFragment<TInternal> extends readonly AnyFragnoRouteConfig[]
87
- ? PrefixInternalRoutes<ExtractRoutesFromFragment<TInternal>>
88
- : readonly []
89
- : readonly []
90
- : readonly [];
101
+ type InternalRoutesFromDefinition<TInternalRoutes extends readonly AnyRouteOrFactory[]> =
102
+ PrefixInternalRoutes<FlattenRouteFactories<TInternalRoutes>>;
91
103
 
92
104
  export type RoutesWithInternal<
93
105
  TRoutes extends readonly AnyFragnoRouteConfig[],
94
- TLinkedFragments,
95
- > = readonly [...TRoutes, ...InternalLinkedRoutes<TLinkedFragments>];
106
+ TInternalRoutes extends readonly AnyRouteOrFactory[],
107
+ > = readonly [...TRoutes, ...InternalRoutesFromDefinition<TInternalRoutes>];
108
+
109
+ type ExtractServiceCallResult<T> = T extends undefined
110
+ ? undefined
111
+ : T extends { _internal: { finalResult?: infer R } }
112
+ ? R
113
+ : Awaited<T>;
114
+
115
+ type ExtractServiceCallResults<T extends readonly unknown[]> = {
116
+ [K in keyof T]: ExtractServiceCallResult<T[K]>;
117
+ };
118
+
119
+ type ExtractServiceCallResultsOrSingle<T> = T extends readonly unknown[]
120
+ ? ExtractServiceCallResults<T>
121
+ : ExtractServiceCallResult<T>;
96
122
 
97
123
  /**
98
124
  * Helper type to extract the instantiated fragment type from a fragment definition.
@@ -119,17 +145,16 @@ export type InstantiatedFragmentFromDefinition<
119
145
  infer TServiceThisContext,
120
146
  infer THandlerThisContext,
121
147
  infer TRequestStorage,
122
- infer TLinkedFragments
148
+ infer TInternalRoutes
123
149
  >
124
150
  ? FragnoInstantiatedFragment<
125
- RoutesWithInternal<readonly AnyFragnoRouteConfig[], TLinkedFragments>,
151
+ RoutesWithInternal<readonly AnyFragnoRouteConfig[], TInternalRoutes>,
126
152
  TDeps,
127
153
  BoundServices<TBaseServices & TServices>,
128
154
  TServiceThisContext,
129
155
  THandlerThisContext,
130
156
  TRequestStorage,
131
- TOptions,
132
- TLinkedFragments
157
+ TOptions
133
158
  >
134
159
  : never;
135
160
 
@@ -217,24 +242,11 @@ export type AnyFragnoInstantiatedFragment = FragnoInstantiatedFragment<
217
242
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
243
  any,
219
244
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
- any,
221
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
245
  any
223
246
  >;
224
247
 
225
- const INTERNAL_LINKED_FRAGMENT_NAME = "_fragno_internal";
226
248
  const INTERNAL_ROUTE_PREFIX = "/_internal";
227
249
 
228
- type InternalLinkedRouteMeta = {
229
- fragment: AnyFragnoInstantiatedFragment;
230
- originalPath: string;
231
- routes: readonly AnyFragnoRouteConfig[];
232
- };
233
-
234
- type InternalLinkedRouteConfig = AnyFragnoRouteConfig & {
235
- __internal?: InternalLinkedRouteMeta;
236
- };
237
-
238
250
  function normalizeRoutePrefix(prefix: string): string {
239
251
  if (!prefix.startsWith("/")) {
240
252
  prefix = `/${prefix}`;
@@ -251,37 +263,6 @@ function joinRoutePath(prefix: string, path: string): string {
251
263
  return `${normalizedPrefix}${normalizedPath}`;
252
264
  }
253
265
 
254
- function collectLinkedFragmentRoutes(
255
- linkedFragments: Record<string, AnyFragnoInstantiatedFragment>,
256
- ): InternalLinkedRouteConfig[] {
257
- const linkedRoutes: InternalLinkedRouteConfig[] = [];
258
-
259
- for (const [name, fragment] of Object.entries(linkedFragments)) {
260
- if (name !== INTERNAL_LINKED_FRAGMENT_NAME) {
261
- continue;
262
- }
263
-
264
- const internalRoutes = (fragment.routes ?? []) as readonly AnyFragnoRouteConfig[];
265
- if (internalRoutes.length === 0) {
266
- continue;
267
- }
268
-
269
- for (const route of internalRoutes) {
270
- linkedRoutes.push({
271
- ...route,
272
- path: joinRoutePath(INTERNAL_ROUTE_PREFIX, route.path),
273
- __internal: {
274
- fragment,
275
- originalPath: route.path,
276
- routes: internalRoutes,
277
- },
278
- });
279
- }
280
- }
281
-
282
- return linkedRoutes;
283
- }
284
-
285
266
  export interface FragnoFragmentSharedConfig<
286
267
  TRoutes extends readonly FragnoRouteConfig<
287
268
  HTTPMethod,
@@ -308,9 +289,7 @@ export class FragnoInstantiatedFragment<
308
289
  THandlerThisContext extends RequestThisContext,
309
290
  TRequestStorage = {},
310
291
  TOptions extends FragnoPublicConfig = FragnoPublicConfig,
311
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment> = {},
312
- > implements IFragnoInstantiatedFragment
313
- {
292
+ > implements IFragnoInstantiatedFragment {
314
293
  readonly [instantiatedFragmentFakeSymbol] = instantiatedFragmentFakeSymbol;
315
294
 
316
295
  // Private fields
@@ -326,7 +305,6 @@ export class FragnoInstantiatedFragment<
326
305
  #contextStorage: RequestContextStorage<TRequestStorage>;
327
306
  #createRequestStorage?: () => TRequestStorage;
328
307
  #options: TOptions;
329
- #linkedFragments: TLinkedFragments;
330
308
  #internalData: Record<string, unknown>;
331
309
 
332
310
  constructor(params: {
@@ -340,7 +318,6 @@ export class FragnoInstantiatedFragment<
340
318
  storage: RequestContextStorage<TRequestStorage>;
341
319
  createRequestStorage?: () => TRequestStorage;
342
320
  options: TOptions;
343
- linkedFragments?: TLinkedFragments;
344
321
  internalData?: Record<string, unknown>;
345
322
  }) {
346
323
  this.#name = params.name;
@@ -353,7 +330,6 @@ export class FragnoInstantiatedFragment<
353
330
  this.#contextStorage = params.storage;
354
331
  this.#createRequestStorage = params.createRequestStorage;
355
332
  this.#options = params.options;
356
- this.#linkedFragments = params.linkedFragments ?? ({} as TLinkedFragments);
357
333
  this.#internalData = params.internalData ?? {};
358
334
 
359
335
  // Build router
@@ -402,7 +378,6 @@ export class FragnoInstantiatedFragment<
402
378
  return {
403
379
  deps: this.#deps,
404
380
  options: this.#options,
405
- linkedFragments: this.#linkedFragments,
406
381
  ...this.#internalData,
407
382
  };
408
383
  }
@@ -424,9 +399,24 @@ export class FragnoInstantiatedFragment<
424
399
  * This is a shared helper used by inContext(), handler(), and callRouteRaw().
425
400
  * @private
426
401
  */
427
- #withRequestStorage<T>(callback: () => T): T;
428
- #withRequestStorage<T>(callback: () => Promise<T>): Promise<T>;
429
- #withRequestStorage<T>(callback: () => T | Promise<T>): T | Promise<T> {
402
+ #withRequestStorage<T>(
403
+ callback: () => T,
404
+ source?: RequestSource,
405
+ routeInfo?: RequestRouteInfo,
406
+ lifecycleContext?: FragnoRequestLifecycleContext,
407
+ ): T;
408
+ #withRequestStorage<T>(
409
+ callback: () => Promise<T>,
410
+ source?: RequestSource,
411
+ routeInfo?: RequestRouteInfo,
412
+ lifecycleContext?: FragnoRequestLifecycleContext,
413
+ ): Promise<T>;
414
+ #withRequestStorage<T>(
415
+ callback: () => T | Promise<T>,
416
+ source: RequestSource = "context",
417
+ routeInfo?: RequestRouteInfo,
418
+ lifecycleContext?: FragnoRequestLifecycleContext,
419
+ ): T | Promise<T> {
430
420
  if (!this.#serviceThisContext && !this.#handlerThisContext) {
431
421
  // No request context configured - just run callback directly
432
422
  return callback();
@@ -436,6 +426,16 @@ export class FragnoInstantiatedFragment<
436
426
  const storageData = this.#createRequestStorage
437
427
  ? this.#createRequestStorage()
438
428
  : ({} as TRequestStorage);
429
+ if (storageData && typeof storageData === "object") {
430
+ const metadataTarget = storageData as Record<symbol, unknown>;
431
+ metadataTarget[requestSourceSymbol] = source;
432
+ if (routeInfo) {
433
+ metadataTarget[requestRouteSymbol] = routeInfo;
434
+ }
435
+ if (lifecycleContext?.waitUntil) {
436
+ metadataTarget[requestWaitUntilSymbol] = lifecycleContext.waitUntil;
437
+ }
438
+ }
439
439
  return this.#contextStorage.run(storageData, callback);
440
440
  }
441
441
 
@@ -463,9 +463,55 @@ export class FragnoInstantiatedFragment<
463
463
  // Always use handler context for inContext - it has full capabilities
464
464
  if (this.#handlerThisContext) {
465
465
  const boundCallback = callback.bind(this.#handlerThisContext);
466
- return this.#withRequestStorage(boundCallback);
466
+ return this.#withRequestStorage(boundCallback, "context");
467
+ }
468
+ return this.#withRequestStorage(callback, "context");
469
+ }
470
+
471
+ /**
472
+ * Execute multiple service calls within a handler context.
473
+ * If called outside a request context, it will create one automatically.
474
+ * Pass a factory so service calls are created inside the active context.
475
+ * Primarily used by database fragments (handlerTx).
476
+ */
477
+ async callServices<TServiceCalls>(
478
+ serviceCalls: () => TServiceCalls,
479
+ ): Promise<ExtractServiceCallResultsOrSingle<TServiceCalls>> {
480
+ const handlerContext = this.#handlerThisContext as
481
+ | {
482
+ handlerTx?: () => {
483
+ withServiceCalls: (fn: () => readonly unknown[]) => { execute: () => Promise<unknown> };
484
+ };
485
+ }
486
+ | undefined;
487
+
488
+ if (!handlerContext?.handlerTx) {
489
+ throw new Error(
490
+ "callServices is only supported for fragments with handlerTx (database fragments).",
491
+ );
492
+ }
493
+
494
+ let callWasArray = false;
495
+ const execute = () => {
496
+ return handlerContext.handlerTx!()
497
+ .withServiceCalls(() => {
498
+ const calls = serviceCalls();
499
+ callWasArray = Array.isArray(calls);
500
+ return (callWasArray ? calls : [calls]) as readonly unknown[];
501
+ })
502
+ .execute();
503
+ };
504
+
505
+ const result = this.#contextStorage.hasStore()
506
+ ? await execute()
507
+ : await this.#withRequestStorage(execute, "context");
508
+
509
+ if (callWasArray) {
510
+ return result as ExtractServiceCallResultsOrSingle<TServiceCalls>;
467
511
  }
468
- return this.#withRequestStorage(callback);
512
+
513
+ const [first] = result as unknown[];
514
+ return first as ExtractServiceCallResultsOrSingle<TServiceCalls>;
469
515
  }
470
516
 
471
517
  /**
@@ -534,7 +580,7 @@ export class FragnoInstantiatedFragment<
534
580
  * Main request handler for this fragment.
535
581
  * Handles routing, middleware, and error handling.
536
582
  */
537
- async handler(req: Request): Promise<Response> {
583
+ async handler(req: Request, lifecycleContext?: FragnoRequestLifecycleContext): Promise<Response> {
538
584
  const url = new URL(req.url);
539
585
  const pathname = url.pathname;
540
586
 
@@ -565,7 +611,7 @@ export class FragnoInstantiatedFragment<
565
611
  }
566
612
 
567
613
  // Get the expected content type from route config (default: application/json)
568
- const routeConfig = route.data as InternalLinkedRouteConfig;
614
+ const routeConfig = route.data as AnyFragnoRouteConfig;
569
615
  const expectedContentType = routeConfig.contentType ?? "application/json";
570
616
 
571
617
  // Parse request body based on route's expected content type
@@ -653,6 +699,17 @@ export class FragnoInstantiatedFragment<
653
699
  headers: new Headers(req.headers),
654
700
  });
655
701
 
702
+ const fullRoutePath =
703
+ this.#mountRoute && this.#mountRoute !== "/"
704
+ ? `${this.#mountRoute}${routeConfig.path}`
705
+ : routeConfig.path;
706
+ const routeInfo: RequestRouteInfo = {
707
+ method: routeConfig.method,
708
+ path: routeConfig.path,
709
+ mountRoute: this.#mountRoute,
710
+ fullPath: fullRoutePath,
711
+ };
712
+
656
713
  // Execute middleware and handler
657
714
  const executeRequest = async (): Promise<Response> => {
658
715
  // Parent middleware execution
@@ -661,46 +718,28 @@ export class FragnoInstantiatedFragment<
661
718
  return middlewareResult;
662
719
  }
663
720
 
664
- // Internal fragment middleware execution (if linked)
665
- const internalMeta = routeConfig.__internal;
666
- if (internalMeta) {
667
- const internalResult = await FragnoInstantiatedFragment.#runMiddlewareForFragment(
668
- internalMeta.fragment as AnyFragnoInstantiatedFragment,
669
- {
670
- req,
671
- method: routeConfig.method,
672
- path: internalMeta.originalPath,
673
- requestState,
674
- routes: internalMeta.routes,
675
- },
676
- );
677
- if (internalResult !== undefined) {
678
- return internalResult;
679
- }
680
- }
681
-
682
721
  // Handler execution
683
722
  return this.#executeHandler(req, route, requestState, rawBody);
684
723
  };
685
724
 
686
725
  // Wrap with request storage context if provided
687
- return this.#withRequestStorage(executeRequest);
726
+ return this.#withRequestStorage(executeRequest, "route", routeInfo, lifecycleContext);
688
727
  }
689
728
 
690
729
  /**
691
730
  * Call a route directly with typed inputs and outputs.
692
731
  * Useful for testing and server-side route calls.
693
732
  */
694
- async callRoute<TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
733
+ async callRoute<TMethod extends HTTPMethod, TPath extends CallRoutePath<TRoutes, TMethod>>(
695
734
  method: TMethod,
696
735
  path: TPath,
697
736
  inputOptions?: RouteHandlerInputOptions<
698
737
  TPath,
699
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
738
+ CallRouteMatch<TRoutes, TMethod, TPath>["inputSchema"]
700
739
  >,
701
740
  ): Promise<
702
741
  FragnoResponse<
703
- InferOrUnknown<NonNullable<ExtractRouteByPath<TRoutes, TPath, TMethod>["outputSchema"]>>
742
+ InferOrUnknown<NonNullable<CallRouteMatch<TRoutes, TMethod, TPath>["outputSchema"]>>
704
743
  >
705
744
  > {
706
745
  const response = await this.callRouteRaw(method, path, inputOptions);
@@ -711,12 +750,12 @@ export class FragnoInstantiatedFragment<
711
750
  * Call a route directly and get the raw Response object.
712
751
  * Useful for testing and server-side route calls.
713
752
  */
714
- async callRouteRaw<TMethod extends HTTPMethod, TPath extends ExtractRoutePath<TRoutes, TMethod>>(
753
+ async callRouteRaw<TMethod extends HTTPMethod, TPath extends CallRoutePath<TRoutes, TMethod>>(
715
754
  method: TMethod,
716
755
  path: TPath,
717
756
  inputOptions?: RouteHandlerInputOptions<
718
757
  TPath,
719
- ExtractRouteByPath<TRoutes, TPath, TMethod>["inputSchema"]
758
+ CallRouteMatch<TRoutes, TMethod, TPath>["inputSchema"]
720
759
  >,
721
760
  ): Promise<Response> {
722
761
  // Find route in this.#routes
@@ -732,7 +771,8 @@ export class FragnoInstantiatedFragment<
732
771
  );
733
772
  }
734
773
 
735
- const { pathParams = {}, body, query, headers } = inputOptions || {};
774
+ const { pathParams = {}, query, headers } = inputOptions || {};
775
+ const body = inputOptions && "body" in inputOptions ? inputOptions.body : undefined;
736
776
 
737
777
  // Convert query to URLSearchParams if needed
738
778
  const searchParams =
@@ -770,6 +810,17 @@ export class FragnoInstantiatedFragment<
770
810
  // Construct RequestOutputContext
771
811
  const outputContext = new RequestOutputContext(route.outputSchema);
772
812
 
813
+ const fullRoutePath =
814
+ this.#mountRoute && this.#mountRoute !== "/"
815
+ ? `${this.#mountRoute}${route.path}`
816
+ : route.path;
817
+ const routeInfo: RequestRouteInfo = {
818
+ method: route.method,
819
+ path: route.path,
820
+ mountRoute: this.#mountRoute,
821
+ fullPath: fullRoutePath,
822
+ };
823
+
773
824
  // Execute handler
774
825
  const executeHandler = async (): Promise<Response> => {
775
826
  try {
@@ -791,7 +842,7 @@ export class FragnoInstantiatedFragment<
791
842
  };
792
843
 
793
844
  // Wrap with request storage context if provided
794
- return this.#withRequestStorage(executeHandler);
845
+ return this.#withRequestStorage(executeHandler, "route", routeInfo);
795
846
  }
796
847
 
797
848
  /**
@@ -970,7 +1021,7 @@ export function instantiateFragment<
970
1021
  const THandlerThisContext extends RequestThisContext,
971
1022
  const TRequestStorage,
972
1023
  const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
973
- const TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment>,
1024
+ const TInternalRoutes extends readonly AnyRouteOrFactory[],
974
1025
  >(
975
1026
  definition: FragmentDefinition<
976
1027
  TConfig,
@@ -983,7 +1034,7 @@ export function instantiateFragment<
983
1034
  TServiceThisContext,
984
1035
  THandlerThisContext,
985
1036
  TRequestStorage,
986
- TLinkedFragments
1037
+ TInternalRoutes
987
1038
  >,
988
1039
  config: TConfig,
989
1040
  routesOrFactories: TRoutesOrFactories,
@@ -991,14 +1042,13 @@ export function instantiateFragment<
991
1042
  serviceImplementations?: TServiceDependencies,
992
1043
  instantiationOptions?: InstantiationOptions,
993
1044
  ): FragnoInstantiatedFragment<
994
- RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TLinkedFragments>,
1045
+ RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TInternalRoutes>,
995
1046
  TDeps,
996
1047
  BoundServices<TBaseServices & TServices>,
997
1048
  TServiceThisContext,
998
1049
  THandlerThisContext,
999
1050
  TRequestStorage,
1000
- TOptions,
1001
- TLinkedFragments
1051
+ TOptions
1002
1052
  > {
1003
1053
  const { dryRun = false } = instantiationOptions ?? {};
1004
1054
 
@@ -1033,36 +1083,18 @@ export function instantiateFragment<
1033
1083
  }
1034
1084
  }
1035
1085
 
1036
- // 3. Instantiate linked fragments FIRST (before any services)
1037
- // Their services will be merged into private services
1038
- const linkedFragmentInstances = {} as TLinkedFragments;
1039
- const linkedFragmentServices: Record<string, unknown> = {};
1040
-
1041
- if (definition.linkedFragments) {
1042
- for (const [name, callback] of Object.entries(definition.linkedFragments)) {
1043
- const linkedFragment = callback({
1044
- config,
1045
- options,
1046
- serviceDependencies: serviceImplementations,
1047
- });
1048
- (linkedFragmentInstances as Record<string, AnyFragnoInstantiatedFragment>)[name] =
1049
- linkedFragment;
1050
-
1051
- // Merge all services from linked fragment into private services directly by their service name
1052
- const services = linkedFragment.services as Record<string, unknown>;
1053
- for (const [serviceName, service] of Object.entries(services)) {
1054
- linkedFragmentServices[serviceName] = service;
1055
- }
1056
- }
1057
- }
1086
+ // 3. Calculate mount route early so internal routes can reference it
1087
+ const mountRoute = getMountRoute({
1088
+ name: definition.name,
1089
+ mountRoute: options.mountRoute,
1090
+ });
1058
1091
 
1059
- // Identity function for service definition (used to set 'this' context)
1092
+ // 4. Identity function for service definition (used to set 'this' context)
1060
1093
  const defineService = <T>(services: T & ThisType<TServiceThisContext>): T => services;
1061
1094
 
1062
- // 4. Call privateServices factories
1095
+ // 5. Call privateServices factories
1063
1096
  // Private services are instantiated in order, so earlier ones are available to later ones
1064
- // Start with linked fragment services, then add explicitly defined private services
1065
- const privateServices = { ...linkedFragmentServices } as TPrivateServices;
1097
+ const privateServices = {} as TPrivateServices;
1066
1098
  if (definition.privateServices) {
1067
1099
  for (const [serviceName, factory] of Object.entries(definition.privateServices)) {
1068
1100
  const serviceFactory = factory as (context: {
@@ -1097,7 +1129,7 @@ export function instantiateFragment<
1097
1129
  }
1098
1130
  }
1099
1131
 
1100
- // 5. Call baseServices callback (with access to private services including linked fragment services)
1132
+ // 6. Call baseServices callback (with access to private services)
1101
1133
  let baseServices: TBaseServices;
1102
1134
  try {
1103
1135
  baseServices =
@@ -1121,7 +1153,7 @@ export function instantiateFragment<
1121
1153
  }
1122
1154
  }
1123
1155
 
1124
- // 6. Call namedServices factories (with access to private services including linked fragment services)
1156
+ // 7. Call namedServices factories (with access to private services)
1125
1157
  const namedServices = {} as TServices;
1126
1158
  if (definition.namedServices) {
1127
1159
  for (const [serviceName, factory] of Object.entries(definition.namedServices)) {
@@ -1157,13 +1189,13 @@ export function instantiateFragment<
1157
1189
  }
1158
1190
  }
1159
1191
 
1160
- // 7. Merge public services (NOT including private services)
1192
+ // 8. Merge public services (NOT including private services)
1161
1193
  const services = {
1162
1194
  ...baseServices,
1163
1195
  ...namedServices,
1164
1196
  };
1165
1197
 
1166
- // 8. Create request context storage and both service & handler contexts
1198
+ // 9. Create request context storage and both service & handler contexts
1167
1199
  // Use external storage if provided, otherwise create new storage
1168
1200
  const storage = definition.getExternalStorage
1169
1201
  ? definition.getExternalStorage({ config, options, deps })
@@ -1179,19 +1211,20 @@ export function instantiateFragment<
1179
1211
 
1180
1212
  const serviceContext = contexts?.serviceContext;
1181
1213
  const handlerContext = contexts?.handlerContext;
1214
+
1215
+ // 10. Bind services to serviceContext (restricted)
1216
+ // Services get the restricted context (for database fragments, this excludes execute methods)
1217
+ const boundServices = serviceContext ? bindServicesToContext(services, serviceContext) : services;
1182
1218
  const internalData =
1183
1219
  definition.internalDataFactory?.({
1184
1220
  config,
1185
1221
  options,
1186
1222
  deps,
1187
- linkedFragments: linkedFragmentInstances,
1223
+ services: boundServices as BoundServices<TBaseServices & TServices>,
1224
+ serviceDeps: (serviceImplementations ?? {}) as TServiceDependencies,
1188
1225
  }) ?? {};
1189
1226
 
1190
- // 9. Bind services to serviceContext (restricted)
1191
- // Services get the restricted context (for database fragments, this excludes execute methods)
1192
- const boundServices = serviceContext ? bindServicesToContext(services, serviceContext) : services;
1193
-
1194
- // 10. Resolve routes with bound services
1227
+ // 11. Resolve routes with bound services
1195
1228
  const context = {
1196
1229
  config,
1197
1230
  deps,
@@ -1199,17 +1232,17 @@ export function instantiateFragment<
1199
1232
  serviceDeps: serviceImplementations ?? ({} as TServiceDependencies),
1200
1233
  };
1201
1234
  const routes = resolveRouteFactories(context, routesOrFactories) as AnyFragnoRouteConfig[];
1202
- const linkedRoutes = collectLinkedFragmentRoutes(
1203
- linkedFragmentInstances as Record<string, AnyFragnoInstantiatedFragment>,
1204
- );
1235
+ const internalRoutes = definition.internalRoutes
1236
+ ? (resolveRouteFactories(context, definition.internalRoutes) as readonly AnyFragnoRouteConfig[])
1237
+ : [];
1238
+ const prefixedInternalRoutes = internalRoutes.map((route) => ({
1239
+ ...route,
1240
+ path: joinRoutePath(INTERNAL_ROUTE_PREFIX, route.path),
1241
+ }));
1205
1242
  const finalRoutes =
1206
- linkedRoutes.length > 0 ? [...routes, ...linkedRoutes] : (routes as AnyFragnoRouteConfig[]);
1207
-
1208
- // 11. Calculate mount route
1209
- const mountRoute = getMountRoute({
1210
- name: definition.name,
1211
- mountRoute: options.mountRoute,
1212
- });
1243
+ prefixedInternalRoutes.length > 0
1244
+ ? [...routes, ...prefixedInternalRoutes]
1245
+ : (routes as AnyFragnoRouteConfig[]);
1213
1246
 
1214
1247
  // 12. Wrap createRequestStorage to capture context
1215
1248
  const createRequestStorageWithContext = definition.createRequestStorage
@@ -1223,7 +1256,7 @@ export function instantiateFragment<
1223
1256
  name: definition.name,
1224
1257
  routes: finalRoutes as unknown as RoutesWithInternal<
1225
1258
  FlattenRouteFactories<TRoutesOrFactories>,
1226
- TLinkedFragments
1259
+ TInternalRoutes
1227
1260
  >,
1228
1261
  deps,
1229
1262
  services: boundServices as BoundServices<TBaseServices & TServices>,
@@ -1233,7 +1266,6 @@ export function instantiateFragment<
1233
1266
  storage,
1234
1267
  createRequestStorage: createRequestStorageWithContext,
1235
1268
  options,
1236
- linkedFragments: linkedFragmentInstances,
1237
1269
  internalData: internalData as Record<string, unknown>,
1238
1270
  });
1239
1271
  }
@@ -1310,7 +1342,6 @@ interface IFragnoInstantiatedFragment {
1310
1342
  get $internal(): {
1311
1343
  deps: unknown;
1312
1344
  options: unknown;
1313
- linkedFragments: unknown;
1314
1345
  } & Record<string, unknown>;
1315
1346
 
1316
1347
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1321,10 +1352,13 @@ interface IFragnoInstantiatedFragment {
1321
1352
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1322
1353
  inContext<T>(callback: any): Promise<T>;
1323
1354
 
1355
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1356
+ callServices(serviceCalls: () => any): Promise<any>;
1357
+
1324
1358
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1325
1359
  handlersFor(framework: FullstackFrameworks): any;
1326
1360
 
1327
- handler(req: Request): Promise<Response>;
1361
+ handler(req: Request, lifecycleContext?: FragnoRequestLifecycleContext): Promise<Response>;
1328
1362
 
1329
1363
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1330
1364
  callRoute(method: HTTPMethod, path: string, inputOptions?: any): Promise<any>;
@@ -1349,9 +1383,8 @@ export class FragmentInstantiationBuilder<
1349
1383
  THandlerThisContext extends RequestThisContext,
1350
1384
  TRequestStorage,
1351
1385
  TRoutesOrFactories extends readonly AnyRouteOrFactory[],
1352
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment>,
1353
- > implements IFragmentInstantiationBuilder
1354
- {
1386
+ TInternalRoutes extends readonly AnyRouteOrFactory[],
1387
+ > implements IFragmentInstantiationBuilder {
1355
1388
  #definition: FragmentDefinition<
1356
1389
  TConfig,
1357
1390
  TOptions,
@@ -1363,7 +1396,7 @@ export class FragmentInstantiationBuilder<
1363
1396
  TServiceThisContext,
1364
1397
  THandlerThisContext,
1365
1398
  TRequestStorage,
1366
- TLinkedFragments
1399
+ TInternalRoutes
1367
1400
  >;
1368
1401
  #config?: TConfig;
1369
1402
  #routes?: TRoutesOrFactories;
@@ -1382,7 +1415,7 @@ export class FragmentInstantiationBuilder<
1382
1415
  TServiceThisContext,
1383
1416
  THandlerThisContext,
1384
1417
  TRequestStorage,
1385
- TLinkedFragments
1418
+ TInternalRoutes
1386
1419
  >,
1387
1420
  routes?: TRoutesOrFactories,
1388
1421
  ) {
@@ -1404,7 +1437,7 @@ export class FragmentInstantiationBuilder<
1404
1437
  TServiceThisContext,
1405
1438
  THandlerThisContext,
1406
1439
  TRequestStorage,
1407
- TLinkedFragments
1440
+ TInternalRoutes
1408
1441
  > {
1409
1442
  return this.#definition;
1410
1443
  }
@@ -1455,7 +1488,7 @@ export class FragmentInstantiationBuilder<
1455
1488
  THandlerThisContext,
1456
1489
  TRequestStorage,
1457
1490
  TNewRoutes,
1458
- TLinkedFragments
1491
+ TInternalRoutes
1459
1492
  > {
1460
1493
  const newBuilder = new FragmentInstantiationBuilder(this.#definition, routes);
1461
1494
  // Preserve config, options, and services from the current instance
@@ -1485,14 +1518,13 @@ export class FragmentInstantiationBuilder<
1485
1518
  * Build and return the instantiated fragment
1486
1519
  */
1487
1520
  build(): FragnoInstantiatedFragment<
1488
- RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TLinkedFragments>,
1521
+ RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TInternalRoutes>,
1489
1522
  TDeps,
1490
1523
  BoundServices<TBaseServices & TServices>,
1491
1524
  TServiceThisContext,
1492
1525
  THandlerThisContext,
1493
1526
  TRequestStorage,
1494
- TOptions,
1495
- TLinkedFragments
1527
+ TOptions
1496
1528
  > {
1497
1529
  // This variable is set by the frango-cli when extracting database schemas
1498
1530
  const dryRun = process.env["FRAGNO_INIT_DRY_RUN"] === "true";
@@ -1531,7 +1563,7 @@ export function instantiate<
1531
1563
  TServiceThisContext extends RequestThisContext,
1532
1564
  THandlerThisContext extends RequestThisContext,
1533
1565
  TRequestStorage,
1534
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment>,
1566
+ TInternalRoutes extends readonly AnyRouteOrFactory[],
1535
1567
  >(
1536
1568
  definition: FragmentDefinition<
1537
1569
  TConfig,
@@ -1544,7 +1576,7 @@ export function instantiate<
1544
1576
  TServiceThisContext,
1545
1577
  THandlerThisContext,
1546
1578
  TRequestStorage,
1547
- TLinkedFragments
1579
+ TInternalRoutes
1548
1580
  >,
1549
1581
  ): FragmentInstantiationBuilder<
1550
1582
  TConfig,
@@ -1558,7 +1590,7 @@ export function instantiate<
1558
1590
  THandlerThisContext,
1559
1591
  TRequestStorage,
1560
1592
  readonly [],
1561
- TLinkedFragments
1593
+ TInternalRoutes
1562
1594
  > {
1563
1595
  return new FragmentInstantiationBuilder(definition);
1564
1596
  }