@bleedingdev/modern-js-bff-effect 3.9.0-ultramodern.10 → 3.9.0-ultramodern.12

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.
@@ -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;
@@ -347,11 +342,11 @@ export function createBatchTransportQueue({
347
342
  return runSingle(request);
348
343
  };
349
344
 
350
- const settleAmbiguousBatchRequests = (
351
- items: QueuedBatchRequest[],
352
- reason: string,
353
- ) =>
354
- 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
+ };
355
350
 
356
351
  const flushBucket = async (bucketKey: string) => {
357
352
  const bucket = buckets.get(bucketKey);
@@ -379,7 +374,6 @@ export function createBatchTransportQueue({
379
374
  sentAt: bucket.sentAt,
380
375
  };
381
376
  bucket.items = [];
382
- bucket.bytes = 0;
383
377
  rotatePayloadIdentity(bucket);
384
378
  const firstItem = items[0];
385
379
  if (!firstItem) {
@@ -396,17 +390,28 @@ export function createBatchTransportQueue({
396
390
  size: items.length,
397
391
  reason: disabledEndpoints.has(endpoint) ? 'batch-disabled' : undefined,
398
392
  });
399
- await settleRequests(items, runSingle);
400
- bucket.flushing = false;
401
- if (bucket.items.length > 0 && !bucket.timer) {
402
- void flushBucket(bucketKey);
393
+ try {
394
+ await settleRequests(items, runSingle);
395
+ } finally {
396
+ finishFlush(bucketKey, bucket);
403
397
  }
404
- buckets.releaseIfIdle(bucketKey, bucket);
405
398
  return;
406
399
  }
407
400
 
408
401
  const payload = createPayload(payloadIdentity, items);
409
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
+ };
410
415
 
411
416
  emitDataBatchTransportEvent(onEvent, {
412
417
  type: 'flush',
@@ -473,20 +478,17 @@ export function createBatchTransportQueue({
473
478
  batchId,
474
479
  reason: `batch-endpoint-unavailable-${String(response.status)}`,
475
480
  });
481
+ await settleRequests(items, request =>
482
+ runAfterAmbiguousBatch(
483
+ request,
484
+ `batch-response-${String(response.status)}`,
485
+ ),
486
+ );
476
487
  } else {
477
- emitDataBatchTransportEvent(onEvent, {
478
- type: 'fallback',
479
- endpoint,
480
- batchId,
481
- size: items.length,
482
- reason: `batch-response-${String(response.status)}`,
483
- });
488
+ await settleAmbiguousBatchRequests(
489
+ `batch-response-${String(response.status)}`,
490
+ );
484
491
  }
485
- await settleAmbiguousBatchRequests(
486
- items,
487
- `batch-response-${String(response.status)}`,
488
- );
489
- bucket.flushing = false;
490
492
  return;
491
493
  }
492
494
 
@@ -494,28 +496,12 @@ export function createBatchTransportQueue({
494
496
  response.json(),
495
497
  );
496
498
  if (!isBatchResponsePayload(result)) {
497
- emitDataBatchTransportEvent(onEvent, {
498
- type: 'fallback',
499
- endpoint,
500
- batchId,
501
- size: items.length,
502
- reason: 'invalid-batch-response',
503
- });
504
- await settleAmbiguousBatchRequests(items, 'invalid-batch-response');
505
- bucket.flushing = false;
499
+ await settleAmbiguousBatchRequests('invalid-batch-response');
506
500
  return;
507
501
  }
508
502
 
509
503
  if (result.batchId !== batchId) {
510
- emitDataBatchTransportEvent(onEvent, {
511
- type: 'fallback',
512
- endpoint,
513
- batchId,
514
- size: items.length,
515
- reason: 'mismatched-batch-id',
516
- });
517
- await settleAmbiguousBatchRequests(items, 'mismatched-batch-id');
518
- bucket.flushing = false;
504
+ await settleAmbiguousBatchRequests('mismatched-batch-id');
519
505
  return;
520
506
  }
521
507
 
@@ -555,23 +541,12 @@ export function createBatchTransportQueue({
555
541
  }
556
542
  return;
557
543
  }
558
- emitDataBatchTransportEvent(onEvent, {
559
- type: 'fallback',
560
- endpoint,
561
- batchId,
562
- size: items.length,
563
- reason: 'batch-transport-error',
564
- });
565
- await settleAmbiguousBatchRequests(items, 'batch-transport-error');
544
+ await settleAmbiguousBatchRequests('batch-transport-error');
566
545
  } finally {
567
546
  for (const item of items) {
568
547
  item.abortBatchIfUnused = undefined;
569
548
  }
570
- bucket.flushing = false;
571
- if (bucket.items.length > 0 && !bucket.timer) {
572
- void flushBucket(bucketKey);
573
- }
574
- buckets.releaseIfIdle(bucketKey, bucket);
549
+ finishFlush(bucketKey, bucket);
575
550
  }
576
551
  };
577
552
 
@@ -617,7 +592,18 @@ export function createBatchTransportQueue({
617
592
  options.endpoint,
618
593
  ).toString();
619
594
 
595
+ const itemHeaders = { ...headers };
596
+ delete itemHeaders.authorization;
597
+ delete itemHeaders.cookie;
620
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
+ }),
621
607
  requestUrl,
