@fluojs/runtime 1.1.8 → 3.0.0

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 (130) hide show
  1. package/README.ko.md +292 -21
  2. package/README.md +292 -21
  3. package/dist/abort.d.ts +6 -1
  4. package/dist/abort.d.ts.map +1 -1
  5. package/dist/abort.js +18 -2
  6. package/dist/adapters/internal-request-response-factory.d.ts +1 -1
  7. package/dist/adapters/internal-request-response-factory.d.ts.map +1 -1
  8. package/dist/adapters/internal-request-response-factory.js +1 -1
  9. package/dist/adapters/request-response-factory.d.ts +20 -1
  10. package/dist/adapters/request-response-factory.d.ts.map +1 -1
  11. package/dist/adapters/request-response-factory.js +75 -22
  12. package/dist/bootstrap.d.ts +3 -9
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +181 -76
  15. package/dist/devtools/contracts.d.ts +3 -135
  16. package/dist/devtools/contracts.d.ts.map +1 -1
  17. package/dist/devtools/index.d.ts +2 -3
  18. package/dist/devtools/index.d.ts.map +1 -1
  19. package/dist/devtools/index.js +1 -2
  20. package/dist/devtools/snapshot.d.ts +1 -1
  21. package/dist/devtools/snapshot.d.ts.map +1 -1
  22. package/dist/devtools/snapshot.js +18 -17
  23. package/dist/devtools/studio-request-observer.d.ts +14 -0
  24. package/dist/devtools/studio-request-observer.d.ts.map +1 -0
  25. package/dist/devtools/studio-request-observer.js +112 -0
  26. package/dist/devtools/studio-runtime-config.d.ts +25 -0
  27. package/dist/devtools/studio-runtime-config.d.ts.map +1 -0
  28. package/dist/devtools/studio-runtime-config.js +59 -0
  29. package/dist/devtools/studio-runtime.d.ts +12 -18
  30. package/dist/devtools/studio-runtime.d.ts.map +1 -1
  31. package/dist/devtools/studio-runtime.js +25 -169
  32. package/dist/errors.d.ts +26 -0
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +38 -0
  35. package/dist/health/health.d.ts +18 -4
  36. package/dist/health/health.d.ts.map +1 -1
  37. package/dist/health/health.js +5 -2
  38. package/dist/http-adapter-shared.d.ts +1 -1
  39. package/dist/http-adapter-shared.d.ts.map +1 -1
  40. package/dist/http-adapter-shared.js +1 -1
  41. package/dist/index.d.ts +6 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -1
  44. package/dist/internal/route-inspection-metadata.d.ts +30 -0
  45. package/dist/internal/route-inspection-metadata.d.ts.map +1 -0
  46. package/dist/internal/route-inspection-metadata.js +68 -0
  47. package/dist/internal.d.ts +6 -1
  48. package/dist/internal.d.ts.map +1 -1
  49. package/dist/internal.js +4 -1
  50. package/dist/module-definition.d.ts +10 -0
  51. package/dist/module-definition.d.ts.map +1 -0
  52. package/dist/module-definition.js +12 -0
  53. package/dist/module-graph.d.ts +42 -1
  54. package/dist/module-graph.d.ts.map +1 -1
  55. package/dist/module-graph.js +112 -16
  56. package/dist/multipart.d.ts +68 -4
  57. package/dist/multipart.d.ts.map +1 -1
  58. package/dist/multipart.js +666 -103
  59. package/dist/platform-component-registry.d.ts +9 -0
  60. package/dist/platform-component-registry.d.ts.map +1 -0
  61. package/dist/platform-component-registry.js +70 -0
  62. package/dist/platform-diagnostic-retention.d.ts +13 -0
  63. package/dist/platform-diagnostic-retention.d.ts.map +1 -0
  64. package/dist/platform-diagnostic-retention.js +43 -0
  65. package/dist/platform-shell-probe-reports.d.ts +8 -0
  66. package/dist/platform-shell-probe-reports.d.ts.map +1 -0
  67. package/dist/platform-shell-probe-reports.js +95 -0
  68. package/dist/platform-shell-probes.d.ts +16 -0
  69. package/dist/platform-shell-probes.d.ts.map +1 -0
  70. package/dist/platform-shell-probes.js +141 -0
  71. package/dist/platform-shell.d.ts +7 -13
  72. package/dist/platform-shell.d.ts.map +1 -1
  73. package/dist/platform-shell.js +58 -310
  74. package/dist/retryable-shutdown.d.ts +7 -0
  75. package/dist/retryable-shutdown.d.ts.map +1 -0
  76. package/dist/retryable-shutdown.js +20 -0
  77. package/dist/route-inspection.d.ts +56 -0
  78. package/dist/route-inspection.d.ts.map +1 -0
  79. package/dist/route-inspection.js +80 -0
  80. package/dist/tokens.d.ts +6 -4
  81. package/dist/tokens.d.ts.map +1 -1
  82. package/dist/tokens.js +5 -0
  83. package/dist/types.d.ts +53 -8
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/web.d.ts +22 -9
  86. package/dist/web.d.ts.map +1 -1
  87. package/dist/web.js +80 -34
  88. package/package.json +13 -26
  89. package/dist/internal-node.d.ts +0 -2
  90. package/dist/internal-node.d.ts.map +0 -1
  91. package/dist/internal-node.js +0 -1
  92. package/dist/logging/json-logger.d.ts +0 -8
  93. package/dist/logging/json-logger.d.ts.map +0 -1
  94. package/dist/logging/json-logger.js +0 -45
  95. package/dist/logging/logger.d.ts +0 -37
  96. package/dist/logging/logger.d.ts.map +0 -1
  97. package/dist/logging/logger.js +0 -103
  98. package/dist/node/internal-node-compression.d.ts +0 -22
  99. package/dist/node/internal-node-compression.d.ts.map +0 -1
  100. package/dist/node/internal-node-compression.js +0 -84
  101. package/dist/node/internal-node-request.d.ts +0 -162
  102. package/dist/node/internal-node-request.d.ts.map +0 -1
  103. package/dist/node/internal-node-request.js +0 -475
  104. package/dist/node/internal-node-response.d.ts +0 -28
  105. package/dist/node/internal-node-response.d.ts.map +0 -1
  106. package/dist/node/internal-node-response.js +0 -205
  107. package/dist/node/internal-node-shutdown.d.ts +0 -34
  108. package/dist/node/internal-node-shutdown.d.ts.map +0 -1
  109. package/dist/node/internal-node-shutdown.js +0 -83
  110. package/dist/node/internal-node.d.ts +0 -117
  111. package/dist/node/internal-node.d.ts.map +0 -1
  112. package/dist/node/internal-node.js +0 -283
  113. package/dist/node/node-compression.d.ts +0 -2
  114. package/dist/node/node-compression.d.ts.map +0 -1
  115. package/dist/node/node-compression.js +0 -1
  116. package/dist/node/node-request.d.ts +0 -2
  117. package/dist/node/node-request.d.ts.map +0 -1
  118. package/dist/node/node-request.js +0 -1
  119. package/dist/node/node-response.d.ts +0 -2
  120. package/dist/node/node-response.d.ts.map +0 -1
  121. package/dist/node/node-response.js +0 -1
  122. package/dist/node/node-shutdown.d.ts +0 -2
  123. package/dist/node/node-shutdown.d.ts.map +0 -1
  124. package/dist/node/node-shutdown.js +0 -1
  125. package/dist/node/node.d.ts +0 -2
  126. package/dist/node/node.d.ts.map +0 -1
  127. package/dist/node/node.js +0 -1
  128. package/dist/node.d.ts +0 -5
  129. package/dist/node.d.ts.map +0 -1
  130. package/dist/node.js +0 -3
