@bleedingdev/modern-js-bff-effect 3.8.3-ultramodern.2

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 (230) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/cjs/data-platform/batch/protocol.js +130 -0
  4. package/dist/cjs/data-platform/batch/queue.js +623 -0
  5. package/dist/cjs/data-platform/batch/request.js +121 -0
  6. package/dist/cjs/data-platform/batch/response.js +65 -0
  7. package/dist/cjs/data-platform/batch/telemetry.js +71 -0
  8. package/dist/cjs/data-platform/batch/url.js +69 -0
  9. package/dist/cjs/data-platform/batch-transport.js +59 -0
  10. package/dist/cjs/data-platform/codec.js +155 -0
  11. package/dist/cjs/data-platform/index.js +124 -0
  12. package/dist/cjs/data-platform/invalidation.js +61 -0
  13. package/dist/cjs/data-platform/trace.js +73 -0
  14. package/dist/cjs/data-platform/types.js +48 -0
  15. package/dist/cjs/data-platform/validation.js +178 -0
  16. package/dist/cjs/effect/context.js +59 -0
  17. package/dist/cjs/effect/dispatch.js +101 -0
  18. package/dist/cjs/effect/edge-context.js +61 -0
  19. package/dist/cjs/effect/edge-dispatcher.js +354 -0
  20. package/dist/cjs/effect/edge.js +62 -0
  21. package/dist/cjs/effect/endpoint-contracts.js +121 -0
  22. package/dist/cjs/effect/entry-shape-registry.js +31 -0
  23. package/dist/cjs/effect/entry-shape.js +113 -0
  24. package/dist/cjs/effect/handler/batch-handler.js +270 -0
  25. package/dist/cjs/effect/handler/batch.js +170 -0
  26. package/dist/cjs/effect/handler/definition.js +99 -0
  27. package/dist/cjs/effect/handler/envelope.js +104 -0
  28. package/dist/cjs/effect/handler/http.js +107 -0
  29. package/dist/cjs/effect/handler/openapi.js +58 -0
  30. package/dist/cjs/effect/handler/routing.js +100 -0
  31. package/dist/cjs/effect/handler/rpc.js +63 -0
  32. package/dist/cjs/effect/handler/types.js +43 -0
  33. package/dist/cjs/effect/handler.js +173 -0
  34. package/dist/cjs/effect/index.js +137 -0
  35. package/dist/cjs/effect/module.js +118 -0
  36. package/dist/cjs/effect/operation-context.js +116 -0
  37. package/dist/cjs/effect/rpcSerialization.js +54 -0
  38. package/dist/cjs/effect-client/batch.js +73 -0
  39. package/dist/cjs/effect-client/envelope.js +145 -0
  40. package/dist/cjs/effect-client/guards.js +44 -0
  41. package/dist/cjs/effect-client/index.js +208 -0
  42. package/dist/cjs/effect-client/request-context.js +64 -0
  43. package/dist/cjs/effect-client/request.js +66 -0
  44. package/dist/cjs/effect-client/runtime.js +100 -0
  45. package/dist/cjs/effect-client/types.js +18 -0
  46. package/dist/cjs/headers.js +46 -0
  47. package/dist/cjs/index.js +83 -0
  48. package/dist/esm/data-platform/batch/protocol.mjs +71 -0
  49. package/dist/esm/data-platform/batch/queue.mjs +582 -0
  50. package/dist/esm/data-platform/batch/request.mjs +68 -0
  51. package/dist/esm/data-platform/batch/response.mjs +24 -0
  52. package/dist/esm/data-platform/batch/telemetry.mjs +27 -0
  53. package/dist/esm/data-platform/batch/url.mjs +25 -0
  54. package/dist/esm/data-platform/batch-transport.mjs +12 -0
  55. package/dist/esm/data-platform/codec.mjs +90 -0
  56. package/dist/esm/data-platform/index.mjs +7 -0
  57. package/dist/esm/data-platform/invalidation.mjs +20 -0
  58. package/dist/esm/data-platform/trace.mjs +29 -0
  59. package/dist/esm/data-platform/types.mjs +4 -0
  60. package/dist/esm/data-platform/validation.mjs +128 -0
  61. package/dist/esm/effect/context.mjs +12 -0
  62. package/dist/esm/effect/dispatch.mjs +63 -0
  63. package/dist/esm/effect/edge-context.mjs +14 -0
  64. package/dist/esm/effect/edge-dispatcher.mjs +115 -0
  65. package/dist/esm/effect/edge.mjs +1 -0
  66. package/dist/esm/effect/endpoint-contracts.mjs +68 -0
  67. package/dist/esm/effect/entry-shape-registry.mjs +17 -0
  68. package/dist/esm/effect/entry-shape.mjs +51 -0
  69. package/dist/esm/effect/handler/batch-handler.mjs +232 -0
  70. package/dist/esm/effect/handler/batch.mjs +105 -0
  71. package/dist/esm/effect/handler/definition.mjs +58 -0
  72. package/dist/esm/effect/handler/envelope.mjs +63 -0
  73. package/dist/esm/effect/handler/http.mjs +69 -0
  74. package/dist/esm/effect/handler/openapi.mjs +20 -0
  75. package/dist/esm/effect/handler/routing.mjs +50 -0
  76. package/dist/esm/effect/handler/rpc.mjs +22 -0
  77. package/dist/esm/effect/handler/types.mjs +5 -0
  78. package/dist/esm/effect/handler.mjs +13 -0
  79. package/dist/esm/effect/index.mjs +7 -0
  80. package/dist/esm/effect/module.mjs +80 -0
  81. package/dist/esm/effect/operation-context.mjs +75 -0
  82. package/dist/esm/effect/rpcSerialization.mjs +16 -0
  83. package/dist/esm/effect-client/batch.mjs +35 -0
  84. package/dist/esm/effect-client/envelope.mjs +104 -0
  85. package/dist/esm/effect-client/guards.mjs +3 -0
  86. package/dist/esm/effect-client/index.mjs +110 -0
  87. package/dist/esm/effect-client/request-context.mjs +23 -0
  88. package/dist/esm/effect-client/request.mjs +28 -0
  89. package/dist/esm/effect-client/runtime.mjs +62 -0
  90. package/dist/esm/effect-client/types.mjs +0 -0
  91. package/dist/esm/headers.mjs +8 -0
  92. package/dist/esm/index.mjs +4 -0
  93. package/dist/esm/rslib-runtime.mjs +18 -0
  94. package/dist/esm-node/data-platform/batch/protocol.mjs +72 -0
  95. package/dist/esm-node/data-platform/batch/queue.mjs +583 -0
  96. package/dist/esm-node/data-platform/batch/request.mjs +69 -0
  97. package/dist/esm-node/data-platform/batch/response.mjs +25 -0
  98. package/dist/esm-node/data-platform/batch/telemetry.mjs +28 -0
  99. package/dist/esm-node/data-platform/batch/url.mjs +26 -0
  100. package/dist/esm-node/data-platform/batch-transport.mjs +13 -0
  101. package/dist/esm-node/data-platform/codec.mjs +91 -0
  102. package/dist/esm-node/data-platform/index.mjs +8 -0
  103. package/dist/esm-node/data-platform/invalidation.mjs +21 -0
  104. package/dist/esm-node/data-platform/trace.mjs +30 -0
  105. package/dist/esm-node/data-platform/types.mjs +5 -0
  106. package/dist/esm-node/data-platform/validation.mjs +129 -0
  107. package/dist/esm-node/effect/context.mjs +13 -0
  108. package/dist/esm-node/effect/dispatch.mjs +64 -0
  109. package/dist/esm-node/effect/edge-context.mjs +15 -0
  110. package/dist/esm-node/effect/edge-dispatcher.mjs +116 -0
  111. package/dist/esm-node/effect/edge.mjs +2 -0
  112. package/dist/esm-node/effect/endpoint-contracts.mjs +69 -0
  113. package/dist/esm-node/effect/entry-shape-registry.mjs +18 -0
  114. package/dist/esm-node/effect/entry-shape.mjs +63 -0
  115. package/dist/esm-node/effect/handler/batch-handler.mjs +233 -0
  116. package/dist/esm-node/effect/handler/batch.mjs +106 -0
  117. package/dist/esm-node/effect/handler/definition.mjs +59 -0
  118. package/dist/esm-node/effect/handler/envelope.mjs +64 -0
  119. package/dist/esm-node/effect/handler/http.mjs +70 -0
  120. package/dist/esm-node/effect/handler/openapi.mjs +21 -0
  121. package/dist/esm-node/effect/handler/routing.mjs +51 -0
  122. package/dist/esm-node/effect/handler/rpc.mjs +23 -0
  123. package/dist/esm-node/effect/handler/types.mjs +6 -0
  124. package/dist/esm-node/effect/handler.mjs +14 -0
  125. package/dist/esm-node/effect/index.mjs +8 -0
  126. package/dist/esm-node/effect/module.mjs +81 -0
  127. package/dist/esm-node/effect/operation-context.mjs +76 -0
  128. package/dist/esm-node/effect/rpcSerialization.mjs +17 -0
  129. package/dist/esm-node/effect-client/batch.mjs +36 -0
  130. package/dist/esm-node/effect-client/envelope.mjs +105 -0
  131. package/dist/esm-node/effect-client/guards.mjs +4 -0
  132. package/dist/esm-node/effect-client/index.mjs +111 -0
  133. package/dist/esm-node/effect-client/request-context.mjs +24 -0
  134. package/dist/esm-node/effect-client/request.mjs +29 -0
  135. package/dist/esm-node/effect-client/runtime.mjs +63 -0
  136. package/dist/esm-node/effect-client/types.mjs +1 -0
  137. package/dist/esm-node/headers.mjs +9 -0
  138. package/dist/esm-node/index.mjs +5 -0
  139. package/dist/esm-node/rslib-runtime.mjs +19 -0
  140. package/dist/types/data-platform/batch/protocol.d.ts +9 -0
  141. package/dist/types/data-platform/batch/queue.d.ts +47 -0
  142. package/dist/types/data-platform/batch/request.d.ts +19 -0
  143. package/dist/types/data-platform/batch/response.d.ts +3 -0
  144. package/dist/types/data-platform/batch/telemetry.d.ts +4 -0
  145. package/dist/types/data-platform/batch/url.d.ts +4 -0
  146. package/dist/types/data-platform/batch-transport.d.ts +3 -0
  147. package/dist/types/data-platform/codec.d.ts +17 -0
  148. package/dist/types/data-platform/index.d.ts +8 -0
  149. package/dist/types/data-platform/invalidation.d.ts +8 -0
  150. package/dist/types/data-platform/trace.d.ts +4 -0
  151. package/dist/types/data-platform/types.d.ts +151 -0
  152. package/dist/types/data-platform/validation.d.ts +29 -0
  153. package/dist/types/effect/context.d.ts +5 -0
  154. package/dist/types/effect/dispatch.d.ts +13 -0
  155. package/dist/types/effect/edge-context.d.ts +5 -0
  156. package/dist/types/effect/edge-dispatcher.d.ts +37 -0
  157. package/dist/types/effect/edge.d.ts +1 -0
  158. package/dist/types/effect/endpoint-contracts.d.ts +67 -0
  159. package/dist/types/effect/entry-shape.d.ts +32 -0
  160. package/dist/types/effect/handler/batch-handler.d.ts +11 -0
  161. package/dist/types/effect/handler/batch.d.ts +15 -0
  162. package/dist/types/effect/handler/definition.d.ts +7 -0
  163. package/dist/types/effect/handler/envelope.d.ts +13 -0
  164. package/dist/types/effect/handler/http.d.ts +15 -0
  165. package/dist/types/effect/handler/openapi.d.ts +4 -0
  166. package/dist/types/effect/handler/routing.d.ts +5 -0
  167. package/dist/types/effect/handler/rpc.d.ts +7 -0
  168. package/dist/types/effect/handler/types.d.ts +153 -0
  169. package/dist/types/effect/handler.d.ts +13 -0
  170. package/dist/types/effect/index.d.ts +6 -0
  171. package/dist/types/effect/module.d.ts +36 -0
  172. package/dist/types/effect/operation-context.d.ts +15 -0
  173. package/dist/types/effect/rpcSerialization.d.ts +3 -0
  174. package/dist/types/effect-client/batch.d.ts +2 -0
  175. package/dist/types/effect-client/envelope.d.ts +15 -0
  176. package/dist/types/effect-client/guards.d.ts +2 -0
  177. package/dist/types/effect-client/index.d.ts +58 -0
  178. package/dist/types/effect-client/request-context.d.ts +3 -0
  179. package/dist/types/effect-client/request.d.ts +1 -0
  180. package/dist/types/effect-client/runtime.d.ts +5 -0
  181. package/dist/types/effect-client/types.d.ts +68 -0
  182. package/dist/types/headers.d.ts +1 -0
  183. package/dist/types/index.d.ts +3 -0
  184. package/package.json +164 -0
  185. package/src/data-platform/batch/protocol.ts +112 -0
  186. package/src/data-platform/batch/queue.ts +876 -0
  187. package/src/data-platform/batch/request.ts +130 -0
  188. package/src/data-platform/batch/response.ts +74 -0
  189. package/src/data-platform/batch/telemetry.ts +47 -0
  190. package/src/data-platform/batch/url.ts +54 -0
  191. package/src/data-platform/batch-transport.ts +22 -0
  192. package/src/data-platform/codec.ts +145 -0
  193. package/src/data-platform/index.ts +63 -0
  194. package/src/data-platform/invalidation.ts +57 -0
  195. package/src/data-platform/trace.ts +49 -0
  196. package/src/data-platform/types.ts +186 -0
  197. package/src/data-platform/validation.ts +303 -0
  198. package/src/effect/context.ts +37 -0
  199. package/src/effect/dispatch.ts +132 -0
  200. package/src/effect/edge-context.ts +51 -0
  201. package/src/effect/edge-dispatcher.ts +310 -0
  202. package/src/effect/edge.ts +1 -0
  203. package/src/effect/endpoint-contracts.ts +191 -0
  204. package/src/effect/entry-shape-registry.cjs +16 -0
  205. package/src/effect/entry-shape-registry.d.cts +6 -0
  206. package/src/effect/entry-shape.ts +155 -0
  207. package/src/effect/handler/batch-handler.ts +475 -0
  208. package/src/effect/handler/batch.ts +182 -0
  209. package/src/effect/handler/definition.ts +121 -0
  210. package/src/effect/handler/envelope.ts +133 -0
  211. package/src/effect/handler/http.ts +133 -0
  212. package/src/effect/handler/openapi.ts +45 -0
  213. package/src/effect/handler/routing.ts +91 -0
  214. package/src/effect/handler/rpc.ts +45 -0
  215. package/src/effect/handler/types.ts +255 -0
  216. package/src/effect/handler.ts +33 -0
  217. package/src/effect/index.ts +22 -0
  218. package/src/effect/module.ts +186 -0
  219. package/src/effect/operation-context.ts +144 -0
  220. package/src/effect/rpcSerialization.ts +20 -0
  221. package/src/effect-client/batch.ts +50 -0
  222. package/src/effect-client/envelope.ts +238 -0
  223. package/src/effect-client/guards.ts +6 -0
  224. package/src/effect-client/index.ts +325 -0
  225. package/src/effect-client/request-context.ts +54 -0
  226. package/src/effect-client/request.ts +51 -0
  227. package/src/effect-client/runtime.ts +122 -0
  228. package/src/effect-client/types.ts +84 -0
  229. package/src/headers.ts +7 -0
  230. package/src/index.ts +3 -0
