@bleedingdev/modern-js-bff-effect 3.9.0-ultramodern.8 → 3.9.3-ultramodern.1

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 (109) hide show
  1. package/dist/cjs/assembly.js +2 -2
  2. package/dist/cjs/data-platform/batch/options.js +48 -0
  3. package/dist/cjs/data-platform/batch/queue.js +48 -83
  4. package/dist/cjs/effect/context.js +28 -4
  5. package/dist/cjs/effect/edge-dispatcher.js +19 -19
  6. package/dist/cjs/effect/edge.js +26 -6
  7. package/dist/cjs/effect/entry-shape.js +38 -10
  8. package/dist/cjs/effect/handler/batch-handler.js +2 -3
  9. package/dist/cjs/effect/handler/batch.js +0 -9
  10. package/dist/cjs/effect/handler/http.js +1 -1
  11. package/dist/cjs/effect/handler/openapi.js +3 -3
  12. package/dist/cjs/effect/handler/rpc.js +2 -2
  13. package/dist/cjs/effect/handler.js +26 -26
  14. package/dist/cjs/effect/index.js +8 -8
  15. package/dist/cjs/effect/module.js +6 -2
  16. package/dist/cjs/effect/operation-context.js +3 -7
  17. package/dist/cjs/effect/rpcSerialization.js +1 -3
  18. package/dist/cjs/effect-client/cross-project.js +8 -11
  19. package/dist/cjs/effect-client/index.js +10 -10
  20. package/dist/cjs/microvertical-api.js +1 -1
  21. package/dist/esm/assembly.mjs +1 -1
  22. package/dist/esm/data-platform/batch/options.mjs +10 -0
  23. package/dist/esm/data-platform/batch/queue.mjs +48 -83
  24. package/dist/esm/effect/context.mjs +21 -6
  25. package/dist/esm/effect/edge-dispatcher.mjs +5 -5
  26. package/dist/esm/effect/edge.mjs +2 -0
  27. package/dist/esm/effect/entry-shape-registry.mjs +2 -2
  28. package/dist/esm/effect/entry-shape.mjs +27 -4
  29. package/dist/esm/effect/handler/batch-handler.mjs +3 -4
  30. package/dist/esm/effect/handler/batch.mjs +1 -7
  31. package/dist/esm/effect/handler/http.mjs +1 -1
  32. package/dist/esm/effect/handler/openapi.mjs +2 -2
  33. package/dist/esm/effect/handler/rpc.mjs +2 -2
  34. package/dist/esm/effect/handler.mjs +6 -6
  35. package/dist/esm/effect/index.mjs +1 -1
  36. package/dist/esm/effect/module.mjs +5 -1
  37. package/dist/esm/effect/operation-context.mjs +2 -3
  38. package/dist/esm/effect/rpcSerialization.mjs +1 -3
  39. package/dist/esm/effect-client/cross-project.mjs +8 -11
  40. package/dist/esm/effect-client/index.mjs +5 -5
  41. package/dist/esm/microvertical-api.mjs +1 -1
  42. package/dist/esm-node/assembly.mjs +1 -1
  43. package/dist/esm-node/data-platform/batch/options.mjs +11 -0
  44. package/dist/esm-node/data-platform/batch/queue.mjs +48 -83
  45. package/dist/esm-node/effect/context.mjs +21 -6
  46. package/dist/esm-node/effect/edge-dispatcher.mjs +5 -5
  47. package/dist/esm-node/effect/edge.mjs +2 -0
  48. package/dist/esm-node/effect/entry-shape.mjs +27 -4
  49. package/dist/esm-node/effect/handler/batch-handler.mjs +3 -4
  50. package/dist/esm-node/effect/handler/batch.mjs +1 -7
  51. package/dist/esm-node/effect/handler/http.mjs +1 -1
  52. package/dist/esm-node/effect/handler/openapi.mjs +2 -2
  53. package/dist/esm-node/effect/handler/rpc.mjs +2 -2
  54. package/dist/esm-node/effect/handler.mjs +6 -6
  55. package/dist/esm-node/effect/index.mjs +1 -1
  56. package/dist/esm-node/effect/module.mjs +5 -1
  57. package/dist/esm-node/effect/operation-context.mjs +2 -3
  58. package/dist/esm-node/effect/rpcSerialization.mjs +1 -3
  59. package/dist/esm-node/effect-client/cross-project.mjs +8 -11
  60. package/dist/esm-node/effect-client/index.mjs +5 -5
  61. package/dist/esm-node/microvertical-api.mjs +1 -1
  62. package/dist/types/assembly.d.ts +2 -2
  63. package/dist/types/data-platform/batch/options.d.ts +7 -0
  64. package/dist/types/data-platform/batch/queue.d.ts +0 -1
  65. package/dist/types/effect/context.d.ts +14 -1
  66. package/dist/types/effect/edge-dispatcher.d.ts +4 -4
  67. package/dist/types/effect/edge.d.ts +2 -0
  68. package/dist/types/effect/entry-shape.d.ts +14 -0
  69. package/dist/types/effect/handler/batch.d.ts +0 -1
  70. package/dist/types/effect/handler/definition.d.ts +2 -2
  71. package/dist/types/effect/handler/http.d.ts +2 -2
  72. package/dist/types/effect/handler/openapi.d.ts +2 -2
  73. package/dist/types/effect/handler/rpc.d.ts +1 -1
  74. package/dist/types/effect/handler/types.d.ts +5 -5
  75. package/dist/types/effect/handler.d.ts +5 -5
  76. package/dist/types/effect/module.d.ts +2 -1
  77. package/dist/types/effect/operation-context.d.ts +0 -1
  78. package/dist/types/effect/rpcSerialization.d.ts +1 -1
  79. package/dist/types/effect-client/cross-project.d.ts +2 -2
  80. package/dist/types/effect-client/index.d.ts +6 -6
  81. package/dist/types/microvertical-api.d.ts +4 -4
  82. package/package.json +39 -45
  83. package/src/assembly.ts +3 -2
  84. package/src/data-platform/batch/options.ts +20 -0
  85. package/src/data-platform/batch/queue.ts +61 -89
  86. package/src/effect/context.ts +64 -12
  87. package/src/effect/edge-dispatcher.ts +13 -12
  88. package/src/effect/edge.ts +5 -0
  89. package/src/effect/entry-shape.ts +97 -7
  90. package/src/effect/handler/batch-handler.ts +6 -12
  91. package/src/effect/handler/batch.ts +0 -12
  92. package/src/effect/handler/definition.ts +2 -3
  93. package/src/effect/handler/http.ts +4 -3
  94. package/src/effect/handler/openapi.ts +2 -2
  95. package/src/effect/handler/rpc.ts +3 -2
  96. package/src/effect/handler/types.ts +6 -12
  97. package/src/effect/handler.ts +5 -5
  98. package/src/effect/index.ts +1 -1
  99. package/src/effect/module.ts +8 -1
  100. package/src/effect/operation-context.ts +1 -5
  101. package/src/effect/rpcSerialization.ts +1 -3
  102. package/src/effect-client/cross-project.ts +11 -16
  103. package/src/effect-client/index.ts +11 -15
  104. package/src/microvertical-api.ts +1 -1
  105. package/dist/cjs/effect/edge-context.js +0 -61
  106. package/dist/esm/effect/edge-context.mjs +0 -14
  107. package/dist/esm-node/effect/edge-context.mjs +0 -15
  108. package/dist/types/effect/edge-context.d.ts +0 -5
  109. package/src/effect/edge-context.ts +0 -51