@@ -2,6 +2,55 @@
2
2
 
3
3
  /** Options for dispatching one raw platform request through a request/response factory. */
4
4
 
5
+ /** An in-progress factory dispatch with its response shell available immediately. */
6
+
7
+ /**
8
+ * Starts one raw platform request dispatch while exposing its mutable response shell.
9
+ *
10
+ * @param options - Factory, dispatcher, and raw platform request/response values for one dispatch.
11
+ * @returns The response shell and its independently settling dispatch lifecycle.
12
+ */
13
+ export function startDispatchWithRequestResponseFactory({
14
+ dispatcher,
15
+ dispatcherNotReadyMessage,
16
+ factory,
17
+ rawRequest,
18
+ rawResponse
19
+ }) {
20
+ const response = factory.createResponse(rawResponse, rawRequest);
21
+ const signal = factory.createRequestSignal(rawResponse);
22
+ let frameworkRequest;
23
+ const completion = (async () => {
24
+ try {
25
+ frameworkRequest = await factory.createRequest(rawRequest, signal);
26
+ const materializeRequest = factory.materializeRequest;
27
+ if (materializeRequest) {
28
+ await materializeRequest(frameworkRequest);
29
+ }
30
+ if (!dispatcher) {
31
+ throw new Error(dispatcherNotReadyMessage);
32
+ }
33
+ await dispatcher.dispatch(frameworkRequest, response);
34
+ if (!response.committed) {
35
+ await response.send(undefined);
36
+ }
37
+ return response;
38
+ } catch (error) {
39
+ if (signal.aborted || response.committed) {
40
+ return response;
41
+ }
42
+ await factory.writeErrorResponse(error, response, factory.resolveRequestId(rawRequest));
43
+ return response;
44
+ } finally {
45
+ await finalizeRouteOwnedMultipartBody(frameworkRequest);
46
+ }
47
+ })();
48
+ return {
49
+ completion,
50
+ response
51
+ };
52
+ }
53
+
5
54
  /**
6
55
  * Dispatches one raw platform request through the shared request/response factory lifecycle.
7
56
  *
@@ -15,27 +64,31 @@ export async function dispatchWithRequestResponseFactory({
15
64
  rawRequest,
16
65
  rawResponse
17
66
  }) {
18
- const frameworkResponse = factory.createResponse(rawResponse, rawRequest);
19
- const signal = factory.createRequestSignal(rawResponse);
20
- try {
21
- const frameworkRequest = await factory.createRequest(rawRequest, signal);
22
- const materializeRequest = factory.materializeRequest;
23
- if (materializeRequest) {
24
- await materializeRequest(frameworkRequest);
25
- }
26
- if (!dispatcher) {
27
- throw new Error(dispatcherNotReadyMessage);
28
- }
29
- await dispatcher.dispatch(frameworkRequest, frameworkResponse);
30
- if (!frameworkResponse.committed) {
31
- await frameworkResponse.send(undefined);
32
- }
33
- return frameworkResponse;
34
- } catch (error) {
35
- if (signal.aborted || frameworkResponse.committed) {
36
- return frameworkResponse;
37
- }
38
- await factory.writeErrorResponse(error, frameworkResponse, factory.resolveRequestId(rawRequest));
39
- return frameworkResponse;
67
+ return await startDispatchWithRequestResponseFactory({
68
+ dispatcher,
69
+ dispatcherNotReadyMessage,
70
+ factory,
71
+ rawRequest,
72
+ rawResponse
73
+ }).completion;
74
+ }
75
+
76
+ /**
77
+ * Finalizes a route-owned multipart body iterator after dispatch completes.
78
+ *
79
+ * @param request Framework request whose multipart body may need finalization.
80
+ * @returns A promise that resolves after the iterator has been returned when applicable.
81
+ */
82
+ export async function finalizeRouteOwnedMultipartBody(request) {
83
+ const body = request?.body;
84
+ if (!isAsyncIterable(body)) {
85
+ return;
86
+ }
87
+ const iterator = body[Symbol.asyncIterator]();
88
+ if (iterator.return) {
89
+ await iterator.return();
40
90
  }
91
+ }
92
+ function isAsyncIterable(value) {
93
+ return value !== null && (typeof value === 'object' || typeof value === 'function') && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === 'function';
41
94
  }
