@fragno-dev/core 0.1.11 → 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 (155) hide show
  1. package/.turbo/turbo-build.log +87 -69
  2. package/CHANGELOG.md +79 -0
  3. package/dist/api/api.d.ts +21 -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 +32 -40
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -21
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -30
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +201 -52
  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 +57 -1
  25. package/dist/api/request-input-context.d.ts.map +1 -1
  26. package/dist/api/request-input-context.js +67 -0
  27. package/dist/api/request-input-context.js.map +1 -1
  28. package/dist/api/request-middleware.d.ts +2 -2
  29. package/dist/api/request-middleware.d.ts.map +1 -1
  30. package/dist/api/request-middleware.js.map +1 -1
  31. package/dist/api/request-output-context.d.ts +1 -1
  32. package/dist/api/request-output-context.d.ts.map +1 -1
  33. package/dist/api/request-output-context.js.map +1 -1
  34. package/dist/api/route-caller.d.ts +30 -0
  35. package/dist/api/route-caller.d.ts.map +1 -0
  36. package/dist/api/route-caller.js +63 -0
  37. package/dist/api/route-caller.js.map +1 -0
  38. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  39. package/dist/api/route.d.ts +8 -8
  40. package/dist/api/route.d.ts.map +1 -1
  41. package/dist/api/route.js.map +1 -1
  42. package/dist/api/shared-types.d.ts.map +1 -1
  43. package/dist/client/client-error.d.ts.map +1 -1
  44. package/dist/client/client-error.js.map +1 -1
  45. package/dist/client/client.d.ts +90 -50
  46. package/dist/client/client.d.ts.map +1 -1
  47. package/dist/client/client.js +128 -16
  48. package/dist/client/client.js.map +1 -1
  49. package/dist/client/client.svelte.d.ts +6 -5
  50. package/dist/client/client.svelte.d.ts.map +1 -1
  51. package/dist/client/client.svelte.js +10 -2
  52. package/dist/client/client.svelte.js.map +1 -1
  53. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  54. package/dist/client/react.d.ts +5 -4
  55. package/dist/client/react.d.ts.map +1 -1
  56. package/dist/client/react.js +104 -12
  57. package/dist/client/react.js.map +1 -1
  58. package/dist/client/solid.d.ts +7 -5
  59. package/dist/client/solid.d.ts.map +1 -1
  60. package/dist/client/solid.js +23 -9
  61. package/dist/client/solid.js.map +1 -1
  62. package/dist/client/vanilla.d.ts +16 -4
  63. package/dist/client/vanilla.d.ts.map +1 -1
  64. package/dist/client/vanilla.js +21 -1
  65. package/dist/client/vanilla.js.map +1 -1
  66. package/dist/client/vue.d.ts +10 -4
  67. package/dist/client/vue.d.ts.map +1 -1
  68. package/dist/client/vue.js +24 -1
  69. package/dist/client/vue.js.map +1 -1
  70. package/dist/id.d.ts +2 -0
  71. package/dist/id.js +3 -0
  72. package/dist/internal/cuid.d.ts +16 -0
  73. package/dist/internal/cuid.d.ts.map +1 -0
  74. package/dist/internal/cuid.js +82 -0
  75. package/dist/internal/cuid.js.map +1 -0
  76. package/dist/internal/trace-context.d.ts +23 -0
  77. package/dist/internal/trace-context.d.ts.map +1 -0
  78. package/dist/internal/trace-context.js +14 -0
  79. package/dist/internal/trace-context.js.map +1 -0
  80. package/dist/mod-client.d.ts +7 -20
  81. package/dist/mod-client.d.ts.map +1 -1
  82. package/dist/mod-client.js +25 -13
  83. package/dist/mod-client.js.map +1 -1
  84. package/dist/mod.d.ts +8 -6
  85. package/dist/mod.js +3 -1
  86. package/dist/runtime.d.ts +15 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +33 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/test/test.d.ts +6 -6
  91. package/dist/test/test.d.ts.map +1 -1
  92. package/dist/test/test.js.map +1 -1
  93. package/dist/util/ssr.js.map +1 -1
  94. package/package.json +42 -52
  95. package/src/api/api.test.ts +3 -1
  96. package/src/api/api.ts +28 -0
  97. package/src/api/bind-services.ts +0 -5
  98. package/src/api/error.ts +1 -0
  99. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  100. package/src/api/fragment-definition-builder.test.ts +2 -1
  101. package/src/api/fragment-definition-builder.ts +56 -112
  102. package/src/api/fragment-instantiator.test.ts +311 -166
  103. package/src/api/fragment-instantiator.ts +470 -131
  104. package/src/api/fragment-services.test.ts +1 -0
  105. package/src/api/internal/path-runtime.test.ts +8 -0
  106. package/src/api/internal/path-type.test.ts +3 -1
  107. package/src/api/internal/route.test.ts +1 -0
  108. package/src/api/request-context-storage.ts +7 -0
  109. package/src/api/request-input-context.test.ts +156 -2
  110. package/src/api/request-input-context.ts +87 -1
  111. package/src/api/request-middleware.test.ts +43 -2
  112. package/src/api/request-middleware.ts +4 -3
  113. package/src/api/request-output-context.test.ts +3 -1
  114. package/src/api/request-output-context.ts +2 -1
  115. package/src/api/route-caller.test.ts +195 -0
  116. package/src/api/route-caller.ts +167 -0
  117. package/src/api/route-handler-input-options.ts +2 -1
  118. package/src/api/route.test.ts +4 -2
  119. package/src/api/route.ts +9 -3
  120. package/src/api/shared-types.ts +2 -1
  121. package/src/client/client-builder.test.ts +4 -2
  122. package/src/client/client-error.test.ts +2 -1
  123. package/src/client/client-error.ts +1 -1
  124. package/src/client/client-types.test.ts +19 -5
  125. package/src/client/client.ssr.test.ts +6 -4
  126. package/src/client/client.svelte.test.ts +18 -9
  127. package/src/client/client.svelte.ts +38 -13
  128. package/src/client/client.test.ts +244 -10
  129. package/src/client/client.ts +473 -148
  130. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  131. package/src/client/internal/ndjson-streaming.ts +1 -0
  132. package/src/client/react.test.ts +176 -6
  133. package/src/client/react.ts +226 -31
  134. package/src/client/solid.test.ts +29 -5
  135. package/src/client/solid.ts +60 -22
  136. package/src/client/vanilla.test.ts +148 -6
  137. package/src/client/vanilla.ts +63 -9
  138. package/src/client/vue.test.ts +397 -8
  139. package/src/client/vue.ts +74 -4
  140. package/src/id.ts +1 -0
  141. package/src/internal/cuid.test.ts +164 -0
  142. package/src/internal/cuid.ts +133 -0
  143. package/src/internal/trace-context.ts +35 -0
  144. package/src/mod-client.ts +55 -9
  145. package/src/mod.ts +9 -3
  146. package/src/runtime.ts +48 -0
  147. package/src/test/test.test.ts +4 -2
  148. package/src/test/test.ts +14 -7
  149. package/src/util/async.test.ts +1 -0
  150. package/src/util/content-type.test.ts +1 -0
  151. package/src/util/nanostores.test.ts +3 -1
  152. package/src/util/ssr.ts +1 -0
  153. package/tsconfig.json +1 -1
  154. package/tsdown.config.ts +2 -0
  155. package/vitest.config.ts +2 -1
