@bleedingdev/modern-js-bff-effect 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

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 (142) hide show
  1. package/README.md +20 -4
  2. package/dist/cjs/assembly.js +50 -0
  3. package/dist/cjs/{effect-client/guards.js → data-platform/batch/options.js} +13 -9
  4. package/dist/cjs/data-platform/batch/queue.js +48 -83
  5. package/dist/cjs/data-platform/trace.js +2 -2
  6. package/dist/cjs/effect/context.js +28 -4
  7. package/dist/cjs/effect/edge-dispatcher.js +12 -12
  8. package/dist/cjs/effect/edge.js +36 -3
  9. package/dist/cjs/effect/endpoint-contracts.js +3 -4
  10. package/dist/cjs/effect/entry-shape.js +40 -12
  11. package/dist/cjs/effect/generated-entry.js +57 -0
  12. package/dist/cjs/effect/handler/batch-handler.js +7 -7
  13. package/dist/cjs/effect/handler/batch.js +8 -10
  14. package/dist/cjs/effect/handler/definition.js +1 -21
  15. package/dist/cjs/effect/index.js +8 -8
  16. package/dist/cjs/effect/module.js +8 -4
  17. package/dist/cjs/effect/operation-context.js +10 -14
  18. package/dist/cjs/effect/rpcSerialization.js +0 -2
  19. package/dist/cjs/effect-client/cross-project.js +89 -0
  20. package/dist/cjs/effect-client/index.js +5 -3
  21. package/dist/cjs/microvertical-api.js +120 -0
  22. package/dist/esm/assembly.mjs +12 -0
  23. package/dist/esm/data-platform/batch/options.mjs +10 -0
  24. package/dist/esm/data-platform/batch/queue.mjs +48 -83
  25. package/dist/esm/data-platform/trace.mjs +1 -1
  26. package/dist/esm/effect/context.mjs +21 -6
  27. package/dist/esm/effect/edge-dispatcher.mjs +6 -6
  28. package/dist/esm/effect/edge.mjs +3 -0
  29. package/dist/esm/effect/endpoint-contracts.mjs +2 -3
  30. package/dist/esm/effect/entry-shape-registry.mjs +2 -2
  31. package/dist/esm/effect/entry-shape.mjs +29 -6
  32. package/dist/esm/effect/generated-entry.mjs +19 -0
  33. package/dist/esm/effect/handler/batch-handler.mjs +8 -8
  34. package/dist/esm/effect/handler/batch.mjs +9 -8
  35. package/dist/esm/effect/handler/definition.mjs +1 -21
  36. package/dist/esm/effect/index.mjs +1 -1
  37. package/dist/esm/effect/module.mjs +8 -4
  38. package/dist/esm/effect/operation-context.mjs +3 -4
  39. package/dist/esm/effect/rpcSerialization.mjs +0 -2
  40. package/dist/esm/effect-client/cross-project.mjs +51 -0
  41. package/dist/esm/effect-client/index.mjs +4 -2
  42. package/dist/esm/microvertical-api.mjs +67 -0
  43. package/dist/esm-node/assembly.mjs +13 -0
  44. package/dist/esm-node/data-platform/batch/options.mjs +11 -0
  45. package/dist/esm-node/data-platform/batch/queue.mjs +48 -83
  46. package/dist/esm-node/data-platform/trace.mjs +1 -1
  47. package/dist/esm-node/effect/context.mjs +21 -6
  48. package/dist/esm-node/effect/edge-dispatcher.mjs +6 -6
  49. package/dist/esm-node/effect/edge.mjs +3 -0
  50. package/dist/esm-node/effect/endpoint-contracts.mjs +2 -3
  51. package/dist/esm-node/effect/entry-shape.mjs +29 -6
  52. package/dist/esm-node/effect/generated-entry.mjs +20 -0
  53. package/dist/esm-node/effect/handler/batch-handler.mjs +8 -8
  54. package/dist/esm-node/effect/handler/batch.mjs +9 -8
  55. package/dist/esm-node/effect/handler/definition.mjs +1 -21
  56. package/dist/esm-node/effect/index.mjs +1 -1
  57. package/dist/esm-node/effect/module.mjs +8 -4
  58. package/dist/esm-node/effect/operation-context.mjs +3 -4
  59. package/dist/esm-node/effect/rpcSerialization.mjs +0 -2
  60. package/dist/esm-node/effect-client/cross-project.mjs +52 -0
  61. package/dist/esm-node/effect-client/index.mjs +4 -2
  62. package/dist/esm-node/microvertical-api.mjs +68 -0
  63. package/dist/types/assembly.d.ts +10 -0
  64. package/dist/types/data-platform/batch/options.d.ts +7 -0
  65. package/dist/types/data-platform/batch/queue.d.ts +0 -1
  66. package/dist/types/effect/context.d.ts +15 -2
  67. package/dist/types/effect/edge-dispatcher.d.ts +3 -3
  68. package/dist/types/effect/edge.d.ts +3 -0
  69. package/dist/types/effect/endpoint-contracts.d.ts +2 -20
  70. package/dist/types/effect/entry-shape.d.ts +16 -2
  71. package/dist/types/effect/generated-entry.d.ts +7 -0
  72. package/dist/types/effect/handler/batch.d.ts +7 -2
  73. package/dist/types/effect/handler/types.d.ts +2 -17
  74. package/dist/types/effect/handler.d.ts +1 -1
  75. package/dist/types/effect/module.d.ts +2 -1
  76. package/dist/types/effect/operation-context.d.ts +1 -2
  77. package/dist/types/effect-client/cross-project.d.ts +10 -0
  78. package/dist/types/effect-client/index.d.ts +7 -4
  79. package/dist/types/microvertical-api.d.ts +65 -0
  80. package/package.json +47 -42
  81. package/src/assembly.ts +43 -0
  82. package/src/data-platform/batch/options.ts +20 -0
  83. package/src/data-platform/batch/queue.ts +63 -92
  84. package/src/data-platform/trace.ts +1 -1
  85. package/src/effect/context.ts +64 -12
  86. package/src/effect/edge-dispatcher.ts +14 -11
  87. package/src/effect/edge.ts +6 -0
  88. package/src/effect/endpoint-contracts.ts +3 -24
  89. package/src/effect/entry-shape.ts +105 -10
  90. package/src/effect/generated-entry.ts +36 -0
  91. package/src/effect/handler/batch-handler.ts +27 -28
  92. package/src/effect/handler/batch.ts +9 -13
  93. package/src/effect/handler/definition.ts +0 -45
  94. package/src/effect/handler/http.ts +2 -2
  95. package/src/effect/handler/types.ts +3 -32
  96. package/src/effect/handler.ts +0 -1
  97. package/src/effect/index.ts +1 -1
  98. package/src/effect/module.ts +11 -4
  99. package/src/effect/operation-context.ts +2 -6
  100. package/src/effect/rpcSerialization.ts +0 -2
  101. package/src/effect-client/cross-project.ts +104 -0
  102. package/src/effect-client/index.ts +17 -7
  103. package/src/microvertical-api.ts +102 -0
  104. package/dist/cjs/effect/edge-context.js +0 -61
  105. package/dist/cjs/effect-client/batch.js +0 -73
  106. package/dist/cjs/effect-client/envelope.js +0 -145
  107. package/dist/cjs/effect-client/request-context.js +0 -64
  108. package/dist/cjs/effect-client/request.js +0 -66
  109. package/dist/cjs/effect-client/runtime.js +0 -100
  110. package/dist/cjs/effect-client/types.js +0 -18
  111. package/dist/esm/effect/edge-context.mjs +0 -14
  112. package/dist/esm/effect-client/batch.mjs +0 -35
  113. package/dist/esm/effect-client/envelope.mjs +0 -104
  114. package/dist/esm/effect-client/guards.mjs +0 -3
  115. package/dist/esm/effect-client/request-context.mjs +0 -23
  116. package/dist/esm/effect-client/request.mjs +0 -28
  117. package/dist/esm/effect-client/runtime.mjs +0 -62
  118. package/dist/esm/effect-client/types.mjs +0 -0
  119. package/dist/esm-node/effect/edge-context.mjs +0 -15
  120. package/dist/esm-node/effect-client/batch.mjs +0 -36
  121. package/dist/esm-node/effect-client/envelope.mjs +0 -105
  122. package/dist/esm-node/effect-client/guards.mjs +0 -4
  123. package/dist/esm-node/effect-client/request-context.mjs +0 -24
  124. package/dist/esm-node/effect-client/request.mjs +0 -29
  125. package/dist/esm-node/effect-client/runtime.mjs +0 -63
  126. package/dist/esm-node/effect-client/types.mjs +0 -1
  127. package/dist/types/effect/edge-context.d.ts +0 -5
  128. package/dist/types/effect-client/batch.d.ts +0 -2
  129. package/dist/types/effect-client/envelope.d.ts +0 -15
  130. package/dist/types/effect-client/guards.d.ts +0 -2
  131. package/dist/types/effect-client/request-context.d.ts +0 -3
  132. package/dist/types/effect-client/request.d.ts +0 -1
  133. package/dist/types/effect-client/runtime.d.ts +0 -5
  134. package/dist/types/effect-client/types.d.ts +0 -68
  135. package/src/effect/edge-context.ts +0 -51
  136. package/src/effect-client/batch.ts +0 -50
  137. package/src/effect-client/envelope.ts +0 -238
  138. package/src/effect-client/guards.ts +0 -6
  139. package/src/effect-client/request-context.ts +0 -54
  140. package/src/effect-client/request.ts +0 -51
  141. package/src/effect-client/runtime.ts +0 -122
  142. package/src/effect-client/types.ts +0 -84
