@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.
package/README.md CHANGED
@@ -23,13 +23,15 @@ The library is **framework- and library-agnostic**: you use the worker via the s
23
23
 
24
24
  ## Response Types and Download Behavior
25
25
 
26
- The library supports three response types to handle different use cases:
26
+ `responseType` controls in-flight deduplication and streaming behavior. For non-stream requests, **response parsing follows the server's `Content-Type` header**, not `responseType` — a GET without `responseType: "binary"` can still return an `ArrayBuffer` when the server sends a binary content type.
27
27
 
28
- - **`responseType: "json"`** (default): Full response is buffered in the worker and sent to the client in a single message. The client receives the complete response (JSON or text) after the entire download completes.
28
+ | `responseType` | Effect |
29
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
30
+ | omitted / `"json"` (default) | In-flight dedupe enabled (keyed by `cacheName`). Full response buffered and sent in one `type: "result"` message. |
31
+ | `"binary"` | Skips in-flight dedupe (always fetches fresh). Response still parsed by `Content-Type`; binary bodies arrive as `ArrayBuffer` with `meta`. |
32
+ | `"stream"` | Enables incremental delivery (`start` → `chunk` → ... → `end`), Range-based retries, and hook `streamChunks`. |
29
33
 
30
- - **`responseType: "binary"`**: Full binary response is buffered in the worker and sent as an `ArrayBuffer` in a single message. Perfect for complete binary files like images, PDFs, or downloadable documents.
31
-
32
- - **`responseType: "stream"`**: Responses are streamed incrementally to the client. The worker sends chunks as they arrive (`start` → `chunk` → `chunk` → ... → `end`), enabling playback of audio/video streams to begin before the full file downloads. The React hook returns `streamChunks` (batches of `ArrayBuffer[]`) as they arrive and a final `Blob` in `data` when complete. Throttling (default: every 5 chunks or 50ms) minimizes re-renders. For vanilla JavaScript, you handle stream events manually for maximum control.
34
+ - **`responseType: "stream"`**: The worker sends chunks as they arrive, enabling audio/video playback before the full file downloads. The React hook exposes the latest flushed batch in `streamChunks` (`ArrayBuffer[]`) and a final `Blob` in `data` when complete. Throttling (default: every 5 chunks, configurable via `streamChunkBatchSize`) minimizes re-renders. For vanilla JavaScript, handle `type: "stream"` messages manually.
33
35
 
34
36
  Binary and stream responses are not stored in the worker cache; only json/text responses are cached.
35
37
 