622
608
  endpoint,
623
609
  method,
@@ -660,20 +646,11 @@ export function createBatchTransportQueue({
660
646
  return undefined;
661
647
  }
662
648
 
663
- const itemHeaders = { ...request.headers };
664
- delete itemHeaders.authorization;
665
- delete itemHeaders.cookie;
666
- const bucketKey = stableStringify({
667
- endpoint: request.endpoint,
668
- authorization: request.headers.authorization ?? null,
669
- cookie: request.headers.cookie ?? null,
670
- credentials: request.credentials,
671
- });
672
649
  return stableStringify({
673
- bucketKey,
674
- path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
650
+ bucketKey: request.bucketKey,
651
+ path: request.itemPath,
675
652
  method: request.method,
676
- headers: itemHeaders,
653
+ headers: request.itemHeaders,
677
654
  body: null,
678
655
  });
679
656
  };
@@ -704,9 +681,7 @@ export function createBatchTransportQueue({
704
681
  .slice(2, 8)}`;
705
682
  nextItemId += 1;
706
683
 
707
- const itemHeaders = { ...request.headers };
708
- delete itemHeaders.authorization;
709
- delete itemHeaders.cookie;
684
+ const itemHeaders = { ...request.itemHeaders };
710
685
  if (
711
686
  preparedBody.body &&
712
687
  preparedBody.inferredContentType &&
@@ -716,18 +691,13 @@ export function createBatchTransportQueue({
716
691
  }
717
692
  const item: DataBatchRequestItem = {
718
693
  id: itemId,
719
- path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
694
+ path: request.itemPath,
720
695
  method: request.method,
721
696
  headers: itemHeaders,
722
697
  ...(preparedBody.body ? { body: preparedBody.body } : {}),
723
698
  };
724
699
 
725
- const bucketKey = stableStringify({
726
- endpoint: request.endpoint,
727
- authorization: request.headers.authorization ?? null,
728
- cookie: request.headers.cookie ?? null,
729
- credentials: request.credentials,
730
- });
700
+ const { bucketKey } = request;
731
701
 
732
702
  const key = stableStringify({
733
703
  bucketKey,
@@ -824,14 +794,16 @@ export function createBatchTransportQueue({
824
794
  }
825
795
 
826
796
  bucket.items.push(queued);
827
- bucket.bytes = measurePayload(bucket, bucket.items);
828
797
  emitDataBatchTransportEvent(onEvent, {
829
798
  type: 'enqueue',
830
799
  endpoint: request.endpoint,
831
800
  size: bucket.items.length,
832
801
  });
833
802
 
834
- if (bucket.items.length >= maxBatchSize || bucket.bytes >= maxBatchBytes) {
803
+ if (
804
+ bucket.items.length >= maxBatchSize ||
805
+ measurePayload(bucket, bucket.items) >= maxBatchBytes
806
+ ) {
835
807
  void flushBucket(bucketKey);
836
808
  return queued.promise;
837
809
  }
@@ -8,6 +8,7 @@ import {
8
8
  isPlainObject,
9
9
  normalizeMethod as normalizeItemMethod,
10
10
  } from '../../data-platform';
11
+ import { normalizeBatchLimits } from '../../data-platform/batch/options';
11
12
  import {
12
13
  decodeBatchBody,
13
14
  isBatchBody,
@@ -18,7 +19,6 @@ import {
18
19
  createBatchValidationResponse,
19
20
  isBatchRequestPayload,
20
21
  mapWithConcurrency,
21
- normalizeBatchAllowedMethods,
22
22
  normalizeBatchPath,
23
23
  promiseWithTimeout,
24
24
  toBatchItemError,
@@ -129,14 +129,11 @@ export function createDataPlatformBatchRequestHandler<TContext>(options: {
129
129
  const dataPlatformBatchOptions = options.dataPlatform?.batch;
130
130
  const batchEnabled = dataPlatformBatchOptions?.enabled !== false;
131
131
  const batchPath = normalizeBatchPath(dataPlatformBatchOptions?.endpoint);
132
- const batchMaxSize = Math.max(
133
- 1,
134
- dataPlatformBatchOptions?.maxBatchSize ?? 16,
135
- );
136
- const batchMaxBytes = Math.max(
137
- 1024,
138
- dataPlatformBatchOptions?.maxBatchBytes ?? 64 * 1024,
139
- );
132
+ const {
133
+ maxBatchSize: batchMaxSize,
134
+ maxBatchBytes: batchMaxBytes,
135
+ allowedMethods: batchAllowedMethods,
136
+ } = normalizeBatchLimits(dataPlatformBatchOptions);
140
137
  const batchConcurrency = Math.max(
141
138
  1,
142
139
  dataPlatformBatchOptions?.maxConcurrency ?? 4,
@@ -145,9 +142,6 @@ export function createDataPlatformBatchRequestHandler<TContext>(options: {
145
142
  0,
146
143
  dataPlatformBatchOptions?.requestTimeoutMs ?? 10_000,
147
144
  );
148
- const batchAllowedMethods = normalizeBatchAllowedMethods(
149
- dataPlatformBatchOptions?.allowedMethods,
150
- );
151
145
  const envelopeHeader =
152
146
  options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
153
147
  const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
@@ -24,16 +24,6 @@ export function normalizeBatchPath(pathname: string | undefined) {
24
24
  return pathname as `/${string}`;
25
25
  }
26
26
 
27
- export function normalizeBatchAllowedMethods(
28
- allowedMethods: string[] | undefined,
29
- ) {
30
- const source =
31
- Array.isArray(allowedMethods) && allowedMethods.length > 0
32
- ? allowedMethods
33
- : ['GET'];
34
- return new Set(source.map(method => method.toUpperCase()));
35
- }
36
-
37
27
  type ParsedBatchRequestPayload = Omit<DataBatchRequestPayload, 'items'> & {
38
28
  items: unknown[];
39
29
  };
@@ -2,6 +2,7 @@ import {
2
2
  BFF_ENVELOPE_HEADER,
3
3
  BFF_OPERATION_CONTEXT_DETAIL_HEADER,
4
4
  BFF_OPERATION_CONTEXT_HEADER,
5
+ digestOperationContract,
5
6
  resolveCrossProjectRequestObservation,
6
7
  } from '@modern-js/server-runtime-extensions/bff-policy';
7
8
  import * as Effect from 'effect/Effect';
@@ -66,22 +67,16 @@ export function withCrossProjectPolicy<
66
67
  );
67
68
  }
68
69
  return Effect.gen(function* () {
69
- // Canonical field order matches the server's route-identity SHA-256.
70
- const input = encodeJson({
71
- httpMethod: contract.method,
72
- name: contract.name,
73
- requestId: options.requestId,
74
- routePath: contract.routePath,
75
- });
76
- const digest = yield* Effect.promise(() =>
77
- globalThis.crypto.subtle.digest(
78
- 'SHA-256',
79
- new TextEncoder().encode(input),
70
+ const schemaHash = yield* Effect.promise(() =>
71
+ digestOperationContract(
72
+ {
73
+ httpMethod: contract.method,
74
+ name: contract.name,
75
+ routePath: contract.routePath,
76
+ },
77
+ options.requestId,
80
78
  ),
81
79
  );
82
- const schemaHash = Array.from(new Uint8Array(digest), value =>
83
- value.toString(16).padStart(2, '0'),
84
- ).join('');
85
80
  return request.pipe(
86
81
  HttpClientRequest.setHeader(
87
82
  BFF_ENVELOPE_HEADER,