@@ -1,12 +1,6 @@
1
- import type { ApplicationContext, Application, MicroserviceApplication, BootstrapApplicationOptions, BootstrapModuleOptions, BootstrapResult, CreateApplicationOptions, CreateApplicationContextOptions, CreateMicroserviceOptions, ModuleDefinition, ModuleType } from './types.js';
2
- /**
3
- * Associates module metadata with a module type.
4
- *
5
- * @param moduleType Module class that should receive runtime module metadata.
6
- * @param definition Module definition contract (`imports`, `providers`, `controllers`, `exports`, etc.).
7
- * @returns The same `moduleType` reference for fluent helper composition.
8
- */
9
- export declare function defineModule<T extends ModuleType>(moduleType: T, definition: ModuleDefinition): T;
1
+ import { defineModule } from './module-definition.js';
2
+ import type { Application, ApplicationContext, BootstrapApplicationOptions, BootstrapModuleOptions, BootstrapResult, CreateApplicationContextOptions, CreateApplicationOptions, CreateMicroserviceOptions, MicroserviceApplication, ModuleType } from './types.js';
3
+ export { defineModule };
10
4
  /**
11
5
  * Bootstraps the module graph and returns the root container baseline.
12
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EAEX,uBAAuB,EAGvB,2BAA2B,EAC3B,sBAAsB,EACtB,eAAe,EAGf,wBAAwB,EACxB,+BAA+B,EAC/B,yBAAyB,EAEzB,gBAAgB,EAChB,UAAU,EAMX,MAAM,YAAY,CAAC;AA6dpB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,gBAAgB,GAAG,CAAC,CAIjG;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B,GAAG,eAAe,CA+B7G;AAo0BD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,CAAC,CAiJrG;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB;;;;;;;OAOG;WACU,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,WAAW,CAAC;IAOzG;;;;;;;OAOG;WACU,wBAAwB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC;IAwH9B;;;;;;;;OAQG;WACU,kBAAkB,CAC7B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC;CA+BpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,oBAAc,CAAC"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAmBtD,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGlB,2BAA2B,EAE3B,sBAAsB,EACtB,eAAe,EAEf,+BAA+B,EAC/B,wBAAwB,EACxB,yBAAyB,EAEzB,uBAAuB,EAEvB,UAAU,EAMX,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,CAAC;AAuexB;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B,GAAG,eAAe,CA+B7G;AA+8BD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,CAAC,CAiJrG;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB;;;;;;;OAOG;WACU,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,WAAW,CAAC;IAOzG;;;;;;;OAOG;WACU,wBAAwB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC;IAwH9B;;;;;;;;OAQG;WACU,kBAAkB,CAC7B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC;CA+BpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,oBAAc,CAAC"}
package/dist/bootstrap.js CHANGED
@@ -1,15 +1,19 @@
1
- import { Container } from '@fluojs/di';
2
1
  import { InvariantError } from '@fluojs/core';
3
- import { createDispatcher, createHandlerMapping } from '@fluojs/http';
2
+ import { Container } from '@fluojs/di';
3
+ import { resolveMultiContribution } from '@fluojs/di/internal';
4
+ import { createDispatcher, createHandlerMapping } from '@fluojs/http/portable';
5
+ import { applyStudioDevtoolsApplicationOptions, applyStudioDevtoolsContextOptions, createStudioDevtoolsRuntimeFromConfig, publishStudioBootstrapSnapshot } from './devtools/studio-runtime.js';
4
6
  import { DuplicateProviderError } from './errors.js';
5
7
  import { createBootstrapTimingDiagnostics } from './health/diagnostics.js';
6
- import { applyStudioDevtoolsApplicationOptions, applyStudioDevtoolsContextOptions, createStudioDevtoolsRuntimeFromConfig, publishStudioBootstrapSnapshot } from './devtools/studio-runtime.js';
7
- import { defineRuntimeModuleMetadata, getRuntimeClassDiMetadata } from './internal/core-metadata.js';
8
+ import { getRuntimeClassDiMetadata } from './internal/core-metadata.js';
8
9
  import { RuntimeDefaultBinder } from './internal/http-runtime.js';
9
10
  import { createDefaultApplicationLogger } from './logging/default-logger.js';
11
+ import { defineModule } from './module-definition.js';
10
12
  import { compileModuleGraph, providerToken } from './module-graph.js';
11
13
  import { createRuntimePlatformShell } from './platform-shell.js';
12
- import { APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, PLATFORM_SHELL, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from './tokens.js';
14
+ import { createLifecycleCloseError, createRetryableShutdownState } from './retryable-shutdown.js';
15
+ import { APPLICATION_LOGGER, BOOTSTRAP_PROVIDER_TOKENS, BOOTSTRAP_READY_SIGNAL, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, PLATFORM_SHELL, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from './tokens.js';
16
+ export { defineModule };
13
17
  const DEFAULT_MICROSERVICE_TOKEN = Symbol.for('fluo.microservices.service');
14
18
  const runtimePerformance = globalThis.performance;
15
19
  async function runExceptionFilters(filters, error, request, response, requestId) {
@@ -46,20 +50,11 @@ async function disposeContainer(container) {
46
50
  }
47
51
  await container.dispose();
48
52
  }
49
- function toError(error) {
50
- return error instanceof Error ? error : new Error(String(error));
51
- }
52
- function createLifecycleCloseError(errors) {
53
- if (errors.length === 1) {
54
- return toError(errors[0]);
55
- }
56
- return new AggregateError(errors, 'Application close failed for one or more shutdown steps.');
57
- }
58
53
  async function runCleanupCallbacks(cleanups) {
59
54
  const errors = [];
60
55
  for (const cleanup of cleanups) {
61
56
  try {
62
- cleanup();
57
+ await cleanup();
63
58
  } catch (error) {
64
59
  errors.push(error);
65
60
  }
@@ -95,24 +90,41 @@ function createBootstrapReadySignal() {
95
90
  }
96
91
  async function closeRuntimeResources(options) {
97
92
  const errors = [];
98
- resetReadinessState(options.modules);
99
- errors.push(...(await runCleanupCallbacks(options.runtimeCleanup)));
100
- try {
101
- await runShutdownHooks(options.lifecycleInstances, options.signal);
102
- } catch (error) {
103
- errors.push(error);
93
+ if (!options.shutdownState.isComplete('readiness')) {
94
+ resetReadinessState(options.modules);
95
+ options.shutdownState.complete('readiness');
96
+ }
97
+ if (!options.shutdownState.isComplete('runtime-cleanup')) {
98
+ const cleanupErrors = await runCleanupCallbacks(options.runtimeCleanup);
99
+ if (cleanupErrors.length === 0) {
100
+ options.shutdownState.complete('runtime-cleanup');
101
+ } else {
102
+ errors.push(...cleanupErrors);
103
+ }
104
104
  }
105
- if (options.adapter) {
105
+ if (!options.shutdownState.isComplete('hooks')) {
106
106
  try {
107
- await options.adapter.close(options.signal);
107
+ await runShutdownHooks(options.lifecycleInstances, options.signal);
108
+ options.shutdownState.complete('hooks');
108
109
  } catch (error) {
109
110
  errors.push(error);
110
111
  }
111
112
  }
112
- try {
113
- await disposeContainer(options.container);
114
- } catch (error) {
115
- errors.push(error);
113
+ if (!options.shutdownState.isComplete('adapter')) {
114
+ try {
115
+ await options.adapter?.close(options.signal);
116
+ options.shutdownState.complete('adapter');
117
+ } catch (error) {
118
+ errors.push(error);
119
+ }
120
+ }
121
+ if (!options.shutdownState.isComplete('container')) {
122
+ try {
123
+ await disposeContainer(options.container);
124
+ options.shutdownState.complete('container');
125
+ } catch (error) {
126
+ errors.push(error);
127
+ }
116
128
  }
117
129
  if (errors.length > 0) {
118
130
  throw createLifecycleCloseError(errors);
@@ -335,18 +347,6 @@ function registerModuleMiddleware(container, modules) {
335
347
  }
336
348
  }
337
349
 
338
- /**
339
- * Associates module metadata with a module type.
340
- *
341
- * @param moduleType Module class that should receive runtime module metadata.
342
- * @param definition Module definition contract (`imports`, `providers`, `controllers`, `exports`, etc.).
343
- * @returns The same `moduleType` reference for fluent helper composition.
344
- */
345
- export function defineModule(moduleType, definition) {
346
- defineRuntimeModuleMetadata(moduleType, definition);
347
- return moduleType;
348
- }
349
-
350
350
  /**
351
351
  * Bootstraps the module graph and returns the root container baseline.
352
352
  *
@@ -384,8 +384,11 @@ export function bootstrapModule(rootModule, options = {}) {
384
384
  class FluoApplication {
385
385
  applicationState = 'bootstrapped';
386
386
  closed = false;
387
+ closeStarted = false;
387
388
  closingPromise;
388
389
  listenPromise;
390
+ connectedMicroservicesClosed = false;
391
+ runtimeShutdownState = createRetryableShutdownState();
389
392
  contextResolutionCache = new Map();
390
393
  lifecycleInstances;
391
394
  connectedMicroservices = [];
@@ -411,7 +414,10 @@ class FluoApplication {
411
414
  if (this.closed) {
412
415
  return this.container.resolve(token);
413
416
  }
414
- return resolveContextToken(this.container, token, this.contextCacheableTokens, this.contextResolutionCache);
417
+ this.assertProviderResolutionAllowed();
418
+ const resolved = await resolveContextToken(this.container, token, this.contextCacheableTokens, this.contextResolutionCache);
419
+ this.assertProviderResolutionAllowed();
420
+ return resolved;
415
421
  }
416
422
 
417
423
  /**
@@ -424,8 +430,10 @@ class FluoApplication {
424
430
  await this.platformShell.assertCriticalReadiness();
425
431
  }
426
432
  async connectMicroservice(options = {}) {
433
+ this.assertApplicationOperationAllowed('connect a microservice');
427
434
  const microserviceToken = options.microserviceToken ?? DEFAULT_MICROSERVICE_TOKEN;
428
435
  const runtime = await this.container.resolve(microserviceToken);
436
+ this.assertApplicationOperationAllowed('connect a microservice');
429
437
  if (!isMicroserviceRuntime(runtime)) {
430
438
  throw new InvariantError('Resolved microservice token does not implement listen().');
431
439
  }
@@ -434,9 +442,11 @@ class FluoApplication {
434
442
  return microservice;
435
443
  }
436
444
  async startAllMicroservices() {
445
+ this.assertApplicationOperationAllowed('start microservices');
437
446
  const startedMicroservices = [];
438
447
  for (const microservice of this.connectedMicroservices) {
439
448
  try {
449
+ this.assertApplicationOperationAllowed('start microservices');
440
450
  await microservice.listen();
441
451
  startedMicroservices.push(microservice);
442
452
  } catch (error) {
@@ -467,12 +477,22 @@ class FluoApplication {
467
477
  throw createLifecycleCloseError(errors);
468
478
  }
469
479
  }
480
+ assertApplicationOperationAllowed(operation) {
481
+ if (this.closeStarted) {
482
+ throw new InvariantError(`Application cannot ${operation} after shutdown has started.`);
483
+ }
484
+ }
485
+ assertProviderResolutionAllowed() {
486
+ if (this.closeStarted) {
487
+ throw new InvariantError('Application cannot resolve providers after shutdown has started.');
488
+ }
489
+ }
470
490
 
471
491
  /**
472
492
  * 준비 검사를 통과한 뒤 어댑터에 바인딩을 위임하고 상태를 `ready`로 전이한다.
473
493
  */
