@mainframework/api-request-worker 1.0.0
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/LICENSE +21 -0
- package/README.md +670 -0
- package/dist/api.worker.js +463 -0
- package/dist/api.worker.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/hooks/useApiWorker.d.ts +4 -0
- package/dist/shared/hooks/useApiWorker.d.ts.map +1 -0
- package/dist/shared/hooks/useCustomCallback.d.ts +2 -0
- package/dist/shared/hooks/useCustomCallback.d.ts.map +1 -0
- package/dist/shared/types/types.d.ts +168 -0
- package/dist/shared/types/types.d.ts.map +1 -0
- package/dist/shared/utils/uniqueId.d.ts +6 -0
- package/dist/shared/utils/uniqueId.d.ts.map +1 -0
- package/dist/shared/workers/api/api.worker.d.ts +2 -0
- package/dist/shared/workers/api/api.worker.d.ts.map +1 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terry Slack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
# @mainframework/api-request-worker
|
|
2
|
+
|
|
3
|
+
**Requires Node.js 18+** (for global `fetch` when running in Node; browsers rely on their native fetch).
|
|
4
|
+
|
|
5
|
+
A framework-agnostic, Web Worker–backed data layer designed to keep your UI thread responsive and your application fast. This library moves all API requests and application state management into a dedicated singleton worker, handling caching, in-flight request deduplication, streaming and binary responses, while exposing data to your main thread on demand.
|
|
6
|
+
|
|
7
|
+
The library is **framework- and library-agnostic**: you use the worker via the standard `postMessage` API from vanilla JavaScript or from any framework (React, Angular, Vue, Preact, SolidJS, etc.). A **React hook (`useApiWorker`) is provided as a convenience** for React engineers; you may use it or implement your own integration against the worker protocol.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why Use This Library?
|
|
12
|
+
|
|
13
|
+
- **Non-blocking UI**: All network requests and state management happen off the main thread, keeping your UI buttery smooth
|
|
14
|
+
- **Built-in caching**: Automatic response caching with flexible cache key management
|
|
15
|
+
- **Request deduplication**: Multiple requests for the same resource are automatically collapsed into a single network call
|
|
16
|
+
- **Streaming support**: Handle large files and real-time streams with incremental chunk delivery
|
|
17
|
+
- **Binary file support**: First-class support for images, PDFs, and other binary content
|
|
18
|
+
- **Framework agnostic**: Works in vanilla JavaScript or with any framework
|
|
19
|
+
- **No framework lock-in**: Use the worker from any stack; the included React hook is optional
|
|
20
|
+
- **TypeScript ready**: Full type definitions included
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Response Types and Download Behavior
|
|
25
|
+
|
|
26
|
+
The library supports three response types to handle different use cases:
|
|
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.
|
|
29
|
+
|
|
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 automatically accumulates chunks and returns a `Blob` when complete. For vanilla JavaScript, you handle stream events manually for maximum control.
|
|
33
|
+
|
|
34
|
+
Binary and stream responses are not stored in the worker cache; only json/text responses are cached.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm i @mainframework/api-request-worker
|
|
42
|
+
# or
|
|
43
|
+
yarn add @mainframework/api-request-worker
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you use the optional React hook, a peer dependency `react >= 19` is required.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Usage with Vanilla TypeScript / JavaScript
|
|
51
|
+
|
|
52
|
+
The core of this library is a Web Worker that you communicate with via the standard `postMessage` API. This approach works in any JavaScript environment—no framework required. You create a Worker instance from the package's built worker script, send **dataRequests** via `postMessage`, and handle responses in `onmessage`.
|
|
53
|
+
|
|
54
|
+
### Setting Up the Worker
|
|
55
|
+
|
|
56
|
+
**With a bundler (Vite, webpack, etc.):**
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const worker = new Worker(
|
|
60
|
+
new URL("node_modules/@mainframework/api-request-worker/dist/api.worker.js", import.meta.url),
|
|
61
|
+
{ type: "module" },
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Without a bundler:**
|
|
66
|
+
|
|
67
|
+
Serve `node_modules/@mainframework/api-request-worker/dist/api.worker.js` from your web server and create the worker with that URL:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const worker = new Worker("/path/to/api.worker.js", { type: "module" });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Message Protocol
|
|
74
|
+
|
|
75
|
+
**Outgoing messages (main thread → worker):**
|
|
76
|
+
|
|
77
|
+
Send a single object: `{ dataRequest: { ... } }`.
|
|
78
|
+
|
|
79
|
+
| dataRequest.type | Description | Required fields | Optional |
|
|
80
|
+
| ---------------- | --------------------------------------------------- | --------------- | ------------------------------------------- |
|
|
81
|
+
| `"get"` | Return cached value for `cacheName`. | `cacheName` | `hookId` |
|
|
82
|
+
| `"set"` | Store payload and/or run API request, then respond. | `cacheName` | `hookId`, `request`, `payload`, `requestId` |
|
|
83
|
+
| `"delete"` | Remove cache entry for `cacheName`. | `cacheName` | `hookId` |
|
|
84
|
+
| `"cancel"` | Abort in-flight request by `requestId`. | — | `requestId` |
|
|
85
|
+
|
|
86
|
+
**Key fields:**
|
|
87
|
+
|
|
88
|
+
- **`cacheName`**: String; required for `get`, `set`, `delete`. Cache keys are normalized to lowercase.
|
|
89
|
+
- **`request`**: API request configuration (`url`, `method`, `headers`, `credentials`, `responseType`, etc.). Required for `set` when making an API call.
|
|
90
|
+
- **`payload`**: Request body for POST/PATCH requests. Required for `set` when there is no `request`, and for non-GET requests when `request` is provided.
|
|
91
|
+
- **`requestId`**: Optional for `set` (enables request cancellation); required for `cancel`.
|
|
92
|
+
|
|
93
|
+
**Incoming messages (worker → main thread):**
|
|
94
|
+
|
|
95
|
+
Every message includes `error: { message: string }`.
|
|
96
|
+
|
|
97
|
+
- **Success**: `data` contains the response body and `error.message` is `""` (empty string).
|
|
98
|
+
- **Failure**: `data` is `null` and `error.message` contains the error description.
|
|
99
|
+
|
|
100
|
+
**Message formats:**
|
|
101
|
+
|
|
102
|
+
- **Success (JSON/text):** `{ cacheName, data, error: { message: "" }, hookId?, httpStatus? }`
|
|
103
|
+
- **Success (binary):** `{ cacheName, data: ArrayBuffer, meta: { contentType?, contentDisposition }, error: { message: "" }, hookId?, httpStatus? }`
|
|
104
|
+
- **Success (stream):** Multiple messages in sequence:
|
|
105
|
+
- `{ cacheName, stream: "start", meta: { contentType?, contentDisposition }, hookId?, httpStatus?, error: { message: "" } }`
|
|
106
|
+
- `{ cacheName, stream: "chunk", data: ArrayBuffer, hookId?, error: { message: "" } }` (one or more)
|
|
107
|
+
- `{ cacheName, stream: "resume", meta: { contentType?, contentDisposition }, hookId?, httpStatus?, error: { message: "" } }` (after retry)
|
|
108
|
+
- `{ cacheName, stream: "end", hookId?, error: { message: "" } }` (final message)
|
|
109
|
+
- **Error:** `{ cacheName?, data: null, error: { message: "..." }, hookId? }`. If the request had no `cacheName`, match by `hookId` instead.
|
|
110
|
+
|
|
111
|
+
**Common error messages:**
|
|
112
|
+
|
|
113
|
+
- `"Invalid request: type is required"`
|
|
114
|
+
- `"Invalid request: cacheName is required"`
|
|
115
|
+
- `"Invalid request: payload is required for set"`
|
|
116
|
+
- `"Invalid request: payload is required for non-GET API request"`
|
|
117
|
+
- `"Cache miss"` (when requesting a non-existent cache key)
|
|
118
|
+
- HTTP status text or fetch error messages for network failures
|
|
119
|
+
|
|
120
|
+
### Request Configuration
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface RequestConfig {
|
|
124
|
+
url: string;
|
|
125
|
+
method: "GET" | "get" | "POST" | "post" | "PATCH" | "patch" | "DELETE" | "delete";
|
|
126
|
+
mode?: "cors" | "no-cors" | "navigate" | "same-origin";
|
|
127
|
+
headers?: Record<string, string>;
|
|
128
|
+
credentials?: "include" | "same-origin" | "omit";
|
|
129
|
+
responseType?: "json" | "binary" | "stream"; // default: "json"
|
|
130
|
+
timeoutMs?: number; // Abort request after this many milliseconds
|
|
131
|
+
formDataFileFieldName?: string; // FormData field name for File/Blob parts (default: "Files")
|
|
132
|
+
formDataKey?: string; // FormData key for root payload when building multipart form data
|
|
133
|
+
retries?: number; // For responseType "stream": retry attempts on connection loss (default: 3, max: 5)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **`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 })`.
|
|
138
|
+
|
|
139
|
+
- **`responseType: "stream"`**: Use for streaming audio/video or large files. The worker sends chunks incrementally. Supports automatic reconnection with configurable retries (default 3, max 5).
|
|
140
|
+
|
|
141
|
+
### Vanilla JavaScript Examples
|
|
142
|
+
|
|
143
|
+
**GET request from API (JSON response):**
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const worker = new Worker(workerUrl, { type: "module" });
|
|
147
|
+
const cacheName = "api-get-" + Date.now();
|
|
148
|
+
|
|
149
|
+
worker.onmessage = (event) => {
|
|
150
|
+
const { cacheName: name, data, error, httpStatus } = event.data;
|
|
151
|
+
if (name === cacheName && error?.message === "" && data != null) {
|
|
152
|
+
console.log("Response:", data, "HTTP status:", httpStatus);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
worker.postMessage({
|
|
157
|
+
dataRequest: {
|
|
158
|
+
type: "set",
|
|
159
|
+
cacheName,
|
|
160
|
+
request: { url: "https://api.example.com/data", method: "GET" },
|
|
161
|
+
hookId: "vanilla-get",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**POST request with JSON payload:**
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
worker.postMessage({
|
|
170
|
+
dataRequest: {
|
|
171
|
+
type: "set",
|
|
172
|
+
cacheName: "api-post-" + Date.now(),
|
|
173
|
+
payload: { name: "New Item", description: "Created from vanilla JS" },
|
|
174
|
+
request: {
|
|
175
|
+
url: "https://api.example.com/items",
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json" },
|
|
178
|
+
},
|
|
179
|
+
hookId: "vanilla-post",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**PATCH request:**
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
worker.postMessage({
|
|
188
|
+
dataRequest: {
|
|
189
|
+
type: "set",
|
|
190
|
+
cacheName: "update-item",
|
|
191
|
+
payload: { status: "completed", priority: "high" },
|
|
192
|
+
request: {
|
|
193
|
+
url: "https://api.example.com/items/123",
|
|
194
|
+
method: "PATCH",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
},
|
|
197
|
+
hookId: "vanilla-patch",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Cache-only operations (no API call):**
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const cacheName = "local-cache-" + Math.random();
|
|
206
|
+
|
|
207
|
+
// Store data in cache without making an API request
|
|
208
|
+
worker.postMessage({
|
|
209
|
+
dataRequest: {
|
|
210
|
+
type: "set",
|
|
211
|
+
cacheName,
|
|
212
|
+
payload: { userId: 42, preferences: { theme: "dark" } },
|
|
213
|
+
hookId: "cache-set",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Retrieve cached data
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
worker.postMessage({
|
|
220
|
+
dataRequest: { type: "get", cacheName, hookId: "cache-get" },
|
|
221
|
+
});
|
|
222
|
+
}, 0);
|
|
223
|
+
// onmessage will receive: { cacheName, data: { userId: 42, preferences: { theme: "dark" } }, error: { message: "" } }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Handling cache misses:**
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
worker.postMessage({
|
|
230
|
+
dataRequest: { type: "get", cacheName: "nonexistent-key", hookId: "cache-miss" },
|
|
231
|
+
});
|
|
232
|
+
// onmessage receives: { cacheName: "nonexistent-key", data: null, error: { message: "Cache miss" }, hookId: "cache-miss" }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Delete cached data:**
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// First, store some data
|
|
239
|
+
worker.postMessage({
|
|
240
|
+
dataRequest: { type: "set", cacheName: "temp-data", payload: { temp: true } },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Later, delete it
|
|
244
|
+
worker.postMessage({
|
|
245
|
+
dataRequest: { type: "delete", cacheName: "temp-data", hookId: "delete-op" },
|
|
246
|
+
});
|
|
247
|
+
// Response: { cacheName: "temp-data", data: { deleted: true }, error: { message: "" }, hookId: "delete-op" }
|
|
248
|
+
|
|
249
|
+
// Subsequent get for the same cacheName returns: { error: { message: "Cache miss" } }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Binary file download (complete file):**
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
worker.postMessage({
|
|
256
|
+
dataRequest: {
|
|
257
|
+
type: "set",
|
|
258
|
+
cacheName: "download-pdf-" + Date.now(),
|
|
259
|
+
request: {
|
|
260
|
+
url: "https://example.com/document.pdf",
|
|
261
|
+
method: "GET",
|
|
262
|
+
responseType: "binary",
|
|
263
|
+
},
|
|
264
|
+
hookId: "binary-download",
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
worker.onmessage = (event) => {
|
|
269
|
+
const { data, meta, error } = event.data;
|
|
270
|
+
if (error?.message === "" && data instanceof ArrayBuffer) {
|
|
271
|
+
// Build a Blob from the ArrayBuffer
|
|
272
|
+
const blob = new Blob([data], {
|
|
273
|
+
type: meta?.contentType ?? "application/octet-stream",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Create download link or object URL
|
|
277
|
+
const url = URL.createObjectURL(blob);
|
|
278
|
+
console.log("Download ready:", url);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Streaming audio/video (incremental chunks):**
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const cacheName = "audio-stream-" + Date.now();
|
|
287
|
+
const chunks: ArrayBuffer[] = [];
|
|
288
|
+
let meta: { contentType?: string; contentDisposition: string | null } | null = null;
|
|
289
|
+
|
|
290
|
+
worker.onmessage = (event) => {
|
|
291
|
+
const msg = event.data;
|
|
292
|
+
if (msg.cacheName !== cacheName) return;
|
|
293
|
+
|
|
294
|
+
if (msg.stream === "start") {
|
|
295
|
+
// Stream started
|
|
296
|
+
chunks.length = 0;
|
|
297
|
+
meta = msg.meta ?? null;
|
|
298
|
+
console.log("Stream started, content type:", meta?.contentType);
|
|
299
|
+
} else if (msg.stream === "chunk" && msg.data) {
|
|
300
|
+
// Received a chunk
|
|
301
|
+
chunks.push(msg.data);
|
|
302
|
+
console.log(`Received chunk, total chunks: ${chunks.length}`);
|
|
303
|
+
} else if (msg.stream === "resume") {
|
|
304
|
+
// Stream resumed after reconnection
|
|
305
|
+
if (msg.meta) meta = msg.meta;
|
|
306
|
+
console.log("Stream resumed");
|
|
307
|
+
} else if (msg.stream === "end") {
|
|
308
|
+
// Stream complete
|
|
309
|
+
if (msg.error?.message === "" && chunks.length > 0) {
|
|
310
|
+
const blob = new Blob(chunks, meta?.contentType ? { type: meta.contentType } : undefined);
|
|
311
|
+
const url = URL.createObjectURL(blob);
|
|
312
|
+
console.log("Stream complete, blob URL:", url);
|
|
313
|
+
|
|
314
|
+
// Use the URL in an audio or video element
|
|
315
|
+
// audioElement.src = url;
|
|
316
|
+
} else {
|
|
317
|
+
console.error("Stream error:", msg.error?.message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
worker.postMessage({
|
|
323
|
+
dataRequest: {
|
|
324
|
+
type: "set",
|
|
325
|
+
cacheName,
|
|
326
|
+
request: {
|
|
327
|
+
url: "https://stream.example.com/audio.mp3",
|
|
328
|
+
method: "GET",
|
|
329
|
+
responseType: "stream",
|
|
330
|
+
retries: 3, // Retry up to 3 times on connection loss
|
|
331
|
+
},
|
|
332
|
+
hookId: "stream-audio",
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Cancel an in-flight request:**
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const requestId = "cancel-request-" + Date.now();
|
|
341
|
+
|
|
342
|
+
// Start a large download
|
|
343
|
+
worker.postMessage({
|
|
344
|
+
dataRequest: {
|
|
345
|
+
type: "set",
|
|
346
|
+
cacheName: "large-file",
|
|
347
|
+
request: {
|
|
348
|
+
url: "https://example.com/large-file.bin",
|
|
349
|
+
method: "GET",
|
|
350
|
+
responseType: "binary",
|
|
351
|
+
},
|
|
352
|
+
requestId,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Cancel it after 100ms
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
worker.postMessage({
|
|
359
|
+
dataRequest: { type: "cancel", requestId },
|
|
360
|
+
});
|
|
361
|
+
}, 100);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Handling validation errors:**
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
// Missing type
|
|
368
|
+
worker.postMessage({ dataRequest: { cacheName: "x" } });
|
|
369
|
+
// Response: { error: { message: "Invalid request: type is required" } }
|
|
370
|
+
|
|
371
|
+
// Missing cacheName
|
|
372
|
+
worker.postMessage({ dataRequest: { type: "get" } });
|
|
373
|
+
// Response: { error: { message: "Invalid request: cacheName is required" } }
|
|
374
|
+
|
|
375
|
+
// Missing payload for set
|
|
376
|
+
worker.postMessage({ dataRequest: { type: "set", cacheName: "k" } });
|
|
377
|
+
// Response: { error: { message: "Invalid request: payload is required for set" } }
|
|
378
|
+
|
|
379
|
+
// Missing payload for POST
|
|
380
|
+
worker.postMessage({
|
|
381
|
+
dataRequest: {
|
|
382
|
+
type: "set",
|
|
383
|
+
cacheName: "k",
|
|
384
|
+
request: { url: "...", method: "POST" },
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
// Response: { error: { message: "Invalid request: payload is required for non-GET API request" } }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Usage with React
|
|
393
|
+
|
|
394
|
+
For React applications, the library provides an optional `useApiWorker` hook that wraps the worker communication. You may use this hook or build your own React integration using the [Message Protocol](#message-protocol) above. No provider or wrapper component is required—use the hook wherever you need to fetch or read cached data.
|
|
395
|
+
|
|
396
|
+
### Hook API
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { useApiWorker } from "@mainframework/api-request-worker";
|
|
400
|
+
|
|
401
|
+
const result = useApiWorker({
|
|
402
|
+
cacheName: "my-cache", // required
|
|
403
|
+
request: { ... }, // optional: request config for API call
|
|
404
|
+
data: { ... }, // optional: payload for POST/PATCH
|
|
405
|
+
runMode: "auto", // optional: "auto" | "manual" | "once" (default "auto")
|
|
406
|
+
enabled: true, // optional: if false, no request is sent (default true)
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// result: { data, meta, loading, error, refetch, deleteCache }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Parameters:**
|
|
413
|
+
|
|
414
|
+
- **`cacheName`** (required): Cache key for storing and retrieving data. Multiple components using the same `cacheName` share the same cached value (see [Shared cacheName](#shared-cachename--multiple-subscribers)).
|
|
415
|
+
- **`request`** (optional): When provided, the worker performs an API request and stores the result. When omitted, the hook only reads from cache.
|
|
416
|
+
- **`data`** (optional): Request body/payload for POST, PATCH, etc.
|
|
417
|
+
- **`runMode`**:
|
|
418
|
+
- **`"auto"`** (default): Sends the request (or cache read) immediately when the hook mounts.
|
|
419
|
+
- **`"manual"`**: Does not send automatically; call `refetch()` to trigger.
|
|
420
|
+
- **`"once"`**: Sends once automatically on mount; subsequent `refetch()` calls do nothing.
|
|
421
|
+
- **`enabled`**: When `false`, no request is sent (useful for conditional fetching based on user state or other conditions).
|
|
422
|
+
|
|
423
|
+
**Return value:**
|
|
424
|
+
|
|
425
|
+
| Property | Type | Description |
|
|
426
|
+
| ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
427
|
+
| `data` | `T \| null` | Response body: JSON/text for `responseType: "json"`, `ArrayBuffer` for `responseType: "binary"`, `Blob` for `responseType: "stream"`. |
|
|
428
|
+
| `meta` | `BinaryResponseMeta \| null` | For binary and stream responses: `contentType`, `contentDisposition`. |
|
|
429
|
+
| `loading` | `boolean` | `true` while a request is in flight. |
|
|
430
|
+
| `error` | `string \| null` | Error message when the request failed; `null` when there is no error. See [Errors](#errors). |
|
|
431
|
+
| `refetch` | `() => void` | Re-runs the same logical request. See [Refetch semantics](#refetch-semantics). |
|
|
432
|
+
| `deleteCache` | `() => void` | Tells the worker to delete the cache entry for this `cacheName`. |
|
|
433
|
+
|
|
434
|
+
### React Examples
|
|
435
|
+
|
|
436
|
+
**GET request with automatic execution:**
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const { data, loading, error, refetch, deleteCache } = useApiWorker({
|
|
440
|
+
cacheName: "todos",
|
|
441
|
+
request: { url: "https://api.example.com/todos", method: "GET" },
|
|
442
|
+
runMode: "auto",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Request is sent immediately when component mounts
|
|
446
|
+
// data/loading/error update when the worker responds
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**POST request with payload:**
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
const { data, loading, error, refetch } = useApiWorker({
|
|
453
|
+
cacheName: "create-post",
|
|
454
|
+
request: {
|
|
455
|
+
url: "https://api.restful-api.dev/objects",
|
|
456
|
+
method: "POST",
|
|
457
|
+
headers: { "Content-Type": "application/json" },
|
|
458
|
+
},
|
|
459
|
+
data: {
|
|
460
|
+
name: "My New Object",
|
|
461
|
+
data: { color: "blue", size: "large" },
|
|
462
|
+
},
|
|
463
|
+
runMode: "auto",
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**PATCH request:**
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
const { data, loading, refetch } = useApiWorker({
|
|
471
|
+
cacheName: "update-item",
|
|
472
|
+
request: {
|
|
473
|
+
url: "https://api.example.com/items/123",
|
|
474
|
+
method: "PATCH",
|
|
475
|
+
headers: { "Content-Type": "application/json" },
|
|
476
|
+
},
|
|
477
|
+
data: { status: "completed", updatedAt: new Date().toISOString() },
|
|
478
|
+
runMode: "auto",
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Manual execution (lazy loading):**
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const { data, loading, refetch } = useApiWorker({
|
|
486
|
+
cacheName: "user-profile",
|
|
487
|
+
request: {
|
|
488
|
+
url: "https://api.example.com/profile",
|
|
489
|
+
method: "GET",
|
|
490
|
+
},
|
|
491
|
+
runMode: "manual",
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Call refetch() when needed (e.g., on button click or in useEffect)
|
|
495
|
+
const handleLoadProfile = () => {
|
|
496
|
+
refetch();
|
|
497
|
+
};
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Run once (single automatic execution):**
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
const { data, refetch } = useApiWorker({
|
|
504
|
+
cacheName: "init-data",
|
|
505
|
+
request: { url: "https://api.example.com/init", method: "GET" },
|
|
506
|
+
runMode: "once",
|
|
507
|
+
});
|
|
508
|
+
// Request is sent once on mount. Calling refetch() does nothing.
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Conditional fetching:**
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const { data, loading } = useApiWorker({
|
|
515
|
+
cacheName: "protected-resource",
|
|
516
|
+
request: { url: "https://api.example.com/protected", method: "GET" },
|
|
517
|
+
runMode: "auto",
|
|
518
|
+
enabled: isAuthenticated, // Only fetch when user is authenticated
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Read from cache only (no API request):**
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
const { data, loading, error, refetch } = useApiWorker({
|
|
526
|
+
cacheName: "shared-state",
|
|
527
|
+
runMode: "auto",
|
|
528
|
+
});
|
|
529
|
+
// Sends a "get" request to the worker
|
|
530
|
+
// If cache is empty, error will be "Cache miss"
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Binary response (complete file):**
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
const { data, meta, loading } = useApiWorker({
|
|
537
|
+
cacheName: "pdf-document",
|
|
538
|
+
request: {
|
|
539
|
+
url: "https://example.com/document.pdf",
|
|
540
|
+
method: "GET",
|
|
541
|
+
responseType: "binary",
|
|
542
|
+
},
|
|
543
|
+
runMode: "auto",
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// When loaded, data is ArrayBuffer
|
|
547
|
+
// meta contains contentType and contentDisposition
|
|
548
|
+
// Create a Blob: new Blob([data], { type: meta?.contentType })
|
|
549
|
+
// Create object URL: URL.createObjectURL(blob)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**Streaming response (audio/video):**
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
const { data, meta, loading, error } = useApiWorker({
|
|
556
|
+
cacheName: "video-stream",
|
|
557
|
+
request: {
|
|
558
|
+
url: "https://example.com/video.mp4",
|
|
559
|
+
method: "GET",
|
|
560
|
+
responseType: "stream",
|
|
561
|
+
retries: 3, // Retry on connection loss (default 3, max 5)
|
|
562
|
+
},
|
|
563
|
+
runMode: "auto",
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// data is a Blob when the stream completes
|
|
567
|
+
// loading is true until stream ends
|
|
568
|
+
// Use with media elements:
|
|
569
|
+
// const videoUrl = data ? URL.createObjectURL(data) : null;
|
|
570
|
+
// <video src={videoUrl} controls />
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Delete cache:**
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
const { data, deleteCache } = useApiWorker({
|
|
577
|
+
cacheName: "temporary-data",
|
|
578
|
+
request: { url: "https://api.example.com/temp", method: "GET" },
|
|
579
|
+
runMode: "manual",
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const handleClearCache = () => {
|
|
583
|
+
deleteCache(); // Removes the cache entry from the worker
|
|
584
|
+
};
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Shared cacheName / Multiple Subscribers
|
|
588
|
+
|
|
589
|
+
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.
|
|
590
|
+
|
|
591
|
+
**Recommendation:** Use unique `cacheName` values per logical resource if you need independent `loading`/`error` state in each component.
|
|
592
|
+
|
|
593
|
+
### Refetch Semantics
|
|
594
|
+
|
|
595
|
+
`refetch()` re-runs the same logical operation as the current hook configuration:
|
|
596
|
+
|
|
597
|
+
- **When `request` is omitted**: Sends a **get** request (reads from cache)
|
|
598
|
+
- **When `request` is provided**: Sends a **set** request (makes an API call or stores data)
|
|
599
|
+
|
|
600
|
+
It does not switch between get and set based on prior runs; it uses the current `cacheName`, `request`, and `data` values at the time `refetch()` is called.
|
|
601
|
+
|
|
602
|
+
### Errors
|
|
603
|
+
|
|
604
|
+
The worker always includes an `error` field in every message: `{ message: string }`.
|
|
605
|
+
|
|
606
|
+
- **No error**: `{ message: "" }` (empty string)
|
|
607
|
+
- **Error occurred**: `{ message: "error description" }`
|
|
608
|
+
|
|
609
|
+
The hook exposes this as `error: string | null`:
|
|
610
|
+
|
|
611
|
+
- `null` when `error.message` is empty
|
|
612
|
+
- The error message string when an error occurred
|
|
613
|
+
|
|
614
|
+
Common error messages:
|
|
615
|
+
|
|
616
|
+
- `"Cache miss"` – Requested cache key doesn't exist
|
|
617
|
+
- `"Invalid request: ..."` – Request validation failed
|
|
618
|
+
- HTTP status text or network error messages
|
|
619
|
+
|
|
620
|
+
Responses are routed to the requesting component by `cacheName` or, when `cacheName` is missing from the worker response, by `hookId`.
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## TypeScript Types
|
|
625
|
+
|
|
626
|
+
**For React:**
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import type { RequestConfig, UseApiWorkerConfig, UseApiWorkerReturn } from "@mainframework/api-request-worker";
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**For vanilla JavaScript/TypeScript:**
|
|
633
|
+
|
|
634
|
+
The worker expects the `dataRequest` shape described in the protocol documentation above. You can define minimal types for message payloads or reuse `RequestConfig` for the `request` field.
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Quick Reference
|
|
639
|
+
|
|
640
|
+
| Use case | Entry point | Primary API |
|
|
641
|
+
| ----------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
642
|
+
| **Vanilla JS/TS** | Worker from `dist/api.worker.js` | `worker.postMessage({ dataRequest: { type, cacheName, request?, payload?, ... } })` and `worker.onmessage` for responses |
|
|
643
|
+
| **React** | Optional: `useApiWorker` hook | `useApiWorker({ cacheName, request?, data?, runMode?, enabled? })` → `{ data, meta, loading, error, refetch, deleteCache }` (or use worker protocol above) |
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Framework Integrations
|
|
648
|
+
|
|
649
|
+
**Core:** The worker and message protocol work with any environment (vanilla JavaScript/TypeScript or any framework).
|
|
650
|
+
|
|
651
|
+
**Optional integration:** A React hook (`useApiWorker`) is included to make adoption easier for React projects; React teams may instead integrate using the worker protocol directly.
|
|
652
|
+
|
|
653
|
+
**Coming soon:** Idiomatic helpers for other frameworks (Angular, Vue, Preact, SolidJS) are planned; until then, use the protocol from those frameworks as with vanilla JS.
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Testing
|
|
658
|
+
|
|
659
|
+
This library is thoroughly tested with comprehensive test suites:
|
|
660
|
+
|
|
661
|
+
- React hook tests (`useApiWorker.test.ts`)
|
|
662
|
+
- Worker protocol tests (`api.worker.test.ts`)
|
|
663
|
+
|
|
664
|
+
Key behaviors and examples in this README are covered by the test suites.
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## License
|
|
669
|
+
|
|
670
|
+
See the License file in the repo
|