@@ -1,14 +1,14 @@
1
1
  import { type RequestContextInput } from '@modern-js/runtime-extensions/request-context';
2
2
  import * as Effect from 'effect/Effect';
3
+ import { HttpClient } from 'effect/http';
4
+ import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from 'effect/http-api';
3
5
  import * as Layer from 'effect/Layer';
4
- import { HttpClient } from 'effect/unstable/http';
5
- import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from 'effect/unstable/httpapi';
6
- import { Rpc, RpcClient, RpcGroup, RpcSchema, RpcSerialization } from 'effect/unstable/rpc';
6
+ import { Rpc, RpcClient, RpcClientError, RpcGroup, RpcSchema, RpcSerialization } from 'effect/rpc';
7
7
  import { type EffectCrossProjectClientOptions } from './cross-project.js';
8
8
  export * as Effect from 'effect/Effect';
9
+ export * as HttpClientError from 'effect/http/HttpClientError';
9
10
  export * as Layer from 'effect/Layer';
10
11
  export * as Schema from 'effect/Schema';
11
- export * as HttpClientError from 'effect/unstable/http/HttpClientError';
12
12
  export type { EffectCrossProjectClientOptions } from './cross-project.js';
13
13
  export { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, Rpc, RpcClient, RpcGroup, RpcSchema, RpcSerialization, };
