@fluojs/platform-fastify 1.0.0-beta.4 → 1.0.0-beta.6

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.
package/README.ko.md CHANGED
@@ -126,11 +126,11 @@ await bootstrapFastifyApplication(AppModule, {
126
126
  ```
127
127
 
128
128
  ### 네이티브 라우트 등록과 안전한 폴백
129
- fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 Fastify 네이티브 per-route 핸들러를 등록합니다. 네이티브 핸들러도 최종적으로는 공통 fluo dispatcher 위임하므로 params, versioning, middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
129
+ fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 Fastify 네이티브 per-route 핸들러를 등록합니다. 의미 보존이 가능한 unversioned route에서는 Fastify가 미리 고른 descriptor와 params를 공유 fluo dispatcher 전달하므로 duplicate route matching을 건너뛰면서도 middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
130
130
 
131
- 여러 라우트가 같은 method와 정규화된 param shape를 공유하는 경우(예: `/:id` 와 `/:slug`), 어댑터는 해당 shape를 의도적으로 와일드카드 fallback 경로에 남겨 둡니다. 이렇게 해서 Fastify 등록 단계에서 부팅 실패가 나거나 fluo의 등록 순서 기반 매칭 의미론이 좁아지지 않도록 보장합니다.
131
+ 여러 라우트가 같은 method와 정규화된 param shape를 공유하는 경우(예: `/:id` 와 `/:slug`), `@All(...)`을 사용하는 경우, non-URI versioning에 의존하는 경우, 또는 duplicate slash / trailing slash 변형으로 들어온 경우에는 어댑터가 해당 요청을 의도적으로 와일드카드 fallback 경로에 남겨 둡니다. 이렇게 해서 Fastify 등록 단계에서 부팅 실패가 나거나 fluo의 등록 순서 기반 매칭 의미론이 좁아지지 않도록 보장합니다. app middleware가 native handoff 이후 framework request의 method 또는 path를 rewrite하면 dispatcher는 stale handoff를 무시하고 rewrite된 요청을 다시 매칭합니다.
132
132
 
133
- 어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다.
133
+ 어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다. CORS 처리는 Fastify 플러그인이 아니라 fluo의 공유 middleware 경로가 계속 소유하고, `OPTIONS` 같은 미지원 메서드는 fluo route가 명시적으로 소유하지 않는 한 fallback dispatcher 경로로 흐릅니다.
134
134
 
135
135
  ## 성능
136
136
 
package/README.md CHANGED
@@ -126,11 +126,11 @@ await bootstrapFastifyApplication(AppModule, {
126
126
  ```
127
127
 
128
128
  ### Native Route Registration with Safe Fallback
129
- When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers instead of sending every request through a single wildcard route. Those native handlers still hand off to the shared fluo dispatcher, so params, versioning, middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, and error handling keep the same framework-level semantics.
129
+ When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers instead of sending every request through a single wildcard route. For semantically safe unversioned routes, those native handlers hand a pre-matched descriptor and params to the shared fluo dispatcher so duplicate route matching is skipped without changing framework-owned guards, interceptors, observers, SSE, multipart, raw body, streaming, or error handling.
130
130
 
131
- When multiple routes share the same method and normalized param shape (for example `/:id` and `/:slug`), the adapter intentionally leaves that shape on the wildcard fallback path so Fastify registration cannot boot-fail or narrow fluo's registration-order matching semantics.
131
+ When multiple routes share the same method and normalized param shape (for example `/:id` and `/:slug`), use `@All(...)`, depend on non-URI versioning, or arrive through duplicate-slash / trailing-slash variants, the adapter intentionally leaves those requests on the wildcard fallback path so Fastify registration cannot boot-fail or narrow fluo's matching semantics. If app middleware rewrites the framework request method or path after a native handoff was attached, the dispatcher ignores that stale handoff and rematches the rewritten request.
132
132
 
133
- The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, and enables Fastify trailing-slash / duplicate-slash normalization so native selection stays aligned with fluo's documented route path contract.
133
+ The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, and enables Fastify trailing-slash / duplicate-slash normalization so native selection stays aligned with fluo's documented route path contract. CORS handling remains owned by fluo's shared middleware path rather than Fastify plugins, and unsupported methods such as `OPTIONS` continue through the fallback dispatcher path unless a fluo route explicitly owns them.
134
134
 
135
135
  ## Performance
136
136
 
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -2,6 +2,7 @@ import { Transform } from 'node:stream';
2
2
  import multipart from '@fastify/multipart';
3
3
  import fastify from 'fastify';
4
4
  import { createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
5
+ import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
5
6
  import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
6
7
  import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
7
8
  import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
@@ -16,7 +17,7 @@ import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/req
16
17
 
17
18
  const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
18
19
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
19
- const FASTIFY_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
20
+ const FASTIFY_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
20
21
 
21
22
  /**
22
23
  * Bootstrap options for creating a Fastify-backed application without
@@ -102,6 +103,14 @@ export class FastifyHttpApplicationAdapter {
102
103
  for (const route of createFastifyNativeRoutes(descriptors)) {
103
104
  this.app.route({
104
105
  handler: async (request, reply) => {
106
+ const requestPath = readRequestPathFromRawUrl(request.raw.url);
107
+ const params = normalizeNativeRouteParams(request.params);
108
+ if (!isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
109
+ bindRawRequestNativeRouteHandoff(request.raw, {
110
+ descriptor: route.descriptor,
111
+ params
112
+ });
113
+ }
105
114
  await this.handleRequest(request, reply);
106
115
  },
107
116
  method: route.method,
@@ -151,6 +160,9 @@ function createFastifyRequestResponseFactory(multipartOptions, maxBodySize = DEF
151
160
  createResponse(reply) {
152
161
  return createFrameworkResponse(reply);
153
162
  },
163
+ async materializeRequest(request) {
164
+ await materializeFrameworkRequestBody(request);
165
+ },
154
166
  resolveRequestId(request) {
155
167
  return resolveRequestIdFromHeaders(request.raw.headers);
156
168
  },
@@ -167,29 +179,34 @@ function resolveDispatcherRouteDescriptors(dispatcher) {
167
179
  function createFastifyNativeRoutes(descriptors) {
168
180
  const candidates = new Map();
169
181
  const shapePaths = new Map();
182
+ const versionSensitiveRouteKeys = collectVersionSensitiveRouteKeys(descriptors);
170
183
  for (const descriptor of descriptors) {
171
- const path = descriptor.route.path;
172
- if (descriptor.route.method === 'ALL') {
173
- for (const method of FASTIFY_NATIVE_ROUTE_METHODS) {
174
- registerFastifyNativeRouteCandidate(candidates, shapePaths, method, path);
175
- }
184
+ if (!isFastifyNativeRouteDescriptor(descriptor) || versionSensitiveRouteKeys.has(`${descriptor.route.method}:${descriptor.route.path}`)) {
176
185
  continue;
177
186
  }
178
- registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor.route.method, path);
187
+ registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor);
179
188
  }
180
189
  return [...candidates.values()].filter(candidate => shapePaths.get(candidate.shapeKey)?.size === 1).map(({
190
+ descriptor,
181
191
  method,
182
192
  path
183
193
  }) => ({
194
+ descriptor,
184
195
  method,
185
196
  path
186
197
  }));
187
198
  }
188
- function registerFastifyNativeRouteCandidate(candidates, shapePaths, method, path) {
199
+ function isFastifyNativeRouteDescriptor(descriptor) {
200
+ return descriptor.route.method !== 'ALL' && FASTIFY_NATIVE_ROUTE_METHODS.includes(descriptor.route.method) && descriptor.route.version === undefined;
201
+ }
202
+ function registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor) {
203
+ const method = descriptor.route.method;
204
+ const path = descriptor.route.path;
189
205
  const routeKey = `${method}:${path}`;
190
206
  const shapeKey = `${method}:${canonicalizeFastifyRouteShape(path)}`;
191
207
  if (!candidates.has(routeKey)) {
192
208
  candidates.set(routeKey, {
209
+ descriptor,
193
210
  method,
194
211
  path,
195
212
  shapeKey
@@ -254,11 +271,15 @@ export async function runFastifyApplication(rootModule, options) {
254
271
  }, adapter);
255
272
  }
256
273
  function createFrameworkResponse(reply) {
274
+ let activeStream;
257
275
  return {
258
276
  committed: reply.sent,
259
277
  headers: {},
260
278
  raw: reply,
261
- stream: createFrameworkResponseStream(reply),
279
+ get stream() {
280
+ activeStream ??= createFrameworkResponseStream(reply);
281
+ return activeStream;
282
+ },
262
283
  redirect(status, location) {
263
284
  this.setStatus(status);
264
285
  this.setHeader('Location', location);
@@ -278,6 +299,17 @@ function createFrameworkResponse(reply) {
278
299
  this.committed = true;
279
300
  await reply.send(serialized.payload);
280
301
  },
302
+ async sendSimpleJson(body) {
303
+ if (reply.sent) {
304
+ this.committed = true;
305
+ return;
306
+ }
307
+ if (!reply.hasHeader('content-type')) {
308
+ reply.header('content-type', 'application/json; charset=utf-8');
309
+ }
310
+ this.committed = true;
311
+ await reply.send(JSON.stringify(body));
312
+ },
281
313
  setHeader(name, value) {
282
314
  const lowerName = name.toLowerCase();
283
315
  if (lowerName === 'set-cookie') {
@@ -356,43 +388,92 @@ function createFrameworkResponseStream(reply) {
356
388
  };
357
389
  }
358
390
  async function createFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
391
+ return createDeferredFrameworkRequest(request, signal, multipartOptions, maxBodySize, preserveRawBody);
392
+ }
393
+ function createDeferredFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
359
394
  const rawUrl = request.raw.url ?? '/';
360
- const url = new URL(rawUrl, 'http://localhost');
361
- const headers = normalizeHeaders(request.headers);
362
- const contentType = headers['content-type'];
363
- const isMultipart = typeof contentType === 'string' && contentType.includes('multipart/form-data');
364
- let body = request.body;
365
- let files;
366
- if (isMultipart) {
367
- const parsed = await parseMultipartRequest(request, {
368
- ...multipartOptions,
369
- maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
370
- });
371
- body = parsed.fields;
372
- files = parsed.files;
373
- }
395
+ const urlParts = splitRawRequestUrl(rawUrl);
396
+ const headerSnapshot = cloneRequestHeaders(request.headers);
397
+ const headers = createMemoizedValue(() => normalizeHeaders(headerSnapshot));
398
+ const cookieHeader = cloneHeaderValue(headerSnapshot.cookie);
399
+ const cookies = createMemoizedValue(() => parseCookieHeader(cookieHeader));
400
+ const query = createMemoizedValue(() => parseQueryParamsFromSearch(urlParts.search));
401
+ const isMultipart = isMultipartRequestContentType(headerSnapshot['content-type']);
402
+ const materializeBody = createMemoizedAsyncValue(async () => {
403
+ let body = request.body;
404
+ let files;
405
+ if (isMultipart) {
406
+ const parsed = await parseMultipartRequest(request, {
407
+ ...multipartOptions,
408
+ maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
409
+ });
410
+ body = parsed.fields;
411
+ files = parsed.files;
412
+ }
413
+ frameworkRequest.body = body;
414
+ if (files) {
415
+ frameworkRequest.files = files;
416
+ }
417
+ if (preserveRawBody && !isMultipart) {
418
+ const rawBodyValue = request.rawBody;
419
+ if (rawBodyValue !== undefined) {
420
+ frameworkRequest.rawBody = rawBodyValue;
421
+ }
422
+ }
423
+ });
374
424
  const frameworkRequest = {
375
- body,
376
- cookies: parseCookieHeader(Array.isArray(headers.cookie) ? headers.cookie[0] : headers.cookie),
377
- headers,
425
+ get cookies() {
426
+ return cookies();
427
+ },
428
+ get headers() {
429
+ return headers();
430
+ },
378
431
  method: request.method,
379
432
  params: {},
380
- path: url.pathname,
381
- query: parseQueryParams(url.searchParams),
433
+ path: urlParts.path,
434
+ get query() {
435
+ return query();
436
+ },
382
437
  raw: request.raw,
383
438
  signal,
384
- url: url.pathname + url.search
439
+ url: urlParts.path + urlParts.search,
440
+ materializeBody
385
441
  };
386
- if (files) {
387
- frameworkRequest.files = files;
442
+ const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request.raw);
443
+ return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
444
+ }
445
+ async function materializeFrameworkRequestBody(request) {
446
+ await request.materializeBody?.();
447
+ delete request.materializeBody;
448
+ }
449
+ function normalizeNativeRouteParams(params) {
450
+ if (typeof params !== 'object' || params === null) {
451
+ return {};
388
452
  }
389
- if (preserveRawBody && !isMultipart) {
390
- const rawBodyValue = request.rawBody;
391
- if (rawBodyValue !== undefined) {
392
- frameworkRequest.rawBody = rawBodyValue;
453
+ return Object.fromEntries(Object.entries(params).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : value === undefined ? [] : [[key, String(value)]]));
454
+ }
455
+ function hasNativeRouteParamSeparators(params) {
456
+ return Object.values(params).some(value => value.includes('/'));
457
+ }
458
+ function collectVersionSensitiveRouteKeys(descriptors) {
459
+ const grouped = new Map();
460
+ for (const descriptor of descriptors) {
461
+ if (!FASTIFY_NATIVE_ROUTE_METHODS.includes(descriptor.route.method)) {
462
+ continue;
393
463
  }
464
+ const routeKey = `${descriptor.route.method}:${descriptor.route.path}`;
465
+ const current = grouped.get(routeKey) ?? {
466
+ count: 0,
467
+ hasVersioned: false
468
+ };
469
+ current.count += 1;
470
+ current.hasVersioned ||= descriptor.route.version !== undefined;
471
+ grouped.set(routeKey, current);
394
472
  }
395
- return frameworkRequest;
473
+ return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
474
+ }
475
+ function readRequestPathFromRawUrl(rawUrl) {
476
+ return splitRawRequestUrl(rawUrl ?? '/').path;
396
477
  }
397
478
  async function parseMultipartRequest(request, options = {}) {
398
479
  const fields = {};
@@ -487,6 +568,9 @@ function normalizeHeaders(headers) {
487
568
  }
488
569
  return normalized;
489
570
  }
571
+ function parseQueryParamsFromSearch(search) {
572
+ return parseQueryParams(new URLSearchParams(search));
573
+ }
490
574
  function parseQueryParams(searchParams) {
491
575
  const query = {};
492
576
  for (const [key, value] of searchParams.entries()) {
@@ -504,10 +588,11 @@ function parseQueryParams(searchParams) {
504
588
  return query;
505
589
  }
506
590
  function parseCookieHeader(cookieHeader) {
507
- if (!cookieHeader) {
591
+ const normalizedCookieHeader = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader;
592
+ if (!normalizedCookieHeader) {
508
593
  return {};
509
594
  }
510
- return Object.fromEntries(cookieHeader.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
595
+ return Object.fromEntries(normalizedCookieHeader.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
511
596
  const index = pair.indexOf('=');
512
597
  if (index === -1) {
513
598
  return [pair.trim(), ''];
@@ -520,6 +605,55 @@ function parseCookieHeader(cookieHeader) {
520
605
  }
521
606
  }));
522
607
  }
608
+ function createMemoizedValue(factory) {
609
+ let initialized = false;
610
+ let value;
611
+ return () => {
612
+ if (!initialized) {
613
+ value = factory();
614
+ initialized = true;
615
+ }
616
+ return value;
617
+ };
618
+ }
619
+ function createMemoizedAsyncValue(factory) {
620
+ let promise;
621
+ return () => {
622
+ promise ??= factory();
623
+ return promise;
624
+ };
625
+ }
626
+ function cloneHeaderValue(value) {
627
+ return Array.isArray(value) ? [...value] : value;
628
+ }
629
+ function cloneRequestHeaders(headers) {
630
+ return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, cloneHeaderValue(value)]));
631
+ }
632
+ function splitRawRequestUrl(rawUrl) {
633
+ if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
634
+ const url = new URL(rawUrl);
635
+ return {
636
+ path: url.pathname,
637
+ search: url.search
638
+ };
639
+ }
640
+ const queryStart = rawUrl.indexOf('?');
641
+ const hashStart = rawUrl.indexOf('#');
642
+ const pathEndCandidates = [queryStart, hashStart].filter(index => index >= 0);
643
+ const pathEnd = pathEndCandidates.length > 0 ? Math.min(...pathEndCandidates) : rawUrl.length;
644
+ const path = rawUrl.slice(0, pathEnd) || '/';
645
+ if (queryStart === -1) {
646
+ return {
647
+ path,
648
+ search: ''
649
+ };
650
+ }
651
+ const searchEnd = hashStart >= 0 && hashStart > queryStart ? hashStart : rawUrl.length;
652
+ return {
653
+ path,
654
+ search: rawUrl.slice(queryStart, searchEnd)
655
+ };
656
+ }
523
657
  function setMultiValue(target, key, value) {
524
658
  const existing = target[key];
525
659
  if (existing === undefined) {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "platform",
9
9
  "server"
10
10
  ],
11
- "version": "1.0.0-beta.4",
11
+ "version": "1.0.0-beta.6",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -38,11 +38,12 @@
38
38
  "@fastify/multipart": "^9.2.1",
39
39
  "fastify": "^5.8.5",
40
40
  "fastify-raw-body": "^5.0.0",
41
- "@fluojs/http": "^1.0.0-beta.3",
42
- "@fluojs/runtime": "^1.0.0-beta.4"
41
+ "@fluojs/http": "^1.0.0-beta.4",
42
+ "@fluojs/runtime": "^1.0.0-beta.6"
43
43
  },
44
44
  "devDependencies": {
45
- "vitest": "^3.2.4"
45
+ "vitest": "^3.2.4",
46
+ "@fluojs/di": "^1.0.0-beta.5"
46
47
  },
47
48
  "scripts": {
48
49
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",