@@ -0,0 +1,475 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import {
3
+ type DataBatchResponsePayload,
4
+ DEFAULT_DATA_BATCH_HEADER,
5
+ DEFAULT_DATA_ENVELOPE_HEADER,
6
+ decodeRequestEnvelopeHeader,
7
+ isPlainObject,
8
+ normalizeMethod as normalizeItemMethod,
9
+ } from '../../data-platform';
10
+ import {
11
+ decodeBatchBody,
12
+ isBatchBody,
13
+ toBatchResponseItem,
14
+ } from '../../data-platform/batch/protocol';
15
+ import {
16
+ BatchItemTimeoutError,
17
+ createBatchValidationResponse,
18
+ isBatchRequestPayload,
19
+ mapWithConcurrency,
20
+ normalizeBatchAllowedMethods,
21
+ normalizeBatchPath,
22
+ promiseWithTimeout,
23
+ toBatchItemError,
24
+ } from './batch';
25
+ import {
26
+ getMountedPrefixFromContext,
27
+ removeMountedPrefixFromBatchPath,
28
+ } from './routing';
29
+ import type { EffectDataPlatformValidationOptions } from './types';
30
+
31
+ type DataPlatformBatchItemHandler<TContext> = (
32
+ request: Request,
33
+ context?: TContext,
34
+ ) => Promise<unknown>;
35
+
36
+ const HOP_BY_HOP_BATCH_ITEM_HEADERS = new Set([
37
+ 'connection',
38
+ 'keep-alive',
39
+ 'proxy-authenticate',
40
+ 'proxy-authorization',
41
+ 'proxy-connection',
42
+ 'te',
43
+ 'trailer',
44
+ 'transfer-encoding',
45
+ 'upgrade',
46
+ ]);
47
+
48
+ const OUTER_BOUND_BATCH_ITEM_HEADERS = new Set([
49
+ 'authorization',
50
+ 'cookie',
51
+ 'forwarded',
52
+ 'origin',
53
+ 'referer',
54
+ 'sec-fetch-dest',
55
+ 'sec-fetch-mode',
56
+ 'sec-fetch-site',
57
+ 'sec-fetch-user',
58
+ 'x-forwarded-client-cert',
59
+ 'x-forwarded-for',
60
+ 'x-forwarded-host',
61
+ 'x-forwarded-port',
62
+ 'x-forwarded-proto',
63
+ 'x-real-ip',
64
+ 'x-subject-id',
65
+ 'x-tenant-id',
66
+ 'x-user-id',
67
+ 'x-verified-producer',
68
+ ]);
69
+
70
+ const TRANSPORT_CONTROLLED_BATCH_ITEM_HEADERS = new Set([
71
+ 'content-length',
72
+ 'host',
73
+ ...HOP_BY_HOP_BATCH_ITEM_HEADERS,
74
+ ]);
75
+
76
+ type BoundedRequestBodyResult =
77
+ | { status: 'ok'; text: string }
78
+ | { status: 'invalid-content-length' }
79
+ | { status: 'read-error' }
80
+ | { status: 'too-large' };
81
+
82
+ async function readRequestTextWithinLimit(
83
+ request: Request,
84
+ maxBytes: number,
85
+ ): Promise<BoundedRequestBodyResult> {
86
+ const contentLength = request.headers.get('content-length');
87
+ if (contentLength !== null) {
88
+ if (!/^\d+$/.test(contentLength)) {
89
+ return { status: 'invalid-content-length' };
90
+ }
91
+ if (BigInt(contentLength) > BigInt(maxBytes)) {
92
+ return { status: 'too-large' };
93
+ }
94
+ }
95
+ if (request.body === null) {
96
+ return { status: 'ok', text: '' };
97
+ }
98
+
99
+ const reader = request.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ const text: string[] = [];
102
+ let bytesRead = 0;
103
+ try {
104
+ while (true) {
105
+ const chunk = await reader.read();
106
+ if (chunk.done) {
107
+ text.push(decoder.decode());
108
+ return { status: 'ok', text: text.join('') };
109
+ }
110
+ bytesRead += chunk.value.byteLength;
111
+ if (bytesRead > maxBytes) {
112
+ await reader.cancel('batch-payload-too-large');
113
+ return { status: 'too-large' };
114
+ }
115
+ text.push(decoder.decode(chunk.value, { stream: true }));
116
+ }
117
+ } catch {
118
+ return { status: 'read-error' };
119
+ } finally {
120
+ reader.releaseLock();
121
+ }
122
+ }
123
+
124
+ export function createDataPlatformBatchRequestHandler<TContext>(options: {
125
+ dataPlatform?: EffectDataPlatformValidationOptions;
126
+ handleItem: DataPlatformBatchItemHandler<TContext>;
127
+ }) {
128
+ const dataPlatformBatchOptions = options.dataPlatform?.batch;
129
+ const batchEnabled = dataPlatformBatchOptions?.enabled !== false;
130
+ const batchPath = normalizeBatchPath(dataPlatformBatchOptions?.endpoint);
131
+ const batchMaxSize = Math.max(
132
+ 1,
133
+ dataPlatformBatchOptions?.maxBatchSize ?? 16,
134
+ );
135
+ const batchMaxBytes = Math.max(
136
+ 1024,
137
+ dataPlatformBatchOptions?.maxBatchBytes ?? 64 * 1024,
138
+ );
139
+ const batchConcurrency = Math.max(
140
+ 1,
141
+ dataPlatformBatchOptions?.maxConcurrency ?? 4,
142
+ );
143
+ const batchItemTimeoutMs = Math.max(
144
+ 0,
145
+ dataPlatformBatchOptions?.requestTimeoutMs ?? 10_000,
146
+ );
147
+ const batchAllowedMethods = normalizeBatchAllowedMethods(
148
+ dataPlatformBatchOptions?.allowedMethods,
149
+ );
150
+ const envelopeHeader =
151
+ options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
152
+ const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
153
+
154
+ const handle = async (request: Request, context?: TContext) => {
155
+ const mountedPrefix = getMountedPrefixFromContext(request, context);
156
+ const method = normalizeItemMethod(request.method);
157
+ if (method !== 'POST') {
158
+ return createBatchValidationResponse(
159
+ 'Batch endpoint only supports POST requests',
160
+ 405,
161
+ );
162
+ }
163
+
164
+ const contentType = request.headers.get('content-type') || '';
165
+ if (!contentType.includes('application/json')) {
166
+ return createBatchValidationResponse(
167
+ 'Batch endpoint requires application/json content-type',
168
+ 415,
169
+ );
170
+ }
171
+
172
+ const bodyResult = await readRequestTextWithinLimit(request, batchMaxBytes);
173
+ if (bodyResult.status === 'too-large') {
174
+ return createBatchValidationResponse(
175
+ `Batch payload exceeds max size (${String(batchMaxBytes)} bytes)`,
176
+ 413,
177
+ );
178
+ }
179
+ if (bodyResult.status === 'invalid-content-length') {
180
+ return createBatchValidationResponse(
181
+ 'Batch payload has invalid content-length',
182
+ );
183
+ }
184
+ if (bodyResult.status === 'read-error') {
185
+ return createBatchValidationResponse('Unable to read batch payload');
186
+ }
187
+ const payloadText = bodyResult.text;
188
+
189
+ let payload: unknown;
190
+ try {
191
+ payload = JSON.parse(payloadText);
192
+ } catch {
193
+ return createBatchValidationResponse('Invalid batch payload JSON');
194
+ }
195
+
196
+ if (!isBatchRequestPayload(payload)) {
197
+ return createBatchValidationResponse('Invalid batch payload shape');
198
+ }
199
+
200
+ if (payload.items.length === 0) {
201
+ return createBatchValidationResponse(
202
+ 'Batch payload items cannot be empty',
203
+ );
204
+ }
205
+
206
+ if (payload.items.length > batchMaxSize) {
207
+ return createBatchValidationResponse(
208
+ `Batch item count exceeds max size (${String(batchMaxSize)})`,
209
+ 413,
210
+ );
211
+ }
212
+
213
+ const batchRequestOrigin = new URL(request.url).origin;
214
+ const responseItems = await mapWithConcurrency(
215
+ payload.items,
216
+ batchConcurrency,
217
+ async (rawItem, index) => {
218
+ const fallbackId = `item_${String(index)}`;
219
+ const itemId =
220
+ isPlainObject(rawItem) && typeof rawItem.id === 'string'
221
+ ? rawItem.id
222
+ : fallbackId;
223
+
224
+ if (!isPlainObject(rawItem)) {
225
+ return toBatchItemError(
226
+ itemId,
227
+ 400,
228
+ 'Invalid batch item; expected object',
229
+ );
230
+ }
231
+
232
+ if (typeof rawItem.path !== 'string' || rawItem.path.length === 0) {
233
+ return toBatchItemError(itemId, 400, 'Invalid batch item path');
234
+ }
235
+
236
+ if (!rawItem.path.startsWith('/')) {
237
+ return toBatchItemError(
238
+ itemId,
239
+ 400,
240
+ 'Batch item path must start with "/"',
241
+ );
242
+ }
243
+
244
+ const normalizedItemPath = removeMountedPrefixFromBatchPath(
245
+ rawItem.path,
246
+ mountedPrefix,
247
+ );
248
+ const itemPathname =
249
+ normalizedItemPath.split('?')[0] || normalizedItemPath;
250
+ if (
251
+ itemPathname === batchPath ||
252
+ itemPathname.startsWith(`${batchPath}/`)
253
+ ) {
254
+ return toBatchItemError(
255
+ itemId,
256
+ 400,
257
+ 'Batch item path cannot target batch endpoint',
258
+ );
259
+ }
260
+
261
+ const itemMethod = normalizeItemMethod(
262
+ typeof rawItem.method === 'string' ? rawItem.method : undefined,
263
+ );
264
+ if (!batchAllowedMethods.has(itemMethod)) {
265
+ return toBatchItemError(
266
+ itemId,
267
+ 405,
268
+ `Batch item method ${itemMethod} is not allowed`,
269
+ );
270
+ }
271
+
272
+ if (typeof rawItem.body !== 'undefined' && !isBatchBody(rawItem.body)) {
273
+ return toBatchItemError(
274
+ itemId,
275
+ 400,
276
+ 'Batch item body must use the base64 body protocol',
277
+ );
278
+ }
279
+
280
+ if (
281
+ (itemMethod === 'GET' || itemMethod === 'HEAD') &&
282
+ typeof rawItem.body !== 'undefined'
283
+ ) {
284
+ return toBatchItemError(
285
+ itemId,
286
+ 400,
287
+ `${itemMethod} batch item cannot include body`,
288
+ );
289
+ }
290
+
291
+ const normalizedHeaders: Record<string, string> = {};
292
+ if (typeof rawItem.headers !== 'undefined') {
293
+ if (!isPlainObject(rawItem.headers)) {
294
+ return toBatchItemError(
295
+ itemId,
296
+ 400,
297
+ 'Batch item headers must be an object',
298
+ );
299
+ }
300
+
301
+ for (const [key, value] of Object.entries(rawItem.headers)) {
302
+ if (typeof value !== 'string') {
303
+ return toBatchItemError(
304
+ itemId,
305
+ 400,
306
+ `Invalid header "${key}" for batch item`,
307
+ );
308
+ }
309
+ normalizedHeaders[key.toLowerCase()] = value;
310
+ }
311
+ }
312
+
313
+ const connectionScopedHeaders = new Set(
314
+ (normalizedHeaders.connection || '')
315
+ .split(',')
316
+ .map(header => header.trim().toLowerCase())
317
+ .filter(Boolean),
318
+ );
319
+ for (const header of Object.keys(normalizedHeaders)) {
320
+ if (
321
+ TRANSPORT_CONTROLLED_BATCH_ITEM_HEADERS.has(header) ||
322
+ OUTER_BOUND_BATCH_ITEM_HEADERS.has(header) ||
323
+ connectionScopedHeaders.has(header)
324
+ ) {
325
+ delete normalizedHeaders[header];
326
+ }
327
+ }
328
+
329
+ if (!normalizedHeaders.traceparent) {
330
+ const encodedEnvelope = normalizedHeaders[normalizedEnvelopeHeader];
331
+ if (typeof encodedEnvelope === 'string') {
332
+ const envelope = decodeRequestEnvelopeHeader(encodedEnvelope);
333
+ if (envelope?.traceparent) {
334
+ normalizedHeaders.traceparent = envelope.traceparent;
335
+ }
336
+ }
337
+ }
338
+
339
+ // Authentication, verified identity, and network-origin context belong
340
+ // to the authenticated outer request. Batch items may carry application
341
+ // and operation metadata, but cannot manufacture or replace this context.
342
+ for (const header of OUTER_BOUND_BATCH_ITEM_HEADERS) {
343
+ const value = request.headers.get(header);
344
+ if (value !== null) {
345
+ normalizedHeaders[header] = value;
346
+ }
347
+ }
348
+
349
+ if (!normalizedHeaders.traceparent) {
350
+ const requestTraceparent = request.headers.get('traceparent');
351
+ if (requestTraceparent) {
352
+ normalizedHeaders.traceparent = requestTraceparent;
353
+ }
354
+ }
355
+
356
+ const targetUrl = new URL(normalizedItemPath, request.url);
357
+ if (targetUrl.origin !== batchRequestOrigin) {
358
+ return toBatchItemError(
359
+ itemId,
360
+ 400,
361
+ 'Batch item path must stay on the same origin',
362
+ );
363
+ }
364
+ let requestHeaders: Headers;
365
+ try {
366
+ requestHeaders = new Headers(normalizedHeaders);
367
+ } catch {
368
+ return toBatchItemError(itemId, 400, 'Invalid batch item headers');
369
+ }
370
+ const body =
371
+ itemMethod === 'GET' || itemMethod === 'HEAD'
372
+ ? undefined
373
+ : typeof rawItem.body === 'undefined'
374
+ ? undefined
375
+ : decodeBatchBody(rawItem.body);
376
+
377
+ if (typeof body === 'undefined') {
378
+ requestHeaders.delete('content-type');
379
+ }
380
+
381
+ const itemAbortController = new AbortController();
382
+ const abortFromBatchRequest = () =>
383
+ itemAbortController.abort(request.signal.reason);
384
+ if (request.signal.aborted) {
385
+ abortFromBatchRequest();
386
+ } else {
387
+ request.signal.addEventListener('abort', abortFromBatchRequest, {
388
+ once: true,
389
+ });
390
+ }
391
+ const itemRequest = new Request(targetUrl.toString(), {
392
+ method: itemMethod,
393
+ headers: requestHeaders,
394
+ body,
395
+ signal: itemAbortController.signal,
396
+ });
397
+
398
+ try {
399
+ const itemResponse = await promiseWithTimeout(
400
+ options.handleItem(itemRequest, context),
401
+ batchItemTimeoutMs,
402
+ error => itemAbortController.abort(error),
403
+ );
404
+
405
+ if (!(itemResponse instanceof Response)) {
406
+ return toBatchItemError(
407
+ itemId,
408
+ 500,
409
+ 'Invalid response returned by batch item handler',
410
+ );
411
+ }
412
+
413
+ return toBatchResponseItem(
414
+ itemId,
415
+ itemResponse,
416
+ itemMethod === 'HEAD',
417
+ );
418
+ } catch (error) {
419
+ if (error instanceof BatchItemTimeoutError) {
420
+ console.error({
421
+ event: 'bff.batch.item.timeout',
422
+ batchId: payload.batchId,
423
+ itemId,
424
+ method: itemMethod,
425
+ path: itemPathname,
426
+ errorName: error.name,
427
+ });
428
+ return toBatchItemError(
429
+ itemId,
430
+ 504,
431
+ 'Batch item request timed out',
432
+ );
433
+ }
434
+ if (error instanceof Response) {
435
+ return toBatchResponseItem(itemId, error, itemMethod === 'HEAD');
436
+ }
437
+
438
+ console.error({
439
+ event: 'bff.batch.item.failure',
440
+ batchId: payload.batchId,
441
+ itemId,
442
+ method: itemMethod,
443
+ path: itemPathname,
444
+ errorName: error instanceof Error ? error.name : 'UnknownError',
445
+ });
446
+ return toBatchItemError(itemId, 500, 'Internal Server Error');
447
+ } finally {
448
+ request.signal.removeEventListener('abort', abortFromBatchRequest);
449
+ }
450
+ },
451
+ );
452
+
453
+ const responsePayload: DataBatchResponsePayload = {
454
+ protocolVersion: 2,
455
+ batchId: payload.batchId,
456
+ receivedAt: Date.now(),
457
+ items: responseItems,
458
+ };
459
+
460
+ return new Response(JSON.stringify(responsePayload), {
461
+ status: 200,
462
+ headers: {
463
+ 'content-type': 'application/json; charset=utf-8',
464
+ [DEFAULT_DATA_BATCH_HEADER]: '2',
465
+ 'x-modernjs-data-batch-id': payload.batchId,
466
+ },
467
+ });
468
+ };
469
+
470
+ return {
471
+ enabled: batchEnabled,
472
+ path: batchPath,
473
+ handle,
474
+ };
475
+ }
@@ -0,0 +1,182 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+
3
+ import {
4
+ type DataBatchRequestPayload,
5
+ type DataBatchResponseItem,
6
+ DEFAULT_DATA_BATCH_ENDPOINT,
7
+ isPlainObject,
8
+ measureTextBytes,
9
+ normalizeMethod as normalizeItemMethod,
10
+ } from '../../data-platform';
11
+ import { encodeBatchText } from '../../data-platform/batch/protocol';
12
+ import { toHeaderRecord } from '../../headers';
13
+
14
+ export { toHeaderRecord } from '../../headers';
15
+
16
+ export function normalizeBatchPath(pathname: string | undefined) {
17
+ if (!pathname || pathname === '/') {
18
+ return DEFAULT_DATA_BATCH_ENDPOINT as `/${string}`;
19
+ }
20
+ if (!pathname.startsWith('/')) {
21
+ return `/${pathname}` as `/${string}`;
22
+ }
23
+ return pathname as `/${string}`;
24
+ }
25
+
26
+ export function normalizeBatchAllowedMethods(
27
+ allowedMethods: string[] | undefined,
28
+ ) {
29
+ const source =
30
+ Array.isArray(allowedMethods) && allowedMethods.length > 0
31
+ ? allowedMethods
32
+ : ['GET'];
33
+ return new Set(source.map(method => method.toUpperCase()));
34
+ }
35
+
36
+ type ParsedBatchRequestPayload = Omit<DataBatchRequestPayload, 'items'> & {
37
+ items: unknown[];
38
+ };
39
+
40
+ export function isBatchRequestPayload(
41
+ value: unknown,
42
+ ): value is ParsedBatchRequestPayload {
43
+ return (
44
+ isPlainObject(value) &&
45
+ value.protocolVersion === 2 &&
46
+ typeof value.batchId === 'string' &&
47
+ typeof value.sentAt === 'number' &&
48
+ Array.isArray(value.items)
49
+ );
50
+ }
51
+
52
+ export function createBatchValidationResponse(message: string, status = 400) {
53
+ return new Response(
54
+ JSON.stringify({
55
+ message,
56
+ }),
57
+ {
58
+ status,
59
+ headers: {
60
+ 'content-type': 'application/json; charset=utf-8',
61
+ },
62
+ },
63
+ );
64
+ }
65
+
66
+ function cloneWithoutJsonBodyHeaders(request: Request) {
67
+ const headers = new Headers(request.headers);
68
+ headers.delete('content-type');
69
+ headers.delete('content-length');
70
+
71
+ return new Request(request.url, {
72
+ method: request.method,
73
+ headers,
74
+ signal: request.signal,
75
+ });
76
+ }
77
+
78
+ export async function prepareJsonRequestBody(request: Request) {
79
+ const method = normalizeItemMethod(request.method);
80
+ if (method === 'GET' || method === 'HEAD') {
81
+ return request;
82
+ }
83
+
84
+ const contentType = (request.headers.get('content-type') || '').toLowerCase();
85
+ if (!contentType.includes('application/json')) {
86
+ return request;
87
+ }
88
+
89
+ if (request.body === null) {
90
+ return cloneWithoutJsonBodyHeaders(request);
91
+ }
92
+
93
+ try {
94
+ const bodyText = await request.clone().text();
95
+ if (bodyText === '') {
96
+ return cloneWithoutJsonBodyHeaders(request);
97
+ }
98
+
99
+ JSON.parse(bodyText);
100
+ } catch {
101
+ return createBatchValidationResponse('Invalid JSON request body');
102
+ }
103
+
104
+ return request;
105
+ }
106
+
107
+ export function toBatchItemError(
108
+ id: string,
109
+ status: number,
110
+ message: string,
111
+ ): DataBatchResponseItem {
112
+ return {
113
+ id,
114
+ status,
115
+ headers: [['content-type', 'application/json; charset=utf-8']],
116
+ body: encodeBatchText(JSON.stringify({ message })),
117
+ };
118
+ }
119
+
120
+ export class BatchItemTimeoutError extends Error {}
121
+
122
+ export function promiseWithTimeout<T>(
123
+ effect: Promise<T>,
124
+ timeoutMs: number,
125
+ onTimeout?: (error: BatchItemTimeoutError) => void,
126
+ ) {
127
+ if (timeoutMs <= 0) {
128
+ return effect;
129
+ }
130
+
131
+ return new Promise<T>((resolve, reject) => {
132
+ const timer = setTimeout(() => {
133
+ const error = new BatchItemTimeoutError(
134
+ `Batch item timeout after ${String(timeoutMs)}ms`,
135
+ );
136
+ onTimeout?.(error);
137
+ reject(error);
138
+ }, timeoutMs);
139
+
140
+ effect.then(
141
+ value => {
142
+ clearTimeout(timer);
143
+ resolve(value);
144
+ },
145
+ error => {
146
+ clearTimeout(timer);
147
+ reject(error);
148
+ },
149
+ );
150
+ });
151
+ }
152
+
153
+ export async function mapWithConcurrency<TItem, TResult>(
154
+ items: readonly TItem[],
155
+ concurrency: number,
156
+ mapper: (item: TItem, index: number) => Promise<TResult>,
157
+ ) {
158
+ if (items.length === 0) {
159
+ return [] as TResult[];
160
+ }
161
+
162
+ const normalizedConcurrency = Math.max(1, concurrency);
163
+ const output = new Array<TResult>(items.length);
164
+ let index = 0;
165
+
166
+ const workers = Array.from(
167
+ { length: Math.min(normalizedConcurrency, items.length) },
168
+ async () => {
169
+ while (true) {
170
+ const currentIndex = index;
171
+ index += 1;
172
+ if (currentIndex >= items.length) {
173
+ return;
174
+ }
175
+ output[currentIndex] = await mapper(items[currentIndex]!, currentIndex);
176
+ }
177
+ },
178
+ );
179
+
180
+ await Promise.all(workers);
181
+ return output;
182
+ }