@fluojs/testing 2.0.0 → 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 (41) hide show
  1. package/README.ko.md +94 -12
  2. package/README.md +96 -12
  3. package/dist/babel-decorators-plugin.d.ts +1 -0
  4. package/dist/babel-decorators-plugin.d.ts.map +1 -1
  5. package/dist/babel-decorators-plugin.js +1 -0
  6. package/dist/byte-range-portability-consumer.test-fixture.d.ts +2 -0
  7. package/dist/byte-range-portability-consumer.test-fixture.d.ts.map +1 -0
  8. package/dist/byte-range-portability-consumer.test-fixture.js +11 -0
  9. package/dist/conformance/fetch-style-websocket-conformance.d.ts +1 -0
  10. package/dist/conformance/fetch-style-websocket-conformance.d.ts.map +1 -1
  11. package/dist/conformance/fetch-style-websocket-conformance.js +1 -1
  12. package/dist/conformance/platform-shell-lifecycle-conformance.d.ts +30 -0
  13. package/dist/conformance/platform-shell-lifecycle-conformance.d.ts.map +1 -0
  14. package/dist/conformance/platform-shell-lifecycle-conformance.js +259 -0
  15. package/dist/http.d.ts +2 -1
  16. package/dist/http.d.ts.map +1 -1
  17. package/dist/http.js +8 -3
  18. package/dist/mock.js +1 -1
  19. package/dist/module.d.ts.map +1 -1
  20. package/dist/module.js +45 -73
  21. package/dist/portability/error-representation-abort-portability.d.ts +36 -0
  22. package/dist/portability/error-representation-abort-portability.d.ts.map +1 -0
  23. package/dist/portability/error-representation-abort-portability.js +190 -0
  24. package/dist/portability/error-representation-portability-fixture.d.ts +58 -0
  25. package/dist/portability/error-representation-portability-fixture.d.ts.map +1 -0
  26. package/dist/portability/error-representation-portability-fixture.js +202 -0
  27. package/dist/portability/error-representation-portability.d.ts +50 -0
  28. package/dist/portability/error-representation-portability.d.ts.map +1 -0
  29. package/dist/portability/error-representation-portability.js +153 -0
  30. package/dist/portability/http-adapter-portability.d.ts +24 -1
  31. package/dist/portability/http-adapter-portability.d.ts.map +1 -1
  32. package/dist/portability/http-adapter-portability.js +441 -68
  33. package/dist/portability/response-cookie-portability.d.ts +16 -0
  34. package/dist/portability/response-cookie-portability.d.ts.map +1 -0
  35. package/dist/portability/response-cookie-portability.js +81 -0
  36. package/dist/portability/web-runtime-adapter-portability.d.ts +23 -1
  37. package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -1
  38. package/dist/portability/web-runtime-adapter-portability.js +356 -31
  39. package/dist/types.d.ts +5 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +19 -15
