@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,876 @@
1
+ // @effect-diagnostics asyncFunction:off globalDate:off globalRandom:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import { stableStringify } from '../codec';
3
+ import type {
4
+ DataBatchRequestItem,
5
+ DataBatchRequestPayload,
6
+ DataBatchResponseItem,
7
+ DataBatchTransportOptions,
8
+ DataTransportRequestInfo,
9
+ } from '../types';
10
+ import { DEFAULT_DATA_BATCH_HEADER } from '../types';
11
+ import { decodeBatchBody, isNullBodyStatus } from './protocol';
12
+ import {
13
+ createBatchId,
14
+ measureTextBytes,
15
+ normalizeMethod,
16
+ shouldBatchRequest,
17
+ toHeaderRecord,
18
+ toRequestBody,
19
+ } from './request';
20
+ import {
21
+ isBatchResponsePayload,
22
+ parseResponseLikeCreateRequest,
23
+ } from './response';
24
+ import { emitDataBatchTransportEvent } from './telemetry';
25
+ import { normalizeBatchEndpoint, toAbsoluteUrl } from './url';
26
+
27
+ type DataBatchFetch = NonNullable<DataBatchTransportOptions['fetch']>;
28
+
29
+ type CreateBatchTransportQueueOptions = {
30
+ options: DataBatchTransportOptions;
31
+ baseFetch: DataBatchFetch;
32
+ bucketRegistry?: BatchBucketRegistry;
33
+ };
34
+
35
+ type NormalizedBatchTransportRequest = {
36
+ requestUrl: URL;
37
+ endpoint: string;
38
+ method: string;
39
+ headers: Record<string, string>;
40
+ credentials: RequestCredentials;
41
+ body: BodyInit | null;
42
+ hasOpaqueSourceBody: boolean;
43
+ requestInput: DataTransportRequestInfo;
44
+ requestInit: RequestInit;
45
+ sourceSignal: AbortSignal | undefined;
46
+ };
47
+
48
+ type RequestLifetime = {
49
+ signal: AbortSignal;
50
+ timedOut: boolean;
51
+ dispose: () => void;
52
+ };
53
+
54
+ type RequestSettlement =
55
+ | { type: 'resolve'; value: unknown }
56
+ | { type: 'reject'; reason: unknown };
57
+
58
+ type QueuedBatchRequest = {
59
+ key: string;
60
+ bucketKey: string;
61
+ endpoint: string;
62
+ requestInput: DataTransportRequestInfo;
63
+ requestInit: RequestInit;
64
+ authorization: string | undefined;
65
+ cookie: string | undefined;
66
+ credentials: RequestCredentials;
67
+ item: DataBatchRequestItem;
68
+ lifetime: RequestLifetime;
69
+ phase: 'queued' | 'flushing' | 'settled';
70
+ promise: Promise<unknown>;
71
+ abortListener: () => void;
72
+ abortBatchIfUnused?: () => void;
73
+ resolve: (value: unknown) => void;
74
+ reject: (reason: unknown) => void;
75
+ };
76
+
77
+ type BatchBucket = {
78
+ items: QueuedBatchRequest[];
79
+ bytes: number;
80
+ batchId: string;
81
+ sentAt: number;
82
+ timer: ReturnType<typeof setTimeout> | null;
83
+ flushing: boolean;
84
+ };
85
+
86
+ const SAFE_REPLAY_METHODS = new Set(['GET', 'HEAD']);
87
+
88
+ const abortReason = (signal: AbortSignal) =>
89
+ signal.reason ?? new DOMException('The request was aborted', 'AbortError');
90
+
91
+ const createRequestLifetime = (
92
+ sourceSignal: AbortSignal | undefined,
93
+ timeoutMs: number | undefined,
94
+ ): RequestLifetime => {
95
+ const controller = new AbortController();
96
+ let timedOut = false;
97
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
98
+ const abortFromSource = () => {
99
+ if (sourceSignal) {
100
+ controller.abort(abortReason(sourceSignal));
101
+ }
102
+ };
103
+
104
+ if (sourceSignal?.aborted) {
105
+ abortFromSource();
106
+ } else if (sourceSignal) {
107
+ sourceSignal.addEventListener('abort', abortFromSource, { once: true });
108
+ }
109
+
110
+ if (!controller.signal.aborted && timeoutMs && timeoutMs > 0) {
111
+ timeoutHandle = setTimeout(() => {
112
+ timedOut = true;
113
+ controller.abort(
114
+ new DOMException('The batched request timed out', 'TimeoutError'),
115
+ );
116
+ }, timeoutMs);
117
+ }
118
+
119
+ return {
120
+ signal: controller.signal,
121
+ get timedOut() {
122
+ return timedOut;
123
+ },
124
+ dispose() {
125
+ if (timeoutHandle !== null) {
126
+ clearTimeout(timeoutHandle);
127
+ timeoutHandle = null;
128
+ }
129
+ sourceSignal?.removeEventListener('abort', abortFromSource);
130
+ },
131
+ };
132
+ };
133
+
134
+ const runWithSignal = <T>(
135
+ signal: AbortSignal,
136
+ operation: () => Promise<T>,
137
+ ): Promise<T> => {
138
+ if (signal.aborted) {
139
+ return Promise.reject(abortReason(signal));
140
+ }
141
+
142
+ return new Promise<T>((resolve, reject) => {
143
+ const onAbort = () => {
144
+ signal.removeEventListener('abort', onAbort);
145
+ reject(abortReason(signal));
146
+ };
147
+ signal.addEventListener('abort', onAbort, { once: true });
148
+ let pending: Promise<T>;
149
+ try {
150
+ pending = operation();
151
+ } catch (error) {
152
+ signal.removeEventListener('abort', onAbort);
153
+ reject(error);
154
+ return;
155
+ }
156
+ pending.then(
157
+ value => {
158
+ signal.removeEventListener('abort', onAbort);
159
+ resolve(value);
160
+ },
161
+ error => {
162
+ signal.removeEventListener('abort', onAbort);
163
+ reject(error);
164
+ },
165
+ );
166
+ });
167
+ };
168
+
169
+ export class BatchBucketRegistry {
170
+ private readonly buckets = new Map<string, BatchBucket>();
171
+
172
+ get size() {
173
+ return this.buckets.size;
174
+ }
175
+
176
+ get(bucketKey: string) {
177
+ return this.buckets.get(bucketKey);
178
+ }
179
+
180
+ ensure(bucketKey: string) {
181
+ const existing = this.buckets.get(bucketKey);
182
+ if (existing) {
183
+ return existing;
184
+ }
185
+
186
+ const next: BatchBucket = {
187
+ items: [],
188
+ bytes: 0,
189
+ batchId: createBatchId(),
190
+ sentAt: Date.now(),
191
+ timer: null,
192
+ flushing: false,
193
+ };
194
+ this.buckets.set(bucketKey, next);
195
+ return next;
196
+ }
197
+
198
+ releaseIfIdle(bucketKey: string, bucket: BatchBucket) {
199
+ if (
200
+ this.buckets.get(bucketKey) !== bucket ||
201
+ bucket.flushing ||
202
+ bucket.timer !== null ||
203
+ bucket.items.length > 0
204
+ ) {
205
+ return false;
206
+ }
207
+
208
+ return this.buckets.delete(bucketKey);
209
+ }
210
+ }
211
+
212
+ export function createBatchTransportQueue({
213
+ options,
214
+ baseFetch,
215
+ bucketRegistry,
216
+ }: CreateBatchTransportQueueOptions) {
217
+ 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 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
+ const onEvent = options.onEvent;
228
+
229
+ const buckets = bucketRegistry ?? new BatchBucketRegistry();
230
+ const pendingByKey = new Map<string, QueuedBatchRequest>();
231
+ const disabledEndpoints = new Set<string>();
232
+ let nextItemId = 0;
233
+
234
+ const createPayload = (
235
+ bucket: Pick<BatchBucket, 'batchId' | 'sentAt'>,
236
+ items: QueuedBatchRequest[],
237
+ ): DataBatchRequestPayload => ({
238
+ protocolVersion: 2,
239
+ batchId: bucket.batchId,
240
+ sentAt: bucket.sentAt,
241
+ items: items.map(item => item.item),
242
+ });
243
+
244
+ const measurePayload = (
245
+ bucket: Pick<BatchBucket, 'batchId' | 'sentAt'>,
246
+ items: QueuedBatchRequest[],
247
+ ) => measureTextBytes(JSON.stringify(createPayload(bucket, items)));
248
+
249
+ const rotatePayloadIdentity = (bucket: BatchBucket) => {
250
+ bucket.batchId = createBatchId();
251
+ bucket.sentAt = Date.now();
252
+ };
253
+
254
+ const settleRequest = (
255
+ request: QueuedBatchRequest,
256
+ settlement: RequestSettlement,
257
+ ) => {
258
+ if (request.phase === 'settled') {
259
+ return false;
260
+ }
261
+
262
+ request.phase = 'settled';
263
+ request.lifetime.signal.removeEventListener('abort', request.abortListener);
264
+ request.lifetime.dispose();
265
+ if (pendingByKey.get(request.key) === request) {
266
+ pendingByKey.delete(request.key);
267
+ }
268
+ if (settlement.type === 'resolve') {
269
+ request.resolve(settlement.value);
270
+ } else {
271
+ request.reject(settlement.reason);
272
+ }
273
+ return true;
274
+ };
275
+
276
+ const removeQueuedRequest = (request: QueuedBatchRequest) => {
277
+ const bucket = buckets.get(request.bucketKey);
278
+ if (!bucket) {
279
+ return;
280
+ }
281
+
282
+ const index = bucket.items.indexOf(request);
283
+ if (index === -1) {
284
+ return;
285
+ }
286
+ bucket.items.splice(index, 1);
287
+ bucket.bytes = measurePayload(bucket, bucket.items);
288
+ if (bucket.items.length === 0 && bucket.timer) {
289
+ clearTimeout(bucket.timer);
290
+ bucket.timer = null;
291
+ }
292
+ buckets.releaseIfIdle(request.bucketKey, bucket);
293
+ };
294
+
295
+ const abortQueuedRequest = (request: QueuedBatchRequest) => {
296
+ if (request.phase === 'settled') {
297
+ return;
298
+ }
299
+ if (request.phase === 'queued') {
300
+ removeQueuedRequest(request);
301
+ }
302
+ const abortBatchIfUnused = request.abortBatchIfUnused;
303
+ settleRequest(request, {
304
+ type: 'reject',
305
+ reason: abortReason(request.lifetime.signal),
306
+ });
307
+ abortBatchIfUnused?.();
308
+ };
309
+
310
+ const runSingle = (request: QueuedBatchRequest) => {
311
+ return runWithSignal(request.lifetime.signal, async () => {
312
+ const response = await baseFetch(
313
+ request.requestInput,
314
+ request.requestInit,
315
+ );
316
+ return parseResponseLikeCreateRequest(response);
317
+ });
318
+ };
319
+
320
+ const settleRequests = async (
321
+ items: QueuedBatchRequest[],
322
+ runner: (item: QueuedBatchRequest) => Promise<unknown>,
323
+ ) => {
324
+ await Promise.all(
325
+ items.map(async item => {
326
+ if (item.phase === 'settled') {
327
+ return;
328
+ }
329
+ try {
330
+ const value = await runner(item);
331
+ settleRequest(item, { type: 'resolve', value });
332
+ } catch (error) {
333
+ settleRequest(item, { type: 'reject', reason: error });
334
+ }
335
+ }),
336
+ );
337
+ };
338
+
339
+ const runAfterAmbiguousBatch = (
340
+ request: QueuedBatchRequest,
341
+ reason: string,
342
+ ) => {
343
+ if (!SAFE_REPLAY_METHODS.has(request.item.method)) {
344
+ throw new Error(
345
+ `Batch result is unknown for ${request.item.method}; automatic replay is disabled (${reason}).`,
346
+ );
347
+ }
348
+ return runSingle(request);
349
+ };
350
+
351
+ const settleAmbiguousBatchRequests = (
352
+ items: QueuedBatchRequest[],
353
+ reason: string,
354
+ ) =>
355
+ settleRequests(items, request => runAfterAmbiguousBatch(request, reason));
356
+
357
+ const flushBucket = async (bucketKey: string) => {
358
+ const bucket = buckets.get(bucketKey);
359
+ if (!bucket || bucket.flushing) {
360
+ return;
361
+ }
362
+
363
+ if (bucket.timer) {
364
+ clearTimeout(bucket.timer);
365
+ bucket.timer = null;
366
+ }
367
+
368
+ if (bucket.items.length === 0) {
369
+ buckets.releaseIfIdle(bucketKey, bucket);
370
+ return;
371
+ }
372
+
373
+ bucket.flushing = true;
374
+ const items = bucket.items;
375
+ for (const item of items) {
376
+ item.phase = 'flushing';
377
+ }
378
+ const payloadIdentity = {
379
+ batchId: bucket.batchId,
380
+ sentAt: bucket.sentAt,
381
+ };
382
+ bucket.items = [];
383
+ bucket.bytes = 0;
384
+ rotatePayloadIdentity(bucket);
385
+ const firstItem = items[0];
386
+ if (!firstItem) {
387
+ bucket.flushing = false;
388
+ buckets.releaseIfIdle(bucketKey, bucket);
389
+ return;
390
+ }
391
+ const endpoint = firstItem.endpoint;
392
+
393
+ if (items.length === 1 || disabledEndpoints.has(endpoint)) {
394
+ emitDataBatchTransportEvent(onEvent, {
395
+ type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
396
+ endpoint,
397
+ size: items.length,
398
+ reason: disabledEndpoints.has(endpoint) ? 'batch-disabled' : undefined,
399
+ });
400
+ await settleRequests(items, runSingle);
401
+ bucket.flushing = false;
402
+ if (bucket.items.length > 0 && !bucket.timer) {
403
+ void flushBucket(bucketKey);
404
+ }
405
+ buckets.releaseIfIdle(bucketKey, bucket);
406
+ return;
407
+ }
408
+
409
+ const payload = createPayload(payloadIdentity, items);
410
+ const batchId = payload.batchId;
411
+
412
+ emitDataBatchTransportEvent(onEvent, {
413
+ type: 'flush',
414
+ endpoint,
415
+ batchId,
416
+ size: items.length,
417
+ });
418
+
419
+ const payloadJson = JSON.stringify(payload);
420
+ const traceparent =
421
+ items.find(item => typeof item.item.headers?.traceparent === 'string')
422
+ ?.item.headers?.traceparent || undefined;
423
+ const authorization = firstItem.authorization;
424
+ const cookie = firstItem.cookie;
425
+
426
+ const requestInit: RequestInit = {
427
+ method: 'POST',
428
+ credentials: firstItem.credentials,
429
+ headers: {
430
+ accept: 'application/json, */*;q=0.8',
431
+ 'content-type': 'application/json; charset=utf-8',
432
+ [DEFAULT_DATA_BATCH_HEADER]: '2',
433
+ ...(traceparent ? { traceparent } : {}),
434
+ ...(typeof authorization === 'string' ? { authorization } : {}),
435
+ ...(typeof cookie === 'string' ? { cookie } : {}),
436
+ },
437
+ body: payloadJson,
438
+ };
439
+
440
+ const controller = new AbortController();
441
+ let abortedBecauseUnused = false;
442
+ const abortBatchIfUnused = () => {
443
+ if (
444
+ !controller.signal.aborted &&
445
+ items.every(item => item.phase === 'settled')
446
+ ) {
447
+ abortedBecauseUnused = true;
448
+ controller.abort(
449
+ items.find(item => item.lifetime.timedOut)
450
+ ? new DOMException('The batch timed out', 'TimeoutError')
451
+ : new DOMException(
452
+ 'Every batched request was aborted',
453
+ 'AbortError',
454
+ ),
455
+ );
456
+ }
457
+ };
458
+ for (const item of items) {
459
+ item.abortBatchIfUnused = abortBatchIfUnused;
460
+ }
461
+ requestInit.signal = controller.signal;
462
+
463
+ try {
464
+ const response = await runWithSignal(controller.signal, () =>
465
+ baseFetch(endpoint, requestInit),
466
+ );
467
+
468
+ if (!response.ok) {
469
+ if (response.status === 404 || response.status === 405) {
470
+ disabledEndpoints.add(endpoint);
471
+ emitDataBatchTransportEvent(onEvent, {
472
+ type: 'disable',
473
+ endpoint,
474
+ batchId,
475
+ reason: `batch-endpoint-unavailable-${String(response.status)}`,
476
+ });
477
+ } else {
478
+ emitDataBatchTransportEvent(onEvent, {
479
+ type: 'fallback',
480
+ endpoint,
481
+ batchId,
482
+ size: items.length,
483
+ reason: `batch-response-${String(response.status)}`,
484
+ });
485
+ }
486
+ await settleAmbiguousBatchRequests(
487
+ items,
488
+ `batch-response-${String(response.status)}`,
489
+ );
490
+ bucket.flushing = false;
491
+ return;
492
+ }
493
+
494
+ const result = await runWithSignal(controller.signal, async () =>
495
+ response.json(),
496
+ );
497
+ 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;
507
+ return;
508
+ }
509
+
510
+ 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;
520
+ return;
521
+ }
522
+
523
+ const itemMap = new Map<string, DataBatchResponseItem>();
524
+ for (const item of result.items) {
525
+ itemMap.set(item.id, item);
526
+ }
527
+
528
+ await settleRequests(items, async request => {
529
+ const resultItem = itemMap.get(request.item.id);
530
+ if (!resultItem) {
531
+ return runAfterAmbiguousBatch(request, 'missing-batch-result');
532
+ }
533
+
534
+ const reconstructedBody = resultItem.body
535
+ ? decodeBatchBody(resultItem.body)
536
+ : null;
537
+ const reconstructedResponse = new Response(
538
+ isNullBodyStatus(resultItem.status) ? null : reconstructedBody,
539
+ {
540
+ status: resultItem.status,
541
+ headers: resultItem.headers,
542
+ },
543
+ );
544
+ return parseResponseLikeCreateRequest(reconstructedResponse);
545
+ });
546
+ } catch {
547
+ if (abortedBecauseUnused) {
548
+ if (items.some(item => item.lifetime.timedOut)) {
549
+ emitDataBatchTransportEvent(onEvent, {
550
+ type: 'fallback',
551
+ endpoint,
552
+ batchId,
553
+ size: items.length,
554
+ reason: 'batch-timeout',
555
+ });
556
+ }
557
+ return;
558
+ }
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');
567
+ } finally {
568
+ for (const item of items) {
569
+ item.abortBatchIfUnused = undefined;
570
+ }
571
+ bucket.flushing = false;
572
+ if (bucket.items.length > 0 && !bucket.timer) {
573
+ void flushBucket(bucketKey);
574
+ }
575
+ buckets.releaseIfIdle(bucketKey, bucket);
576
+ }
577
+ };
578
+
579
+ const pendingReadPromises = new Map<string, Promise<unknown>>();
580
+ const normalizeRequest = (
581
+ input: DataTransportRequestInfo,
582
+ init?: RequestInit,
583
+ ): NormalizedBatchTransportRequest => {
584
+ const sourceRequest =
585
+ typeof Request !== 'undefined' && input instanceof Request
586
+ ? input
587
+ : undefined;
588
+ const initDefinesSignal = init !== undefined && 'signal' in init;
589
+ const initDefinesBody = init !== undefined && 'body' in init;
590
+ const initSnapshot = init ? { ...init } : undefined;
591
+ const getInitValue = <Key extends keyof RequestInit>(key: Key) => {
592
+ if (!init) {
593
+ return undefined;
594
+ }
595
+ return Object.prototype.hasOwnProperty.call(initSnapshot, key)
596
+ ? initSnapshot?.[key]
597
+ : init[key];
598
+ };
599
+ const method = normalizeMethod(
600
+ getInitValue('method') ?? sourceRequest?.method,
601
+ );
602
+ const headers = toHeaderRecord(
603
+ getInitValue('headers') ?? sourceRequest?.headers,
604
+ );
605
+ const credentials =
606
+ getInitValue('credentials') ??
607
+ sourceRequest?.credentials ??
608
+ 'same-origin';
609
+ const initBody = getInitValue('body');
610
+ const body = initBody ?? null;
611
+ const initSignal = initDefinesSignal ? getInitValue('signal') : undefined;
612
+ const sourceSignal = initDefinesSignal
613
+ ? (initSignal ?? undefined)
614
+ : sourceRequest?.signal;
615
+ const requestUrl = toAbsoluteUrl(input);
616
+ const endpoint = normalizeBatchEndpoint(
617
+ requestUrl,
618
+ options.endpoint,
619
+ ).toString();
620
+
621
+ return {
622
+ requestUrl,
623
+ endpoint,
624
+ method,
625
+ headers,
626
+ credentials,
627
+ body,
628
+ hasOpaqueSourceBody:
629
+ initBody === undefined &&
630
+ sourceRequest !== undefined &&
631
+ sourceRequest.body !== null,
632
+ requestInput: sourceRequest ?? requestUrl.toString(),
633
+ requestInit: {
634
+ ...initSnapshot,
635
+ method,
636
+ headers,
637
+ credentials,
638
+ ...(initDefinesBody ? { body: initBody } : {}),
639
+ ...(initDefinesSignal ? { signal: initSignal } : {}),
640
+ },
641
+ sourceSignal,
642
+ };
643
+ };
644
+
645
+ const getPendingReadKey = (request: NormalizedBatchTransportRequest) => {
646
+ if (
647
+ !SAFE_REPLAY_METHODS.has(request.method) ||
648
+ request.sourceSignal !== undefined ||
649
+ request.body !== null ||
650
+ request.hasOpaqueSourceBody ||
651
+ disabledEndpoints.has(request.endpoint) ||
652
+ !shouldBatchRequest({
653
+ method: request.method,
654
+ body: undefined,
655
+ headers: request.headers,
656
+ allowedMethods,
657
+ batchEndpoint: request.endpoint,
658
+ requestUrl: request.requestUrl,
659
+ })
660
+ ) {
661
+ return undefined;
662
+ }
663
+
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
+ return stableStringify({
674
+ bucketKey,
675
+ path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
676
+ method: request.method,
677
+ headers: itemHeaders,
678
+ body: null,
679
+ });
680
+ };
681
+
682
+ const enqueue = async (request: NormalizedBatchTransportRequest) => {
683
+ const preparedBody = await toRequestBody(request.body);
684
+ const hasOpaqueBody =
685
+ !preparedBody.batchable || request.hasOpaqueSourceBody;
686
+
687
+ if (
688
+ disabledEndpoints.has(request.endpoint) ||
689
+ !shouldBatchRequest({
690
+ method: request.method,
691
+ body: hasOpaqueBody ? '[opaque request body]' : undefined,
692
+ headers: request.headers,
693
+ allowedMethods,
694
+ batchEndpoint: request.endpoint,
695
+ requestUrl: request.requestUrl,
696
+ })
697
+ ) {
698
+ return baseFetch(request.requestInput, request.requestInit).then(
699
+ parseResponseLikeCreateRequest,
700
+ );
701
+ }
702
+
703
+ const itemId = `${Date.now().toString(36)}_${nextItemId.toString(36)}_${Math.random()
704
+ .toString(16)
705
+ .slice(2, 8)}`;
706
+ nextItemId += 1;
707
+
708
+ const itemHeaders = { ...request.headers };
709
+ delete itemHeaders.authorization;
710
+ delete itemHeaders.cookie;
711
+ if (
712
+ preparedBody.body &&
713
+ preparedBody.inferredContentType &&
714
+ !itemHeaders['content-type']
715
+ ) {
716
+ itemHeaders['content-type'] = preparedBody.inferredContentType;
717
+ }
718
+ const item: DataBatchRequestItem = {
719
+ id: itemId,
720
+ path: `${request.requestUrl.pathname}${request.requestUrl.search}`,
721
+ method: request.method,
722
+ headers: itemHeaders,
723
+ ...(preparedBody.body ? { body: preparedBody.body } : {}),
724
+ };
725
+
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
+ });
732
+
733
+ const key = stableStringify({
734
+ bucketKey,
735
+ path: item.path,
736
+ method: item.method,
737
+ headers: item.headers,
738
+ body: item.body ?? null,
739
+ });
740
+
741
+ const canSharePending =
742
+ SAFE_REPLAY_METHODS.has(request.method) &&
743
+ request.sourceSignal === undefined;
744
+ const existing = canSharePending ? pendingByKey.get(key) : undefined;
745
+ if (existing) {
746
+ return existing.promise;
747
+ }
748
+
749
+ const lifetime = createRequestLifetime(
750
+ request.sourceSignal,
751
+ requestTimeoutMs,
752
+ );
753
+ request.requestInit.signal = lifetime.signal;
754
+ const deferred = Promise.withResolvers<unknown>();
755
+ const queued: QueuedBatchRequest = {
756
+ key,
757
+ bucketKey,
758
+ endpoint: request.endpoint,
759
+ requestInput: request.requestInput,
760
+ requestInit: request.requestInit,
761
+ authorization: request.headers.authorization,
762
+ cookie: request.headers.cookie,
763
+ credentials: request.credentials,
764
+ item,
765
+ lifetime,
766
+ phase: 'queued',
767
+ promise: deferred.promise,
768
+ abortListener: () => abortQueuedRequest(queued),
769
+ resolve: deferred.resolve,
770
+ reject: deferred.reject,
771
+ };
772
+ lifetime.signal.addEventListener('abort', queued.abortListener, {
773
+ once: true,
774
+ });
775
+ if (lifetime.signal.aborted) {
776
+ abortQueuedRequest(queued);
777
+ return queued.promise;
778
+ }
779
+ if (canSharePending) {
780
+ pendingByKey.set(key, queued);
781
+ }
782
+
783
+ const bucket = buckets.ensure(bucketKey);
784
+
785
+ const singletonBytes = measurePayload(bucket, [queued]);
786
+ if (singletonBytes > maxBatchBytes) {
787
+ emitDataBatchTransportEvent(onEvent, {
788
+ type: 'fallback',
789
+ endpoint: request.endpoint,
790
+ size: 1,
791
+ reason: 'batch-item-too-large',
792
+ });
793
+ queued.phase = 'flushing';
794
+ void settleRequests([queued], runSingle);
795
+ buckets.releaseIfIdle(bucketKey, bucket);
796
+ return queued.promise;
797
+ }
798
+
799
+ const candidateItems = [...bucket.items, queued];
800
+ const candidateBytes = measurePayload(bucket, candidateItems);
801
+ if (bucket.items.length > 0 && candidateBytes > maxBatchBytes) {
802
+ if (bucket.flushing) {
803
+ emitDataBatchTransportEvent(onEvent, {
804
+ type: 'fallback',
805
+ endpoint: request.endpoint,
806
+ size: 1,
807
+ reason: 'batch-capacity-during-flush',
808
+ });
809
+ queued.phase = 'flushing';
810
+ void settleRequests([queued], runSingle);
811
+ return queued.promise;
812
+ }
813
+ void flushBucket(bucketKey);
814
+ if (measurePayload(bucket, [queued]) > maxBatchBytes) {
815
+ emitDataBatchTransportEvent(onEvent, {
816
+ type: 'fallback',
817
+ endpoint: request.endpoint,
818
+ size: 1,
819
+ reason: 'batch-item-too-large',
820
+ });
821
+ queued.phase = 'flushing';
822
+ void settleRequests([queued], runSingle);
823
+ return queued.promise;
824
+ }
825
+ }
826
+
827
+ bucket.items.push(queued);
828
+ bucket.bytes = measurePayload(bucket, bucket.items);
829
+ emitDataBatchTransportEvent(onEvent, {
830
+ type: 'enqueue',
831
+ endpoint: request.endpoint,
832
+ size: bucket.items.length,
833
+ });
834
+
835
+ if (bucket.items.length >= maxBatchSize || bucket.bytes >= maxBatchBytes) {
836
+ void flushBucket(bucketKey);
837
+ return queued.promise;
838
+ }
839
+
840
+ if (!bucket.timer) {
841
+ bucket.timer = setTimeout(() => {
842
+ bucket.timer = null;
843
+ void flushBucket(bucketKey);
844
+ }, flushIntervalMs);
845
+ }
846
+ return queued.promise;
847
+ };
848
+
849
+ return (input: DataTransportRequestInfo, init?: RequestInit) => {
850
+ let request: NormalizedBatchTransportRequest;
851
+ try {
852
+ request = normalizeRequest(input, init);
853
+ } catch (error) {
854
+ return Promise.reject(error);
855
+ }
856
+ const pendingReadKey = getPendingReadKey(request);
857
+ const existing = pendingReadKey
858
+ ? pendingReadPromises.get(pendingReadKey)
859
+ : undefined;
860
+ if (existing) {
861
+ return existing;
862
+ }
863
+
864
+ const pending = enqueue(request);
865
+ if (pendingReadKey) {
866
+ pendingReadPromises.set(pendingReadKey, pending);
867
+ const clearPendingRead = () => {
868
+ if (pendingReadPromises.get(pendingReadKey) === pending) {
869
+ pendingReadPromises.delete(pendingReadKey);
870
+ }
871
+ };
872
+ void pending.then(clearPendingRead, clearPendingRead);
873
+ }
874
+ return pending;
875
+ };
876
+ }