@mainframework/api-request-worker 1.0.3 → 1.0.4

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.
@@ -3,23 +3,36 @@ const DEFAULT_FILES_FIELD = "Files";
3
3
  const EMPTY_BUFFER = new ArrayBuffer(0);
4
4
  const NO_ERROR = { message: "" };
5
5
  const DEFAULT_ERROR = "An error occurred";
6
- const callerResponse = (cacheName, data, hookId, httpStatus, error = NO_ERROR) => {
6
+ const callerResponse = (cacheName, data, hookId, httpStatus, error = NO_ERROR, type = "result", requestId) => {
7
7
  self.postMessage({
8
+ type,
9
+ ...requestId != null && requestId !== "" && { requestId },
8
10
  cacheName,
9
- data: error.message !== "" ? null : data ?? null,
11
+ data: type === "delete" ? null : error.message !== "" ? null : data ?? null,
10
12
  hookId,
11
13
  httpStatus,
12
14
  error
13
15
  });
14
16
  };
15
- const makeError = (message) => ({ message });
16
- const callerResponseBinary = (cacheName, data, meta, hookId, httpStatus, error = NO_ERROR) => {
17
+ const makeError = (message, code) => code !== void 0 ? { message, code } : { message };
18
+ const callerResponseBinary = (cacheName, data, meta, hookId, httpStatus, error = NO_ERROR, requestId) => {
17
19
  const buffer = data.byteLength ? data : EMPTY_BUFFER;
18
- const payload = { cacheName, data: buffer, meta, hookId, httpStatus, error };
20
+ const payload = {
21
+ type: "result",
22
+ ...requestId != null && requestId !== "" && { requestId },
23
+ cacheName,
24
+ data: buffer,
25
+ meta,
26
+ hookId,
27
+ httpStatus,
28
+ error
29
+ };
19
30
  self.postMessage(payload, buffer.byteLength > 0 ? [buffer] : []);
20
31
  };
21
- const callerResponseStreamStart = (cacheName, meta, hookId, httpStatus, error = NO_ERROR) => {
32
+ const callerResponseStreamStart = (cacheName, meta, hookId, httpStatus, error = NO_ERROR, requestId) => {
22
33
  self.postMessage({
34
+ type: "stream",
35
+ ...requestId != null && requestId !== "" && { requestId },
23
36
  cacheName,
24
37
  stream: "start",
25
38
  meta,
@@ -28,8 +41,10 @@ const callerResponseStreamStart = (cacheName, meta, hookId, httpStatus, error =
28
41
  error
29
42
  });
30
43
  };
31
- const callerResponseStreamResume = (cacheName, meta, hookId, httpStatus, error = NO_ERROR) => {
44
+ const callerResponseStreamResume = (cacheName, meta, hookId, httpStatus, error = NO_ERROR, requestId) => {
32
45
  self.postMessage({
46
+ type: "stream",
47
+ ...requestId != null && requestId !== "" && { requestId },
33
48
  cacheName,
34
49
  stream: "resume",
35
50
  meta,
@@ -38,23 +53,52 @@ const callerResponseStreamResume = (cacheName, meta, hookId, httpStatus, error =
38
53
  error
39
54
  });
40
55
  };
41
- const callerResponseStreamChunk = (cacheName, data, hookId, error = NO_ERROR) => {
56
+ const callerResponseStreamChunk = (cacheName, data, hookId, error = NO_ERROR, requestId) => {
42
57
  const buffer = data.byteLength ? data : EMPTY_BUFFER;
43
- const payload = { cacheName, stream: "chunk", data: buffer, hookId, error };
58
+ const payload = {
59
+ type: "stream",
60
+ ...requestId != null && requestId !== "" && { requestId },
61
+ cacheName,
62
+ stream: "chunk",
63
+ data: buffer,
64
+ hookId,
65
+ error
66
+ };
44
67
  self.postMessage(payload, buffer.byteLength > 0 ? [buffer] : []);
45
68
  };
46
- const callerResponseStreamEnd = (cacheName, hookId, error = NO_ERROR) => {
47
- self.postMessage({ cacheName, stream: "end", hookId, error });
69
+ const callerResponseStreamEnd = (cacheName, hookId, error = NO_ERROR, requestId) => {
70
+ self.postMessage({
71
+ type: "stream",
72
+ ...requestId != null && requestId !== "" && { requestId },
73
+ cacheName,
74
+ stream: "end",
75
+ hookId,
76
+ error
77
+ });
48
78
  };
49
79
  const transferableBuffer = (view) => view.byteOffset === 0 && view.byteLength === view.buffer.byteLength ? view.buffer : view.slice(0).buffer;
50
80
  const store = /* @__PURE__ */ Object.create(null);
81
+ const storeActivity = /* @__PURE__ */ new Map();
82
+ const STALE_ENTRY_MS = 5e3;
83
+ const CLEANUP_INTERVAL_MS = 3e4;
51
84
  const normalizeKey = (key) => key.toLocaleLowerCase();
52
- const get = (key) => store[normalizeKey(key)];
85
+ const touchActivity = (key) => {
86
+ storeActivity.set(normalizeKey(key), Date.now());
87
+ };
88
+ const get = (key) => {
89
+ const nk = normalizeKey(key);
90
+ touchActivity(nk);
91
+ return store[nk];
92
+ };
53
93
  const set = (key, value) => {
54
- store[normalizeKey(key)] = value;
94
+ const nk = normalizeKey(key);
95
+ store[nk] = value;
96
+ touchActivity(nk);
55
97
  };
56
98
  const remove = (key) => {
57
- delete store[normalizeKey(key)];
99
+ const nk = normalizeKey(key);
100
+ delete store[nk];
101
+ storeActivity.delete(nk);
58
102
  };
59
103
  const isNonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
60
104
  const omitContentType = (headers) => {
@@ -64,13 +108,22 @@ const omitContentType = (headers) => {
64
108
  return rest;
65
109
  };
66
110
  const isBinaryResponse = (r) => typeof r === "object" && r !== null && BINARY_MARKER in r && r.data instanceof ArrayBuffer;
67
- const commit = (cacheName, data, hookId, httpStatus) => {
111
+ const commit = (cacheName, data, hookId, httpStatus, requestId) => {
68
112
  if (!cacheName) {
69
- callerResponse("", null, hookId, void 0, makeError("Invalid commit: cacheName is required"));
113
+ callerResponse(
114
+ "",
115
+ null,
116
+ hookId,
117
+ void 0,
118
+ makeError("Invalid commit: cacheName is required", "validation"),
119
+ "result",
120
+ requestId
121
+ );
70
122
  return;
71
123
  }
72
124
  set(cacheName, data);
73
- callerResponse(cacheName, data, hookId, httpStatus);
125
+ touchActivity(cacheName);
126
+ callerResponse(cacheName, data, hookId, httpStatus, NO_ERROR, "result", requestId);
74
127
  };
75
128
  const parseResponseByContentType = async (response) => {
76
129
  const contentType = response.headers.get("content-type")?.toLocaleLowerCase() || "";
@@ -106,14 +159,14 @@ const appendToFormData = (formData, key, value, fileFieldName = DEFAULT_FILES_FI
106
159
  return true;
107
160
  }
108
161
  if (value !== null && value !== void 0 && typeof value === "object") {
109
- const set2 = visited ?? /* @__PURE__ */ new WeakSet();
110
- if (set2.has(value)) return false;
111
- set2.add(value);
162
+ const visitedSet = visited ?? /* @__PURE__ */ new WeakSet();
163
+ if (visitedSet.has(value)) return false;
164
+ visitedSet.add(value);
112
165
  if (Array.isArray(value)) {
113
166
  let hasFile2 = false;
114
167
  let i = 0;
115
168
  while (i < value.length) {
116
- hasFile2 = appendToFormData(formData, key ? `${key}.${i}` : String(i), value[i], fileFieldName, set2) || hasFile2;
169
+ hasFile2 = appendToFormData(formData, key ? `${key}.${i}` : String(i), value[i], fileFieldName, visitedSet) || hasFile2;
117
170
  i++;
118
171
  }
119
172
  return hasFile2;
@@ -128,7 +181,7 @@ const appendToFormData = (formData, key, value, fileFieldName = DEFAULT_FILES_FI
128
181
  key ? `${key}.${k}` : k,
129
182
  value[k],
130
183
  fileFieldName,
131
- set2
184
+ visitedSet
132
185
  ) || hasFile;
133
186
  ki++;
134
187
  }
@@ -239,8 +292,8 @@ const prepareRequestBody = (payload, headers, options) => {
239
292
  return { headers: { ...headers } };
240
293
  }
241
294
  };
242
- const inFlightControllers = /* @__PURE__ */ new Map();
243
295
  const inFlightByCacheName = /* @__PURE__ */ new Map();
296
+ const requestIdToCacheName = /* @__PURE__ */ new Map();
244
297
  const apiRequest = async (cacheName, payload, {
245
298
  url,
246
299
  method,
@@ -259,21 +312,32 @@ const apiRequest = async (cacheName, payload, {
259
312
  if (!skipInFlightDedupe) {
260
313
  const existing = inFlightByCacheName.get(cacheName);
261
314
  if (existing) {
315
+ if (requestId) {
316
+ existing.requestIds.add(requestId);
317
+ requestIdToCacheName.set(requestId, cacheName);
318
+ }
262
319
  const cached = get(cacheName);
263
- if (cached !== void 0) callerResponse(cacheName, cached, hookId);
264
- await existing;
320
+ if (cached !== void 0) callerResponse(cacheName, cached, hookId, void 0, NO_ERROR, "result", requestId);
321
+ await existing.promise;
265
322
  const fresh = get(cacheName);
266
- if (fresh !== void 0) callerResponse(cacheName, fresh, hookId);
323
+ if (fresh !== void 0) callerResponse(cacheName, fresh, hookId, void 0, NO_ERROR, "result", requestId);
267
324
  return;
268
325
  }
269
326
  }
270
327
  const controller = new AbortController();
328
+ const requestIds = /* @__PURE__ */ new Set();
329
+ const inflightKey = skipInFlightDedupe && requestId ? requestId : cacheName;
271
330
  if (requestId) {
272
- inFlightControllers.set(requestId, controller);
331
+ requestIds.add(requestId);
332
+ requestIdToCacheName.set(requestId, inflightKey);
273
333
  }
274
334
  let timeoutId;
335
+ let abortedByTimeout = false;
275
336
  if (timeoutMs != null && timeoutMs > 0) {
276
- timeoutId = setTimeout(() => controller.abort(), timeoutMs);
337
+ timeoutId = setTimeout(() => {
338
+ abortedByTimeout = true;
339
+ controller.abort();
340
+ }, timeoutMs);
277
341
  }
278
342
  const promise = (async () => {
279
343
  const fetchOptions = {
@@ -299,6 +363,7 @@ const apiRequest = async (cacheName, payload, {
299
363
  let bytesReceived = 0;
300
364
  let streamError = NO_ERROR;
301
365
  let attempt = 0;
366
+ let isPermanentError = false;
302
367
  while (attempt <= maxRetries) {
303
368
  try {
304
369
  const reqHeaders = methodLower === "get" ? omitContentType({ ...fetchOptions.headers }) : { ...fetchOptions.headers };
@@ -308,24 +373,30 @@ const apiRequest = async (cacheName, payload, {
308
373
  headers: reqHeaders
309
374
  });
310
375
  if (streamResponse.status >= 400) {
311
- streamError = makeError(streamResponse.statusText || DEFAULT_ERROR);
376
+ streamError = makeError(streamResponse.statusText || DEFAULT_ERROR, "http");
377
+ isPermanentError = true;
312
378
  break;
313
379
  }
314
380
  if (streamResponse.status === 204) {
315
- callerResponseStreamEnd(cacheName, hookId);
381
+ callerResponseStreamEnd(cacheName, hookId, NO_ERROR, requestId);
316
382
  return;
317
383
  }
318
- if (streamResponse.status === 416) break;
384
+ if (streamResponse.status === 416) {
385
+ streamError = makeError("Range Not Satisfiable", "http");
386
+ isPermanentError = true;
387
+ break;
388
+ }
319
389
  const contentType = streamResponse.headers.get("content-type") ?? void 0;
320
390
  const meta = {
321
391
  contentDisposition: streamResponse.headers.get("content-disposition") ?? null,
322
392
  ...contentType !== void 0 && { contentType }
323
393
  };
324
- if (bytesReceived === 0) callerResponseStreamStart(cacheName, meta, hookId, streamResponse.status);
325
- else callerResponseStreamResume(cacheName, meta, hookId, streamResponse.status);
394
+ if (bytesReceived === 0)
395
+ callerResponseStreamStart(cacheName, meta, hookId, streamResponse.status, NO_ERROR, requestId);
396
+ else callerResponseStreamResume(cacheName, meta, hookId, streamResponse.status, NO_ERROR, requestId);
326
397
  const body = streamResponse.body;
327
398
  if (!body) {
328
- callerResponseStreamEnd(cacheName, hookId);
399
+ callerResponseStreamEnd(cacheName, hookId, NO_ERROR, requestId);
329
400
  return;
330
401
  }
331
402
  const reader = body.getReader();
@@ -342,31 +413,45 @@ const apiRequest = async (cacheName, payload, {
342
413
  const offset = skipRemaining;
343
414
  skipRemaining = 0;
344
415
  const tail = value.subarray(offset);
345
- callerResponseStreamChunk(cacheName, transferableBuffer(tail), hookId);
416
+ callerResponseStreamChunk(cacheName, transferableBuffer(tail), hookId, NO_ERROR, requestId);
346
417
  bytesReceived += tail.byteLength;
347
418
  } else {
348
- callerResponseStreamChunk(cacheName, transferableBuffer(value), hookId);
419
+ callerResponseStreamChunk(cacheName, transferableBuffer(value), hookId, NO_ERROR, requestId);
349
420
  bytesReceived += value.byteLength;
350
421
  }
351
422
  }
352
423
  break;
353
424
  } catch (err) {
354
- streamError = err.name === "AbortError" ? makeError("Request aborted") : makeError(err.message);
425
+ if (err.name === "AbortError") {
426
+ streamError = makeError("Request aborted", abortedByTimeout ? "timeout" : "aborted");
427
+ isPermanentError = true;
428
+ break;
429
+ }
430
+ streamError = makeError(err.message, "network");
355
431
  if (attempt === maxRetries) break;
356
432
  }
433
+ if (isPermanentError) break;
357
434
  attempt++;
358
435
  }
359
- callerResponseStreamEnd(cacheName, hookId, streamError);
436
+ callerResponseStreamEnd(cacheName, hookId, streamError, requestId);
360
437
  return;
361
438
  }
362
439
  const response = await fetch(url, fetchOptions);
363
440
  if (response.status >= 400) {
364
- callerResponse(cacheName, null, hookId, response.status, makeError(response.statusText || DEFAULT_ERROR));
441
+ callerResponse(
442
+ cacheName,
443
+ null,
444
+ hookId,
445
+ response.status,
446
+ makeError(response.statusText || DEFAULT_ERROR, "http"),
447
+ "result",
448
+ requestId
449
+ );
365
450
  return;
366
451
  }
367
452
  if (response.status === 204) {
368
453
  set(cacheName, null);
369
- callerResponse(cacheName, null, hookId, 204);
454
+ callerResponse(cacheName, null, hookId, 204, NO_ERROR, "result", requestId);
370
455
  return;
371
456
  }
372
457
  const responseData = await parseResponseByContentType(response);
@@ -379,36 +464,52 @@ const apiRequest = async (cacheName, payload, {
379
464
  contentDisposition: responseData.contentDisposition ?? null
380
465
  },
381
466
  hookId,
382
- response.status
467
+ response.status,
468
+ NO_ERROR,
469
+ requestId
383
470
  );
384
471
  } else {
385
- commit(cacheName, responseData, hookId, response.status);
472
+ commit(cacheName, responseData, hookId, response.status, requestId);
386
473
  }
387
474
  } catch (error) {
388
475
  if (error.name === "AbortError") {
389
- callerResponse(cacheName, null, hookId, void 0, makeError("Request aborted"));
476
+ callerResponse(
477
+ cacheName,
478
+ null,
479
+ hookId,
480
+ void 0,
481
+ makeError("Request aborted", abortedByTimeout ? "timeout" : "aborted"),
482
+ "result",
483
+ requestId
484
+ );
390
485
  return;
391
486
  }
392
487
  const err = error;
393
- callerResponse(cacheName, null, hookId, void 0, makeError(err.message));
488
+ callerResponse(cacheName, null, hookId, void 0, makeError(err.message, "network"), "result", requestId);
394
489
  } finally {
395
490
  if (timeoutId != null) clearTimeout(timeoutId);
396
- if (requestId) {
397
- inFlightControllers.delete(requestId);
491
+ for (const id of requestIds) {
492
+ requestIdToCacheName.delete(id);
398
493
  }
399
- inFlightByCacheName.delete(cacheName);
494
+ inFlightByCacheName.delete(inflightKey);
400
495
  }
401
496
  })();
402
- if (!skipInFlightDedupe) {
403
- inFlightByCacheName.set(cacheName, promise);
404
- }
497
+ const entry = { promise, controller, requestIds };
498
+ inFlightByCacheName.set(inflightKey, entry);
405
499
  await promise;
406
500
  };
407
501
  const onRequest = (dataRequest) => {
408
502
  const { cacheName, type, payload, request, requestId, hookId } = dataRequest;
409
- const responseCacheName = dataRequest.cacheName ?? "";
410
503
  if (!isNonEmptyString(type)) {
411
- callerResponse(responseCacheName, null, hookId, void 0, makeError("Invalid request: type is required"));
504
+ callerResponse(
505
+ cacheName ?? "",
506
+ null,
507
+ hookId,
508
+ void 0,
509
+ makeError("Invalid request: type is required", "validation"),
510
+ "result",
511
+ requestId
512
+ );
412
513
  return;
413
514
  }
414
515
  const lowerType = normalizeKey(type);
@@ -417,26 +518,44 @@ const onRequest = (dataRequest) => {
417
518
  return;
418
519
  }
419
520
  if (!isNonEmptyString(cacheName)) {
420
- callerResponse(responseCacheName, null, hookId, void 0, makeError("Invalid request: cacheName is required"));
521
+ callerResponse(
522
+ cacheName ?? "",
523
+ null,
524
+ hookId,
525
+ void 0,
526
+ makeError("Invalid request: cacheName is required", "validation"),
527
+ "result",
528
+ requestId
529
+ );
421
530
  return;
422
531
  }
423
532
  const lowerCacheName = normalizeKey(cacheName);
424
533
  if (lowerType === "get") {
425
534
  const requestedData = get(lowerCacheName);
426
535
  if (requestedData === void 0) {
427
- callerResponse(lowerCacheName, null, hookId, void 0, makeError("Cache miss"));
536
+ callerResponse(
537
+ lowerCacheName,
538
+ null,
539
+ hookId,
540
+ void 0,
541
+ makeError("Cache miss", "validation"),
542
+ "result",
543
+ requestId
544
+ );
428
545
  } else {
429
- callerResponse(lowerCacheName, requestedData, hookId);
546
+ callerResponse(lowerCacheName, requestedData, hookId, void 0, NO_ERROR, "result", requestId);
430
547
  }
431
548
  } else if (lowerType === "set") {
432
549
  if (!request) {
433
550
  if (payload == null) {
434
551
  callerResponse(
435
- responseCacheName,
552
+ cacheName ?? "",
436
553
  null,
437
554
  hookId,
438
555
  void 0,
439
- makeError("Invalid request: payload is required for set")
556
+ makeError("Invalid request: payload is required for set", "validation"),
557
+ "result",
558
+ requestId
440
559
  );
441
560
  return;
442
561
  }
@@ -445,11 +564,13 @@ const onRequest = (dataRequest) => {
445
564
  const methodLower = normalizeKey(request.method);
446
565
  if (methodLower !== "get" && payload == null) {
447
566
  callerResponse(
448
- responseCacheName,
567
+ cacheName ?? "",
449
568
  null,
450
569
  hookId,
451
570
  void 0,
452
- makeError("Invalid request: payload is required for non-GET API request")
571
+ makeError("Invalid request: payload is required for non-GET API request", "validation"),
572
+ "result",
573
+ requestId
453
574
  );
454
575
  return;
455
576
  }
@@ -457,16 +578,33 @@ const onRequest = (dataRequest) => {
457
578
  }
458
579
  } else if (lowerType === "delete") {
459
580
  remove(lowerCacheName);
460
- callerResponse(lowerCacheName, { deleted: true }, hookId);
581
+ callerResponse(lowerCacheName, null, hookId, void 0, NO_ERROR, "delete", requestId);
461
582
  }
462
583
  };
463
584
  const onCancel = (requestId) => {
464
- const controller = inFlightControllers.get(requestId);
465
- if (controller) {
466
- controller.abort();
467
- inFlightControllers.delete(requestId);
585
+ const inflightKey = requestIdToCacheName.get(requestId);
586
+ if (!inflightKey) return;
587
+ const entry = inFlightByCacheName.get(inflightKey);
588
+ if (!entry) {
589
+ requestIdToCacheName.delete(requestId);
590
+ return;
591
+ }
592
+ entry.requestIds.delete(requestId);
593
+ requestIdToCacheName.delete(requestId);
594
+ if (entry.requestIds.size === 0) {
595
+ entry.controller.abort();
596
+ }
597
+ };
598
+ const runStaleStoreCleanup = () => {
599
+ const now = Date.now();
600
+ for (const [key, lastAccessAt] of storeActivity) {
601
+ if (now - lastAccessAt < STALE_ENTRY_MS) continue;
602
+ if (inFlightByCacheName.has(key)) continue;
603
+ delete store[key];
604
+ storeActivity.delete(key);
468
605
  }
469
606
  };
607
+ setInterval(runStaleStoreCleanup, CLEANUP_INTERVAL_MS);
470
608
  onmessage = (event) => {
471
609
  const payload = event.data;
472
610
  if (payload === null || typeof payload !== "object") return;