14
14
  export type EffectHttpApiClientOptions = {
@@ -30,7 +30,7 @@ export type EffectViewSelection<T> = {
30
30
  export type EffectViewData<T, Selection> = Selection extends true ? T : Selection extends Record<string, unknown> ? {
31
31
  [K in keyof Selection & keyof T]: EffectMaskedValue<T[K], Selection[K]>;
32
32
  } : T;
33
- export type EffectRpcSerialization = 'json' | 'ndjson' | 'jsonRpc' | 'ndJsonRpc' | 'msgPack';
33
+ export type EffectRpcSerialization = 'json' | 'ndjson' | 'jsonRpc' | 'ndJsonRpc';
34
34
  declare const EffectRpcClientError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
35
35
  readonly _tag: "EffectRpcClientError";
36
36
  } & Readonly<A>;
@@ -50,7 +50,7 @@ export type EffectRpcClientOptions<Rpcs extends Rpc.Any, Flatten extends boolean
50
50
  flatten?: Flatten;
51
51
  serialization?: EffectRpcSerialization;
52
52
  };
53
- export type EffectRpcClientHandle<Rpcs extends Rpc.Any, Flatten extends boolean = false> = (Flatten extends true ? RpcClient.RpcClient.Flat<Rpcs, unknown> : RpcClient.RpcClient<Rpcs, unknown>) & {
53
+ export type EffectRpcClientHandle<Rpcs extends Rpc.Any, Flatten extends boolean = false> = (Flatten extends true ? RpcClient.RpcClient.Flat<Rpcs, RpcClientError.RpcClientError> : RpcClient.RpcClient<Rpcs, RpcClientError.RpcClientError>) & {
54
54
  dispose: () => Promise<void>;
55
55
  };
56
56
  export declare function view<T>(): <const Selection extends EffectViewSelection<T>>(selection: Selection) => Selection;
@@ -33,13 +33,13 @@ export declare const MicroVerticalReadinessSchema: Schema.Struct<{
33
33
  readonly versionSkew: Schema.Literal<"none">;
34
34
  }>;
35
35
  export type MicroVerticalReadiness = typeof MicroVerticalReadinessSchema.Type;
36
- export declare const MicroVerticalOperationSourceSchema: Schema.Literals<readonly ["client", "server", "generated-client", "effect-adapter", "data-platform", "unknown"]>;
36
+ export declare const MicroVerticalOperationSourceSchema: Schema.Literals<readonly ["client", "server", "generated-client", "effect-server", "data-platform", "unknown"]>;
37
37
  export type MicroVerticalOperationSource = typeof MicroVerticalOperationSourceSchema.Type;
38
38
  export declare const MicroVerticalOperationContextSchema: Schema.Struct<{
39
39
  readonly method: Schema.String;
40
40
  readonly operationId: Schema.compose<Schema.String, Schema.brand<Schema.String, "MicroVerticalOperationId">>;
41
41
  readonly routePath: Schema.String;
42
- readonly source: Schema.Literals<readonly ["client", "server", "generated-client", "effect-adapter", "data-platform", "unknown"]>;
42
+ readonly source: Schema.Literals<readonly ["client", "server", "generated-client", "effect-server", "data-platform", "unknown"]>;
43
43
  readonly traceId: Schema.optionalKey<Schema.compose<Schema.String, Schema.brand<Schema.String, "MicroVerticalTraceId">>>;
44
44
  }>;
45
45
  export type MicroVerticalOperationContext = typeof MicroVerticalOperationContextSchema.Type;
@@ -55,11 +55,11 @@ export declare const microVerticalOperationAttributes: (operationContext: MicroV
55
55
  'modernjs.operation.id': string;
56
56
  'modernjs.operation.method': string;
57
57
  'modernjs.operation.route': string;
58
- 'modernjs.operation.source': "client" | "data-platform" | "effect-adapter" | "generated-client" | "server" | "unknown";
58
+ 'modernjs.operation.source': "client" | "data-platform" | "effect-server" | "generated-client" | "server" | "unknown";
59
59
  } | {
60
60
  'modernjs.operation.id': string;
61
61
  'modernjs.operation.method': string;
62
62
  'modernjs.operation.route': string;
63
- 'modernjs.operation.source': "client" | "data-platform" | "effect-adapter" | "generated-client" | "server" | "unknown";
63
+ 'modernjs.operation.source': "client" | "data-platform" | "effect-server" | "generated-client" | "server" | "unknown";
64
64
  'modernjs.trace.id': string;
65
65
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@bleedingdev/modern-js-bff-effect",
3
3
  "description": "Fork-owned Effect runtime, client, edge dispatcher, and data-platform primitives for UltraModern.js BFFs.",
4
- "homepage": "https://github.com/BleedingDev/ultramodern.js#readme",
4
+ "homepage": "https://github.com/ultramodernjs/ultramodern.js#readme",
5
5
  "bugs": {
6
- "url": "https://github.com/BleedingDev/ultramodern.js/issues"
6
+ "url": "https://github.com/ultramodernjs/ultramodern.js/issues"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/BleedingDev/ultramodern.js.git",
10
+ "url": "git+https://github.com/ultramodernjs/ultramodern.js.git",
11
11
  "directory": "packages/server/bff-effect"
12
12
  },
13
13
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "modern.js",
19
19
  "ultramodern.js"
20
20
  ],
21
- "version": "3.9.0-ultramodern.8",
21
+ "version": "3.9.3-ultramodern.1",
22
22
  "files": [
23
23
  "dist",
24
24
  "src"
@@ -35,62 +35,49 @@
35
35
  "./package.json": "./package.json",
36
36
  ".": {
37
37
  "types": "./dist/types/index.d.ts",
38
- "node": {
39
- "import": "./dist/esm-node/index.mjs",
40
- "require": "./dist/cjs/index.js"
41
- },
38
+ "node": "./dist/esm-node/index.mjs",
39
+ "import": "./dist/esm/index.mjs",
42
40
  "default": "./dist/cjs/index.js"
43
41
  },
44
42
  "./effect": {
45
43
  "types": "./dist/types/effect/index.d.ts",
46
- "node": {
47
- "import": "./dist/esm-node/effect/index.mjs",
48
- "require": "./dist/cjs/effect/index.js"
49
- },
44
+ "node": "./dist/esm-node/effect/index.mjs",
45
+ "import": "./dist/esm/effect/index.mjs",
50
46
  "default": "./dist/cjs/effect/index.js"
51
47
  },
48
+ "./context": {
49
+ "types": "./dist/types/effect/context.d.ts",
50
+ "node": "./dist/esm-node/effect/context.mjs",
51
+ "import": "./dist/esm/effect/context.mjs",
52
+ "default": "./dist/cjs/effect/context.js"
53
+ },
52
54
  "./effect-edge": {
53
55
  "types": "./dist/types/effect/edge.d.ts",
54
- "node": {
55
- "import": "./dist/esm-node/effect/edge.mjs",
56
- "require": "./dist/cjs/effect/edge.js"
57
- },
56
+ "node": "./dist/esm-node/effect/edge.mjs",
58
57
  "import": "./dist/esm/effect/edge.mjs",
59
58
  "default": "./dist/esm/effect/edge.mjs"
60
59
  },
61
60
  "./effect-client": {
62
61
  "types": "./dist/types/effect-client/index.d.ts",
63
- "node": {
64
- "import": "./dist/esm-node/effect-client/index.mjs",
65
- "require": "./dist/cjs/effect-client/index.js"
66
- },
62
+ "node": "./dist/esm-node/effect-client/index.mjs",
67
63
  "import": "./dist/esm/effect-client/index.mjs",
68
64
  "default": "./dist/esm/effect-client/index.mjs"
69
65
  },
70
66
  "./microvertical-api": {
71
67
  "types": "./dist/types/microvertical-api.d.ts",
72
- "node": {
73
- "import": "./dist/esm-node/microvertical-api.mjs",
74
- "require": "./dist/cjs/microvertical-api.js"
75
- },
68
+ "node": "./dist/esm-node/microvertical-api.mjs",
76
69
  "import": "./dist/esm/microvertical-api.mjs",
77
70
  "default": "./dist/esm/microvertical-api.mjs"
78
71
  },
79
72
  "./data-platform": {
80
73
  "types": "./dist/types/data-platform/index.d.ts",
81
- "node": {
82
- "import": "./dist/esm-node/data-platform/index.mjs",
83
- "require": "./dist/cjs/data-platform/index.js"
84
- },
74
+ "node": "./dist/esm-node/data-platform/index.mjs",
85
75
  "import": "./dist/esm/data-platform/index.mjs",
86
76
  "default": "./dist/esm/data-platform/index.mjs"
87
77
  },
88
78
  "./assembly": {
89
79
  "types": "./dist/types/assembly.d.ts",
90
- "node": {
91
- "import": "./dist/esm-node/assembly.mjs",
92
- "require": "./dist/cjs/assembly.js"
93
- },
80
+ "node": "./dist/esm-node/assembly.mjs",
94
81
  "default": "./dist/esm/assembly.mjs"
95
82
  }
96
83
  },
@@ -104,6 +91,10 @@
104
91
  "./dist/types/effect/index.d.ts",
105
92
  "./src/effect/index.ts"
106
93
  ],
