@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.5 → 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.
Files changed (31) hide show
  1. package/dist/cjs/routerProvider.js +9 -74
  2. package/dist/esm/routerProvider.mjs +8 -67
  3. package/dist/esm-node/routerProvider.mjs +8 -67
  4. package/dist/types/routerProvider.d.ts +2 -24
  5. package/dist/types/server-config/bffRuntime.d.ts +2 -2
  6. package/dist/types/server-config/serverTelemetry.d.ts +27 -27
  7. package/package.json +4 -4
  8. package/src/routerProvider.ts +14 -194
  9. package/src/server-config/bffRuntime.ts +2 -2
  10. package/src/server-config/serverTelemetry.ts +27 -27
  11. package/tests/buildIdentity.test.ts +0 -19
  12. package/tests/contextExtensions.test.ts +0 -44
  13. package/tests/headRuntime.test.tsx +0 -140
  14. package/tests/rendererHead.test.ts +0 -41
  15. package/tests/request-policy/browser-policy.test.ts +0 -500
  16. package/tests/request-policy/client-isolation.test.ts +0 -40
  17. package/tests/request-policy/node-policy.test.ts +0 -294
  18. package/tests/request-policy/policyCore.test.ts +0 -121
  19. package/tests/request-policy/requestContext.test.ts +8 -22
  20. package/tests/request-policy/requestFactory.outbound-contract.test.ts +109 -510
  21. package/tests/request-policy/traceparent.test.ts +2 -5
  22. package/tests/request-policy/transport-retry.test.ts +44 -47
  23. package/tests/routerProvider.test.ts +2 -11
  24. package/tests/routerState.test.ts +0 -79
  25. package/tests/routerStatePlugin.test.ts +1 -46
  26. package/tests/runtimeContextProjection.test.ts +0 -52
  27. package/tests/safeFailure.test.ts +10 -28
  28. package/tests/workspaceEvents.test.ts +31 -201
  29. package/tests/nativeSafeFailureCompatibility.test.ts +0 -117
  30. package/tests/prefetchPolicy.test.ts +0 -51
  31. package/tests/request-policy/identityBinding.test.ts +0 -75
@@ -11,7 +11,6 @@ import { storage } from '@modern-js/runtime-utils/node';
11
11
  import nock from 'nock';
12
12
 
