1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | import { RequestMethod } from "../RequestMethod";
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | interface NodeBody {
|
19 | buffer(): Promise<Buffer>;
|
20 | }
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | interface IsomorphicRequest extends Request, NodeBody {}
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | export interface BatchRequestStep {
|
38 | id: string;
|
39 | dependsOn?: string[];
|
40 | request: Request;
|
41 | }
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | export interface RequestData extends RequestInit {
|
53 | url: string;
|
54 | }
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | export interface BatchRequestData extends RequestData {
|
63 | id: string;
|
64 | dependsOn?: string[];
|
65 | }
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | export interface BatchRequestBody {
|
74 | requests: BatchRequestData[];
|
75 | }
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | export class BatchRequestContent {
|
82 | |
83 |
|
84 |
|
85 |
|
86 |
|
87 | private static requestLimit = 20;
|
88 |
|
89 | |
90 |
|
91 |
|
92 |
|
93 | public requests: Map<string, BatchRequestStep>;
|
94 |
|
95 | |
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | private static validateDependencies(requests: Map<string, BatchRequestStep>): boolean {
|
113 | const isParallel = (reqs: Map<string, BatchRequestStep>): boolean => {
|
114 | const iterator = reqs.entries();
|
115 | let cur = iterator.next();
|
116 | while (!cur.done) {
|
117 | const curReq = cur.value[1];
|
118 | if (curReq.dependsOn !== undefined && curReq.dependsOn.length > 0) {
|
119 | return false;
|
120 | }
|
121 | cur = iterator.next();
|
122 | }
|
123 | return true;
|
124 | };
|
125 | const isSerial = (reqs: Map<string, BatchRequestStep>): boolean => {
|
126 | const iterator = reqs.entries();
|
127 | let cur = iterator.next();
|
128 | const firstRequest: BatchRequestStep = cur.value[1];
|
129 | if (firstRequest.dependsOn !== undefined && firstRequest.dependsOn.length > 0) {
|
130 | return false;
|
131 | }
|
132 | let prev = cur;
|
133 | cur = iterator.next();
|
134 | while (!cur.done) {
|
135 | const curReq: BatchRequestStep = cur.value[1];
|
136 | if (curReq.dependsOn === undefined || curReq.dependsOn.length !== 1 || curReq.dependsOn[0] !== prev.value[1].id) {
|
137 | return false;
|
138 | }
|
139 | prev = cur;
|
140 | cur = iterator.next();
|
141 | }
|
142 | return true;
|
143 | };
|
144 | const isSame = (reqs: Map<string, BatchRequestStep>): boolean => {
|
145 | const iterator = reqs.entries();
|
146 | let cur = iterator.next();
|
147 | const firstRequest: BatchRequestStep = cur.value[1];
|
148 | let dependencyId: string;
|
149 | if (firstRequest.dependsOn === undefined || firstRequest.dependsOn.length === 0) {
|
150 | dependencyId = firstRequest.id;
|
151 | } else {
|
152 | if (firstRequest.dependsOn.length === 1) {
|
153 | const fDependencyId = firstRequest.dependsOn[0];
|
154 | if (fDependencyId !== firstRequest.id && reqs.has(fDependencyId)) {
|
155 | dependencyId = fDependencyId;
|
156 | } else {
|
157 | return false;
|
158 | }
|
159 | } else {
|
160 | return false;
|
161 | }
|
162 | }
|
163 | cur = iterator.next();
|
164 | while (!cur.done) {
|
165 | const curReq = cur.value[1];
|
166 | if ((curReq.dependsOn === undefined || curReq.dependsOn.length === 0) && dependencyId !== curReq.id) {
|
167 | return false;
|
168 | }
|
169 | if (curReq.dependsOn !== undefined && curReq.dependsOn.length !== 0) {
|
170 | if (curReq.dependsOn.length === 1 && (curReq.id === dependencyId || curReq.dependsOn[0] !== dependencyId)) {
|
171 | return false;
|
172 | }
|
173 | if (curReq.dependsOn.length > 1) {
|
174 | return false;
|
175 | }
|
176 | }
|
177 | cur = iterator.next();
|
178 | }
|
179 | return true;
|
180 | };
|
181 | if (requests.size === 0) {
|
182 | const error = new Error("Empty requests map, Please provide at least one request.");
|
183 | error.name = "Empty Requests Error";
|
184 | throw error;
|
185 | }
|
186 | return isParallel(requests) || isSerial(requests) || isSame(requests);
|
187 | }
|
188 |
|
189 | |
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | private static async getRequestData(request: IsomorphicRequest): Promise<RequestData> {
|
198 | const requestData: RequestData = {
|
199 | url: "",
|
200 | };
|
201 | const hasHttpRegex = new RegExp("^https?://");
|
202 |
|
203 | requestData.url = hasHttpRegex.test(request.url) ? "/" + request.url.split(/.*?\/\/.*?\//)[1] : request.url;
|
204 | requestData.method = request.method;
|
205 | const headers = {};
|
206 | request.headers.forEach((value, key) => {
|
207 | headers[key] = value;
|
208 | });
|
209 | if (Object.keys(headers).length) {
|
210 | requestData.headers = headers;
|
211 | }
|
212 | if (request.method === RequestMethod.PATCH || request.method === RequestMethod.POST || request.method === RequestMethod.PUT) {
|
213 | requestData.body = await BatchRequestContent.getRequestBody(request);
|
214 | }
|
215 | |
216 |
|
217 |
|
218 | return requestData;
|
219 | }
|
220 |
|
221 | |
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 | private static async getRequestBody(request: IsomorphicRequest): Promise<any> {
|
230 | let bodyParsed = false;
|
231 | let body;
|
232 | try {
|
233 | const cloneReq = request.clone();
|
234 | body = await cloneReq.json();
|
235 | bodyParsed = true;
|
236 | } catch (e) {
|
237 |
|
238 | }
|
239 | if (!bodyParsed) {
|
240 | try {
|
241 | if (typeof Blob !== "undefined") {
|
242 | const blob = await request.blob();
|
243 | const reader = new FileReader();
|
244 | body = await new Promise((resolve) => {
|
245 | reader.addEventListener(
|
246 | "load",
|
247 | () => {
|
248 | const dataURL = reader.result as string;
|
249 | |
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 | const regex = new RegExp("^s*data:(.+?/.+?(;.+?=.+?)*)?(;base64)?,(.*)s*$");
|
259 | const segments = regex.exec(dataURL);
|
260 | resolve(segments[4]);
|
261 | },
|
262 | false,
|
263 | );
|
264 | reader.readAsDataURL(blob);
|
265 | });
|
266 | } else if (typeof Buffer !== "undefined") {
|
267 | const buffer = await request.buffer();
|
268 | body = buffer.toString("base64");
|
269 | }
|
270 | bodyParsed = true;
|
271 | } catch (e) {
|
272 |
|
273 | }
|
274 | }
|
275 | return body;
|
276 | }
|
277 |
|
278 | |
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | public constructor(requests?: BatchRequestStep[]) {
|
286 | this.requests = new Map();
|
287 | if (typeof requests !== "undefined") {
|
288 | const limit = BatchRequestContent.requestLimit;
|
289 | if (requests.length > limit) {
|
290 | const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
|
291 | error.name = "Limit Exceeded Error";
|
292 | throw error;
|
293 | }
|
294 | for (const req of requests) {
|
295 | this.addRequest(req);
|
296 | }
|
297 | }
|
298 | }
|
299 |
|
300 | |
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 | public addRequest(request: BatchRequestStep): string {
|
307 | const limit = BatchRequestContent.requestLimit;
|
308 | if (request.id === "") {
|
309 | const error = new Error(`Id for a request is empty, Please provide an unique id`);
|
310 | error.name = "Empty Id For Request";
|
311 | throw error;
|
312 | }
|
313 | if (this.requests.size === limit) {
|
314 | const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
|
315 | error.name = "Limit Exceeded Error";
|
316 | throw error;
|
317 | }
|
318 | if (this.requests.has(request.id)) {
|
319 | const error = new Error(`Adding request with duplicate id ${request.id}, Make the id of the requests unique`);
|
320 | error.name = "Duplicate RequestId Error";
|
321 | throw error;
|
322 | }
|
323 | this.requests.set(request.id, request);
|
324 | return request.id;
|
325 | }
|
326 |
|
327 | |
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 | public removeRequest(requestId: string): boolean {
|
334 | const deleteStatus = this.requests.delete(requestId);
|
335 | const iterator = this.requests.entries();
|
336 | let cur = iterator.next();
|
337 | |
338 |
|
339 |
|
340 | while (!cur.done) {
|
341 | const dependencies = cur.value[1].dependsOn;
|
342 | if (typeof dependencies !== "undefined") {
|
343 | const index = dependencies.indexOf(requestId);
|
344 | if (index !== -1) {
|
345 | dependencies.splice(index, 1);
|
346 | }
|
347 | if (dependencies.length === 0) {
|
348 | delete cur.value[1].dependsOn;
|
349 | }
|
350 | }
|
351 | cur = iterator.next();
|
352 | }
|
353 | return deleteStatus;
|
354 | }
|
355 |
|
356 | |
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 | public async getContent(): Promise<BatchRequestBody> {
|
363 | const requests: BatchRequestData[] = [];
|
364 | const requestBody: BatchRequestBody = {
|
365 | requests,
|
366 | };
|
367 | const iterator = this.requests.entries();
|
368 | let cur = iterator.next();
|
369 | if (cur.done) {
|
370 | const error = new Error("No requests added yet, Please add at least one request.");
|
371 | error.name = "Empty Payload";
|
372 | throw error;
|
373 | }
|
374 | if (!BatchRequestContent.validateDependencies(this.requests)) {
|
375 | const error = new Error(`Invalid dependency found, Dependency should be:
|
376 | 1. Parallel - no individual request states a dependency in the dependsOn property.
|
377 | 2. Serial - all individual requests depend on the previous individual request.
|
378 | 3. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.`);
|
379 | error.name = "Invalid Dependency";
|
380 | throw error;
|
381 | }
|
382 | while (!cur.done) {
|
383 | const requestStep: BatchRequestStep = cur.value[1];
|
384 | const batchRequestData: BatchRequestData = (await BatchRequestContent.getRequestData(requestStep.request as IsomorphicRequest)) as BatchRequestData;
|
385 | |
386 |
|
387 |
|
388 |
|
389 |
|
390 | if (batchRequestData.body !== undefined && (batchRequestData.headers === undefined || batchRequestData.headers["content-type"] === undefined)) {
|
391 | const error = new Error(`Content-type header is not mentioned for request #${requestStep.id}, For request having body, Content-type header should be mentioned`);
|
392 | error.name = "Invalid Content-type header";
|
393 | throw error;
|
394 | }
|
395 | batchRequestData.id = requestStep.id;
|
396 | if (requestStep.dependsOn !== undefined && requestStep.dependsOn.length > 0) {
|
397 | batchRequestData.dependsOn = requestStep.dependsOn;
|
398 | }
|
399 | requests.push(batchRequestData);
|
400 | cur = iterator.next();
|
401 | }
|
402 | requestBody.requests = requests;
|
403 | return requestBody;
|
404 | }
|
405 |
|
406 | |
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 | public addDependency(dependentId: string, dependencyId?: string): void {
|
414 | if (!this.requests.has(dependentId)) {
|
415 | const error = new Error(`Dependent ${dependentId} does not exists, Please check the id`);
|
416 | error.name = "Invalid Dependent";
|
417 | throw error;
|
418 | }
|
419 | if (typeof dependencyId !== "undefined" && !this.requests.has(dependencyId)) {
|
420 | const error = new Error(`Dependency ${dependencyId} does not exists, Please check the id`);
|
421 | error.name = "Invalid Dependency";
|
422 | throw error;
|
423 | }
|
424 | if (typeof dependencyId !== "undefined") {
|
425 | const dependent = this.requests.get(dependentId);
|
426 | if (dependent.dependsOn === undefined) {
|
427 | dependent.dependsOn = [];
|
428 | }
|
429 | if (dependent.dependsOn.indexOf(dependencyId) !== -1) {
|
430 | const error = new Error(`Dependency ${dependencyId} is already added for the request ${dependentId}`);
|
431 | error.name = "Duplicate Dependency";
|
432 | throw error;
|
433 | }
|
434 | dependent.dependsOn.push(dependencyId);
|
435 | } else {
|
436 | const iterator = this.requests.entries();
|
437 | let prev;
|
438 | let cur = iterator.next();
|
439 | while (!cur.done && cur.value[1].id !== dependentId) {
|
440 | prev = cur;
|
441 | cur = iterator.next();
|
442 | }
|
443 | if (typeof prev !== "undefined") {
|
444 | const dId = prev.value[0];
|
445 | if (cur.value[1].dependsOn === undefined) {
|
446 | cur.value[1].dependsOn = [];
|
447 | }
|
448 | if (cur.value[1].dependsOn.indexOf(dId) !== -1) {
|
449 | const error = new Error(`Dependency ${dId} is already added for the request ${dependentId}`);
|
450 | error.name = "Duplicate Dependency";
|
451 | throw error;
|
452 | }
|
453 | cur.value[1].dependsOn.push(dId);
|
454 | } else {
|
455 | const error = new Error(`Can't add dependency ${dependencyId}, There is only a dependent request in the batch`);
|
456 | error.name = "Invalid Dependency Addition";
|
457 | throw error;
|
458 | }
|
459 | }
|
460 | }
|
461 |
|
462 | |
463 |
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 | public removeDependency(dependentId: string, dependencyId?: string): boolean {
|
470 | const request = this.requests.get(dependentId);
|
471 | if (typeof request === "undefined" || request.dependsOn === undefined || request.dependsOn.length === 0) {
|
472 | return false;
|
473 | }
|
474 | if (typeof dependencyId !== "undefined") {
|
475 | const index = request.dependsOn.indexOf(dependencyId);
|
476 | if (index === -1) {
|
477 | return false;
|
478 | }
|
479 | request.dependsOn.splice(index, 1);
|
480 | return true;
|
481 | } else {
|
482 | delete request.dependsOn;
|
483 | return true;
|
484 | }
|
485 | }
|
486 | }
|