@@ -0,0 +1,259 @@
1
+ import { PlatformLifecycleConflictError } from '@fluojs/runtime';
2
+ class Deferred {
3
+ promise;
4
+ settle = () => {
5
+ throw new Error('Deferred promise was not initialized.');
6
+ };
7
+ constructor() {
8
+ this.promise = new Promise(resolve => {
9
+ this.settle = resolve;
10
+ });
11
+ }
12
+ resolve() {
13
+ this.settle();
14
+ }
15
+ }
16
+ class ControlledPlatformComponent {
17
+ id = 'platform-shell-lifecycle-conformance';
18
+ kind = 'platform-shell-lifecycle-conformance';
19
+ entered = {
20
+ start: new Deferred(),
21
+ stop: new Deferred()
22
+ };
23
+ startCalls = 0;
24
+ stopCalls = 0;
25
+ currentState = 'created';
26
+ startFailuresRemaining;
27
+ stopFailuresRemaining;
28
+ constructor(options = {}) {
29
+ this.options = options;
30
+ this.startFailuresRemaining = options.failStartTimes ?? 0;
31
+ this.stopFailuresRemaining = options.failStopTimes ?? 0;
32
+ }
33
+ health() {
34
+ return Promise.resolve({
35
+ status: 'healthy'
36
+ });
37
+ }
38
+ ready() {
39
+ return Promise.resolve({
40
+ critical: true,
41
+ status: 'ready'
42
+ });
43
+ }
44
+ snapshot() {
45
+ return {
46
+ dependencies: [],
47
+ details: {},
48
+ health: {
49
+ status: 'healthy'
50
+ },
51
+ id: this.id,
52
+ kind: this.kind,
53
+ ownership: {
54
+ externallyManaged: false,
55
+ ownsResources: true
56
+ },
57
+ readiness: {
58
+ critical: true,
59
+ status: 'ready'
60
+ },
61
+ state: this.currentState,
62
+ telemetry: {
63
+ namespace: 'fluo.platform-shell-lifecycle-conformance',
64
+ tags: {}
65
+ }
66
+ };
67
+ }
68
+ async start() {
69
+ this.startCalls += 1;
70
+ this.entered.start.resolve();
71
+ await this.options.onStart?.();
72
+ await this.options.startGate;
73
+ if (this.startFailuresRemaining > 0) {
74
+ this.startFailuresRemaining -= 1;
75
+ throw new Error('controlled start failure');
76
+ }
77
+ this.currentState = 'ready';
78
+ }
79
+ state() {
80
+ return this.currentState;
81
+ }
82
+ async stop() {
83
+ this.stopCalls += 1;
84
+ this.entered.stop.resolve();
85
+ await this.options.onStop?.();
86
+ await this.options.stopGate;
87
+ if (this.stopFailuresRemaining > 0) {
88
+ this.stopFailuresRemaining -= 1;
89
+ throw new Error('controlled stop failure');
90
+ }
91
+ this.currentState = 'stopped';
92
+ }
93
+ validate() {
94
+ return Promise.resolve({
95
+ issues: [],
96
+ ok: true
97
+ });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Describes the PlatformShell lifecycle conformance harness options contract.
103
+ */
104
+
105
+ /**
106
+ * Represents lifecycle exclusivity checks for a PlatformShell implementation.
107
+ */
108
+ export class PlatformShellLifecycleConformanceHarness {
109
+ constructor(options) {
110
+ this.options = options;
111
+ }
112
+ async assertAll() {
113
+ await this.assertOverlappingTransitionsRejectImmediately();
114
+ await this.assertCallbackReentryRejects();
115
+ await this.assertSettledTransitionsPermitRetries();
116
+ await this.assertSequentialTransitionsRemainIdempotent();
117
+ }
118
+ async assertOverlappingTransitionsRejectImmediately() {
119
+ const overlapPairs = [['start', 'start'], ['start', 'stop'], ['stop', 'start'], ['stop', 'stop']];
120
+ for (const [active, requested] of overlapPairs) {
121
+ const gate = new Deferred();
122
+ const component = new ControlledPlatformComponent(active === 'start' ? {
123
+ startGate: gate.promise
124
+ } : {
125
+ stopGate: gate.promise
126
+ });
127
+ const shell = this.options.createShell(component);
128
+ if (active === 'stop') {
129
+ await shell.start();
130
+ }
131
+ const activeTransition = shell[active]();
132
+ await component.entered[active].promise;
133
+ try {
134
+ await assertLifecycleConflict(shell[requested](), active, requested);
135
+ } finally {
136
+ gate.resolve();
137
+ }
138
+ await activeTransition;
139
+ if (active === 'start') {
140
+ await shell.stop();
141
+ }
142
+ }
143
+ }
144
+ async assertCallbackReentryRejects() {
145
+ await this.assertSynchronousStartCallbackReentryRejects();
146
+ await this.assertAwaitedStopCallbackReentryRejects();
147
+ }
148
+ async assertSettledTransitionsPermitRetries() {
149
+ await this.assertFailedTransitionPermitsRetry('start');
150
+ await this.assertFailedTransitionPermitsRetry('stop');
151
+ }
152
+ async assertSequentialTransitionsRemainIdempotent() {
153
+ const component = new ControlledPlatformComponent();
154
+ const shell = this.options.createShell(component);
155
+ await shell.start();
156
+ await shell.start();
157
+ await shell.stop();
158
+ await shell.stop();
159
+ if (component.startCalls !== 1 || component.stopCalls !== 1) {
160
+ throw new Error(`Settled lifecycle calls must be idempotent. Received ${component.startCalls} start() and ${component.stopCalls} stop() calls.`);
161
+ }
162
+ }
163
+ async assertSynchronousStartCallbackReentryRejects() {
164
+ let reenter;
165
+ const component = new ControlledPlatformComponent({
166
+ onStart: async () => {
167
+ if (!reenter) {
168
+ throw new Error('Lifecycle reentry was not initialized.');
169
+ }
170
+ await assertLifecycleConflict(reenter(), 'start', 'stop');
171
+ }
172
+ });
173
+ const shell = this.options.createShell(component);
174
+ reenter = () => shell.stop();
175
+ await shell.start();
176
+ await shell.stop();
177
+ }
178
+ async assertAwaitedStopCallbackReentryRejects() {
179
+ let reenter;
180
+ const component = new ControlledPlatformComponent({
181
+ onStop: async () => {
182
+ await Promise.resolve();
183
+ await Promise.resolve();
184
+ if (!reenter) {
185
+ throw new Error('Lifecycle reentry was not initialized.');
186
+ }
187
+ await assertLifecycleConflict(reenter(), 'stop', 'start');
188
+ }
189
+ });
190
+ const shell = this.options.createShell(component);
191
+ reenter = () => shell.start();
192
+ await shell.start();
193
+ await shell.stop();
194
+ }
195
+ async assertFailedTransitionPermitsRetry(operation) {
196
+ const component = new ControlledPlatformComponent(operation === 'start' ? {
197
+ failStartTimes: 1
198
+ } : {
199
+ failStopTimes: 1
200
+ });
201
+ const shell = this.options.createShell(component);
202
+ if (operation === 'stop') {
203
+ await shell.start();
204
+ }
205
+ await expectTransitionFailure(shell[operation](), operation);
206
+ await shell[operation]();
207
+ const callCount = operation === 'start' ? component.startCalls : component.stopCalls;
208
+ if (callCount !== 2) {
209
+ throw new Error(`Failed ${operation}() transitions must permit one explicit retry after settlement.`);
210
+ }
211
+ if (operation === 'start') {
212
+ await shell.stop();
213
+ }
214
+ }
215
+ }
216
+ async function assertLifecycleConflict(transition, active, requested) {
217
+ const outcome = await Promise.race([transition.then(() => ({
218
+ status: 'fulfilled'
219
+ }), error => ({
220
+ error,
221
+ status: 'rejected'
222
+ })), Promise.resolve().then(() => ({
223
+ status: 'pending'
224
+ }))]);
225
+ if (outcome.status === 'pending') {
226
+ throw new Error(`${requested}() must reject with PlatformLifecycleConflictError while ${active}() is active.`);
227
+ }
228
+ if (outcome.status === 'fulfilled') {
229
+ throw new Error(`${requested}() must reject while ${active}() is active.`);
230
+ }
231
+ if (!(outcome.error instanceof PlatformLifecycleConflictError)) {
232
+ throw new Error(`${requested}() must reject with PlatformLifecycleConflictError while ${active}() is active.`);
233
+ }
234
+ const {
235
+ error
236
+ } = outcome;
237
+ const metadata = error.meta;
238
+ if (error.code !== 'PLATFORM_LIFECYCLE_CONFLICT' || error.activeOperation !== active || error.requestedOperation !== requested || metadata?.['activeOperation'] !== active || metadata?.['requestedOperation'] !== requested) {
239
+ throw new Error(`${requested}() returned incorrect PlatformLifecycleConflictError metadata.`);
240
+ }
241
+ }
242
+ async function expectTransitionFailure(transition, operation) {
243
+ try {
244
+ await transition;
245
+ } catch {
246
+ return;
247
+ }
248
+ throw new Error(`The first ${operation}() transition must fail before retry conformance is checked.`);
249
+ }
250
+
251
+ /**
252
+ * Create PlatformShell lifecycle conformance harness.
253
+ *
254
+ * @param options The options.
255
+ * @returns The create PlatformShell lifecycle conformance harness result.
256
+ */
257
+ export function createPlatformShellLifecycleConformanceHarness(options) {
258
+ return new PlatformShellLifecycleConformanceHarness(options);
259
+ }
package/dist/http.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Dispatcher, Middleware } from '@fluojs/http';
1
+ import type { Dispatcher, FrameworkRequest, Middleware } from '@fluojs/http';
2
2
  /**
3
3
  * Principal payload used by testing request helpers.
4
4
  */
@@ -20,6 +20,7 @@ export interface TestRequest {
20
20
  body?: unknown;
21
21
  headers?: Record<string, string>;
22
22
  query?: Record<string, string | string[]>;
23
+ cookies?: FrameworkRequest['cookies'];
23
24
  principal?: TestPrincipal;
24
25
  }
25
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAmD,UAAU,EAAa,MAAM,cAAc,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAAC;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,cAAc,CAAC;IAC7D,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAAC;IAChD,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CAC/B;AA2ED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,sBAAsB,GAAG,cAAc,CAqD5G;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,UAAU,CAa/D;AAuFD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAO5G"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAiC,UAAU,EAAa,MAAM,cAAc,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAAC;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,cAAc,CAAC;IAC7D,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAAC;IAChD,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CAC/B;AA2ED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,sBAAsB,GAAG,cAAc,CAsD5G;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,UAAU,CAa/D;AA4FD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAO5G"}
package/dist/http.js CHANGED
@@ -79,6 +79,9 @@ export function createRequestBuilder(dispatcher, request) {
79
79
  query: request.query ? {
80
80
  ...request.query
81
81
  } : undefined,
82
+ cookies: request.cookies ? {
83
+ ...request.cookies
84
+ } : undefined,
82
85
  principal: request.principal
83
86
  };