@@ -8,6 +8,7 @@ import type {
8
8
  DataTransportRequestInfo,
9
9
  } from '../types';
10
10
  import { DEFAULT_DATA_BATCH_HEADER } from '../types';
11
+ import { normalizeBatchLimits } from './options';
11
12
  import { decodeBatchBody, isNullBodyStatus } from './protocol';
12
13
  import {
13
14
  createBatchId,
@@ -37,6 +38,9 @@ type NormalizedBatchTransportRequest = {
37
38
  endpoint: string;
38
39
  method: string;
39
40
  headers: Record<string, string>;
41
+ itemHeaders: Record<string, string>;
42
+ bucketKey: string;
43
+ itemPath: string;
40
44
  credentials: RequestCredentials;
41
45
  body: BodyInit | null;
42
46
  hasOpaqueSourceBody: boolean;
@@ -76,7 +80,6 @@ type QueuedBatchRequest = {
76
80
 
77
81
  type BatchBucket = {
78
82
  items: QueuedBatchRequest[];
79
- bytes: number;
80
83
  batchId: string;
81
84
  sentAt: number;
82
85
  timer: ReturnType<typeof setTimeout> | null;
@@ -185,7 +188,6 @@ export class BatchBucketRegistry {
185
188
 
186
189
  const next: BatchBucket = {
187
190
  items: [],
188
- bytes: 0,
189
191
  batchId: createBatchId(),
190
192
  sentAt: Date.now(),
191
193
  timer: null,
@@ -215,15 +217,9 @@ export function createBatchTransportQueue({
215
217
  bucketRegistry,
216
218
  }: CreateBatchTransportQueueOptions) {
217
219
  const flushIntervalMs = Math.max(0, options.flushIntervalMs ?? 8);
218
- const maxBatchSize = Math.max(1, options.maxBatchSize ?? 16);
219
- const maxBatchBytes = Math.max(1024, options.maxBatchBytes ?? 64 * 1024);
220
+ const { maxBatchSize, maxBatchBytes, allowedMethods } =
221
+ normalizeBatchLimits(options);
220
222
  const requestTimeoutMs = options.requestTimeoutMs;
221
- const allowedMethods = new Set(
222
- (options.allowedMethods && options.allowedMethods.length > 0
223
- ? options.allowedMethods
224
- : ['GET']
225
- ).map(method => method.toUpperCase()),
226
- );
227
223
  const onEvent = options.onEvent;
228
224
 
229
225
  const buckets = bucketRegistry ?? new BatchBucketRegistry();
@@ -284,7 +280,6 @@ export function createBatchTransportQueue({
284
280
  return;
285
281
  }
286
282
  bucket.items.splice(index, 1);
287
- bucket.bytes = measurePayload(bucket, bucket.items);
288
283
  if (bucket.items.length === 0 && bucket.timer) {
289
284
  clearTimeout(bucket.timer);
290
285
  bucket.timer = null;
@@ -307,15 +302,14 @@ export function createBatchTransportQueue({
307
302
  abortBatchIfUnused?.();
308
303
  };
309
304
 
310
- const runSingle = (request: QueuedBatchRequest) => {
311
- return runWithSignal(request.lifetime.signal, async () => {
305
+ const runSingle = (request: QueuedBatchRequest) =>
306
+ runWithSignal(request.lifetime.signal, async () => {
312
307
  const response = await baseFetch(
313
308
  request.requestInput,
314
309
  request.requestInit,
315
310
  );
316
311
  return parseResponseLikeCreateRequest(response);
317
312
  });
318
- };
319
313
 
320
314
  const settleRequests = async (
321
315
  items: QueuedBatchRequest[],
@@ -348,11 +342,11 @@ export function createBatchTransportQueue({
348
342
  return runSingle(request);
349
343
  };
350
344
 
351
- const settleAmbiguousBatchRequests = (
352
- items: QueuedBatchRequest[],
353
- reason: string,
354
- ) =>
355
- settleRequests(items, request => runAfterAmbiguousBatch(request, reason));
345
+ const finishFlush = (bucketKey: string, bucket: BatchBucket) => {
346
+ bucket.flushing = false;
347
+ if (bucket.items.length > 0 && !bucket.timer) void flushBucket(bucketKey);
348
+ buckets.releaseIfIdle(bucketKey, bucket);
349
+ };
356
350
 
357
351
  const flushBucket = async (bucketKey: string) => {
358
352
  const bucket = buckets.get(bucketKey);
@@ -380,7 +374,6 @@ export function createBatchTransportQueue({
380
374
  sentAt: bucket.sentAt,
381
375
  };
382
376
  bucket.items = [];
383
- bucket.bytes = 0;
384
377
  rotatePayloadIdentity(bucket);
385
378
  const firstItem = items[0];
386
379
  if (!firstItem) {
@@ -397,17 +390,28 @@ export function createBatchTransportQueue({
397
390
  size: items.length,
398
391
  reason: disabledEndpoints.has(endpoint) ? 'batch-disabled' : undefined,
399
392
  });
400
- await settleRequests(items, runSingle);
401
- bucket.flushing = false;
402
- if (bucket.items.length > 0 && !bucket.timer) {
403
- void flushBucket(bucketKey);
393
+ try {
394
+ await settleRequests(items, runSingle);
395
+ } finally {
396
+ finishFlush(bucketKey, bucket);
404
397
  }
405
- buckets.releaseIfIdle(bucketKey, bucket);
406
398
  return;
407
399
  }
408
400
 
409
401
  const payload = createPayload(payloadIdentity, items);
410
402
  const batchId = payload.batchId;
403
+ const settleAmbiguousBatchRequests = (reason: string) => {
404
+ emitDataBatchTransportEvent(onEvent, {
405
+ type: 'fallback',
406
+ endpoint,
407
+ batchId,
408
+ size: items.length,
409
+ reason,
410
+ });
411
+ return settleRequests(items, request =>
412
+ runAfterAmbiguousBatch(request, reason),
413
+ );
414
+ };
411
415
 
412
416
  emitDataBatchTransportEvent(onEvent, {
413
417
  type: 'flush',
@@ -474,20 +478,17 @@ export function createBatchTransportQueue({
474
478
  batchId,
475
479
  reason: `batch-endpoint-unavailable-${String(response.status)}`,
476
480
  });
481
+ await settleRequests(items, request =>
482
+ runAfterAmbiguousBatch(
483
+ request,
484
+ `batch-response-${String(response.status)}`,
485
+ ),
486
+ );
477
487
  } else {
478
- emitDataBatchTransportEvent(onEvent, {
479
- type: 'fallback',
480
- endpoint,
481
- batchId,
482
- size: items.length,
483
- reason: `batch-response-${String(response.status)}`,
484
- });
488
+ await settleAmbiguousBatchRequests(
489
+ `batch-response-${String(response.status)}`,
490
+ );
485
491
  }
486
- await settleAmbiguousBatchRequests(
487
- items,
488
- `batch-response-${String(response.status)}`,
489
- );
490
- bucket.flushing = false;
491
492
  return;
492
493
  }
493
494
 
@@ -495,28 +496,12 @@ export function createBatchTransportQueue({
495
496
  response.json(),
496
497
  );
497
498
  if (!isBatchResponsePayload(result)) {
498
- emitDataBatchTransportEvent(onEvent, {
499
- type: 'fallback',
500
- endpoint,
501
- batchId,
502
- size: items.length,
503
- reason: 'invalid-batch-response',
504
- });
505
- await settleAmbiguousBatchRequests(items, 'invalid-batch-response');
506
- bucket.flushing = false;
499
+ await settleAmbiguousBatchRequests('invalid-batch-response');
507
500
  return;
508
501
  }
509
502
 
510
503
  if (result.batchId !== batchId) {
511
- emitDataBatchTransportEvent(onEvent, {
512
- type: 'fallback',
513
- endpoint,
514
- batchId,
515
- size: items.length,
516
- reason: 'mismatched-batch-id',
517
- });
518
- await settleAmbiguousBatchRequests(items, 'mismatched-batch-id');
519
- bucket.flushing = false;
504
+ await settleAmbiguousBatchRequests('mismatched-batch-id');
520
505
  return;
521
506
  }
522
507
 
@@ -556,23 +541,12 @@ export function createBatchTransportQueue({
556
541
  }
557
542
  return;
558
543
  }
559
- emitDataBatchTransportEvent(onEvent, {
560
- type: 'fallback',
561
- endpoint,
562
- batchId,
563
- size: items.length,
564
- reason: 'batch-transport-error',
565
- });
566
- await settleAmbiguousBatchRequests(items, 'batch-transport-error');
544
+ await settleAmbiguousBatchRequests('batch-transport-error');
567
545
  } finally {
568
546
  for (const item of items) {
569
547
  item.abortBatchIfUnused = undefined;
570
548
  }
571
- bucket.flushing = false;
572
- if (bucket.items.length > 0 && !bucket.timer) {
573
- void flushBucket(bucketKey);
574
- }
575
- buckets.releaseIfIdle(bucketKey, bucket);
549
+ finishFlush(bucketKey, bucket);
576
550
  }
577
551
  };
578
552
 
@@ -618,7 +592,18 @@ export function createBatchTransportQueue({
618
592
  options.endpoint,
619
593
  ).toString();
620
594
 
595
+ const itemHeaders = { ...headers };
596
+ delete itemHeaders.authorization;
597
+ delete itemHeaders.cookie;
621
598
  return {
599
+ itemHeaders,
600
+ itemPath: `${requestUrl.pathname}${requestUrl.search}`,
601
+ bucketKey: stableStringify({
602
+ endpoint,
603
+ authorization: headers.authorization ?? null,
604
+ cookie: headers.cookie ?? null,
605
+ credentials,
606
+ }),
622
607
  requestUrl,
623
608
  endpoint,
624
609
  method,
@@ -661,20 +646,11 @@ export function createBatchTransportQueue({
661
646
  return undefined;
662
647
  }
663
648
 
664
- const itemHeaders = { ...request.headers };
665
- delete itemHeaders.authorization;
666
- delete itemHeaders.cookie;
667
- const bucketKey = stableStringify({
668
- endpoint: request.endpoint,
669
- authorization: request.headers.authorization ?? null,
670
- cookie: request.headers.cookie ?? null,
671
- credentials: request.credentials,
672
- });
673
649
  return stableStringify({
674
- bucketKey,
675
- path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
650
+ bucketKey: request.bucketKey,
651
+ path: request.itemPath,
676
652
  method: request.method,
677
- headers: itemHeaders,
653
+ headers: request.itemHeaders,
678
654
  body: null,
679
655
  });
680
656
  };
@@ -705,9 +681,7 @@ export function createBatchTransportQueue({
705
681
  .slice(2, 8)}`;
706
682
  nextItemId += 1;
707
683
 
708
- const itemHeaders = { ...request.headers };
709
- delete itemHeaders.authorization;
710
- delete itemHeaders.cookie;
684
+ const itemHeaders = { ...request.itemHeaders };
711
685
  if (
712
686
  preparedBody.body &&
713
687
  preparedBody.inferredContentType &&
@@ -717,18 +691,13 @@ export function createBatchTransportQueue({
717
691
  }
718
692
  const item: DataBatchRequestItem = {
719
693
  id: itemId,
720
- path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
694
+ path: request.itemPath,
721
695
  method: request.method,
722
696
  headers: itemHeaders,
723
697
  ...(preparedBody.body ? { body: preparedBody.body } : {}),
724
698
  };
725
699
 
726
- const bucketKey = stableStringify({
727
- endpoint: request.endpoint,
728
- authorization: request.headers.authorization ?? null,
729
- cookie: request.headers.cookie ?? null,
730
- credentials: request.credentials,
731
- });
700
+ const { bucketKey } = request;
732
701
 
733
702
  const key = stableStringify({
734
703
  bucketKey,
@@ -825,14 +794,16 @@ export function createBatchTransportQueue({
825
794
  }
826
795
 
827
796
  bucket.items.push(queued);
828
- bucket.bytes = measurePayload(bucket, bucket.items);
829
797
  emitDataBatchTransportEvent(onEvent, {
830
798
  type: 'enqueue',
831
799
  endpoint: request.endpoint,
832
800
  size: bucket.items.length,
833
801
  });
834
802
 
835
- if (bucket.items.length >= maxBatchSize || bucket.bytes >= maxBatchBytes) {
803
+ if (
804
+ bucket.items.length >= maxBatchSize ||
805
+ measurePayload(bucket, bucket.items) >= maxBatchBytes
806
+ ) {
836
807
  void flushBucket(bucketKey);
837
808
  return queued.promise;
838
809
  }
@@ -1,5 +1,5 @@
1
1
  // @effect-diagnostics asyncFunction:off globalDate:off globalRandom:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
- import { parseTraceparent } from '@modern-js/create-request';
2
+ import { parseTraceparent } from '@modern-js/runtime-extensions/request-context';
3
3
 
4
4
  import type { TraceContext } from './types';
5
5
 
@@ -1,10 +1,5 @@
1
1
  // @effect-diagnostics strictBooleanExpressions:off
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
- import {
4
- type EffectContext,
5
- type EffectContextStorage,
6
- kEffectContextStorage,
7
- } from './operation-context';
2
+ import type { EffectContext, EffectContextStorage } from './operation-context';
8
3
 
9
4
  export {
10
5
  type CreateEffectOperationContextOptions,
@@ -12,21 +7,51 @@ export {
12
7
  type EffectContext,
13
8
  } from './operation-context';
14
9
 
10
+ type EffectContextStorageConstructor = new () => EffectContextStorage;
11
+
12
+ // One storage for every entry: each imports this module as
13
+ // `@modern-js/bff-effect/context`, the request that Module Federation shares
14
+ // and that the BFF bundle keeps external, so the server and its lambdas load
15
+ // the same instance. A backend federation container bundles its own copy and
16
+ // adopts the host's storage in init() instead.
15
17
  const globalStore = globalThis as typeof globalThis & {
16
- [kEffectContextStorage]?: EffectContextStorage;
18
+ process?: {
19
+ getBuiltinModule?: (id: string) => unknown;
20
+ };
17
21
  };
18
22
 
19
- const effectContextStorage =
20
- globalStore[kEffectContextStorage] ??
21
- (globalStore[kEffectContextStorage] = new AsyncLocalStorage<EffectContext>());
23
+ const asyncHooks = globalStore.process?.getBuiltinModule?.(
24
+ 'node:async_hooks',
25
+ ) as { AsyncLocalStorage?: EffectContextStorageConstructor } | undefined;
26
+ const AsyncLocalStorage = asyncHooks?.AsyncLocalStorage;
27
+ if (typeof AsyncLocalStorage !== 'function') {
28
+ throw new Error(
29
+ '[BFF][Effect] The edge runtime must provide AsyncLocalStorage. Enable Node.js compatibility or the nodejs_als compatibility flag.',
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Module Federation share through which a backend federation host hands its
35
+ * request storage to the containers it initializes.
36
+ */
37
+ export const EFFECT_BFF_CONTEXT_STORAGE_SHARE =
38
+ '@modern-js/bff-effect/context-storage';
39
+
40
+ let effectContextStorage: EffectContextStorage = new AsyncLocalStorage();
41
+ let effectContextStorageUsed = false;
42
+
43
+ const usedEffectContextStorage = () => {
44
+ effectContextStorageUsed = true;
45
+ return effectContextStorage;
46
+ };
22
47
 
23
48
  export const runWithEffectContext = <T>(
24
49
  context: EffectContext,
25
50
  cb: () => T,
26
- ): T => effectContextStorage.run(context, cb);
51
+ ): T => usedEffectContextStorage().run(context, cb);
27
52
 
28
53
  export const useEffectContext = (): EffectContext => {
29
- const context = effectContextStorage.getStore();
54
+ const context = usedEffectContextStorage().getStore();
30
55
  if (!context) {
31
56
  throw new Error(`Can't call useEffectContext out of Effect runtime scope`);
32
57
  }
@@ -34,4 +59,31 @@ export const useEffectContext = (): EffectContext => {
34
59
  return context;
35
60
  };
36
61
 
62
+ /** @internal The storage a backend federation host shares with containers. */
63
+ export const getEffectContextStorage = (): EffectContextStorage =>
64
+ effectContextStorage;
65
+
66
+ /**
67
+ * @internal Called through `adoptEffectBffShareScope()` from a backend
68
+ * federation container's `init()`, so its endpoints read the context the
69
+ * host dispatcher enters.
70
+ */
71
+ export function adoptEffectContextStorage(storage: unknown): void {
72
+ const candidate = storage as Partial<EffectContextStorage> | undefined;
73
+ if (
74
+ typeof candidate?.run !== 'function' ||
75
+ typeof candidate.getStore !== 'function'
76
+ ) {
77
+ throw new Error(
78
+ `[BFF][Effect] Share ${EFFECT_BFF_CONTEXT_STORAGE_SHARE} must provide an AsyncLocalStorage.`,
79
+ );
80
+ }
81
+ if (effectContextStorageUsed && candidate !== effectContextStorage) {
82
+ throw new Error(
83
+ '[BFF][Effect] A backend federation container received the host request storage after an Effect API already used its own. Evaluate exposes only after container init().',
84
+ );
85
+ }
86
+ effectContextStorage = candidate as EffectContextStorage;
87
+ }
88
+
37
89
  export const useOperationContext = () => useEffectContext().operationContext;
@@ -1,15 +1,17 @@
1
1
  // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off strictBooleanExpressions:off
2
+
3
+ import { runWithEffectContext } from '@modern-js/bff-effect/context';
2
4
  import {
3
5
  evaluateCrossProjectPolicy,
4
6
  type NormalizedCrossProjectPolicy,
5
7
  resolveCrossProjectRequestObservation,
6
- } from '@modern-js/bff-core/security/cross-project-policy';
8
+ } from '@modern-js/server-runtime-extensions/bff-policy';
9
+ import * as Effect from 'effect/Effect';
7
10
  import { toHeaderRecord } from '../headers';
8
11
  import {
9
12
  type DispatchEffectBffRequestOptions,
10
13
  dispatchEffectBffRequestWithContext,
11
14
  } from './dispatch';
12
- import { runWithEffectContext } from './edge-context';
13
15
  import type {
14
16
  EffectBffOpenApiConfig,
15
17
  EffectDataPlatformValidationOptions,
@@ -21,6 +23,11 @@ import {
21
23
  } from './module';
22
24
  import type { EffectContext } from './operation-context';
23
25
 
26
+ export {
27
+ runWithEffectContext,
28
+ useEffectContext,
29
+ useOperationContext,
30
+ } from '@modern-js/bff-effect/context';
24
31
  export * as Config from 'effect/Config';
25
32
  export * as Effect from 'effect/Effect';
26
33
  export * as Layer from 'effect/Layer';
@@ -84,14 +91,8 @@ export {
84
91
  RpcWorker,
85
92
  Utils,
86
93
  } from 'effect/unstable/rpc';
87
- export {
88
- runWithEffectContext,
89
- useEffectContext,
90
- useOperationContext,
91
- } from './edge-context';
92
94
  export type {
93
95
  EffectApiClientFromApi,
94
- EffectApiPromiseClientFromApi,
95
96
  EffectBffDefinition,
96
97
  EffectBffHandlerFactory,
97
98
  EffectBffOpenApiConfig,
@@ -285,9 +286,11 @@ export async function createEffectBffEdgeDispatcher(
285
286
  disposePromise ??= (async () => {
286
287
  acceptingRequests = false;
287
288
  if (activeDispatches > 0) {
288
- await new Promise<void>(resolve => {
289
- resolveDrain = resolve;
290
- });
289
+ await Effect.runPromise(
290
+ Effect.callback<void>(resume => {
291
+ resolveDrain = () => resume(Effect.void);
292
+ }),
293
+ );
291
294
  }
292
295
  await loaded.dispose?.();
293
296
  })();
@@ -1 +1,7 @@
1
+ export { EFFECT_BFF_CONTEXT_STORAGE_SHARE } from '@modern-js/bff-effect/context';
1
2
  export * from './edge-dispatcher';
3
+ export {
4
+ adoptEffectBffShareScope,
5
+ EFFECT_BFF_HANDLER_FACTORY_REGISTRY_SHARE,
6
+ } from './entry-shape';
7
+ export { createEffectBffEdgeDispatcherFactory } from './generated-entry';
@@ -1,21 +1,9 @@
1
1
  // @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
2
- /**
3
- * Shared Effect HttpApi endpoint reflection used by BOTH sides of the
4
- * cross-project contract:
5
- *
6
- * - the `@modern-js/plugin-bff-extensions/client-generator` package stamps
7
- * each generated operation with a per-endpoint contract hash;
8
- * - the `@modern-js/plugin-bff-extensions/effect-adapter` package derives the
9
- * expected operation-contract map for the cross-project policy from the
10
- * same endpoints.
11
- *
12
- * Keeping route-path normalization and the endpoint -> contract mapping in
13
- * one module is what guarantees the two hashes agree.
14
- */
2
+ /** Shared endpoint normalization and identity hashing for server policy metadata. */
15
3
  import {
16
4
  createOperationContractHash,
17
5
  type OperationContractSource,
18
- } from '@modern-js/bff-core';
6
+ } from '@modern-js/server-runtime-extensions/bff-policy/node';
19
7
  import {
20
8
  classifyEffectBffEntryModule,
21
9
  isValidatorAwareHandlerFactory,
@@ -38,13 +26,7 @@ type HttpApiGroupLike = {
38
26
  };
39
27
 
40
28
  type HttpApiEndpointLike = {
41
- /**
42
- * effect 4.0.0-beta.98 renamed the endpoint `name` property to `identifier`,
43
- * matching `HttpApiGroup`. `name` is kept as a fallback so reflection over an
44
- * older Effect build does not silently degrade to the class name.
45
- */
46
29
  identifier?: unknown;
47
- name?: unknown;
48
30
  method?: unknown;
49
31
  path?: unknown;
50
32
  };
@@ -53,9 +35,6 @@ function resolveEffectEndpointName(endpoint: HttpApiEndpointLike): string {
53
35
  if (typeof endpoint.identifier === 'string' && endpoint.identifier) {
54
36
  return endpoint.identifier;
55
37
  }
56
- if (typeof endpoint.name === 'string' && endpoint.name) {
57
- return endpoint.name;
58
- }
59
38
  return '';
60
39
  }
61
40
 
@@ -168,7 +147,7 @@ export async function extractHttpApiFromModule(
168
147
  isValidatorAwareHandlerFactory,
169
148
  isHttpApi,
170
149
  });
171
- return facts?.legacyShape ||
150
+ return facts?.unsupportedShape ||
172
151
  facts?.api === undefined ||
173
152
  !facts.hasRuntimeLayer ||
174
153
  (facts.createHandler !== undefined && !facts.createHandlerValidatorAware)