94
+ "context": [
95
+ "./dist/types/effect/context.d.ts",
96
+ "./src/effect/context.ts"
97
+ ],
107
98
  "effect-edge": [
108
99
  "./dist/types/effect/edge.d.ts",
109
100
  "./src/effect/edge.ts"
@@ -127,13 +118,13 @@
127
118
  }
128
119
  },
129
120
  "engines": {
130
- "node": ">=26.7.0"
121
+ "node": ">=26.10.0"
131
122
  },
132
123
  "dependencies": {
133
- "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.9.0-ultramodern.8",
134
- "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.9.0-ultramodern.8",
135
- "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.8",
136
- "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.8",
124
+ "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.9.3-ultramodern.1",
125
+ "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.9.3-ultramodern.1",
126
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.3-ultramodern.1",
127
+ "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.3-ultramodern.1",
137
128
  "@opentelemetry/api": "1.9.1",
138
129
  "@opentelemetry/api-logs": "0.222.0",
139
130
  "@opentelemetry/resources": "2.11.0",
@@ -145,8 +136,8 @@
145
136
  "@opentelemetry/semantic-conventions": "1.43.0"
146
137
  },
147
138
  "peerDependencies": {
148
- "@effect/opentelemetry": "4.0.0-rc.112",
149
- "effect": "4.0.0-rc.112"
139
+ "@effect/opentelemetry": "4.0.0",
140
+ "effect": "4.0.0"
150
141
  },