@@ -1,22 +1,58 @@
1
1
  import { instantiatedFragmentFakeSymbol } from "../internal/symbols.js";
2
2
  import { resolveRouteFactories } from "./route.js";
3
+ import { recordTraceEvent } from "../internal/trace-context.js";
3
4
  import { FragnoApiError } from "./error.js";
5
+ import { parseFragnoResponse } from "./fragno-response.js";
6
+ import { bindServicesToContext } from "./bind-services.js";
4
7
  import { getMountRoute } from "./internal/route.js";
8
+ import { MutableRequestState } from "./mutable-request-state.js";
9
+ import { RequestContextStorage } from "./request-context-storage.js";
5
10
  import { RequestInputContext } from "./request-input-context.js";
6
11
  import { RequestOutputContext } from "./request-output-context.js";
7
- import { MutableRequestState } from "./mutable-request-state.js";
8
12
  import { RequestMiddlewareInputContext, RequestMiddlewareOutputContext } from "./request-middleware.js";
9
- import { parseFragnoResponse } from "./fragno-response.js";
10
- import { RequestContextStorage } from "./request-context-storage.js";
11
- import { bindServicesToContext } from "./bind-services.js";
12
13
  import { addRoute, createRouter, findRoute } from "rou3";
13
14
 
14
15
  //#region src/api/fragment-instantiator.ts