474
494
  async listen() {
475
- if (this.closed || this.closingPromise || this.applicationState === 'closed') {
495
+ if (this.closeStarted) {
476
496
  throw new InvariantError('Application cannot listen after it has been closed.');
477
497
  }
478
498
  if (this.applicationState === 'ready') {
@@ -490,7 +510,7 @@ class FluoApplication {
490
510
  }
491
511
  }
492
512
  async startListening() {
493
- if (this.closed || this.closingPromise || this.applicationState === 'closed') {
513
+ if (this.closeStarted) {
494
514
  throw new InvariantError('Application cannot listen after it has been closed.');
495
515
  }
496
516
  if (!this.hasHttpAdapter) {
@@ -503,13 +523,14 @@ class FluoApplication {
503
523
  this.logger.error('Failed to start the HTTP adapter.', error, 'FluoApplication');
504
524
  throw error;
505
525
  }
506
- if (this.closed || this.closingPromise) {
526
+ if (this.closeStarted) {
507
527
  throw new InvariantError('Application startup was interrupted by shutdown.');
508
528
  }
509
529
  this.applicationState = 'ready';
510
530
  this.logger.log('fluo application successfully started.', 'FluoApplication');
511
531
  }
512
532
  dispatch = async (...args) => {
533
+ this.assertApplicationOperationAllowed('dispatch');
513
534
  await this.dispatcher.dispatch(...args);
514
535
  };
515
536
 
@@ -524,6 +545,7 @@ class FluoApplication {
524
545
  await this.closingPromise;
525
546
  return;
526
547
  }
548
+ this.closeStarted = true;
527
549
  this.closingPromise = (async () => {
528
550
  const errors = [];
529
551
  if (this.listenPromise) {
@@ -531,10 +553,13 @@ class FluoApplication {
531
553
  await this.listenPromise;
532
554
  } catch {}
533
555
  }
534
- try {
535
- await this.closeConnectedMicroservices(signal);
536
- } catch (error) {
537
- errors.push(error);
556
+ if (!this.connectedMicroservicesClosed) {
557
+ try {
558
+ await this.closeConnectedMicroservices(signal);
559
+ this.connectedMicroservicesClosed = true;
560
+ } catch (error) {
561
+ errors.push(error);
562
+ }
538
563
  }
539
564
  try {
540
565
  await closeRuntimeResources({
@@ -543,6 +568,7 @@ class FluoApplication {
543
568
  lifecycleInstances: this.lifecycleInstances,
544
569
  modules: this.modules,
545
570
  runtimeCleanup: this.runtimeCleanup,
571
+ shutdownState: this.runtimeShutdownState,
546
572
  signal
547
573
  });
548
574
  } catch (error) {
@@ -564,8 +590,10 @@ class FluoApplication {
564
590
  }
565
591
  class FluoApplicationContext {
566
592
  closed = false;
593
+ closeStarted = false;
567
594
  closingPromise;
568
595
  contextResolutionCache = new Map();
596
+ runtimeShutdownState = createRetryableShutdownState();
569
597
  constructor(container, modules, rootModule, bootstrapTiming, lifecycleInstances, runtimeCleanup, contextCacheableTokens) {
570
598
  this.container = container;
571
599
  this.modules = modules;
@@ -580,7 +608,15 @@ class FluoApplicationContext {
580
608
  if (this.closed) {
581
609
  return this.container.resolve(token);
582
610
  }
583
- return resolveContextToken(this.container, token, this.contextCacheableTokens, this.contextResolutionCache);
611
+ this.assertProviderResolutionAllowed();
612
+ const resolved = await resolveContextToken(this.container, token, this.contextCacheableTokens, this.contextResolutionCache);
613
+ this.assertProviderResolutionAllowed();
614
+ return resolved;
615
+ }
616
+ assertProviderResolutionAllowed() {
617
+ if (this.closeStarted) {
618
+ throw new InvariantError('Application context cannot resolve providers after shutdown has started.');
619
+ }
584
620
  }
585
621
  async close(signal) {
586
622
  if (this.closed) {
@@ -590,12 +626,14 @@ class FluoApplicationContext {
590
626
  await this.closingPromise;
591
627
  return;
592
628
  }
629
+ this.closeStarted = true;
593
630
  this.closingPromise = (async () => {
594
631
  await closeRuntimeResources({
595
632
  container: this.container,
596
633
  lifecycleInstances: this.lifecycleInstances,
597
634
  modules: this.modules,
598
635
  runtimeCleanup: this.runtimeCleanup,
636
+ shutdownState: this.runtimeShutdownState,
599
637
  signal
600
638
  });
601
639
  this.closed = true;
@@ -610,6 +648,7 @@ class FluoApplicationContext {
610
648
  }
611
649
  class FluoMicroserviceApplication {
612
650
  closed = false;
651
+ closeStarted = false;
613
652
  closingPromise;
614
653
  listenPromise;
615
654
  microserviceState = 'bootstrapped';
@@ -635,7 +674,7 @@ class FluoMicroserviceApplication {
635
674
  return this.context.get(token);
636
675
  }
637
676
  async listen() {
638
- if (this.closed || this.closingPromise || this.microserviceState === 'closed') {
677
+ if (this.closeStarted) {
639
678
  throw new InvariantError('Microservice cannot listen after it has been closed.');
640
679
  }
641
680
  if (this.microserviceState === 'ready') {
@@ -653,23 +692,25 @@ class FluoMicroserviceApplication {
653
692
  }
654
693
  }
655
694
  async startListening() {
656
- if (this.closed || this.closingPromise || this.microserviceState === 'closed') {
695
+ if (this.closeStarted) {
657
696
  throw new InvariantError('Microservice cannot listen after it has been closed.');
658
697
  }
659
698
  await this.runtime.listen();
660
- if (this.closed || this.closingPromise) {
699
+ if (this.closeStarted) {
661
700
  throw new InvariantError('Microservice startup was interrupted by shutdown.');
662
701
  }
663
702
  this.microserviceState = 'ready';
664
703
  this.logger.log('fluo microservice successfully started.', 'FluoFactory');
665
704
  }
666
705
  async send(pattern, payload, signal) {
706
+ this.assertTransportIngressOpen('send');
667
707
  if (!this.runtime.send) {
668
708
  throw new InvariantError('Resolved microservice runtime does not implement send().');
669
709
  }
670
710
  return await this.runtime.send(pattern, payload, signal);
671
711
  }
672
712
  async emit(pattern, payload) {
713
+ this.assertTransportIngressOpen('emit');
673
714
  if (!this.runtime.emit) {
674
715
  throw new InvariantError('Resolved microservice runtime does not implement emit().');
675
716
  }
@@ -683,7 +724,15 @@ class FluoMicroserviceApplication {
683
724
  await this.closingPromise;
684
725
  return;
685
726
  }
686
- this.closingPromise = (async () => {
727
+ this.closeStarted = true;
728
+ let resolveClose = () => undefined;
729
+ let rejectClose = () => undefined;
730
+ this.closingPromise = new Promise((resolve, reject) => {
731
+ resolveClose = resolve;
732
+ rejectClose = reject;
733
+ });
734
+ void (async () => {
735
+ this.runtime.markShutdownStarted?.();
687
736
  if (this.listenPromise) {
688
737
  try {
689
738
  await this.listenPromise;
@@ -709,12 +758,12 @@ class FluoMicroserviceApplication {
709
758
  }
710
759
  this.closed = true;
711
760
  this.microserviceState = 'closed';
712
- })();
713
- try {
714
- await this.closingPromise;
715
- } catch (error) {
716
- this.closingPromise = undefined;
717
- throw error;
761
+ })().then(resolveClose, rejectClose);
762
+ await this.closingPromise;
763
+ }
764
+ assertTransportIngressOpen(operation) {
765
+ if (this.closeStarted) {
766
+ throw new InvariantError(`Microservice cannot ${operation} after shutdown has started.`);
718
767
  }
719
768
  }
720
769
  }
@@ -724,14 +773,33 @@ class FluoMicroserviceApplication {
724
773
  */
725
774
  async function resolveLifecycleInstances(container, providers, resolvedInstances = []) {
726
775
  const lifecycleEntries = [];
727
- const seen = new Set();
776
+ const seenSingleTokens = new Set();
777
+ const multiContributionIndexes = new Map();
728
778
  for (const provider of providers) {
729
779
  const token = providerToken(provider);
730
- if (seen.has(token)) {
780
+ if (isMultiProvider(provider)) {
781
+ const contributionIndex = multiContributionIndexes.get(token) ?? 0;
782
+ multiContributionIndexes.set(token, contributionIndex + 1);
783
+ if (isHookBearingValueProvider(provider)) {
784
+ lifecycleEntries.push({
785
+ token,
786
+ useValue: provider.useValue
787
+ });
788
+ continue;
789
+ }
790
+ if (isDirectSingletonContextProvider(provider, true)) {
791
+ lifecycleEntries.push({
792
+ contributionIndex,
793
+ token
794
+ });
795
+ }
796
+ continue;
797
+ }
798
+ if (seenSingleTokens.has(token)) {
731
799
  continue;
732
800
  }
733
801
  if (isHookBearingValueProvider(provider)) {
734
- seen.add(token);
802
+ seenSingleTokens.add(token);
735
803
  lifecycleEntries.push({
736
804
  token,
737
805
  useValue: provider.useValue
@@ -741,17 +809,28 @@ async function resolveLifecycleInstances(container, providers, resolvedInstances
741
809
  if (!isDirectSingletonContextProvider(provider)) {
742
810
  continue;
743
811
  }
744
- seen.add(token);
812
+ seenSingleTokens.add(token);
745
813
  lifecycleEntries.push({
746
814
  token
747
815
  });
748
816
  }
749
- const resolutionResults = await Promise.allSettled(lifecycleEntries.map(entry => entry.useValue ?? container.resolve(entry.token)));
817
+ const resolutionResults = await Promise.allSettled(lifecycleEntries.map(entry => {
818
+ if (entry.useValue !== undefined) {
819
+ return entry.useValue;
820
+ }
821
+ if (entry.contributionIndex === undefined) {
822
+ return container.resolve(entry.token);
823
+ }
824
+ return resolveMultiContribution(container, entry.token, entry.contributionIndex);
825
+ }));
750
826
  let resolutionError;
751
827
  let hasResolutionError = false;
752
- for (const result of resolutionResults) {
828
+ for (const [index, result] of resolutionResults.entries()) {
753
829
  if (result.status === 'fulfilled') {
754
- resolvedInstances.push(result.value);
830
+ const entry = lifecycleEntries[index];
831
+ if (entry.contributionIndex === undefined || hasLifecycleHook(result.value)) {
832
+ resolvedInstances.push(result.value);
833
+ }
755
834
  continue;
756
835
  }
757
836
  if (!hasResolutionError) {
@@ -780,8 +859,8 @@ function createContextCacheableTokenSet(effectiveProviders, runtimeTokens) {
780
859
  }
781
860
  return cacheableTokens;
782
861
  }
783
- function isDirectSingletonContextProvider(provider) {
784
- if (isMultiProvider(provider)) {
862
+ function isDirectSingletonContextProvider(provider, includeMulti = false) {
863
+ if (isMultiProvider(provider) && !includeMulti) {
785
864
  return false;
786
865
  }
787
866
  if (typeof provider === 'function') {
@@ -851,21 +930,34 @@ async function runBootstrapHooks(instances) {
851
930
  * 종료 단계의 hook을 역순으로 실행해 이미 시작한 리소스를 정리한다.
852
931
  */
853
932
  async function runShutdownHooks(instances, signal) {
933
+ const errors = [];
854
934
  for (const instance of [...instances].reverse()) {
855
- if (isOnModuleDestroy(instance)) {
856
- await instance.onModuleDestroy();
935
+ try {
936
+ if (isOnModuleDestroy(instance)) {
937
+ await instance.onModuleDestroy();
938
+ }
939
+ } catch (error) {
940
+ errors.push(error);
857
941
  }
858
942
  }
859
943
  for (const instance of [...instances].reverse()) {
860
- if (isOnApplicationShutdown(instance)) {
861
- await instance.onApplicationShutdown(signal);
944
+ try {
945
+ if (isOnApplicationShutdown(instance)) {
946
+ await instance.onApplicationShutdown(signal);
947
+ }
948
+ } catch (error) {
949
+ errors.push(error);
862
950
  }
863
951
  }
952
+ if (errors.length > 0) {
953
+ throw createLifecycleCloseError(errors);
954
+ }
864
955
  }
865
956
  function createHandlerSources(modules) {
957
+ const globalModules = modules.filter(compiledModule => compiledModule.definition.global);
866
958
  return modules.flatMap(compiledModule => (compiledModule.definition.controllers ?? []).map(controllerToken => ({
867
959
  controllerToken,
868
- moduleMiddleware: compiledModule.definition.middleware ?? [],
960
+ moduleMiddleware: [...globalModules.filter(globalModule => globalModule !== compiledModule).flatMap(globalModule => globalModule.definition.middleware ?? []), ...(compiledModule.definition.middleware ?? [])],
869
961
  moduleType: compiledModule.type
870
962
  })));
871
963
  }
@@ -896,7 +988,11 @@ function logRouteMappings(logger, descriptors) {
896
988
  }
897
989
  }
898
990
  function createRuntimeProviders(options, logger) {
991
+ const bootstrapProviderTokens = new Set((options.providers ?? []).map(providerToken));
899
992
  return [...(options.providers ?? []), {
993
+ provide: BOOTSTRAP_PROVIDER_TOKENS,
994
+ useValue: bootstrapProviderTokens
995
+ }, {
900
996
  provide: APPLICATION_LOGGER,
901
997
  useValue: logger
902
998
  }];
@@ -959,6 +1055,15 @@ function createRuntimeDispatcherOptions(bootstrapped, options, handlerMapping, e
959
1055
  const converters = options.converters ?? [];
960
1056
  const dispatcherOptions = {
961
1057
  appMiddleware: options.middleware ?? [],
1058
+ ...(options.contentNegotiation === undefined ? {} : {
1059
+ contentNegotiation: options.contentNegotiation
1060
+ }),
1061
+ ...(options.conditionalRequest === undefined ? {} : {
1062
+ conditionalRequest: options.conditionalRequest
1063
+ }),
1064
+ ...(options.errorRepresentation === undefined ? {} : {
1065
+ errorRepresentation: options.errorRepresentation
1066
+ }),
962
1067
  handlerMapping,
963
1068
  interceptors: options.interceptors ?? [],
964
1069
  logger,
@@ -995,7 +1100,7 @@ function createRuntimeDispatcher(bootstrapped, options, logger) {
995
1100
  * @throws {Error} Propagates module-graph, lifecycle, or runtime initialization failures.
996
1101
  */
997
1102
  export async function bootstrapApplication(options) {
998
- const studioDevtools = createStudioDevtoolsRuntimeFromConfig();
1103
+ const studioDevtools = options.studioDevtools ?? createStudioDevtoolsRuntimeFromConfig();
999
1104
  const effectiveOptions = applyStudioDevtoolsApplicationOptions(options, studioDevtools);
1000
1105
  const logger = effectiveOptions.logger ?? createDefaultApplicationLogger();
1001
1106
  let lifecycleInstances = [];
@@ -1044,7 +1149,7 @@ export async function bootstrapApplication(options) {
1044
1149
  bootstrappedModules = bootstrapped.modules;
1045
1150
  const resolveLifecycleStart = timingEnabled ? runtimePerformance.now() : 0;
1046
1151
  lifecycleInstances = await resolveBootstrapLifecycleInstances(bootstrapped, lifecycleInstances);
1047
- lifecycleInstances.push({
1152
+ lifecycleInstances.unshift({
1048
1153
  onModuleDestroy() {
1049
1154
  return platformShell.stop();
1050
1155
  }
@@ -1122,7 +1227,7 @@ export class FluoFactory {
1122
1227
  * @throws {Error} Propagates module-graph, lifecycle, and context bootstrap failures.
1123
1228
  */
1124
1229
  static async createApplicationContext(rootModule, options = {}) {
1125
- const studioDevtools = createStudioDevtoolsRuntimeFromConfig();
1230
+ const studioDevtools = options.studioDevtools ?? createStudioDevtoolsRuntimeFromConfig();
1126
1231
  const effectiveOptions = applyStudioDevtoolsContextOptions(options, studioDevtools);
1127
1232
  const logger = createDefaultApplicationLogger();
1128
1233
  let lifecycleInstances = [];
@@ -1166,7 +1271,7 @@ export class FluoFactory {
1166
1271
  bootstrappedModules = bootstrapped.modules;
1167
1272
  const resolveLifecycleStart = timingEnabled ? runtimePerformance.now() : 0;
1168
1273
  lifecycleInstances = await resolveBootstrapLifecycleInstances(bootstrapped, lifecycleInstances);
1169
- lifecycleInstances.push({
1274
+ lifecycleInstances.unshift({
1170
1275
  onModuleDestroy() {
1171
1276
  return platformShell.stop();
1172
1277
  }