@@ -101,10 +103,10 @@ worker.postMessage({
101
103
 
102
104
  Use only these import paths. Do not import the worker script directly.
103
105
 
104
- | Use case | Import from | What you get |
105
- | ----------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
106
- | **Vanilla** | `@mainframework/api-request-worker` | `createApiWorker`, `RequestConfig`, `DataRequest`, `BinaryResponseMeta`, `WorkerMessagePayload`, and other protocol types |
107
- | **React** | `@mainframework/api-request-worker/react` | `useApiWorker`, `RequestConfig`, hook types |
106
+ | Use case | Import from | What you get |
107
+ | ----------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
108
+ | **Vanilla** | `@mainframework/api-request-worker` | `createApiWorker`, `RequestConfig`, `DataRequest`, `BinaryResponseMeta`, `WorkerMessagePayload`, `WorkerErrorCode`, `WorkerResponseType`, and other protocol types |
109
+ | **React** | `@mainframework/api-request-worker/react` | `useApiWorker`, `RequestConfig`, `UseApiWorkerConfig`, `UseApiWorkerReturn` |
108
110
 
109
111
  The worker is not a public entry. Obtain it only by calling `createApiWorker()` from the main package (or use the React hook, which uses `createApiWorker` internally).
110
112
 
@@ -119,6 +121,7 @@ The worker is not a public entry. Obtain it only by calling `createApiWorker()`
119
121
  - **Next.js**: Use **client-only** code paths.
120
122
  - Add `"use client";` at the top of any file that imports `@mainframework/api-request-worker/react`.
121
123
  - Do not call `createApiWorker()` during SSR.
124
+ - `useApiWorker` returns `null` when `window` is undefined — guard before destructuring in SSR or shared modules.
122
125
 
123
126
  ---
124
127
 
@@ -160,21 +163,49 @@ Send a single object: `{ dataRequest: { ... } }`.
160
163
 
161
164
  **Incoming messages (worker → main thread):**
162
165
 
163
- Every message includes `error: { message: string }`.
166
+ Every message includes:
167
+
168
+ - **`type`**: `"result"` | `"delete"` | `"stream"` — disambiguates delete acknowledgements from real payloads and stream events.
169
+ - **`requestId`**: Echoes the originating request when present; omitted for cache-only `get` / `delete` without a client `requestId`.
170
+ - **`error`**: `{ message: string; code?: WorkerErrorCode }` — always present.
171
+
172
+ - **Success**: `type: "result"`, `data` contains the response body, `error.message` is `""` (empty string).
173
+ - **Failure**: `type: "result"`, `data` is `null`, `error.message` contains the error description, optional `error.code` classifies the failure.
174
+ - **Delete ack**: `type: "delete"`, `data: null`, `error: { message: "" }` — the `type` field is the signal; there is no `{ deleted: true }` payload.
175
+
176
+ **Error codes (`error.code`):**
164
177
 
165
- - **Success**: `data` contains the response body and `error.message` is `""` (empty string).
166
- - **Failure**: `data` is `null` and `error.message` contains the error description.
178
+ | Code | When |
179
+ | -------------- | ----------------------------------------------- |
180
+ | `"aborted"` | Manual `cancel` or `AbortError` without timeout |
181
+ | `"timeout"` | `timeoutMs` fired |
182
+ | `"http"` | HTTP status ≥ 400 |
183
+ | `"network"` | Fetch, parse, or other runtime failure |
184
+ | `"validation"` | Cache miss, invalid request, etc. |
167
185
 
168
186
  **Message formats:**
169
187
 
170
- - **Success (JSON/text):** `{ cacheName, data, error: { message: "" }, hookId?, httpStatus? }`
171
- - **Success (binary):** `{ cacheName, data: ArrayBuffer, meta: { contentType?, contentDisposition }, error: { message: "" }, hookId?, httpStatus? }`
188
+ - **Success (JSON/text):** `{ type: "result", cacheName, data, error: { message: "" }, requestId?, hookId?, httpStatus? }`
189
+ - **Success (binary):** `{ type: "result", cacheName, data: ArrayBuffer, meta: { contentType?, contentDisposition }, error: { message: "" }, requestId?, hookId?, httpStatus? }`
190
+ - **Delete ack:** `{ type: "delete", cacheName, data: null, error: { message: "" }, requestId?, hookId? }`
172
191
  - **Success (stream):** Multiple messages in sequence:
173
- - `{ cacheName, stream: "start", meta: { contentType?, contentDisposition }, hookId?, httpStatus?, error: { message: "" } }`
174
- - `{ cacheName, stream: "chunk", data: ArrayBuffer, hookId?, error: { message: "" } }` (one or more)
175
- - `{ cacheName, stream: "resume", meta: { contentType?, contentDisposition }, hookId?, httpStatus?, error: { message: "" } }` (after retry)
176
- - `{ cacheName, stream: "end", hookId?, error: { message: "" } }` (final message)
177
- - **Error:** `{ cacheName?, data: null, error: { message: "..." }, hookId? }`. If the request had no `cacheName`, match by `hookId` instead.
192
+ - `{ type: "stream", cacheName, stream: "start", meta: { contentType?, contentDisposition }, requestId?, hookId?, httpStatus?, error: { message: "" } }`
193
+ - `{ type: "stream", cacheName, stream: "chunk", data: ArrayBuffer, requestId?, hookId?, error: { message: "" } }` (one or more)
194
+ - `{ type: "stream", cacheName, stream: "resume", meta: { contentType?, contentDisposition }, requestId?, hookId?, httpStatus?, error: { message: "" } }` (after retry)
195
+ - `{ type: "stream", cacheName, stream: "end", requestId?, hookId?, error: { message: "" } }` (final message)
196
+ - **Error:** `{ type: "result", cacheName?, data: null, error: { message: "...", code?: "..." }, requestId?, hookId? }`. If the request had no `cacheName`, match by `hookId` instead.
197
+
198
+ **Design notes:**
199
+
200
+ | # | Note |
201
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
202
+ | 7 | In-flight dedupe for json/text is keyed by `cacheName` only (not HTTP method). Two concurrent mutations on the same key coalesce — intentional for read caching; use distinct `cacheName` values for independent writes. |
203
+ | 8 | `retries` applies only when `responseType: "stream"`. |
204
+ | 9 | Auto-run refires when `data` / `request` object identity changes — callers must memoize inline literals. |
205
+ | 10 | `streamChunks` exposes the latest flushed batch (`ArrayBuffer[]`), not the cumulative stream — accumulate in consumer if needed. |
206
+ | 11 | Joiners on a deduped json/text fetch may receive **two** `type: "result"` messages: cached value immediately, then fresh value after the shared fetch completes. Vanilla consumers should handle or ignore the first message. |
207
+
208
+ **Joiner cancel:** Multiple subscribers sharing a `cacheName` coalesce into one in-flight fetch (json/text). Each caller registers its own `requestId`. Cancel removes only that `requestId`; the shared fetch is aborted only when the last registered `requestId` is cancelled. For `responseType: "binary"` or `"stream"`, in-flight tracking is keyed by `requestId` (not `cacheName`), so those requests do not dedupe across callers.
178
209
 
179
210
  **Common error messages:**
180
211
 
@@ -203,9 +234,15 @@ interface RequestConfig {
203
234
  }
204
235
  ```
205
236
 
206
- - **`responseType: "binary"`**: Use for complete binary files. The worker returns an `ArrayBuffer` and sets `meta.contentType` and `meta.contentDisposition` so you can construct a proper `Blob`: `new Blob([data], { type: meta?.contentType })`.
237
+ - **`responseType: "binary"`**: Skips in-flight dedupe so each request fetches fresh data. Response parsing still follows `Content-Type`; when the server returns binary content, the worker sends an `ArrayBuffer` with `meta.contentType` and `meta.contentDisposition` so you can construct a `Blob`: `new Blob([data], { type: meta?.contentType })`.
207
238
 
208
- - **`responseType: "stream"`**: Use for streaming audio/video or large files. The worker sends chunks incrementally. The hook returns `streamChunks` (batches of `ArrayBuffer[]`) as they arrive and a final `Blob` in `data` when complete. Batching via `streamChunkBatchSize` (default 5) controls how many chunks are delivered per update. Supports automatic reconnection with configurable retries (default 3, max 5).
239
+ - **`responseType: "stream"`**: Enables incremental chunk delivery. The hook exposes the latest flushed batch in `streamChunks` (`ArrayBuffer[]`) and a final `Blob` in `data` when complete. Batching via `streamChunkBatchSize` (default 5) controls how many chunks are delivered per update. Supports automatic reconnection with configurable retries (default 3, max 5).
240
+
241
+ - **`formDataFileFieldName`** (default `"Files"`): FormData field name for all `File`/`Blob` parts when the worker auto-builds multipart form data.
242
+
243
+ - **`formDataKey`** (default `"Files"`): FormData key for the root payload object when building multipart form data.
244
+
245
+ **File upload:** When a `set` payload contains `File` or `Blob` values (including nested in objects or arrays), the worker automatically sends the request as `multipart/form-data` and omits any caller `Content-Type` header so the browser sets the boundary. No manual `Content-Type: multipart/form-data` header is required.
209
246
 
210
247
  ### Vanilla JavaScript Examples
211
248
 
@@ -252,6 +289,24 @@ worker.postMessage({
252
289
  });
253
290
  ```
254
291
 
292
+ **POST request with file upload (auto multipart):**
293
+
294
+ ```ts
295
+ worker.postMessage({
296
+ dataRequest: {
297
+ type: "set",
298
+ cacheName: "upload-" + Date.now(),
299
+ payload: { title: "My Photo", photos: [fileInput.files[0]] },
300
+ request: {
301
+ url: "https://api.example.com/upload",
302
+ method: "POST",
303
+ // No Content-Type header needed — worker detects File/Blob and sends multipart/form-data
304
+ },
305
+ hookId: "vanilla-upload",
306
+ },
307
+ });
308
+ ```
309
+
255
310
  **PATCH request:**
256
311
 
257
312
  ```ts
@@ -300,7 +355,7 @@ setTimeout(() => {
300
355
  worker.postMessage({
301
356
  dataRequest: { type: "get", cacheName: "nonexistent-key", hookId: "cache-miss" },
302
357
  });
303
- // onmessage receives: { cacheName: "nonexistent-key", data: null, error: { message: "Cache miss" }, hookId: "cache-miss" }
358
+ // onmessage receives: { type: "result", cacheName: "nonexistent-key", data: null, error: { message: "Cache miss", code: "validation" }, hookId: "cache-miss" }
304
359
  ```
305
360
 
306
361
  **Delete cached data:**
@@ -315,7 +370,7 @@ worker.postMessage({
315
370
  worker.postMessage({
316
371
  dataRequest: { type: "delete", cacheName: "temp-data", hookId: "delete-op" },
317
372
  });
318
- // Response: { cacheName: "temp-data", data: { deleted: true }, error: { message: "" }, hookId: "delete-op" }
373
+ // Response: { type: "delete", cacheName: "temp-data", data: null, error: { message: "" }, hookId: "delete-op" }
319
374
 
320
375
  // Subsequent get for the same cacheName returns: { error: { message: "Cache miss" } }
321
376
  ```
@@ -360,7 +415,7 @@ let meta: { contentType?: string; contentDisposition: string | null } | null = n
360
415
 
361
416
  worker.onmessage = (event) => {
362
417
  const msg = event.data;
363
- if (msg.cacheName !== cacheName) return;
418
+ if (msg.cacheName !== cacheName || msg.type !== "stream") return;
364
419
 
365
420
  if (msg.stream === "start") {
366
421
  // Stream started
@@ -477,7 +532,8 @@ const result = useApiWorker({
477
532
  enabled: true, // optional: if false, no request is sent (default true)
478
533
  });
479
534
 
480
- // result: { data, meta, loading, error, refetch, deleteCache }
535
+ // result: UseApiWorkerReturn<T> | null null when window is undefined (SSR)
536
+ // { data, meta, loading, error, errorCode, refetch, deleteCache, streamChunks? }
481
537
  ```
482
538
 
483
539
  **Parameters:**
@@ -493,15 +549,16 @@ const result = useApiWorker({
493
549
 
494
550
  **Return value:**
495
551
 
496
- | Property | Type | Description |
497
- | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
498
- | `data` | `T \| null` | Response body: JSON/text for `responseType: "json"`, `ArrayBuffer` for `responseType: "binary"`, `Blob` for `responseType: "stream"`. |
499
- | `meta` | `BinaryResponseMeta \| null` | For binary and stream responses: `contentType`, `contentDisposition`. |
500
- | `loading` | `boolean` | `true` while a request is in flight. |
501
- | `error` | `string \| null` | Error message when the request failed; `null` when there is no error. See [Errors](#errors). |
502
- | `refetch` | `() => void` | Re-runs the same logical request. See [Refetch semantics](#refetch-semantics). |
503
- | `deleteCache` | `() => void` | Tells the worker to delete the cache entry for this `cacheName`. |
504
- | `streamChunks` | `ArrayBuffer[] \| undefined` | For `responseType: "stream"`: batches of chunks as they arrive. Append to `MediaSource` or process incrementally. `undefined` for non-stream. |
552
+ | Property | Type | Description |
553
+ | -------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
554
+ | `data` | `T \| null` | Response body: JSON/text or `ArrayBuffer` per server `Content-Type`; `Blob` when `responseType: "stream"` completes. |
555
+ | `meta` | `BinaryResponseMeta \| null` | For binary and stream responses: `contentType`, `contentDisposition`. |
556
+ | `loading` | `boolean` | `true` while a request is in flight. |
557
+ | `error` | `string \| null` | Error message when the request failed; `null` when there is no error. See [Errors](#errors). |
558
+ | `errorCode` | `WorkerErrorCode \| null` | Structured error classification (`"aborted"`, `"timeout"`, `"http"`, `"network"`, `"validation"`); `null` when there is no error. |
559
+ | `refetch` | `() => void` | Re-runs the same logical request. See [Refetch semantics](#refetch-semantics). |
560
+ | `deleteCache` | `() => void` | Tells the worker to delete the cache entry for this `cacheName`. |
561
+ | `streamChunks` | `ArrayBuffer[] \| undefined` | For `responseType: "stream"`: the latest flushed batch of chunks. Append to `MediaSource` or process incrementally. `undefined` for non-stream. |
505
562
 
506
563
  ### React Examples
507
564
 
@@ -636,7 +693,7 @@ const { data, meta, loading, error, streamChunks } = useApiWorker({
636
693
  runMode: "auto",
637
694
  });
638
695
 
639
- // streamChunks: batches of ArrayBuffer[] as chunks arrive (append to MediaSource, etc.)
696
+ // streamChunks: latest flushed batch (ArrayBuffer[]) as chunks arrive (append to MediaSource, etc.)
640
697
  // data: Blob when the stream completes
641
698
  // loading: true until stream ends
642
699
  // const videoUrl = data ? URL.createObjectURL(data) : null;
@@ -659,9 +716,11 @@ const handleClearCache = () => {
659
716
 
660
717
  ### Shared cacheName / Multiple Subscribers
661
718
 
662
- When multiple components use the same `cacheName`, they share a single cache entry in the worker. However, only one queue entry exists per normalized cache name, and the last-mounted component's state updater receives the worker's responses. This means only that component will re-render when the worker responds.
719
+ When multiple components use the same `cacheName`, they share a single client queue entry and a single worker cache key. All mounted subscribers register on that entry; when the worker responds, **all subscribers re-render** with the same `data`, `loading`, and `error` state.
720
+
721
+ **Recommendation:** Use distinct `cacheName` values when components need independent state, different `request` configs, or separate `loading`/`error` tracking.
663
722
 
664
- **Recommendation:** Use unique `cacheName` values per logical resource if you need independent `loading`/`error` state in each component.
723
+ **Unmount cancel:** When a component unmounts, its in-flight `requestId` is cancelled only if it is the last subscriber for that `cacheName`. Other subscribers keep the shared fetch alive.
665
724
 
666
725
  ### Refetch Semantics
667
726
 
@@ -674,15 +733,16 @@ It does not switch between get and set based on prior runs; it uses the current
674
733
 
675
734
  ### Errors
676
735
 
677
- The worker always includes an `error` field in every message: `{ message: string }`.
736
+ The worker always includes an `error` field in every message: `{ message: string; code?: WorkerErrorCode }`.
678
737
 
679
738
  - **No error**: `{ message: "" }` (empty string)
680
- - **Error occurred**: `{ message: "error description" }`
739
+ - **Error occurred**: `{ message: "error description", code?: "aborted" | "timeout" | "http" | "network" | "validation" }`
681
740
 
682
- The hook exposes this as `error: string | null`:
741
+ The hook exposes this as `error: string | null` and `errorCode: WorkerErrorCode | null`:
683
742
 
684
743
  - `null` when `error.message` is empty
685
744
  - The error message string when an error occurred
745
+ - `errorCode` mirrors `error.code` when present
686
746
 
687
747
  Common error messages:
688
748
 
@@ -705,13 +765,14 @@ import type {
705
765
  BinaryResponseMeta,
706
766
  WorkerMessagePayload,
707
767
  WorkerErrorPayload,
768
+ WorkerErrorCode,
769
+ WorkerResponseType,
708
770
  WorkerResponseMessage,
709
771
  ResponseType,
710
772
  RunMode,
711
773
  WorkerDataRequestType,
712
774
  WorkerMessageData,
713
775
  BinaryParseResult,
714
- ContentType,
715
776
  } from "@mainframework/api-request-worker";
716
777
  ```
717
778
 
@@ -729,10 +790,10 @@ import type { RequestConfig, UseApiWorkerConfig, UseApiWorkerReturn } from "@mai
729
790
 
730
791
  ## Quick Reference
731
792
 
732
- | Use case | Entry point | Primary API |
733
- | ----------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
734
- | **Vanilla JS/TS** | `@mainframework/api-request-worker` | `createApiWorker()`; set `worker.onmessage`, use `worker.postMessage` |
735
- | **React** | `@mainframework/api-request-worker/react` | `useApiWorker({ cacheName, request?, data?, runMode?, enabled? })` → `{ data, meta, loading, error, refetch, deleteCache }` |
793
+ | Use case | Entry point | Primary API |
794
+ | ----------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
795
+ | **Vanilla JS/TS** | `@mainframework/api-request-worker` | `createApiWorker()`; set `worker.onmessage`, use `worker.postMessage` |
796
+ | **React** | `@mainframework/api-request-worker/react` | `useApiWorker({ cacheName, request?, data?, runMode?, enabled? })` → `UseApiWorkerReturn<T> \| null` (`data`, `meta`, `loading`, `error`, `errorCode`, `refetch`, `deleteCache`, `streamChunks?`) |
736
797
 
737
798
  ---
738
799
 
@@ -746,13 +807,16 @@ import type { RequestConfig, UseApiWorkerConfig, UseApiWorkerReturn } from "@mai
746
807
 
747
808
  ---
748
809
 
749
- ## Testing
750
-
751
- Tests use Vitest in browser mode (Playwright Chromium). The worker is created inside `useApiWorker`; there are no mocks.
810
+ ## Changelog
752
811
 
753
- - **useApiWorker** — `src/shared/hooks/useApiWorker.test.ts` (React hook, real Worker and network)
812
+ ### 1.1.0
754
813
 
755
- Run tests: `yarn test` (or `yarn test:watch`, `yarn test:coverage`). Ensure Chromium is installed: `npx playwright install chromium`.
814
+ - Added `type` and `requestId` to all worker→main messages for protocol disambiguation and request correlation.
815
+ - Added structured `error.code` (`WorkerErrorCode`) alongside existing `error.message`.
816
+ - Delete responses now use `type: "delete"` with `data: null` instead of `{ deleted: true }` — **breaking** for consumers relying on the old delete payload shape.
817
+ - Joiner-aware cancel: shared in-flight fetches track multiple `requestId`s; abort only when the last subscriber cancels.
818
+ - Last-activity eviction for idle queue entries (client) and cache store entries (worker).
819
+ - Stream `responseType` detection in the React hook now uses `toLocaleLowerCase()` consistently (was `toLowerCase()`).
756
820
 
757
821
  ---
758
822
 
@@ -1,6 +1,6 @@
1
- import { U as UseApiWorkerConfig, a as UseApiWorkerReturn } from '../../types-LYmxol0c.js';
2
- export { R as RequestConfig } from '../../types-LYmxol0c.js';
1
+ import { U as UseApiWorkerConfig, a as UseApiWorkerReturn } from '../../types-C6nnvGO7.js';
2
+ export { R as RequestConfig } from '../../types-C6nnvGO7.js';
3
3
 
4
- declare const useApiWorker: <T>(config: UseApiWorkerConfig) => UseApiWorkerReturn<T>;
4
+ declare const useApiWorker: <T>(config: UseApiWorkerConfig) => UseApiWorkerReturn<T> | null;
5
5
 
6
6
  export { UseApiWorkerConfig, UseApiWorkerReturn, useApiWorker };