13
13
  describe('configure', () => {
14
- const url = 'http://127.0.0.1:8080';
15
14
  const path = '/api';
16
15
  const method = 'GET';
17
16
  const response = {
@@ -88,99 +87,6 @@ describe('configure', () => {
88
87
  ).toThrow(ProducerDomainNotConfiguredError);
89
88
  });
90
89
 
91
- test('should isolate custom request by requestId', async () => {
92
- const producerA = 'producer-a';
93
- const producerB = 'producer-b';
94
- const urlA = 'http://127.0.0.1:9081';
95
- const urlB = 'http://127.0.0.1:9082';
96
-
97
- nock(urlA).get(path).reply(200, response);
98
- nock(urlB).get(path).reply(200, response);
99
-
100
- const customRequestA = rs.fn((requestPath: any) => fetch(requestPath));
101
- const customRequestB = rs.fn((requestPath: any) => fetch(requestPath));
102
-
103
- configure({
104
- request: customRequestA as unknown as typeof fetch,
105
- requestId: producerA,
106
- operationContract: {
107
- enabled: false,
108
- },
109
- setDomain: () => urlA,
110
- });
111
- configure({
112
- request: customRequestB as unknown as typeof fetch,
113
- requestId: producerB,
114
- operationContract: {
115
- enabled: false,
116
- },
117
- setDomain: () => urlB,
118
- });
119
-
120
- const requestA = createRequest({
121
- path,
122
- method,
123
- port: 8080,
124
- requestId: producerA,
125
- });
126
- const requestB = createRequest({
127
- path,
128
- method,
129
- port: 8080,
130
- requestId: producerB,
131
- });
132
-
133
- const resA = await requestA();
134
- const resB = await requestB();
135
-
136
- expect(customRequestA).toHaveBeenCalledTimes(1);
137
- expect(customRequestB).toHaveBeenCalledTimes(1);
138
- expect(resA instanceof Response).toBe(true);
139
- expect(resB instanceof Response).toBe(true);
140
- });
141
-
142
- test('should propagate allowed headers for non-default requestId', async () => {
143
- const producer = 'producer-non-default';
144
- const authKey = 'token-abc';
145
- const producerUrl = 'http://127.0.0.1:9083';
146
-
147
- await run(
148
- {
149
- authorization: authKey,
150
- },
151
- async () => {
152
- nock(producerUrl, {
153
- reqheaders: {
154
- authorization: authKey,
155
- },
156
- })
157
- .get(path)
158
- .reply(200, response);
159
-
160
- configure({
161
- requestId: producer,
162
- allowedHeaders: ['authorization'],
163
- operationContract: {
164
- enabled: false,
165
- },
166
- setDomain: () => producerUrl,
167
- });
168
-
169
- const request = createRequest(
170
- path,
171
- method,
172
- 8080,
173
- undefined,
174
- undefined,
175
- producer,
176
- );
177
- const data = await request();
178
-
179
- expect(data).toStrictEqual(response);
180
- },
181
- );
182
- });
183
-
184
90
  test('should support secure resolveHeaders callback for non-default requestId', async () => {
185
91
  const producer = 'producer-with-resolver';
186
92
  const authKey = 'token-def';
@@ -538,204 +444,4 @@ describe('configure', () => {
538
444
  delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
539
445
  }
540
446
  });
541
-
542
- test('should retry with backoff and emit degraded telemetry events', async () => {
543
- rs.useFakeTimers();
544
- const onDegraded = rs.fn();
545
- let attempts = 0;
546
-
547
- const customRequest = rs.fn(async () => {
548
- attempts += 1;
549
- if (attempts < 3) {
550
- const error: any = new Error('temporary upstream error');
551
- error.status = 503;
552
- throw error;
553
- }
554
- return response;
555
- });
556
-
557
- try {
558
- configure({
559
- request: customRequest as unknown as typeof fetch,
560
- transport: {
561
- retry: {
562
- retries: 2,
563
- baseDelayMs: 20,
564
- maxDelayMs: 20,
565
- jitterRatio: 0,
566
- },
567
- onDegraded,
568
- },
569
- });
570
-
571
- const request = createRequest(path, method, 8080);
572
- const pending = request();
573
-
574
- await Promise.resolve();
575
- await rs.advanceTimersByTimeAsync(20);
576
- await Promise.resolve();
577
- await rs.advanceTimersByTimeAsync(20);
578
- await Promise.resolve();
579
-
580
- await expect(pending).resolves.toStrictEqual(response);
581
- expect(customRequest).toHaveBeenCalledTimes(3);
582
- expect(onDegraded).toHaveBeenCalledWith(
583
- expect.objectContaining({
584
- reason: 'retry',
585
- attempt: 1,
586
- maxAttempts: 3,
587
- backoffMs: 20,
588
- }),
589
- );
590
- expect(onDegraded).toHaveBeenCalledWith(
591
- expect.objectContaining({
592
- reason: 'retry',
593
- attempt: 2,
594
- maxAttempts: 3,
595
- backoffMs: 20,
596
- }),
597
- );
598
- } finally {
599
- rs.useRealTimers();
600
- }
601
- });
602
-
603
- test('should not retry non-idempotent requests by default', async () => {
604
- rs.useFakeTimers();
605
- const onDegraded = rs.fn();
606
- const customRequest = rs.fn(async () => {
607
- const error: any = new Error('post failed');
608
- error.status = 503;
609
- throw error;
610
- });
611
-
612
- try {
613
- configure({
614
- request: customRequest as unknown as typeof fetch,
615
- transport: {
616
- retry: {
617
- retries: 2,
618
- baseDelayMs: 1,
619
- maxDelayMs: 1,
620
- jitterRatio: 0,
621
- },
622
- onDegraded,
623
- },
624
- });
625
-
626
- const request = createRequest(path, 'POST', 8080);
627
- const pending = request({ body: 'payload' });
628
- const failure = expect(pending).rejects.toThrow('post failed');
629
-
630
- await Promise.resolve();
631
- await rs.advanceTimersByTimeAsync(1);
632
- await Promise.resolve();
633
- await rs.advanceTimersByTimeAsync(1);
634
- await failure;
635
-
636
- expect(customRequest).toHaveBeenCalledTimes(1);
637
- expect(onDegraded).not.toHaveBeenCalledWith(
638
- expect.objectContaining({ reason: 'retry' }),
639
- );
640
- } finally {
641
- rs.useRealTimers();
642
- }
643
- });
644
-
645
- test('should emit retry_exhausted when retry budget is consumed', async () => {
646
- rs.useFakeTimers();
647
- const onDegraded = rs.fn();
648
- const customRequest = rs.fn(async () => {
649
- const error: any = new Error('upstream unavailable');
650
- error.status = 503;
651
- throw error;
652
- });
653
-
654
- try {
655
- configure({
656
- request: customRequest as unknown as typeof fetch,
657
- transport: {
658
- retry: {
659
- retries: 1,
660
- baseDelayMs: 10,
661
- maxDelayMs: 10,
662
- jitterRatio: 0,
663
- },
664
- onDegraded,
665
- },
666
- });
667
-
668
- const request = createRequest(path, method, 8080);
669
- const pending = request();
670
- const failure = expect(pending).rejects.toThrow('upstream unavailable');
671
-
672
- await Promise.resolve();
673
- await rs.advanceTimersByTimeAsync(10);
674
- await failure;
675
-
676
- expect(customRequest).toHaveBeenCalledTimes(2);
677
- expect(onDegraded).toHaveBeenCalledWith(
678
- expect.objectContaining({
679
- reason: 'retry',
680
- attempt: 1,
681
- maxAttempts: 2,
682
- }),
683
- );
684
- expect(onDegraded).toHaveBeenCalledWith(
685
- expect.objectContaining({
686
- reason: 'retry_exhausted',
687
- attempt: 2,
688
- maxAttempts: 2,
689
- }),
690
- );
691
- } finally {
692
- rs.useRealTimers();
693
- }
694
- });
695
-
696
- test('should abort timed out requests and emit timeout degraded event', async () => {
697
- rs.useFakeTimers();
698
- const onDegraded = rs.fn();
699
-
700
- const customRequest = rs.fn((_requestPath: any, init?: any) => {
701
- return new Promise((_, reject) => {
702
- init?.signal?.addEventListener('abort', () => {
703
- const error: any = new Error('aborted');
704
- error.name = 'AbortError';
705
- reject(error);
706
- });
707
- });
708
- });
709
-
710
- try {
711
- configure({
712
- request: customRequest as unknown as typeof fetch,
713
- transport: {
714
- timeoutMs: 50,
715
- onDegraded,
716
- },
717
- });
718
-
719
- const request = createRequest(path, method, 8080);
720
- const pending = request();
721
- const failure = expect(pending).rejects.toMatchObject({
722
- name: 'TimeoutError',
723
- });
724
-
725
- await rs.advanceTimersByTimeAsync(50);
726
- await failure;
727
-
728
- expect(customRequest).toHaveBeenCalledTimes(1);
729
- expect(onDegraded).toHaveBeenCalledWith(
730
- expect.objectContaining({
731
- reason: 'timeout',
732
- timeoutMs: 50,
733
- attempt: 1,
734
- maxAttempts: 1,
735
- }),
736
- );
737
- } finally {
738
- rs.useRealTimers();
739
- }
740
- });
741
447
  });
@@ -8,8 +8,6 @@ import {
8
8
  isSecuredRequestId,
9
9
  OperationContractViolationError,
10
10
  readHeader,
11
- resolveConfiguredRequest,
12
- toOrigin,
13
11
  validateOperationContract,
14
12
  writeHeader,
15
13
  } from '../../src/request-policy/policyCore';
@@ -28,34 +26,11 @@ describe('policyCore (shared browser/node policy module)', () => {
28
26
  expect(headers).toEqual({});
29
27
  });
30
28
 
31
- test('toOrigin behaves identically for both targets', () => {
32
- expect(toOrigin('https://example.com/a/b?c=1')).toBe('https://example.com');
33
- expect(toOrigin('not-a-url')).toBeUndefined();
34
- });
35
-
36
29
  test('isSecuredRequestId treats only "default" as unsecured by default', () => {
37
30
  expect(isSecuredRequestId('default')).toBe(false);
38
31
  expect(isSecuredRequestId('crm-producer')).toBe(true);
39
32
  });
40
33
 
41
- test('buildOperationContext prefixes operationId with the requestId', () => {
42
- const context = buildOperationContext({
43
- requestId: 'crm',
44
- method: 'get',
45
- path: '/hello',
46
- operationContext: {
47
- operationId: 'getHello',
48
- schemaHash: 'abc',
49
- operationVersion: 2,
50
- },
51
- });
52
-
53
- expect(context.operationId).toBe('crm:getHello');
54
- expect(context.method).toBe('GET');
55
- expect(context.schemaHash).toBe('abc');
56
- expect(context.operationVersion).toBe(2);
57
- });
58
-
59
34
  test('validateOperationContract throws on missing schema hash for secured ids', () => {
60
35
  const contextPayload = buildOperationContext({
61
36
  requestId: 'crm',
@@ -151,100 +126,4 @@ describe('policyCore (shared browser/node policy module)', () => {
151
126
  operationVersion: 1,
152
127
  });
153
128
  });
154
-
155
- test('resolveConfiguredRequest throws for unconfigured non-default producers', () => {
156
- const map = new Map<string, string>();
157
- expect(() => resolveConfiguredRequest(map, 'crm', 'fallback')).toThrow(
158
- 'Producer client "crm" is not initialized',
159
- );
160
- expect(resolveConfiguredRequest(map, 'default', 'fallback')).toBe(
161
- 'fallback',
162
- );
163
- map.set('crm', 'configured');
164
- expect(resolveConfiguredRequest(map, 'crm', 'fallback')).toBe('configured');
165
- });
166
- });
167
-
168
- describe('createUploader policy headers (node entry)', () => {
169
- test('attaches envelope and operation-context headers for secured producers', async () => {
170
- const { configure, createUploader } = await import(
171
- '@modern-js/runtime-extensions/request-policy/server'
172
- );
173
-
174
- const seenInit: Array<{ url: string; init: any }> = [];
175
- const fakeFetch = (async (url: any, init: any) => {
176
- seenInit.push({ url: String(url), init });
177
- return new Response('{}', { status: 200 });
178
- }) as typeof fetch;
179
-
180
- configure({
181
- requestId: 'upload-producer',
182
- request: fakeFetch,
183
- setDomain: () => 'http://producer.example',
184
- });
185
-
186
- const upload = createUploader({
187
- path: '/api/upload',
188
- requestId: 'upload-producer',
189
- operationContext: {
190
- operationId: 'upload',
191
- routePath: '/api/upload',
192
- method: 'POST',
193
- schemaHash: 'hash-1',
194
- operationVersion: 1,
195
- },
196
- });
197
-
198
- const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
199
- await upload({ files: { file } });
200
-
201
- expect(seenInit).toHaveLength(1);
202
- const { url, init } = seenInit[0]!;
203
- expect(url).toBe('http://producer.example/api/upload');
204
- expect(init.method).toBe('POST');
205
-
206
- const envelope = JSON.parse(init.headers['x-modernjs-bff-envelope']);
207
- expect(envelope.requestId).toBe('upload-producer');
208
-
209
- expect(init.headers['x-operation-id']).toBe('upload-producer:upload');
210
- const details = JSON.parse(
211
- init.headers['x-modernjs-bff-operation-context'],
212
- );
213
- expect(details).toMatchObject({
214
- requestId: 'upload-producer',
215
- operationId: 'upload-producer:upload',
216
- routePath: '/api/upload',
217
- method: 'POST',
218
- schemaHash: 'hash-1',
219
- operationVersion: 1,
220
- });
221
- });
222
-
223
- test('keeps the default uploader free of policy headers', async () => {
224
- const { createUploader } = await import(
225
- '@modern-js/runtime-extensions/request-policy/server'
226
- );
227
-
228
- const seenInit: Array<{ init: any }> = [];
229
- const fakeFetch = (async (_url: any, init: any) => {
230
- seenInit.push({ init });
231
- return new Response('{}', { status: 200 });
232
- }) as typeof fetch;
233
-
234
- // default requestId falls back to the passed fetch only via configure-less
235
- // path; install a custom request for determinism.
236
- const { configure } = await import(
237
- '@modern-js/runtime-extensions/request-policy/server'
238
- );
239
- configure({ request: fakeFetch });
240
-
241
- const upload = createUploader({ path: '/api/upload' });
242
- const file = new File(['hi'], 'hi.txt', { type: 'text/plain' });
243
- await upload({ files: { file } });
244
-
245
- expect(seenInit).toHaveLength(1);
246
- const headers = seenInit[0]!.init.headers as Record<string, unknown>;
247
- expect(headers['x-modernjs-bff-envelope']).toBeUndefined();
248
- expect(headers['x-operation-id']).toBeUndefined();
249
- });
250
129
  });
@@ -54,27 +54,13 @@ describe('request context helpers', () => {
54
54
  },
55
55
  });
56
56
 
57
- expect(snapshot.headers).toEqual({
58
- [BFF_LOCALE_HEADER]: 'cs-CZ',
59
- [BFF_TRACEPARENT_HEADER]:
60
- '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
61
- });
62
- expect(snapshot).toMatchObject({
63
- locale: 'cs-CZ',
64
- traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
65
- traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
66
- spanId: 'bbbbbbbbbbbbbbbb',
67
- operationContext: {
68
- operationId: 'shell:list',
69
- routePath: '/effect/recommendations',
70
- method: 'GET',
71
- source: 'generated-client',
72
- scope: { workspace: 'demo' },
73
- sessionClaims: { role: 'viewer' },
74
- traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
75
- traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
76
- spanId: 'bbbbbbbbbbbbbbbb',
77
- },
78
- });
57
+ expect(Object.keys(snapshot.headers)).toEqual([
58
+ BFF_LOCALE_HEADER,
59
+ BFF_TRACEPARENT_HEADER,
60
+ ]);
61
+ expect(snapshot.operationContext?.traceId).toBe(
62
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
63
+ );
64
+ expect(snapshot.operationContext?.spanId).toBe('bbbbbbbbbbbbbbbb');
79
65
  });
80
66
  });