151
142
  "peerDependenciesMeta": {
152
143
  "@effect/opentelemetry": {
@@ -157,13 +148,13 @@
157
148
  }
158
149
  },
159
150
  "devDependencies": {
160
- "@effect/opentelemetry": "4.0.0-rc.112",
161
- "@rslib/core": "1.0.0",
151
+ "@effect/opentelemetry": "4.0.0",
152
+ "@rslib/core": "1.0.3",
162
153
  "@scripts/rstest-config": "2.66.0",
163
- "@types/node": "^26.4.1",
164
- "@types/react": "^19.2.18",
165
- "@typescript/native-preview": "7.0.0-dev.20260707.2",
166
- "effect": "4.0.0-rc.112"
154
+ "@types/node": "^26.6.3",
155
+ "@types/react": "^19.3.0",
156
+ "effect": "4.0.0",
157
+ "esbuild": "^0.28.2"
167
158
  },
168
159
  "sideEffects": false,
169
160
  "publishConfig": {
@@ -173,5 +164,8 @@
173
164
  "dev": "rslib build --watch",
174
165
  "build": "rslib build",
175
166
  "test": "rstest"
167
+ },
168
+ "ultramodern": {
169
+ "sourceName": "@modern-js/bff-effect"
176
170
  }
177
171
  }
package/src/assembly.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Server-only assembly for the invariant tail of a strict Effect BFF runtime factory. */
2
+
3
+ import type { HttpApi, HttpApiGroup } from 'effect/http-api';
4
+ import { HttpApiBuilder } from 'effect/http-api';
2
5
  import * as Layer from 'effect/Layer';
3
- import type { HttpApi, HttpApiGroup } from 'effect/unstable/httpapi';
4
- import { HttpApiBuilder } from 'effect/unstable/httpapi';
5
6
  import { defineEffectBff } from './effect/handler/definition';
6
7
  import type { EffectRuntimeRequirements } from './effect/handler/types';
7
8
 
@@ -0,0 +1,20 @@
1
+ import type { DataBatchTransportOptions } from '../types';
2
+
3
+ /** Wire limits and method eligibility are identical on both sides. */
4
+ export function normalizeBatchLimits(
5
+ options: Pick<
6
+ DataBatchTransportOptions,
7
+ 'maxBatchSize' | 'maxBatchBytes' | 'allowedMethods'
8
+ > = {},
9
+ ) {
10
+ return {
11
+ maxBatchSize: Math.max(1, options.maxBatchSize ?? 16),
12
+ maxBatchBytes: Math.max(1024, options.maxBatchBytes ?? 64 * 1024),
13
+ allowedMethods: new Set(
14
+ (options.allowedMethods !== undefined && options.allowedMethods.length > 0
15
+ ? options.allowedMethods
16
+ : ['GET']
17
+ ).map(method => method.toUpperCase()),
18
+ ),
19
+ };
20
+ }
@@ -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
  }
@@ -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;