@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.6 → 3.9.0-ultramodern.7

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.
@@ -1,16 +1,10 @@
1
1
  import {
2
- CrossOriginEnvelopePolicyError,
3
2
  configure,
4
3
  createRequest,
5
- IdentityBindingViolationError,
6
- OperationContractViolationError,
7
- ProducerClientNotInitializedError,
8
- ProducerDomainNotConfiguredError,
9
4
  } from '@modern-js/runtime-extensions/request-policy/client';
10
5
  import nock from 'nock';
11
6
 
12
7
  describe('configure', () => {
13
- const url = 'http://localhost:8080';
14
8
  const path = '/api';
15
9
  const method = 'GET';
16
10
  const response = {
@@ -20,238 +14,6 @@ describe('configure', () => {
20
14
  },
21
15
  };
22
16
 
23
- // TODO: 如果 disableNetConnect 之后,会影响到其他的 testcase 偶发性的出现 NetConnectNotAllowedError: Nock: Disallowed net connect for "127.0.0.1:49552/" 的错误
24
- // beforeEach(() => {
25
- // nock.disableNetConnect();
26
- // });
27
-
28
- // afterEach(() => {
29
- // nock.cleanAll();
30
- // });
31
-
32
- test('should throw when non-default requestId is used before bootstrap', async () => {
33
- const request = createRequest({
34
- path,
35
- method,
36
- port: 8080,
37
- requestId: 'producer-app',
38
- });
39
-
40
- await expect(request()).rejects.toBeInstanceOf(
41
- ProducerClientNotInitializedError,
42
- );
43
- });
44
-
45
- test('should require setDomain when configuring non-default requestId', () => {
46
- expect(() =>
47
- configure({
48
- requestId: 'producer-a',
49
- }),
50
- ).toThrow(ProducerDomainNotConfiguredError);
51
- });
52
-
53
- test('should isolate custom request by requestId', async () => {
54
- const producerA = 'producer-a';
55
- const producerB = 'producer-b';
56
- const urlA = 'http://localhost:9081';
57
- const urlB = 'http://localhost:9082';
58
-
59
- nock(urlA).get(path).reply(200, response);
60
- nock(urlB).get(path).reply(200, response);
61
-
62
- const customRequestA = rs.fn((requestPath: RequestInfo) =>
63
- fetch(requestPath as string),
64
- );
65
-
66
- const customRequestB = rs.fn((requestPath: RequestInfo) =>
67
- fetch(requestPath as string),
68
- );
69
-
70
- configure({
71
- request: customRequestA,
72
- requestId: producerA,
73
- operationContract: {
74
- enabled: false,
75
- },
76
- allowCrossOriginEnvelope: true,
77
- setDomain: () => urlA,
78
- });
79
- configure({
80
- request: customRequestB,
81
- requestId: producerB,
82
- operationContract: {
83
- enabled: false,
84
- },
85
- allowCrossOriginEnvelope: true,
86
- setDomain: () => urlB,
87
- });
88
-
89
- const requestA = createRequest(
90
- path,
91
- method,
92
- 8080,
93
- undefined,
94
- undefined,
95
- producerA,
96
- );
97
- const requestB = createRequest(
98
- path,
99
- method,
100
- 8080,
101
- undefined,
102
- undefined,
103
- producerB,
104
- );
105
-
106
- const resA = await requestA();
107
- const resB = await requestB();
108
-
109
- expect(customRequestA).toHaveBeenCalledTimes(1);
110
- expect(customRequestB).toHaveBeenCalledTimes(1);
111
- expect(resA instanceof Response).toBe(true);
112
- expect(resB instanceof Response).toBe(true);
113
- });
114
-
115
- test('should reject client identity headers by default for non-default producer clients', async () => {
116
- const producer = 'producer-browser-identity-strip';
117
- const producerUrl = 'http://localhost:9083';
118
-
119
- nock(producerUrl).get(path).reply(200, response);
120
- const customRequest = rs.fn(
121
- (requestPath: RequestInfo, init?: RequestInit) =>
122
- fetch(requestPath, init),
123
- );
124
-
125
- configure({
126
- request: customRequest,
127
- requestId: producer,
128
- operationContract: {
129
- enabled: false,
130
- },
131
- setDomain: () => producerUrl,
132
- });
133
-
134
- const request = createRequest(
135
- path,
136
- method,
137
- 8080,
138
- undefined,
139
- undefined,
140
- producer,
141
- );
142
- await expect(
143
- request({
144
- headers: {
145
- 'x-tenant-id': 'tenant-client',
146
- },
147
- }),
148
- ).rejects.toBeInstanceOf(IdentityBindingViolationError);
149
- });
150
-
151
- test('should support derived identity binding headers when explicitly configured', async () => {
152
- const producer = 'producer-browser-identity-derived';
153
- const producerUrl = 'http://localhost:9084';
154
-
155
- nock(producerUrl).get(path).reply(200, response);
156
- const customRequest = rs.fn(
157
- (requestPath: RequestInfo, init?: RequestInit) =>
158
- fetch(requestPath, init),
159
- );
160
-
161
- configure({
162
- request: customRequest,
163
- requestId: producer,
164
- operationContract: {
165
- enabled: false,
166
- },
167
- allowCrossOriginEnvelope: true,
168
- setDomain: () => producerUrl,
169
- identityBinding: {
170
- strict: false,
171
- deriveHeaders: () => ({
172
- 'x-tenant-id': 'tenant-derived',
173
- }),
174
- },
175
- });
176
-
177
- const request = createRequest(
178
- path,
179
- method,
180
- 8080,
181
- undefined,
182
- undefined,
183
- producer,
184
- );
185
- await request();
186
-
187
- const sentHeaders = customRequest.mock.calls[0][1].headers as Record<
188
- string,
189
- string
190
- >;
191
- expect(sentHeaders['x-tenant-id']).toBe('tenant-derived');
192
- });
193
-
194
- test('should reject client identity override in strict identity binding mode', async () => {
195
- const producer = 'producer-browser-identity-strict';
196
- configure({
197
- requestId: producer,
198
- setDomain: () => 'http://localhost:9085',
199
- operationContract: {
200
- enabled: false,
201
- },
202
- identityBinding: {
203
- strict: true,
204
- },
205
- });
206
-
207
- const request = createRequest(
208
- path,
209
- method,
210
- 8080,
211
- undefined,
212
- undefined,
213
- producer,
214
- );
215
-
216
- await expect(
217
- request({
218
- headers: {
219
- 'x-subject-id': 'subject-client',
220
- },
221
- }),
222
- ).rejects.toBeInstanceOf(IdentityBindingViolationError);
223
- });
224
-
225
- test('should require envelope and block cross-origin producer calls in production by default', async () => {
226
- const producer = 'producer-browser-envelope';
227
- const previousEnv = process.env.NODE_ENV;
228
- process.env.NODE_ENV = 'production';
229
-
230
- try {
231
- configure({
232
- requestId: producer,
233
- operationContract: {
234
- enabled: false,
235
- },
236
- setDomain: () => 'https://producer.internal',
237
- });
238
- const request = createRequest(
239
- path,
240
- method,
241
- 8080,
242
- undefined,
243
- undefined,
244
- producer,
245
- );
246
-
247
- await expect(request()).rejects.toBeInstanceOf(
248
- CrossOriginEnvelopePolicyError,
249
- );
250
- } finally {
251
- process.env.NODE_ENV = previousEnv;
252
- }
253
- });
254
-
255
17
  test('should allow explicit cross-origin policy and attach envelope header', async () => {
256
18
  const producer = 'producer-browser-policy';
257
19
  const producerUrl = 'https://producer.internal';
@@ -298,107 +60,6 @@ describe('configure', () => {
298
60
  }
299
61
  });
300
62
 
301
- test('should attach operation context headers for non-default producer client', async () => {
302
- const producer = 'crm.producer-a';
303
- const producerUrl = 'http://localhost:18080';
304
- const traceparent =
305
- '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
306
- nock(producerUrl).get(path).reply(200, response);
307
-
308
- const customRequest = rs.fn(
309
- (requestPath: RequestInfo, init?: RequestInit) =>
310
- fetch(requestPath, init),
311
- );
312
-
313
- configure({
314
- request: customRequest,
315
- requestId: producer,
316
- allowCrossOriginEnvelope: true,
317
- setDomain: () => producerUrl,
318
- });
319
-
320
- const request = createRequest(
321
- path,
322
- method,
323
- 8080,
324
- undefined,
325
- undefined,
326
- producer,
327
- {
328
- traceparent,
329
- operationId: `GET:${path}`,
330
- routePath: path,
331
- method,
332
- schemaHash: 'schema-test',
333
- operationVersion: 1,
334
- },
335
- );
336
- await request();
337
-
338
- const headers = customRequest.mock.calls[0][1].headers as Record<
339
- string,
340
- string
341
- >;
342
- expect(headers['x-operation-id']).toBe(`${producer}:GET:${path}`);
343
- expect(headers.traceparent).toBe(traceparent);
344
- const operationContext = JSON.parse(
345
- headers['x-modernjs-bff-operation-context'],
346
- );
347
- expect(operationContext.requestId).toBe(producer);
348
- expect(operationContext.operationId).toBe(`${producer}:GET:${path}`);
349
- expect(operationContext.traceparent).toBe(traceparent);
350
- expect(operationContext.traceId).toBe('4bf92f3577b34da6a3ce929d0e0e4736');
351
- expect(operationContext.spanId).toBe('00f067aa0ba902b7');
352
- });
353
-
354
- test('should reject requests missing schema/version operation contract metadata by default', async () => {
355
- const producer = 'producer-browser-operation-contract-default';
356
- configure({
357
- requestId: producer,
358
- allowCrossOriginEnvelope: true,
359
- setDomain: () => 'http://localhost:9086',
360
- });
361
-
362
- const request = createRequest(
363
- path,
364
- method,
365
- 8080,
366
- undefined,
367
- undefined,
368
- producer,
369
- );
370
-
371
- await expect(request()).rejects.toBeInstanceOf(
372
- OperationContractViolationError,
373
- );
374
- });
375
-
376
- test('should enforce operation contract metadata for default requestId when strict-default mode is enabled', async () => {
377
- process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID = 'true';
378
- try {
379
- configure({
380
- operationContract: {
381
- enabled: true,
382
- strict: true,
383
- requireSchemaHash: true,
384
- requireOperationVersion: true,
385
- },
386
- });
387
-
388
- const request = createRequest(path, method, 8080, undefined);
389
- await expect(request()).rejects.toBeInstanceOf(
390
- OperationContractViolationError,
391
- );
392
- } finally {
393
- configure({
394
- operationContract: {
395
- enabled: false,
396
- },
397
- });
398
- delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
399
- }
400
- });
401
-
402
63
  test('should not require process to exist in browser runtime', async () => {
403
64
  const previousProcess = globalThis.process;
404
65
  const customRequest = rs.fn(() =>
@@ -444,165 +105,4 @@ describe('configure', () => {
444
105
  });
445
106
  }
446
107
  });
447
-
448
- test('should retry with backoff and emit degraded telemetry events', async () => {
449
- rs.useFakeTimers();
450
- const onDegraded = rs.fn();
451
- let attempts = 0;
452
-
453
- const customRequest = rs.fn(async () => {
454
- attempts += 1;
455
- if (attempts < 3) {
456
- const error: any = new Error('temporary upstream error');
457
- error.status = 503;
458
- throw error;
459
- }
460
-
461
- return response;
462
- });
463
-
464
- try {
465
- configure({
466
- request: customRequest,
467
- transport: {
468
- retry: {
469
- retries: 2,
470
- baseDelayMs: 20,
471
- maxDelayMs: 20,
472
- jitterRatio: 0,
473
- },
474
- onDegraded,
475
- },
476
- });
477
-
478
- const request = createRequest(path, method, 8080, undefined);
479
- const pending = request();
480
-
481
- await Promise.resolve();
482
- await rs.advanceTimersByTimeAsync(20);
483
- await Promise.resolve();
484
- await rs.advanceTimersByTimeAsync(20);
485
- await Promise.resolve();
486
-
487
- await expect(pending).resolves.toStrictEqual(response);
488
- expect(customRequest).toHaveBeenCalledTimes(3);
489
- expect(onDegraded).toHaveBeenCalledWith(
490
- expect.objectContaining({
491
- reason: 'retry',
492
- attempt: 1,
493
- maxAttempts: 3,
494
- backoffMs: 20,
495
- }),
496
- );
497
- expect(onDegraded).toHaveBeenCalledWith(
498
- expect.objectContaining({
499
- reason: 'retry',
500
- attempt: 2,
501
- maxAttempts: 3,
502
- backoffMs: 20,
503
- }),
504
- );
505
- } finally {
506
- rs.useRealTimers();
507
- }
508
- });
509
-
510
- test('should emit retry_exhausted when retry budget is consumed', async () => {
511
- rs.useFakeTimers();
512
- const onDegraded = rs.fn();
513
- const customRequest = rs.fn(async () => {
514
- const error: any = new Error('upstream unavailable');
515
- error.status = 503;
516
- throw error;
517
- });
518
-
519
- try {
520
- configure({
521
- request: customRequest,
522
- transport: {
523
- retry: {
524
- retries: 1,
525
- baseDelayMs: 10,
526
- maxDelayMs: 10,
527
- jitterRatio: 0,
528
- },
529
- onDegraded,
530
- },
531
- });
532
-
533
- const request = createRequest(path, method, 8080, undefined);
534
- const pending = request();
535
- const failure = expect(pending).rejects.toThrow('upstream unavailable');
536
-
537
- await Promise.resolve();
538
- await rs.advanceTimersByTimeAsync(10);
539
- await failure;
540
-
541
- expect(customRequest).toHaveBeenCalledTimes(2);
542
- expect(onDegraded).toHaveBeenCalledWith(
543
- expect.objectContaining({
544
- reason: 'retry',
545
- attempt: 1,
546
- maxAttempts: 2,
547
- }),
548
- );
549
- expect(onDegraded).toHaveBeenCalledWith(
550
- expect.objectContaining({
551
- reason: 'retry_exhausted',
552
- attempt: 2,
553
- maxAttempts: 2,
554
- }),
555
- );
556
- } finally {
557
- rs.useRealTimers();
558
- }
559
- });
560
-
561
- test('should abort timed out requests and emit timeout degraded event', async () => {
562
- rs.useFakeTimers();
563
- const onDegraded = rs.fn();
564
-
565
- const customRequest = rs.fn(
566
- (_requestPath: RequestInfo, init?: RequestInit) => {
567
- return new Promise((_, reject) => {
568
- init?.signal?.addEventListener('abort', () => {
569
- const error: any = new Error('aborted');
570
- error.name = 'AbortError';
571
- reject(error);
572
- });
573
- });
574
- },
575
- );
576
-
577
- try {
578
- configure({
579
- request: customRequest,
580
- transport: {
581
- timeoutMs: 50,
582
- onDegraded,
583
- },
584
- });
585
-
586
- const request = createRequest(path, method, 8080, undefined);
587
- const pending = request();
588
- const failure = expect(pending).rejects.toMatchObject({
589
- name: 'TimeoutError',
590
- });
591
-
592
- await rs.advanceTimersByTimeAsync(50);
593
- await failure;
594
-
595
- expect(customRequest).toHaveBeenCalledTimes(1);
596
- expect(onDegraded).toHaveBeenCalledWith(
597
- expect.objectContaining({
598
- reason: 'timeout',
599
- timeoutMs: 50,
600
- attempt: 1,
601
- maxAttempts: 1,
602
- }),
603
- );
604
- } finally {
605
- rs.useRealTimers();
606
- }
607
- });
608
108
  });
@@ -1,6 +1,4 @@
1
1
  import { createClient as createNativeClient } from '@modern-js/create-request/server';
2
- import * as context from '@modern-js/runtime-extensions/request-context';
3
- import * as canonical from '@modern-js/runtime-extensions/request-policy';
4
2
  import * as server from '@modern-js/runtime-extensions/request-policy/server';
5
3
 
6
4
  const options = {
@@ -108,42 +106,4 @@ describe('isolated producer policy clients', () => {
108
106
  );
109
107
  expect(replacement).not.toHaveBeenCalled();
110
108
  });
111
-
112
- test('shares source configure across canonical require/import and explicit server', async () => {
113
- const required: typeof canonical = require('@modern-js/runtime-extensions/request-policy');
114
- expect(required.configure).toBe(canonical.configure);
115
- expect(server.configure).toBe(canonical.configure);
116
- const request = rs.fn(async () => new Response('shared'));
117
- required.configure({
118
- requestId: options.requestId,
119
- request,
120
- setDomain: () => 'https://shared.example',
121
- operationContract: { enabled: false },
122
- });
123
- expect(await (await canonical.createRequest(options)()).text()).toBe(
124
- 'shared',
125
- );
126
- canonical.configure({
127
- requestId: options.requestId,
128
- request,
129
- setDomain: () => 'https://updated.example',
130
- operationContract: { enabled: false },
131
- });
132
- await required.createRequest(options)();
133
- expect(request).toHaveBeenLastCalledWith(
134
- 'https://updated.example/api',
135
- expect.any(Object),
136
- );
137
- });
138
-
139
- test('exports universal context helpers without a request client', () => {
140
- expect(context.createRequestContextHeaders).toBe(
141
- canonical.createRequestContextHeaders,
142
- );
143
- expect(context.parseTraceparent).toBe(canonical.parseTraceparent);
144
- expect(context).not.toHaveProperty('configure');
145
- expect(context).not.toHaveProperty('createClient');
146
- expect(context).not.toHaveProperty('createRequest');
147
- expect(context).not.toHaveProperty('createUploader');
148
- });
149
109
  });