16
+ const requestSourceSymbol = Symbol.for("fragno-request-source");
17
+ const requestRouteSymbol = Symbol.for("fragno-request-route");
18
+ const requestWaitUntilSymbol = Symbol.for("fragno-request-wait-until");
19
+ const serializeHeadersForTrace = (headers) => Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b));
20
+ const serializeQueryForTrace = (query) => Array.from(query.entries()).sort(([a], [b]) => a.localeCompare(b));
21
+ const serializeBodyForTrace = (body) => {
22
+ if (body instanceof FormData) return {
23
+ type: "form-data",
24
+ entries: Array.from(body.entries()).map(([key, value]) => {
25
+ if (value instanceof Blob) return [key, {
26
+ type: "blob",
27
+ size: value.size,
28
+ mime: value.type
29
+ }];
30
+ return [key, value];
31
+ })
32
+ };
33
+ if (body instanceof Blob) return {
34
+ type: "blob",
35
+ size: body.size,
36
+ mime: body.type
37
+ };
38
+ if (body instanceof ReadableStream) return { type: "stream" };
39
+ return body;
40
+ };
41
+ const INTERNAL_ROUTE_PREFIX = "/_internal";
42
+ function normalizeRoutePrefix(prefix) {
43
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
44
+ return prefix.endsWith("/") && prefix.length > 1 ? prefix.slice(0, -1) : prefix;
45
+ }
46
+ function joinRoutePath(prefix, path) {
47
+ const normalizedPrefix = normalizeRoutePrefix(prefix);
48
+ if (!path || path === "/") return normalizedPrefix;
49
+ return `${normalizedPrefix}${path.startsWith("/") ? path : `/${path}`}`;
50
+ }
15
51
  /**
16
52
  * Instantiated fragment class with encapsulated state.
17
53
  * Provides the same public API as the old FragnoInstantiatedFragment but with better encapsulation.
18
54
  */