84
87
  return {
@@ -162,7 +165,7 @@ function buildFrameworkRequest(req) {
162
165
  url: req.path + queryString,
163
166
  headers: req.headers ?? {},
164
167
  query: req.query ?? {},
165
- cookies: {},
168
+ cookies: req.cookies ?? {},
166
169
  params: {},
167
170
  body: req.body,
168
171
  raw: req,
@@ -197,8 +200,10 @@ function buildFrameworkResponse() {
197
200
  const lowerName = name.toLowerCase();
198
201
  const responseHeaders = this.headers;
199
202
  if (lowerName === 'set-cookie') {
200
- result.headers[name] = mergeHeaderValue(result.headers[name], value);
201
- responseHeaders[name] = mergeHeaderValue(responseHeaders[name], value);
203
+ const existingHeaderName = Object.keys(result.headers).find(headerName => headerName.toLowerCase() === lowerName);
204
+ const canonicalHeaderName = existingHeaderName ?? name;
205
+ result.headers[canonicalHeaderName] = mergeHeaderValue(result.headers[canonicalHeaderName], value);
206
+ responseHeaders[canonicalHeaderName] = mergeHeaderValue(responseHeaders[canonicalHeaderName], value);
202
207
  return;
203
208
  }
204
209
  result.headers[name] = value;
package/dist/mock.js CHANGED
@@ -38,7 +38,7 @@ export function createMock(partial = {}, options = {}) {
38
38
  * @returns The as mock result.
39
39
  */
40
40
  export function asMock(fn) {
41
- return vi.mocked(fn);
41
+ return fn;
42
42
  }
43
43
 
44
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EAKd,KAAK,QAAQ,EAGd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAGV,UAAU,EACX,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EAA2B,oBAAoB,EAAE,oBAAoB,EAAoB,MAAM,YAAY,CAAC;AAYxH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,EAAE,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAQzE;AAyuBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;;CAEhB,CAAC"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,SAAS,EAOd,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAGV,UAAU,EACX,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAA2B,oBAAoB,EAAE,oBAAoB,EAAoB,MAAM,YAAY,CAAC;AAYxH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,EAAE,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAQzE;AA2rBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;;CAEhB,CAAC"}
package/dist/module.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getModuleMetadata } from '@fluojs/core';
2
- import { isForwardRef, isOptionalToken, RequestScopeResolutionError, Scope } from '@fluojs/di';
3
- import { bootstrapModule } from '@fluojs/runtime';
2
+ import { isForwardRef, isOptionalToken, Scope } from '@fluojs/di';
3
+ import { resolveMultiContribution } from '@fluojs/di/internal';
4
4
  import { createDispatcher, createHandlerMapping } from '@fluojs/http';
5
+ import { bootstrapModule } from '@fluojs/runtime';
5
6
  import { createTestRequestContextMiddleware, makeRequest } from './http.js';
6
7
  /**
7
8
  * Returns providers declared on a module metadata definition.
@@ -118,33 +119,7 @@ function isSingletonLifecycleProvider(provider) {
118
119
  if (provider.scope !== Scope.DEFAULT) {
119
120
  return false;
120
121
  }
121
- return provider.type === 'class' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
122
- }
123
- async function resolveLifecycleDependency(entry, bootstrapped, introspection) {
124
- if (isOptionalToken(entry) && !hasTokenInContainer(introspection, entry.token)) {
125
- return undefined;
126
- }
127
- return bootstrapped.container.resolve(dependencyToken(entry));
128
- }
129
- async function instantiateLifecycleProvider(provider, bootstrapped, introspection) {
130
- if (provider.type !== 'class' || !provider.useClass) {
131
- throw new Error(`Lifecycle provider ${String(provider.provide)} must use a class provider.`);
132
- }
133
- const dependencies = await Promise.all(provider.inject.map(entry => resolveLifecycleDependency(entry, bootstrapped, introspection)));
134
- return new provider.useClass(...dependencies);
135
- }
136
- async function resolveMultiLifecycleProvider(provider, bootstrapped, introspection) {
137
- const rootIntrospection = rootContainerIntrospection(introspection);
138
- const cached = rootIntrospection.multiSingletonCache.get(provider);
139
- if (cached) {
140
- return cached;
141
- }
142
- const instance = instantiateLifecycleProvider(provider, bootstrapped, introspection).catch(error => {
143
- rootIntrospection.cacheOwner.deleteMultiSingleton(provider);
144
- throw error;
145
- });
146
- rootIntrospection.cacheOwner.setMultiSingleton(provider, instance);
147
- return instance;
122
+ return provider.type === 'class' || provider.type === 'factory' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
148
123
  }
149
124
  async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
150
125
  const lifecycleProviders = [...bootstrapped.effectiveProviders.runtimeProviders, ...bootstrapped.effectiveProviders.moduleProviders, ...overrides];
@@ -154,35 +129,27 @@ async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
154
129
  for (const provider of lifecycleProviders) {
155
130
  const token = providerToken(provider);
156
131
  const effectiveProviders = effectiveProvidersForToken(introspection, token);
157
- for (let index = 0; index < effectiveProviders.length; index += 1) {
158
- const effectiveProvider = effectiveProviders[index];
159
- if (!effectiveProvider) {
160
- continue;
161
- }
162
- if (seenProviders.has(effectiveProvider)) {
163
- continue;
164
- }
165
- seenProviders.add(effectiveProvider);
166
- if (!isSingletonLifecycleProvider(effectiveProvider)) {
167
- continue;
168
- }
169
- if (effectiveProvider.type === 'value') {
170
- instances.push(effectiveProvider.useValue);
171
- continue;
172
- }
173
- try {
174
- if (effectiveProvider.multi === true) {
175
- instances.push(await resolveMultiLifecycleProvider(effectiveProvider, bootstrapped, introspection));
176
- continue;
177
- }
178
- instances.push(await bootstrapped.container.resolve(token));
179
- } catch (error) {
180
- if (error instanceof RequestScopeResolutionError) {
181
- continue;
182
- }
183
- throw error;
184
- }
132
+ const nextUnseenProviderIndex = effectiveProviders.findIndex(effectiveProvider => !seenProviders.has(effectiveProvider));
133
+ if (nextUnseenProviderIndex === -1) {
134
+ continue;
135
+ }
136
+ const effectiveProvider = effectiveProviders[nextUnseenProviderIndex];
137
+ if (!effectiveProvider) {
138
+ continue;
185
139
  }
140
+ seenProviders.add(effectiveProvider);
141
+ if (!isSingletonLifecycleProvider(effectiveProvider)) {
142
+ continue;
143
+ }
144
+ if (effectiveProvider.type === 'value') {
145
+ instances.push(effectiveProvider.useValue);
146
+ continue;
147
+ }
148
+ if (effectiveProvider.multi === true) {
149
+ instances.push(await resolveMultiContribution(bootstrapped.container, token, nextUnseenProviderIndex));
150
+ continue;
151
+ }
152
+ instances.push(await bootstrapped.container.resolve(token));
186
153
  }
187
154
  return instances;
188
155
  }
@@ -540,24 +507,29 @@ class DefaultTestingModuleBuilder {
540
507
  return this;
541
508
  }
542
509
  async compile() {
543
- const bootstrapped = this.bootstrapTestingModule();
544
- const syncResolver = createSyncResolver(bootstrapped.container);
545
- await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
546
- await syncResolver.syncFromContainer();
547
- const resolve = bootstrapped.container.resolve.bind(bootstrapped.container);
548
- bootstrapped.container.resolve = async token => {
549
- const value = await resolve(token);
550
- await syncResolver.syncFromContainer();
551
- return value;
552
- };
553
- return this.createTestingModuleRef(bootstrapped, syncResolver);
554
- }
555
- bootstrapTestingModule() {
556
510
  const bootstrapped = this.bootstrapWithModuleReplacements();
557
- if (this.overrides.length > 0) {
558
- bootstrapped.container.override(...this.overrides);
511
+ try {
512
+ if (this.overrides.length > 0) {
513
+ bootstrapped.container.override(...this.overrides);
514
+ }
515
+ const syncResolver = createSyncResolver(bootstrapped.container);
516
+ await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
517
+ await syncResolver.syncFromContainer();
518
+ const resolve = bootstrapped.container.resolve.bind(bootstrapped.container);
519
+ bootstrapped.container.resolve = async token => {
520
+ const value = await resolve(token);
521
+ await syncResolver.syncFromContainer();
522
+ return value;
523
+ };
524
+ return this.createTestingModuleRef(bootstrapped, syncResolver);
525
+ } catch (error) {
526
+ try {
527
+ await bootstrapped.container.dispose();
528
+ } catch (cleanupError) {
529
+ throw new AggregateError([error, cleanupError], 'Testing module compilation and container cleanup both failed.');
530
+ }
531
+ throw error;
559
532
  }
560
- return bootstrapped;
561
533
  }
562
534
  bootstrapWithModuleReplacements() {
563
535
  return bootstrapModule(this.options.rootModule, this.createBootstrapModuleOptions());
@@ -0,0 +1,36 @@
1
+ import { type ModuleType } from '@fluojs/runtime';
2
+ import type { NetworkHttpErrorRepresentationBootstrapOptions, WebHttpErrorRepresentationBootstrapOptions } from './error-representation-portability.js';
3
+ type NetworkApp = {
4
+ close(): Promise<void>;
5
+ listen(): Promise<void>;
6
+ };
7
+ type WebApp = {
8
+ close(): Promise<void>;
9
+ dispatch(request: Request): Promise<Response>;
10
+ };
11
+ type NetworkHarnessOptions<TBootstrapOptions extends object, TApp extends NetworkApp> = {
12
+ readonly bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
13
+ readonly createBootstrapOptions: (options: NetworkHttpErrorRepresentationBootstrapOptions) => TBootstrapOptions;
14
+ readonly name: string;
15
+ };
16
+ type WebHarnessOptions<TBootstrapOptions extends object, TApp extends WebApp> = {
17
+ readonly bootstrap: (rootModule: ModuleType, options: TBootstrapOptions) => Promise<TApp>;
18
+ readonly createBootstrapOptions: (options: WebHttpErrorRepresentationBootstrapOptions) => TBootstrapOptions;
19
+ readonly name: string;
20
+ };
21
+ /**
22
+ * Verifies that a disconnected network request commits neither HTML nor canonical JSON fallback.
23
+ *
24
+ * @param options Adapter bootstrap and identity callbacks.
25
+ * @returns A promise that resolves after native disconnect and no-representation-write checks pass.
26
+ */
27
+ export declare function assertNetworkHttpErrorRepresentationAbortPortability<TBootstrapOptions extends object, TApp extends NetworkApp>(options: NetworkHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
28
+ /**
29
+ * Verifies that an aborted Web request commits neither HTML nor canonical JSON fallback.
30
+ *
31
+ * @param options Adapter bootstrap and identity callbacks.
32
+ * @returns A promise that resolves after Web abort and no-representation-write checks pass.
33
+ */
34
+ export declare function assertWebHttpErrorRepresentationAbortPortability<TBootstrapOptions extends object, TApp extends WebApp>(options: WebHarnessOptions<TBootstrapOptions, TApp>): Promise<void>;
35
+ export {};
36
+ //# sourceMappingURL=error-representation-abort-portability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-representation-abort-portability.d.ts","sourceRoot":"","sources":["../../src/portability/error-representation-abort-portability.ts"],"names":[],"mappings":"AAKA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,KAAK,EACV,8CAA8C,EAC9C,0CAA0C,EAC3C,MAAM,uCAAuC,CAAC;AAE/C,KAAK,UAAU,GAAG;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C,CAAC;AAEF,KAAK,qBAAqB,CAAC,iBAAiB,SAAS,MAAM,EAAE,IAAI,SAAS,UAAU,IAAI;IACtF,QAAQ,CAAC,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,QAAQ,CAAC,sBAAsB,EAAE,CAC/B,OAAO,EAAE,8CAA8C,KACpD,iBAAiB,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,iBAAiB,CAAC,iBAAiB,SAAS,MAAM,EAAE,IAAI,SAAS,MAAM,IAAI;IAC9E,QAAQ,CAAC,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,QAAQ,CAAC,sBAAsB,EAAE,CAC/B,OAAO,EAAE,0CAA0C,KAChD,iBAAiB,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAoKF;;;;;GAKG;AACH,wBAAsB,oDAAoD,CACxE,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,UAAU,EACvB,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BxE;AAED;;;;;GAKG;AACH,wBAAsB,gDAAgD,CACpE,iBAAiB,SAAS,MAAM,EAChC,IAAI,SAAS,MAAM,EACnB,OAAO,EAAE,iBAAiB,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBpE"}