19
- var FragnoInstantiatedFragment = class {
55
+ var FragnoInstantiatedFragment = class FragnoInstantiatedFragment {
20
56
  [instantiatedFragmentFakeSymbol] = instantiatedFragmentFakeSymbol;
21
57
  #name;
22
58
  #routes;
@@ -30,7 +66,7 @@ var FragnoInstantiatedFragment = class {
30
66
  #contextStorage;
31
67
  #createRequestStorage;
32
68
  #options;
33
- #linkedFragments;
69
+ #internalData;
34
70
  constructor(params) {
35
71
  this.#name = params.name;
36
72
  this.#routes = params.routes;
@@ -42,7 +78,7 @@ var FragnoInstantiatedFragment = class {
42
78
  this.#contextStorage = params.storage;
43
79
  this.#createRequestStorage = params.createRequestStorage;
44
80
  this.#options = params.options;
45
- this.#linkedFragments = params.linkedFragments ?? {};
81
+ this.#internalData = params.internalData ?? {};
46
82
  this.#router = createRouter();
47
83
  for (const routeConfig of this.#routes) addRoute(this.#router, routeConfig.method.toUpperCase(), routeConfig.path, routeConfig);
48
84
  this.handler = this.handler.bind(this);
@@ -66,7 +102,7 @@ var FragnoInstantiatedFragment = class {
66
102
  return {
67
103
  deps: this.#deps,
68
104
  options: this.#options,
69
- linkedFragments: this.#linkedFragments
105
+ ...this.#internalData
70
106
  };
71
107
  }
72
108
  /**
@@ -78,17 +114,45 @@ var FragnoInstantiatedFragment = class {
78
114
  this.#middlewareHandler = handler;
79
115
  return this;
80
116
  }
81
- #withRequestStorage(callback) {
117
+ #withRequestStorage(callback, source = "context", routeInfo, lifecycleContext) {
82
118
  if (!this.#serviceThisContext && !this.#handlerThisContext) return callback();
83
119
  const storageData = this.#createRequestStorage ? this.#createRequestStorage() : {};
120
+ if (storageData && typeof storageData === "object") {
121
+ const metadataTarget = storageData;
122
+ metadataTarget[requestSourceSymbol] = source;
123
+ if (routeInfo) metadataTarget[requestRouteSymbol] = routeInfo;
124
+ if (lifecycleContext?.waitUntil) metadataTarget[requestWaitUntilSymbol] = lifecycleContext.waitUntil;
125
+ }
84
126
  return this.#contextStorage.run(storageData, callback);
85
127
  }
86
128
  inContext(callback) {
87
129
  if (this.#handlerThisContext) {
88
130
  const boundCallback = callback.bind(this.#handlerThisContext);
89
- return this.#withRequestStorage(boundCallback);
131
+ return this.#withRequestStorage(boundCallback, "context");
90
132
  }
91
- return this.#withRequestStorage(callback);
133
+ return this.#withRequestStorage(callback, "context");
134
+ }
135
+ /**
136
+ * Execute multiple service calls within a handler context.
137
+ * If called outside a request context, it will create one automatically.
138
+ * Pass a factory so service calls are created inside the active context.
139
+ * Primarily used by database fragments (handlerTx).
140
+ */
141
+ async callServices(serviceCalls) {
142
+ const handlerContext = this.#handlerThisContext;
143
+ if (!handlerContext?.handlerTx) throw new Error("callServices is only supported for fragments with handlerTx (database fragments).");
144
+ let callWasArray = false;
145
+ const execute = () => {
146
+ return handlerContext.handlerTx().withServiceCalls(() => {
147
+ const calls = serviceCalls();
148
+ callWasArray = Array.isArray(calls);
149
+ return callWasArray ? calls : [calls];
150
+ }).execute();
151
+ };
152
+ const result = this.#contextStorage.hasStore() ? await execute() : await this.#withRequestStorage(execute, "context");
153
+ if (callWasArray) return result;
154
+ const [first] = result;
155
+ return first;
92
156
  }
93
157
  /**
94
158
  * Get framework-specific handlers for this fragment.
@@ -147,7 +211,7 @@ var FragnoInstantiatedFragment = class {
147
211
  * Main request handler for this fragment.
148
212
  * Handles routing, middleware, and error handling.
149
213
  */
150
- async handler(req) {
214
+ async handler(req, lifecycleContext) {
151
215
  const url = new URL(req.url);
152
216
  const pathname = url.pathname;
153
217
  const matchRoute = pathname.startsWith(this.#mountRoute) ? pathname.slice(this.#mountRoute.length) : null;
@@ -160,30 +224,65 @@ var FragnoInstantiatedFragment = class {
160
224
  error: `Fragno: Route for '${this.#name}' not found`,
161
225
  code: "ROUTE_NOT_FOUND"
162
226
  }, { status: 404 });
227
+ const routeConfig = route.data;
228
+ const expectedContentType = routeConfig.contentType ?? "application/json";
163
229
  let requestBody = void 0;
164
230
  let rawBody = void 0;
165
231
  if (req.body instanceof ReadableStream) {
166
- rawBody = await req.clone().text();
167
- if (rawBody) try {
168
- requestBody = JSON.parse(rawBody);
169
- } catch {
170
- requestBody = void 0;
232
+ const requestContentType = (req.headers.get("content-type") ?? "").toLowerCase();
233
+ if (expectedContentType === "multipart/form-data") {
234
+ if (!requestContentType.includes("multipart/form-data")) return Response.json({
235
+ error: `This endpoint expects multipart/form-data, but received: ${requestContentType || "no content-type"}`,
236
+ code: "UNSUPPORTED_MEDIA_TYPE"
237
+ }, { status: 415 });
238
+ try {
239
+ requestBody = await req.formData();
240
+ } catch {
241
+ return Response.json({
242
+ error: "Failed to parse multipart form data",
243
+ code: "INVALID_REQUEST_BODY"
244
+ }, { status: 400 });
245
+ }
246
+ } else if (expectedContentType === "application/octet-stream") {
247
+ if (!requestContentType.includes("application/octet-stream")) return Response.json({
248
+ error: `This endpoint expects application/octet-stream, but received: ${requestContentType || "no content-type"}`,
249
+ code: "UNSUPPORTED_MEDIA_TYPE"
250
+ }, { status: 415 });
251
+ requestBody = req.body ?? new ReadableStream();
252
+ } else {
253
+ if (requestContentType.includes("multipart/form-data")) return Response.json({
254
+ error: `This endpoint expects JSON, but received multipart/form-data. Use a route with contentType: "multipart/form-data" for file uploads.`,
255
+ code: "UNSUPPORTED_MEDIA_TYPE"
256
+ }, { status: 415 });
257
+ rawBody = await req.clone().text();
258
+ if (rawBody) try {
259
+ requestBody = JSON.parse(rawBody);
260
+ } catch {
261
+ requestBody = void 0;
262
+ }
171
263
  }
172
264
  }
265
+ const decodedRouteParams = {};
266
+ for (const [key, value] of Object.entries(route.params ?? {})) decodedRouteParams[key] = decodeURIComponent(value);
173
267
  const requestState = new MutableRequestState({
174
- pathParams: route.params ?? {},
268
+ pathParams: decodedRouteParams,
175
269
  searchParams: url.searchParams,
176
270
  body: requestBody,
177
271
  headers: new Headers(req.headers)
178
272
  });
273
+ const fullRoutePath = this.#mountRoute && this.#mountRoute !== "/" ? `${this.#mountRoute}${routeConfig.path}` : routeConfig.path;
274
+ const routeInfo = {
275
+ method: routeConfig.method,
276
+ path: routeConfig.path,
277
+ mountRoute: this.#mountRoute,
278
+ fullPath: fullRoutePath
279
+ };
179
280
  const executeRequest = async () => {
180
- if (this.#middlewareHandler) {
181
- const middlewareResult = await this.#executeMiddleware(req, route, requestState);
182
- if (middlewareResult !== void 0) return middlewareResult;
183
- }
281
+ const middlewareResult = await this.#executeMiddleware(req, route, requestState);
282
+ if (middlewareResult !== void 0) return middlewareResult;
184
283
  return this.#executeHandler(req, route, requestState, rawBody);
185
284
  };
186
- return this.#withRequestStorage(executeRequest);
285
+ return this.#withRequestStorage(executeRequest, "route", routeInfo, lifecycleContext);
187
286
  }
188
287
  /**
189
288
  * Call a route directly with typed inputs and outputs.
@@ -202,7 +301,8 @@ var FragnoInstantiatedFragment = class {
202
301
  error: `Route ${method} ${path} not found`,
203
302
  code: "ROUTE_NOT_FOUND"
204
303
  }, { status: 404 });
205
- const { pathParams = {}, body, query, headers } = inputOptions || {};
304
+ const { pathParams = {}, query, headers } = inputOptions || {};
305
+ const body = inputOptions && "body" in inputOptions ? inputOptions.body : void 0;
206
306
  const searchParams = query instanceof URLSearchParams ? query : query ? new URLSearchParams(query) : new URLSearchParams();
207
307
  const requestHeaders = headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();
208
308
  const inputContext = new RequestInputContext({
@@ -215,7 +315,23 @@ var FragnoInstantiatedFragment = class {
215
315
  inputSchema: route.inputSchema,
216
316
  shouldValidateInput: true
217
317
  });
318
+ recordTraceEvent({
319
+ type: "route-input",
320
+ method: route.method,
321
+ path: route.path,
322
+ pathParams: pathParams ?? {},
323
+ queryParams: serializeQueryForTrace(searchParams),
324
+ headers: serializeHeadersForTrace(requestHeaders),
325
+ body: serializeBodyForTrace(body)
326
+ });
218
327
  const outputContext = new RequestOutputContext(route.outputSchema);
328
+ const fullRoutePath = this.#mountRoute && this.#mountRoute !== "/" ? `${this.#mountRoute}${route.path}` : route.path;
329
+ const routeInfo = {
330
+ method: route.method,
331
+ path: route.path,
332
+ mountRoute: this.#mountRoute,
333
+ fullPath: fullRoutePath
334
+ };
219
335
  const executeHandler = async () => {
220
336
  try {
221
337
  const thisContext = this.#handlerThisContext ?? {};
@@ -229,27 +345,50 @@ var FragnoInstantiatedFragment = class {
229
345
  }, { status: 500 });
230
346
  }
231
347
  };
232
- return this.#withRequestStorage(executeHandler);
348
+ return this.#withRequestStorage(executeHandler, "route", routeInfo);
233
349
  }
234
350
  /**
235
351
  * Execute middleware for a request.
236
352
  * Returns undefined if middleware allows the request to continue to the handler.
237
353
  */
238
354
  async #executeMiddleware(req, route, requestState) {
239
- if (!this.#middlewareHandler || !route) return;
355
+ if (!route) return;
240
356
  const { path } = route.data;
241
- const middlewareInputContext = new RequestMiddlewareInputContext(this.#routes, {
357
+ return FragnoInstantiatedFragment.#runMiddlewareForFragment(this, {
358
+ req,
242
359
  method: req.method,
243
360
  path,
244
- request: req,
245
- state: requestState
361
+ requestState
362
+ });
363
+ }
364
+ static async #runMiddlewareForFragment(fragment, options) {
365
+ if (!fragment.#middlewareHandler) return;
366
+ const middlewareInputContext = new RequestMiddlewareInputContext(options.routes ?? fragment.#routes, {
367
+ method: options.method,
368
+ path: options.path,
369
+ request: options.req,
370
+ state: options.requestState
246
371
  });
247
- const middlewareOutputContext = new RequestMiddlewareOutputContext(this.#deps, this.#services);
372
+ const middlewareOutputContext = new RequestMiddlewareOutputContext(fragment.#deps, fragment.#services);
248
373
  try {
249
- const middlewareResult = await this.#middlewareHandler(middlewareInputContext, middlewareOutputContext);
374
+ const middlewareResult = await fragment.#middlewareHandler(middlewareInputContext, middlewareOutputContext);
375
+ recordTraceEvent({
376
+ type: "middleware-decision",
377
+ method: options.method,
378
+ path: options.path,
379
+ outcome: middlewareResult ? "deny" : "allow",
380
+ status: middlewareResult?.status
381
+ });
250
382
  if (middlewareResult !== void 0) return middlewareResult;
251
383
  } catch (error) {
252
384
  console.error("Error in middleware", error);
385
+ recordTraceEvent({
386
+ type: "middleware-decision",
387
+ method: options.method,
388
+ path: options.path,
389
+ outcome: "deny",
390
+ status: error instanceof FragnoApiError ? error.status : 500
391
+ });
253
392
  if (error instanceof FragnoApiError) return error.toResponse();
254
393
  return Response.json({
255
394
  error: "Internal server error",
@@ -275,6 +414,15 @@ var FragnoInstantiatedFragment = class {
275
414
  state: requestState,
276
415
  rawBody
277
416
  });
417
+ recordTraceEvent({
418
+ type: "route-input",
419
+ method: req.method,
420
+ path,
421
+ pathParams: inputContext.pathParams,
422
+ queryParams: serializeQueryForTrace(requestState.searchParams),
423
+ headers: serializeHeadersForTrace(requestState.headers),
424
+ body: serializeBodyForTrace(requestState.body)
425
+ });
278
426
  const outputContext = new RequestOutputContext(outputSchema);
279
427
  try {
280
428
  const contextForHandler = this.#handlerThisContext ?? {};
@@ -313,20 +461,12 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
313
461
  deps = {};
314
462
  } else throw error;
315
463
  }
316
- const linkedFragmentInstances = {};
317
- const linkedFragmentServices = {};
318
- if (definition.linkedFragments) for (const [name, callback] of Object.entries(definition.linkedFragments)) {
319
- const linkedFragment = callback({
320
- config,
321
- options,
322
- serviceDependencies: serviceImplementations
323
- });
324
- linkedFragmentInstances[name] = linkedFragment;
325
- const services$1 = linkedFragment.services;
326
- for (const [serviceName, service] of Object.entries(services$1)) linkedFragmentServices[serviceName] = service;
327
- }
464
+ const mountRoute = getMountRoute({
465
+ name: definition.name,
466
+ mountRoute: options.mountRoute
467
+ });
328
468
  const defineService = (services$1) => services$1;
329
- const privateServices = { ...linkedFragmentServices };
469
+ const privateServices = {};
330
470
  if (definition.privateServices) for (const [serviceName, factory] of Object.entries(definition.privateServices)) {
331
471
  const serviceFactory = factory;
332
472
  try {
@@ -398,16 +538,25 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
398
538
  const serviceContext = contexts?.serviceContext;
399
539
  const handlerContext = contexts?.handlerContext;
400
540
  const boundServices = serviceContext ? bindServicesToContext(services, serviceContext) : services;
401
- const routes = resolveRouteFactories({
541
+ const internalData = definition.internalDataFactory?.({
402
542
  config,
543
+ options,
403
544
  deps,
404
545
  services: boundServices,
405
546
  serviceDeps: serviceImplementations ?? {}
406
- }, routesOrFactories);
407
- const mountRoute = getMountRoute({
408
- name: definition.name,
409
- mountRoute: options.mountRoute
410
- });
547
+ }) ?? {};
548
+ const context = {
549
+ config,
550
+ deps,
551
+ services: boundServices,
552
+ serviceDeps: serviceImplementations ?? {}
553
+ };
554
+ const routes = resolveRouteFactories(context, routesOrFactories);
555
+ const prefixedInternalRoutes = (definition.internalRoutes ? resolveRouteFactories(context, definition.internalRoutes) : []).map((route) => ({
556
+ ...route,
557
+ path: joinRoutePath(INTERNAL_ROUTE_PREFIX, route.path)
558
+ }));
559
+ const finalRoutes = prefixedInternalRoutes.length > 0 ? [...routes, ...prefixedInternalRoutes] : routes;
411
560
  const createRequestStorageWithContext = definition.createRequestStorage ? () => definition.createRequestStorage({
412
561
  config,
413
562
  options,
@@ -415,7 +564,7 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
415
564
  }) : void 0;
416
565
  return new FragnoInstantiatedFragment({
417
566
  name: definition.name,
418
- routes,
567
+ routes: finalRoutes,
419
568
  deps,
420
569
  services: boundServices,
421
570
  mountRoute,
@@ -424,7 +573,7 @@ function instantiateFragment(definition, config, routesOrFactories, options, ser
424
573
  storage,
425
574
  createRequestStorage: createRequestStorageWithContext,
426
575
  options,
427
- linkedFragments: linkedFragmentInstances
576
+ internalData
428
577
  });
429
578
  }
430
579
  /**