@angular-helpers/worker-http 21.0.0 → 21.2.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/README.md +118 -21
- package/fesm2022/angular-helpers-worker-http-backend.mjs +41 -7
- package/fesm2022/angular-helpers-worker-http-serializer.mjs +155 -4
- package/fesm2022/angular-helpers-worker-http-transport.mjs +68 -5
- package/package.json +6 -2
- package/types/angular-helpers-worker-http-backend.d.ts +40 -1
- package/types/angular-helpers-worker-http-serializer.d.ts +74 -4
- package/types/angular-helpers-worker-http-transport.d.ts +45 -3
package/README.md
CHANGED
|
@@ -88,6 +88,9 @@ Then follow the setup in the `/backend` section below.
|
|
|
88
88
|
|
|
89
89
|
A framework-agnostic, type-safe bridge between the main thread and a Web Worker. Wraps `postMessage` with request/response correlation, Observable API, and automatic cancellation on unsubscribe.
|
|
90
90
|
|
|
91
|
+
<details>
|
|
92
|
+
<summary><strong>API and examples</strong></summary>
|
|
93
|
+
|
|
91
94
|
```typescript
|
|
92
95
|
import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
|
|
93
96
|
|
|
@@ -113,6 +116,11 @@ transport.terminate();
|
|
|
113
116
|
- **Per-request timeout** (default `30_000` ms) via `requestTimeout`; errors
|
|
114
117
|
with `WorkerHttpTimeoutError` and sends a cancel message to the worker.
|
|
115
118
|
Set to `0` to disable.
|
|
119
|
+
- **Per-call `AbortSignal` and `timeout` overrides** via the second argument
|
|
120
|
+
to `execute(request, { signal, timeout })`. The signal triggers a typed
|
|
121
|
+
`WorkerHttpAbortError`; an aborted-at-call-time signal fails fast with no
|
|
122
|
+
postMessage round-trip. Distinct error class from `WorkerHttpTimeoutError`
|
|
123
|
+
so consumers can branch on `instanceof`.
|
|
116
124
|
- **Opt-in transferable detection** via `transferDetection: 'auto'` — passes
|
|
117
125
|
detected `ArrayBuffer` / `MessagePort` / `ImageBitmap` /
|
|
118
126
|
`OffscreenCanvas` / streams as the transfer list of `postMessage`, enabling
|
|
@@ -141,12 +149,17 @@ transport.execute(request).subscribe({
|
|
|
141
149
|
});
|
|
142
150
|
```
|
|
143
151
|
|
|
152
|
+
</details>
|
|
153
|
+
|
|
144
154
|
---
|
|
145
155
|
|
|
146
156
|
### `/interceptors` — Worker-side pipeline
|
|
147
157
|
|
|
148
158
|
Pure-function interceptors that run inside the worker. No Angular DI, no DOM access — just `(req, next) => Promise<response>`.
|
|
149
159
|
|
|
160
|
+
<details>
|
|
161
|
+
<summary><strong>Setup, built-in interceptors, and custom interceptors</strong></summary>
|
|
162
|
+
|
|
150
163
|
#### Setup in your worker file
|
|
151
164
|
|
|
152
165
|
```typescript
|
|
@@ -278,11 +291,22 @@ export const authTokenInterceptor: WorkerInterceptorFn = (req, next) => {
|
|
|
278
291
|
};
|
|
279
292
|
```
|
|
280
293
|
|
|
294
|
+
</details>
|
|
295
|
+
|
|
281
296
|
---
|
|
282
297
|
|
|
283
298
|
### `/serializer` — Pluggable serialization
|
|
284
299
|
|
|
285
|
-
Handles the `postMessage` serialization boundary.
|
|
300
|
+
Handles the `postMessage` serialization boundary. Three strategies, each with a clear sweet spot:
|
|
301
|
+
|
|
302
|
+
- `structuredCloneSerializer` — zero overhead, default
|
|
303
|
+
- `createToonSerializer()` — 30–60% smaller for uniform arrays of objects
|
|
304
|
+
- `createSerovalSerializer()` — full type fidelity (`Date`, `Map`, `Set`, circular refs)
|
|
305
|
+
|
|
306
|
+
The auto-serializer picks the best strategy per payload.
|
|
307
|
+
|
|
308
|
+
<details>
|
|
309
|
+
<summary><strong>Per-strategy API and examples</strong></summary>
|
|
286
310
|
|
|
287
311
|
#### `structuredCloneSerializer` (default)
|
|
288
312
|
|
|
@@ -311,14 +335,49 @@ const original = serializer.deserialize(payload);
|
|
|
311
335
|
// original.tags instanceof Set → true
|
|
312
336
|
```
|
|
313
337
|
|
|
338
|
+
#### `createToonSerializer()` — Token-Oriented Object Notation
|
|
339
|
+
|
|
340
|
+
Requires `@toon-format/toon` as an optional peer dependency (`npm install @toon-format/toon`).
|
|
341
|
+
|
|
342
|
+
[TOON](https://toonformat.dev) declares object keys once and emits values as CSV-like rows. For uniform arrays of objects (the most common API response shape — `User[]`, `Product[]`, paginated lists), it cuts payload size by **30–60%** compared to JSON, with negligible parsing overhead.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { createToonSerializer } from '@angular-helpers/worker-http/serializer';
|
|
346
|
+
|
|
347
|
+
const serializer = await createToonSerializer();
|
|
348
|
+
|
|
349
|
+
const payload = serializer.serialize([
|
|
350
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
351
|
+
{ id: 2, name: 'Bob', role: 'member' },
|
|
352
|
+
{ id: 3, name: 'Carol', role: 'member' },
|
|
353
|
+
{ id: 4, name: 'Dave', role: 'guest' },
|
|
354
|
+
{ id: 5, name: 'Eve', role: 'admin' },
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
// payload.data is a TOON string:
|
|
358
|
+
// [5]{id,name,role}:
|
|
359
|
+
// 1,Alice,admin
|
|
360
|
+
// 2,Bob,member
|
|
361
|
+
// 3,Carol,member
|
|
362
|
+
// 4,Dave,guest
|
|
363
|
+
// 5,Eve,admin
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**When TOON shines**: uniform arrays of objects with primitive values (numbers, strings, booleans, nulls) at depth-1.
|
|
367
|
+
|
|
368
|
+
**When TOON does NOT help**: payloads with `Date`, `Map`, `Set`, nested objects, or single objects — use `seroval` or structured clone instead.
|
|
369
|
+
|
|
314
370
|
#### `createAutoSerializer()` — Smart auto-detection
|
|
315
371
|
|
|
316
|
-
Automatically picks the best strategy per payload. The factory is async (pre-loads `seroval` during initialization), but the returned serializer is fully synchronous.
|
|
372
|
+
Automatically picks the best strategy per payload. The factory is async (pre-loads `seroval` and `@toon-format/toon` during initialization, both optional), but the returned serializer is fully synchronous.
|
|
317
373
|
|
|
318
|
-
**Detection logic (depth-1):**
|
|
374
|
+
**Detection logic (depth-1, top-down, first match wins):**
|
|
319
375
|
|
|
320
|
-
|
|
321
|
-
|
|
376
|
+
1. Contains `Date`, `Map`, `Set`, or `RegExp` at the top level or as direct array/object values → `seroval`
|
|
377
|
+
2. Uniform array of plain objects with primitive values, length ≥ 5 → `toon`
|
|
378
|
+
3. Otherwise → structured clone (zero overhead)
|
|
379
|
+
|
|
380
|
+
The TOON threshold is conservative (length ≥ 5). Smaller arrays don't justify the encoding overhead.
|
|
322
381
|
|
|
323
382
|
Payloads larger than `transferThreshold` (default: 100 KiB) are encoded to `ArrayBuffer` and transferred zero-copy.
|
|
324
383
|
|
|
@@ -341,12 +400,17 @@ auto.serialize(hugeDataset); // transferables: [ArrayBuffer]
|
|
|
341
400
|
|
|
342
401
|
> **Depth-1 limitation**: `[{ createdAt: new Date() }]` — the `Date` is inside a nested object; not detected at depth-1. For deeply nested complex types, use `createSerovalSerializer()` directly.
|
|
343
402
|
|
|
403
|
+
</details>
|
|
404
|
+
|
|
344
405
|
---
|
|
345
406
|
|
|
346
407
|
### `/crypto` — WebCrypto primitives
|
|
347
408
|
|
|
348
409
|
Standalone WebCrypto utilities. Useful in both workers and the main thread, but workers provide memory isolation for key material.
|
|
349
410
|
|
|
411
|
+
<details>
|
|
412
|
+
<summary><strong>HMAC, AES, hashing examples</strong></summary>
|
|
413
|
+
|
|
350
414
|
#### `createHmacSigner(config)`
|
|
351
415
|
|
|
352
416
|
```typescript
|
|
@@ -381,12 +445,17 @@ const hasher = createContentHasher();
|
|
|
381
445
|
const hash = await hasher.hash('SHA-256', data); // → hex string
|
|
382
446
|
```
|
|
383
447
|
|
|
448
|
+
</details>
|
|
449
|
+
|
|
384
450
|
---
|
|
385
451
|
|
|
386
452
|
### `/backend` — Angular `HttpBackend` replacement
|
|
387
453
|
|
|
388
454
|
Drop-in replacement for Angular's `HttpBackend` that transparently routes `HttpClient` requests to Web Workers. Use `WorkerHttpClient` exactly like `HttpClient` — the routing is invisible to application code.
|
|
389
455
|
|
|
456
|
+
<details>
|
|
457
|
+
<summary><strong>Configuration, providers, and consumer code</strong></summary>
|
|
458
|
+
|
|
390
459
|
```typescript
|
|
391
460
|
// app.config.ts
|
|
392
461
|
import {
|
|
@@ -470,17 +539,23 @@ manual control.
|
|
|
470
539
|
- `withWorkerSerialization(serializer)` — plug in `createSerovalSerializer()` for complex request bodies (`Date`, `Map`, `Set`)
|
|
471
540
|
- `withWorkerInterceptors(specs | specsByWorker)` — configure the worker-side pipeline from Angular DI; pairs with `createConfigurableWorkerPipeline()` in the worker file
|
|
472
541
|
- `WORKER_TARGET` — `HttpContextToken<string | null>` for per-request worker routing via `HttpContext`
|
|
473
|
-
- `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field
|
|
542
|
+
- `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field, plus per-request `signal?: AbortSignal` and `timeout?: number` for cancellation. Aborted requests error with `WorkerHttpAbortError`; expired timeouts with `WorkerHttpTimeoutError`.
|
|
474
543
|
- `WorkerHttpBackend` — the `HttpBackend` implementation (injectable for advanced use)
|
|
475
544
|
- `matchWorkerRoute(url, routes)` — pure utility to test routing rules
|
|
545
|
+
- `WORKER_HTTP_SIGNAL`, `WORKER_HTTP_TIMEOUT` — `HttpContextToken`s used internally by `WorkerHttpClient`; set them directly on the `HttpContext` if you're using `HttpClient` rather than the wrapper.
|
|
546
|
+
|
|
547
|
+
</details>
|
|
476
548
|
|
|
477
549
|
---
|
|
478
550
|
|
|
479
551
|
## Telemetry
|
|
480
552
|
|
|
481
|
-
Main-thread extension point for APM / metrics. `withTelemetry(...)` registers
|
|
482
|
-
|
|
483
|
-
|
|
553
|
+
Main-thread extension point for APM / metrics. `withTelemetry(...)` registers a subscriber that fires synchronously at three lifecycle points of every request handled by `WorkerHttpBackend` (`onRequest`, `onResponse`, `onError`).
|
|
554
|
+
|
|
555
|
+
<details>
|
|
556
|
+
<summary><strong>Subscriber semantics, examples, and event interface</strong></summary>
|
|
557
|
+
|
|
558
|
+
Lifecycle points:
|
|
484
559
|
|
|
485
560
|
- **`onRequest`** — after worker resolution, before dispatch
|
|
486
561
|
- **`onResponse`** — when a successful response is emitted
|
|
@@ -547,12 +622,17 @@ interface WorkerHttpTelemetryEventBase {
|
|
|
547
622
|
// onError adds: kind: 'error', error, durationMs
|
|
548
623
|
```
|
|
549
624
|
|
|
625
|
+
</details>
|
|
626
|
+
|
|
550
627
|
---
|
|
551
628
|
|
|
552
629
|
### `/esbuild-plugin` — Interceptor auto-bundling
|
|
553
630
|
|
|
554
631
|
An esbuild plugin that automatically discovers and bundles interceptor files into your worker builds. When using Angular with a custom webpack/esbuild configuration, this ensures your interceptors are included in the worker bundle without manual imports.
|
|
555
632
|
|
|
633
|
+
<details>
|
|
634
|
+
<summary><strong>Plugin options and example</strong></summary>
|
|
635
|
+
|
|
556
636
|
```typescript
|
|
557
637
|
// esbuild.config.ts
|
|
558
638
|
import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
@@ -579,12 +659,17 @@ export default {
|
|
|
579
659
|
|
|
580
660
|
Discovered interceptors are merged with explicit ones. Test files (`.spec.ts`, `.test.ts`) are automatically excluded.
|
|
581
661
|
|
|
662
|
+
</details>
|
|
663
|
+
|
|
582
664
|
---
|
|
583
665
|
|
|
584
666
|
### `/streams-polyfill` — Safari transferable streams
|
|
585
667
|
|
|
586
668
|
Safari 16-17 lack native transferable `ReadableStream`/`TransformStream` support. This ponyfill enables stream transfer in workers for those browsers, loaded lazily only when needed.
|
|
587
669
|
|
|
670
|
+
<details>
|
|
671
|
+
<summary><strong>Setup and bundle impact</strong></summary>
|
|
672
|
+
|
|
588
673
|
```typescript
|
|
589
674
|
// Enable in your app config (main thread)
|
|
590
675
|
import { withWorkerStreamsPolyfill } from '@angular-helpers/worker-http/backend';
|
|
@@ -602,12 +687,16 @@ provideWorkerHttpClient(
|
|
|
602
687
|
|
|
603
688
|
**Bundle impact:** Zero for modern browsers. The polyfill is lazy-loaded only on affected Safari versions when streams are actually used.
|
|
604
689
|
|
|
690
|
+
</details>
|
|
691
|
+
|
|
605
692
|
---
|
|
606
693
|
|
|
607
694
|
## SSR + hydration
|
|
608
695
|
|
|
609
|
-
Worker HTTP integrates transparently with Angular SSR.
|
|
610
|
-
|
|
696
|
+
Worker HTTP integrates transparently with Angular SSR. SSR's two problems for worker-based HTTP — missing `Worker` global on the server and the post-hydration re-fetch — are both handled out of the box.
|
|
697
|
+
|
|
698
|
+
<details>
|
|
699
|
+
<summary><strong>How SSR fallback and the transfer cache work</strong></summary>
|
|
611
700
|
|
|
612
701
|
**1. Workers do not exist on the server.**
|
|
613
702
|
During SSR, `typeof Worker === 'undefined'`. `WorkerHttpBackend` detects this
|
|
@@ -657,6 +746,8 @@ To customise which headers are captured or to cache `POST` requests, pass
|
|
|
657
746
|
`withHttpTransferCacheOptions(...)` to `provideClientHydration()` — both are
|
|
658
747
|
re-exported from `@angular/platform-browser`.
|
|
659
748
|
|
|
749
|
+
</details>
|
|
750
|
+
|
|
660
751
|
---
|
|
661
752
|
|
|
662
753
|
## Design principles
|
|
@@ -671,13 +762,14 @@ re-exported from `@angular/platform-browser`.
|
|
|
671
762
|
|
|
672
763
|
## Serialization strategy decision guide
|
|
673
764
|
|
|
674
|
-
| Payload type
|
|
675
|
-
|
|
|
676
|
-
| Simple objects, arrays of primitives
|
|
677
|
-
|
|
|
678
|
-
|
|
|
679
|
-
|
|
|
680
|
-
|
|
|
765
|
+
| Payload type | Recommended serializer | Reason |
|
|
766
|
+
| ------------------------------------------ | -------------------------------------- | --------------------------- |
|
|
767
|
+
| Simple objects, arrays of primitives | `structuredCloneSerializer` (default) | Zero overhead |
|
|
768
|
+
| Uniform array of plain objects (≥ 5 items) | `createToonSerializer()` | 30–60% size reduction |
|
|
769
|
+
| Objects with `Date`, `Map`, `Set` | `createSerovalSerializer()` | Full type fidelity |
|
|
770
|
+
| Unknown payload shape | `createAutoSerializer()` | Depth-1 auto-detect |
|
|
771
|
+
| Large arrays (> 100 KiB) | `createAutoSerializer()` | Auto ArrayBuffer transfer |
|
|
772
|
+
| Deeply nested complex types | `createSerovalSerializer()` explicitly | Auto-detect is depth-1 only |
|
|
681
773
|
|
|
682
774
|
---
|
|
683
775
|
|
|
@@ -695,9 +787,12 @@ Server-Side Rendering (SSR) is supported via automatic fallback to the main thre
|
|
|
695
787
|
|
|
696
788
|
## Benchmarks
|
|
697
789
|
|
|
698
|
-
A reproducible benchmark suite ships with the demo app at
|
|
699
|
-
|
|
700
|
-
|
|
790
|
+
A reproducible benchmark suite ships with the demo app at [`/demo/worker-http-benchmark`](../../src/app/demo/worker-http-benchmark).
|
|
791
|
+
|
|
792
|
+
<details>
|
|
793
|
+
<summary><strong>Modes, workloads, metrics, and how to run</strong></summary>
|
|
794
|
+
|
|
795
|
+
It compares three transport modes across four workloads:
|
|
701
796
|
|
|
702
797
|
| Mode | What it measures |
|
|
703
798
|
| --------------- | ---------------------------------------------------------- |
|
|
@@ -733,6 +828,8 @@ npm start
|
|
|
733
828
|
Numbers vary by hardware, browser, and current system load — always run a scenario several times
|
|
734
829
|
and watch the trend, not a single value.
|
|
735
830
|
|
|
831
|
+
</details>
|
|
832
|
+
|
|
736
833
|
---
|
|
737
834
|
|
|
738
835
|
## Related documentation
|
|
@@ -22,6 +22,31 @@ import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
const WORKER_TARGET = new HttpContextToken(() => null);
|
|
25
|
+
/**
|
|
26
|
+
* Per-request HttpContextToken carrying an external `AbortSignal`.
|
|
27
|
+
*
|
|
28
|
+
* When the signal fires, the backend posts a `cancel` message to the worker
|
|
29
|
+
* and the request errors with `WorkerHttpAbortError` (wrapped by Angular into
|
|
30
|
+
* `HttpErrorResponse`).
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const ac = new AbortController();
|
|
35
|
+
* this.http.get('/api/users', { signal: ac.signal });
|
|
36
|
+
* // later
|
|
37
|
+
* ac.abort();
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
const WORKER_HTTP_SIGNAL = new HttpContextToken(() => null);
|
|
41
|
+
/**
|
|
42
|
+
* Per-request HttpContextToken carrying a timeout in milliseconds.
|
|
43
|
+
*
|
|
44
|
+
* When set, overrides the transport-level `requestTimeout` for this single
|
|
45
|
+
* call. `0` or non-finite disables the timeout for this request only.
|
|
46
|
+
*
|
|
47
|
+
* On expiry the request errors with `WorkerHttpTimeoutError`.
|
|
48
|
+
*/
|
|
49
|
+
const WORKER_HTTP_TIMEOUT = new HttpContextToken(() => null);
|
|
25
50
|
/**
|
|
26
51
|
* Registered worker definitions provided via `withWorkerConfigs()`.
|
|
27
52
|
*/
|
|
@@ -188,7 +213,12 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
188
213
|
const payload = body !== serializable.body ? { ...serializable, body } : serializable;
|
|
189
214
|
const base = this.buildEventBase(req, workerId, 'worker');
|
|
190
215
|
this.emitRequest(base);
|
|
191
|
-
|
|
216
|
+
const signal = req.context.get(WORKER_HTTP_SIGNAL) ?? undefined;
|
|
217
|
+
const timeout = req.context.get(WORKER_HTTP_TIMEOUT);
|
|
218
|
+
const executeOptions = signal !== undefined || timeout !== null
|
|
219
|
+
? { signal, timeout: timeout ?? undefined }
|
|
220
|
+
: undefined;
|
|
221
|
+
return transport.execute(payload, executeOptions).pipe(map((res) => toHttpResponse(res, req)), tap((event) => {
|
|
192
222
|
if (event instanceof HttpResponse) {
|
|
193
223
|
this.emitResponse(base, event.status);
|
|
194
224
|
}
|
|
@@ -352,11 +382,15 @@ class WorkerHttpClient {
|
|
|
352
382
|
return this.http.head(url, this.withWorker(options));
|
|
353
383
|
}
|
|
354
384
|
withWorker(options) {
|
|
355
|
-
const { worker = null, context, ...rest } = options ?? {};
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
385
|
+
const { worker = null, signal, timeout, context, ...rest } = options ?? {};
|
|
386
|
+
let ctx = (context ?? new HttpContext()).set(WORKER_TARGET, worker);
|
|
387
|
+
if (signal !== undefined) {
|
|
388
|
+
ctx = ctx.set(WORKER_HTTP_SIGNAL, signal);
|
|
389
|
+
}
|
|
390
|
+
if (timeout !== undefined) {
|
|
391
|
+
ctx = ctx.set(WORKER_HTTP_TIMEOUT, timeout);
|
|
392
|
+
}
|
|
393
|
+
return { ...rest, context: ctx };
|
|
360
394
|
}
|
|
361
395
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
362
396
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient });
|
|
@@ -603,4 +637,4 @@ function withWorkerStreamsPolyfill() {
|
|
|
603
637
|
* Generated bundle index. Do not edit.
|
|
604
638
|
*/
|
|
605
639
|
|
|
606
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
640
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_SIGNAL, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_HTTP_TIMEOUT, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
@@ -71,6 +71,138 @@ async function createSerovalSerializer() {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
let cachedToon = null;
|
|
75
|
+
async function loadToon() {
|
|
76
|
+
if (!cachedToon) {
|
|
77
|
+
try {
|
|
78
|
+
// Dynamic import via variable — keeps @toon-format/toon as optional peer dep (no static reference)
|
|
79
|
+
const id = '@toon-format/toon';
|
|
80
|
+
cachedToon = (await import(/* @vite-ignore */ id));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new Error('@toon-format/toon is required as a peer dependency. ' +
|
|
84
|
+
'Install it with: npm install @toon-format/toon');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return cachedToon;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates a `WorkerSerializer` backed by `@toon-format/toon` (Token-Oriented Object Notation).
|
|
91
|
+
*
|
|
92
|
+
* TOON is a compact, schema-aware encoding of the JSON data model that declares object
|
|
93
|
+
* keys once and emits values as CSV-like rows. It typically reduces size by **30–60%**
|
|
94
|
+
* for uniform arrays of objects (e.g. `User[]`, `Product[]`, paginated lists), with
|
|
95
|
+
* negligible parsing overhead.
|
|
96
|
+
*
|
|
97
|
+
* **When to use it**:
|
|
98
|
+
* - Worker↔main `postMessage` payloads dominated by uniform arrays of objects
|
|
99
|
+
* - Cases where `structuredClone` cost is dominated by repeated key strings
|
|
100
|
+
*
|
|
101
|
+
* **When NOT to use it**:
|
|
102
|
+
* - Payloads containing `Date`, `Map`, `Set`, `RegExp` (use `seroval` instead)
|
|
103
|
+
* - Small / single-object payloads (overhead not justified)
|
|
104
|
+
*
|
|
105
|
+
* The factory is async because it dynamically imports the optional `@toon-format/toon` peer.
|
|
106
|
+
*
|
|
107
|
+
* `@toon-format/toon` must be installed separately:
|
|
108
|
+
* ```
|
|
109
|
+
* npm install @toon-format/toon
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const serializer = await createToonSerializer();
|
|
115
|
+
* const payload = serializer.serialize([
|
|
116
|
+
* { id: 1, name: 'Alice' },
|
|
117
|
+
* { id: 2, name: 'Bob' },
|
|
118
|
+
* { id: 3, name: 'Carol' },
|
|
119
|
+
* { id: 4, name: 'Dave' },
|
|
120
|
+
* { id: 5, name: 'Eve' },
|
|
121
|
+
* ]);
|
|
122
|
+
* worker.postMessage({ payload }, payload.transferables);
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @see https://toonformat.dev
|
|
126
|
+
*/
|
|
127
|
+
async function createToonSerializer() {
|
|
128
|
+
const { encode, decode } = await loadToon();
|
|
129
|
+
return {
|
|
130
|
+
serialize(data) {
|
|
131
|
+
return {
|
|
132
|
+
data: encode(data),
|
|
133
|
+
transferables: [],
|
|
134
|
+
format: 'toon',
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
deserialize(payload) {
|
|
138
|
+
if (payload.format !== 'toon') {
|
|
139
|
+
throw new Error(`Expected format 'toon', got '${payload.format}'`);
|
|
140
|
+
}
|
|
141
|
+
return decode(payload.data);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Conservative threshold below which TOON's overhead outweighs its size benefit.
|
|
147
|
+
* Auto-serializer keeps shorter arrays on `structured-clone`.
|
|
148
|
+
*
|
|
149
|
+
* Exported for testing and for consumers building custom routing logic.
|
|
150
|
+
*/
|
|
151
|
+
const MIN_UNIFORM_ARRAY_LENGTH = 5;
|
|
152
|
+
/**
|
|
153
|
+
* Detects whether a value is a depth-1 uniform array of plain objects with primitive values.
|
|
154
|
+
*
|
|
155
|
+
* Pure function — no side effects. Used by `createAutoSerializer()` to decide
|
|
156
|
+
* whether to route a payload through TOON.
|
|
157
|
+
*
|
|
158
|
+
* Conditions checked (all must pass):
|
|
159
|
+
* 1. Value is an array with `length >= MIN_UNIFORM_ARRAY_LENGTH`
|
|
160
|
+
* 2. Every item is a non-null, non-array plain object
|
|
161
|
+
* 3. Every item has the same set of keys as the first item
|
|
162
|
+
* 4. Every value across all items is a primitive (string, number, boolean, null)
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* isUniformObjectArray([{a:1},{a:2},{a:3},{a:4},{a:5}]); // true
|
|
167
|
+
* isUniformObjectArray([{a:1},{b:2}]); // false (heterogeneous keys)
|
|
168
|
+
* isUniformObjectArray([{a:[1,2]},{a:[3,4]}]); // false (nested array value)
|
|
169
|
+
* isUniformObjectArray([{a:1}]); // false (length < 5)
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
function isUniformObjectArray(value) {
|
|
173
|
+
if (!Array.isArray(value) || value.length < MIN_UNIFORM_ARRAY_LENGTH) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const first = value[0];
|
|
177
|
+
if (first === null || typeof first !== 'object' || Array.isArray(first)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const expectedKeys = Object.keys(first)
|
|
181
|
+
.sort()
|
|
182
|
+
.join('\u0000');
|
|
183
|
+
if (expectedKeys === '') {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
for (const item of value) {
|
|
187
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const record = item;
|
|
191
|
+
if (Object.keys(record).sort().join('\u0000') !== expectedKeys) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
for (const v of Object.values(record)) {
|
|
195
|
+
if (v === null)
|
|
196
|
+
continue;
|
|
197
|
+
const t = typeof v;
|
|
198
|
+
if (t !== 'string' && t !== 'number' && t !== 'boolean') {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
74
206
|
/**
|
|
75
207
|
* Shallow check for complex types at depth-1 that structured-clone cannot preserve.
|
|
76
208
|
* Depth-1 is intentional: fast and predictable. For deeply nested complex types,
|
|
@@ -107,9 +239,11 @@ function encodeToTransferable(str) {
|
|
|
107
239
|
* The factory is async because it pre-loads `seroval` during initialization
|
|
108
240
|
* so the returned serializer methods are fully synchronous (no await in hot path).
|
|
109
241
|
*
|
|
110
|
-
* Strategy selection per `serialize()` call:
|
|
111
|
-
*
|
|
112
|
-
*
|
|
242
|
+
* Strategy selection per `serialize()` call (top-down, first match wins):
|
|
243
|
+
* 1. Contains `Date`, `Map`, `Set`, or `RegExp` at depth-1 → `seroval` (full fidelity)
|
|
244
|
+
* 2. Uniform array of plain objects (length ≥ 5, primitive values, identical key set)
|
|
245
|
+
* → `toon` (30–60% size reduction)
|
|
246
|
+
* 3. Otherwise → structured clone (native, zero overhead)
|
|
113
247
|
*
|
|
114
248
|
* Large payloads (> `transferThreshold`, default 100 KiB) are encoded to
|
|
115
249
|
* `ArrayBuffer` and added to `transferables` for zero-copy `postMessage` transfer.
|
|
@@ -130,6 +264,13 @@ async function createAutoSerializer(config) {
|
|
|
130
264
|
catch {
|
|
131
265
|
// seroval not installed — complex types will throw at serialize time with a clear message
|
|
132
266
|
}
|
|
267
|
+
let toon = null;
|
|
268
|
+
try {
|
|
269
|
+
toon = await createToonSerializer();
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// @toon-format/toon not installed — uniform arrays will fall back to structured-clone
|
|
273
|
+
}
|
|
133
274
|
return {
|
|
134
275
|
serialize(data) {
|
|
135
276
|
let payload;
|
|
@@ -140,6 +281,9 @@ async function createAutoSerializer(config) {
|
|
|
140
281
|
}
|
|
141
282
|
payload = sv.serialize(data);
|
|
142
283
|
}
|
|
284
|
+
else if (toon && isUniformObjectArray(data)) {
|
|
285
|
+
payload = toon.serialize(data);
|
|
286
|
+
}
|
|
143
287
|
else {
|
|
144
288
|
payload = structuredCloneSerializer.serialize(data);
|
|
145
289
|
}
|
|
@@ -168,6 +312,13 @@ async function createAutoSerializer(config) {
|
|
|
168
312
|
}
|
|
169
313
|
return sv.deserialize(resolved);
|
|
170
314
|
}
|
|
315
|
+
if (resolved.format === 'toon') {
|
|
316
|
+
if (!toon) {
|
|
317
|
+
throw new Error('@toon-format/toon is required to deserialize this payload. ' +
|
|
318
|
+
'Install it with: npm install @toon-format/toon');
|
|
319
|
+
}
|
|
320
|
+
return toon.deserialize(resolved);
|
|
321
|
+
}
|
|
171
322
|
throw new Error(`Unknown serialization format: '${resolved.format}'`);
|
|
172
323
|
},
|
|
173
324
|
};
|
|
@@ -177,4 +328,4 @@ async function createAutoSerializer(config) {
|
|
|
177
328
|
* Generated bundle index. Do not edit.
|
|
178
329
|
*/
|
|
179
330
|
|
|
180
|
-
export { createAutoSerializer, createSerovalSerializer, structuredCloneSerializer };
|
|
331
|
+
export { MIN_UNIFORM_ARRAY_LENGTH, createAutoSerializer, createSerovalSerializer, createToonSerializer, isUniformObjectArray, structuredCloneSerializer };
|
|
@@ -74,6 +74,41 @@ function isTransferable(value) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Thrown by `createWorkerTransport` when a request is aborted via an external
|
|
79
|
+
* `AbortSignal` passed to `execute(request, { signal })`.
|
|
80
|
+
*
|
|
81
|
+
* Distinct from `WorkerHttpTimeoutError` (which fires when the per-request
|
|
82
|
+
* `timeout` elapses) and from a silent unsubscribe (which sends a `cancel`
|
|
83
|
+
* message but does not surface an error to the subscriber, since RxJS already
|
|
84
|
+
* tore down the stream).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const ac = new AbortController();
|
|
89
|
+
* transport.execute(req, { signal: ac.signal }).subscribe({
|
|
90
|
+
* error: (err) => {
|
|
91
|
+
* if (err instanceof WorkerHttpAbortError) {
|
|
92
|
+
* // user-driven cancellation; usually safe to ignore in UI
|
|
93
|
+
* }
|
|
94
|
+
* },
|
|
95
|
+
* });
|
|
96
|
+
* ac.abort('user navigated away');
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
class WorkerHttpAbortError extends Error {
|
|
100
|
+
name = 'WorkerHttpAbortError';
|
|
101
|
+
/** The reason passed to `AbortController.abort(reason)`, if any. */
|
|
102
|
+
reason;
|
|
103
|
+
constructor(reason) {
|
|
104
|
+
super(reason === undefined
|
|
105
|
+
? 'Worker request aborted'
|
|
106
|
+
: `Worker request aborted: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
107
|
+
this.reason = reason;
|
|
108
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
77
112
|
/**
|
|
78
113
|
* Thrown by `createWorkerTransport` when a request exceeds its configured
|
|
79
114
|
* `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
|
|
@@ -161,14 +196,22 @@ function createWorkerTransport(config) {
|
|
|
161
196
|
roundRobinIndex++;
|
|
162
197
|
return worker;
|
|
163
198
|
}
|
|
164
|
-
function execute(request) {
|
|
199
|
+
function execute(request, options) {
|
|
165
200
|
if (terminated) {
|
|
166
201
|
return new Observable((subscriber) => {
|
|
167
202
|
subscriber.error(new Error('WorkerTransport has been terminated'));
|
|
168
203
|
});
|
|
169
204
|
}
|
|
170
205
|
const requestId = crypto.randomUUID();
|
|
206
|
+
const externalSignal = options?.signal;
|
|
207
|
+
const effectiveTimeout = options?.timeout ?? requestTimeout;
|
|
171
208
|
return new Observable((subscriber) => {
|
|
209
|
+
// Fail-fast: if the caller's signal was already aborted before we even
|
|
210
|
+
// touched a worker, surface it immediately with no postMessage roundtrip.
|
|
211
|
+
if (externalSignal?.aborted) {
|
|
212
|
+
subscriber.error(new WorkerHttpAbortError(externalSignal.reason));
|
|
213
|
+
return () => undefined;
|
|
214
|
+
}
|
|
172
215
|
// Lazy-load polyfill on first request if enabled
|
|
173
216
|
if (streamsPolyfill && !polyfillLoaded) {
|
|
174
217
|
loadStreamsPolyfill().catch((err) => {
|
|
@@ -178,6 +221,7 @@ function createWorkerTransport(config) {
|
|
|
178
221
|
const worker = getOrCreateWorker();
|
|
179
222
|
let settled = false;
|
|
180
223
|
let timeoutHandle;
|
|
224
|
+
let abortListener;
|
|
181
225
|
const cleanup = () => {
|
|
182
226
|
worker.removeEventListener('message', messageHandler);
|
|
183
227
|
worker.removeEventListener('error', errorHandler);
|
|
@@ -185,6 +229,10 @@ function createWorkerTransport(config) {
|
|
|
185
229
|
clearTimeout(timeoutHandle);
|
|
186
230
|
timeoutHandle = undefined;
|
|
187
231
|
}
|
|
232
|
+
if (abortListener && externalSignal) {
|
|
233
|
+
externalSignal.removeEventListener('abort', abortListener);
|
|
234
|
+
abortListener = undefined;
|
|
235
|
+
}
|
|
188
236
|
};
|
|
189
237
|
const messageHandler = (event) => {
|
|
190
238
|
const data = event.data;
|
|
@@ -219,7 +267,7 @@ function createWorkerTransport(config) {
|
|
|
219
267
|
else {
|
|
220
268
|
worker.postMessage({ type: 'request', requestId, payload: request });
|
|
221
269
|
}
|
|
222
|
-
if (
|
|
270
|
+
if (effectiveTimeout > 0 && Number.isFinite(effectiveTimeout)) {
|
|
223
271
|
timeoutHandle = setTimeout(() => {
|
|
224
272
|
if (settled)
|
|
225
273
|
return;
|
|
@@ -228,8 +276,23 @@ function createWorkerTransport(config) {
|
|
|
228
276
|
// Ask the worker to abort any in-flight work for this id. The
|
|
229
277
|
// cancellation fix wires this through to `fetch()`.
|
|
230
278
|
worker.postMessage({ type: 'cancel', requestId });
|
|
231
|
-
subscriber.error(new WorkerHttpTimeoutError(
|
|
232
|
-
},
|
|
279
|
+
subscriber.error(new WorkerHttpTimeoutError(effectiveTimeout));
|
|
280
|
+
}, effectiveTimeout);
|
|
281
|
+
}
|
|
282
|
+
// External AbortSignal: surface a typed abort error and cancel the
|
|
283
|
+
// worker-side fetch. Distinct from a silent unsubscribe (no error) and
|
|
284
|
+
// from a timeout (different error type).
|
|
285
|
+
if (externalSignal) {
|
|
286
|
+
abortListener = () => {
|
|
287
|
+
if (settled)
|
|
288
|
+
return;
|
|
289
|
+
settled = true;
|
|
290
|
+
const reason = externalSignal.reason;
|
|
291
|
+
cleanup();
|
|
292
|
+
worker.postMessage({ type: 'cancel', requestId });
|
|
293
|
+
subscriber.error(new WorkerHttpAbortError(reason));
|
|
294
|
+
};
|
|
295
|
+
externalSignal.addEventListener('abort', abortListener, { once: true });
|
|
233
296
|
}
|
|
234
297
|
// Teardown: send cancel message on unsubscribe
|
|
235
298
|
return () => {
|
|
@@ -284,4 +347,4 @@ function createWorkerTransport(config) {
|
|
|
284
347
|
* Generated bundle index. Do not edit.
|
|
285
348
|
*/
|
|
286
349
|
|
|
287
|
-
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
350
|
+
export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/worker-http",
|
|
3
|
-
"version": "21.
|
|
3
|
+
"version": "21.2.0",
|
|
4
4
|
"description": "Angular HTTP over Web Workers — off-main-thread HTTP pipelines with configurable interceptors, WebCrypto security, and pluggable serialization",
|
|
5
5
|
"schematics": "./schematics/collection.json",
|
|
6
6
|
"exports": {
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"@angular/common": "^21.0.0",
|
|
77
77
|
"@angular/core": "^21.0.0",
|
|
78
78
|
"rxjs": "^7.0.0",
|
|
79
|
-
"seroval": "^1.0.0"
|
|
79
|
+
"seroval": "^1.0.0",
|
|
80
|
+
"@toon-format/toon": "^2.0.0"
|
|
80
81
|
},
|
|
81
82
|
"peerDependenciesMeta": {
|
|
82
83
|
"@angular/common": {
|
|
@@ -88,6 +89,9 @@
|
|
|
88
89
|
"seroval": {
|
|
89
90
|
"optional": true
|
|
90
91
|
},
|
|
92
|
+
"@toon-format/toon": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
91
95
|
"esbuild": {
|
|
92
96
|
"optional": true
|
|
93
97
|
},
|
|
@@ -168,6 +168,31 @@ type WorkerInterceptorSpecsMap = Readonly<Record<string, readonly WorkerIntercep
|
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
170
|
declare const WORKER_TARGET: HttpContextToken<string>;
|
|
171
|
+
/**
|
|
172
|
+
* Per-request HttpContextToken carrying an external `AbortSignal`.
|
|
173
|
+
*
|
|
174
|
+
* When the signal fires, the backend posts a `cancel` message to the worker
|
|
175
|
+
* and the request errors with `WorkerHttpAbortError` (wrapped by Angular into
|
|
176
|
+
* `HttpErrorResponse`).
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const ac = new AbortController();
|
|
181
|
+
* this.http.get('/api/users', { signal: ac.signal });
|
|
182
|
+
* // later
|
|
183
|
+
* ac.abort();
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare const WORKER_HTTP_SIGNAL: HttpContextToken<AbortSignal>;
|
|
187
|
+
/**
|
|
188
|
+
* Per-request HttpContextToken carrying a timeout in milliseconds.
|
|
189
|
+
*
|
|
190
|
+
* When set, overrides the transport-level `requestTimeout` for this single
|
|
191
|
+
* call. `0` or non-finite disables the timeout for this request only.
|
|
192
|
+
*
|
|
193
|
+
* On expiry the request errors with `WorkerHttpTimeoutError`.
|
|
194
|
+
*/
|
|
195
|
+
declare const WORKER_HTTP_TIMEOUT: HttpContextToken<number>;
|
|
171
196
|
/**
|
|
172
197
|
* Optional serializer for crossing the worker boundary.
|
|
173
198
|
* Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
|
|
@@ -422,6 +447,20 @@ declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
|
|
|
422
447
|
interface WorkerRequestOptions {
|
|
423
448
|
/** Target worker ID. Overrides URL-pattern routing for this specific request. */
|
|
424
449
|
worker?: string | null;
|
|
450
|
+
/**
|
|
451
|
+
* External `AbortSignal`. When it fires, the backend posts a `cancel` to
|
|
452
|
+
* the worker and the request errors with `WorkerHttpAbortError` (wrapped in
|
|
453
|
+
* `HttpErrorResponse`). Useful with `AbortController` or
|
|
454
|
+
* `takeUntilDestroyed()`.
|
|
455
|
+
*/
|
|
456
|
+
signal?: AbortSignal;
|
|
457
|
+
/**
|
|
458
|
+
* Per-request timeout in milliseconds. Overrides the transport-level
|
|
459
|
+
* `requestTimeout` for this single call. On expiry the request errors with
|
|
460
|
+
* `WorkerHttpTimeoutError`. `0` or non-finite disables the timeout for this
|
|
461
|
+
* request only.
|
|
462
|
+
*/
|
|
463
|
+
timeout?: number;
|
|
425
464
|
context?: HttpContext;
|
|
426
465
|
headers?: Record<string, string | string[]>;
|
|
427
466
|
params?: Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;
|
|
@@ -494,5 +533,5 @@ declare function matchWorkerRoute(url: string, routes: Array<{
|
|
|
494
533
|
priority?: number;
|
|
495
534
|
}>): string | null;
|
|
496
535
|
|
|
497
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
536
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_SIGNAL, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_HTTP_TIMEOUT, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
498
537
|
export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpErrorEvent, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerHttpRequestEvent, WorkerHttpResponseEvent, WorkerHttpTelemetry, WorkerHttpTelemetryEventBase, WorkerHttpTransportKind, WorkerInterceptorSpecsMap, WorkerRequestOptions, WorkerRoute };
|
|
@@ -63,15 +63,85 @@ declare const structuredCloneSerializer: WorkerSerializer;
|
|
|
63
63
|
*/
|
|
64
64
|
declare function createSerovalSerializer(): Promise<WorkerSerializer>;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Creates a `WorkerSerializer` backed by `@toon-format/toon` (Token-Oriented Object Notation).
|
|
68
|
+
*
|
|
69
|
+
* TOON is a compact, schema-aware encoding of the JSON data model that declares object
|
|
70
|
+
* keys once and emits values as CSV-like rows. It typically reduces size by **30–60%**
|
|
71
|
+
* for uniform arrays of objects (e.g. `User[]`, `Product[]`, paginated lists), with
|
|
72
|
+
* negligible parsing overhead.
|
|
73
|
+
*
|
|
74
|
+
* **When to use it**:
|
|
75
|
+
* - Worker↔main `postMessage` payloads dominated by uniform arrays of objects
|
|
76
|
+
* - Cases where `structuredClone` cost is dominated by repeated key strings
|
|
77
|
+
*
|
|
78
|
+
* **When NOT to use it**:
|
|
79
|
+
* - Payloads containing `Date`, `Map`, `Set`, `RegExp` (use `seroval` instead)
|
|
80
|
+
* - Small / single-object payloads (overhead not justified)
|
|
81
|
+
*
|
|
82
|
+
* The factory is async because it dynamically imports the optional `@toon-format/toon` peer.
|
|
83
|
+
*
|
|
84
|
+
* `@toon-format/toon` must be installed separately:
|
|
85
|
+
* ```
|
|
86
|
+
* npm install @toon-format/toon
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const serializer = await createToonSerializer();
|
|
92
|
+
* const payload = serializer.serialize([
|
|
93
|
+
* { id: 1, name: 'Alice' },
|
|
94
|
+
* { id: 2, name: 'Bob' },
|
|
95
|
+
* { id: 3, name: 'Carol' },
|
|
96
|
+
* { id: 4, name: 'Dave' },
|
|
97
|
+
* { id: 5, name: 'Eve' },
|
|
98
|
+
* ]);
|
|
99
|
+
* worker.postMessage({ payload }, payload.transferables);
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @see https://toonformat.dev
|
|
103
|
+
*/
|
|
104
|
+
declare function createToonSerializer(): Promise<WorkerSerializer>;
|
|
105
|
+
/**
|
|
106
|
+
* Conservative threshold below which TOON's overhead outweighs its size benefit.
|
|
107
|
+
* Auto-serializer keeps shorter arrays on `structured-clone`.
|
|
108
|
+
*
|
|
109
|
+
* Exported for testing and for consumers building custom routing logic.
|
|
110
|
+
*/
|
|
111
|
+
declare const MIN_UNIFORM_ARRAY_LENGTH = 5;
|
|
112
|
+
/**
|
|
113
|
+
* Detects whether a value is a depth-1 uniform array of plain objects with primitive values.
|
|
114
|
+
*
|
|
115
|
+
* Pure function — no side effects. Used by `createAutoSerializer()` to decide
|
|
116
|
+
* whether to route a payload through TOON.
|
|
117
|
+
*
|
|
118
|
+
* Conditions checked (all must pass):
|
|
119
|
+
* 1. Value is an array with `length >= MIN_UNIFORM_ARRAY_LENGTH`
|
|
120
|
+
* 2. Every item is a non-null, non-array plain object
|
|
121
|
+
* 3. Every item has the same set of keys as the first item
|
|
122
|
+
* 4. Every value across all items is a primitive (string, number, boolean, null)
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* isUniformObjectArray([{a:1},{a:2},{a:3},{a:4},{a:5}]); // true
|
|
127
|
+
* isUniformObjectArray([{a:1},{b:2}]); // false (heterogeneous keys)
|
|
128
|
+
* isUniformObjectArray([{a:[1,2]},{a:[3,4]}]); // false (nested array value)
|
|
129
|
+
* isUniformObjectArray([{a:1}]); // false (length < 5)
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function isUniformObjectArray(value: unknown): boolean;
|
|
133
|
+
|
|
66
134
|
/**
|
|
67
135
|
* Creates an auto-detecting `WorkerSerializer` that picks the best strategy per payload.
|
|
68
136
|
*
|
|
69
137
|
* The factory is async because it pre-loads `seroval` during initialization
|
|
70
138
|
* so the returned serializer methods are fully synchronous (no await in hot path).
|
|
71
139
|
*
|
|
72
|
-
* Strategy selection per `serialize()` call:
|
|
73
|
-
*
|
|
74
|
-
*
|
|
140
|
+
* Strategy selection per `serialize()` call (top-down, first match wins):
|
|
141
|
+
* 1. Contains `Date`, `Map`, `Set`, or `RegExp` at depth-1 → `seroval` (full fidelity)
|
|
142
|
+
* 2. Uniform array of plain objects (length ≥ 5, primitive values, identical key set)
|
|
143
|
+
* → `toon` (30–60% size reduction)
|
|
144
|
+
* 3. Otherwise → structured clone (native, zero overhead)
|
|
75
145
|
*
|
|
76
146
|
* Large payloads (> `transferThreshold`, default 100 KiB) are encoded to
|
|
77
147
|
* `ArrayBuffer` and added to `transferables` for zero-copy `postMessage` transfer.
|
|
@@ -85,5 +155,5 @@ declare function createSerovalSerializer(): Promise<WorkerSerializer>;
|
|
|
85
155
|
*/
|
|
86
156
|
declare function createAutoSerializer(config?: AutoSerializerConfig): Promise<WorkerSerializer>;
|
|
87
157
|
|
|
88
|
-
export { createAutoSerializer, createSerovalSerializer, structuredCloneSerializer };
|
|
158
|
+
export { MIN_UNIFORM_ARRAY_LENGTH, createAutoSerializer, createSerovalSerializer, createToonSerializer, isUniformObjectArray, structuredCloneSerializer };
|
|
89
159
|
export type { AutoSerializerConfig, SerializedPayload, SerializerStrategy, WorkerSerializer };
|
|
@@ -109,13 +109,26 @@ interface WorkerErrorResponse {
|
|
|
109
109
|
statusText?: string;
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Per-request options accepted by `WorkerTransport.execute()`.
|
|
114
|
+
*
|
|
115
|
+
* - `signal` — external `AbortSignal`. When it fires, the transport posts a
|
|
116
|
+
* `cancel` message to the worker and rejects the Observable with
|
|
117
|
+
* `WorkerHttpAbortError`.
|
|
118
|
+
* - `timeout` — overrides the transport-level `requestTimeout` for this single
|
|
119
|
+
* call. `0` or non-finite disables the timeout for this request.
|
|
120
|
+
*/
|
|
121
|
+
interface WorkerExecuteOptions {
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
timeout?: number;
|
|
124
|
+
}
|
|
112
125
|
/**
|
|
113
126
|
* Typed transport interface for communicating with a web worker.
|
|
114
127
|
* Observable-based: unsubscribing sends a cancel message to the worker.
|
|
115
128
|
*/
|
|
116
129
|
interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
|
|
117
130
|
/** Send a request to the worker and get an Observable response */
|
|
118
|
-
execute(request: TRequest): Observable<TResponse>;
|
|
131
|
+
execute(request: TRequest, options?: WorkerExecuteOptions): Observable<TResponse>;
|
|
119
132
|
/** Terminate all workers and release resources */
|
|
120
133
|
terminate(): void;
|
|
121
134
|
/** Whether the transport has active workers */
|
|
@@ -173,6 +186,35 @@ declare class WorkerHttpTimeoutError extends Error {
|
|
|
173
186
|
constructor(timeoutMs: number);
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Thrown by `createWorkerTransport` when a request is aborted via an external
|
|
191
|
+
* `AbortSignal` passed to `execute(request, { signal })`.
|
|
192
|
+
*
|
|
193
|
+
* Distinct from `WorkerHttpTimeoutError` (which fires when the per-request
|
|
194
|
+
* `timeout` elapses) and from a silent unsubscribe (which sends a `cancel`
|
|
195
|
+
* message but does not surface an error to the subscriber, since RxJS already
|
|
196
|
+
* tore down the stream).
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const ac = new AbortController();
|
|
201
|
+
* transport.execute(req, { signal: ac.signal }).subscribe({
|
|
202
|
+
* error: (err) => {
|
|
203
|
+
* if (err instanceof WorkerHttpAbortError) {
|
|
204
|
+
* // user-driven cancellation; usually safe to ignore in UI
|
|
205
|
+
* }
|
|
206
|
+
* },
|
|
207
|
+
* });
|
|
208
|
+
* ac.abort('user navigated away');
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
declare class WorkerHttpAbortError extends Error {
|
|
212
|
+
readonly name = "WorkerHttpAbortError";
|
|
213
|
+
/** The reason passed to `AbortController.abort(reason)`, if any. */
|
|
214
|
+
readonly reason: unknown;
|
|
215
|
+
constructor(reason?: unknown);
|
|
216
|
+
}
|
|
217
|
+
|
|
176
218
|
/**
|
|
177
219
|
* Scans a payload one level deep and collects every `Transferable` instance
|
|
178
220
|
* (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
|
|
@@ -193,5 +235,5 @@ declare class WorkerHttpTimeoutError extends Error {
|
|
|
193
235
|
*/
|
|
194
236
|
declare function detectTransferables(payload: unknown): Transferable[];
|
|
195
237
|
|
|
196
|
-
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
197
|
-
export type { WorkerErrorResponse, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
|
|
238
|
+
export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
239
|
+
export type { WorkerErrorResponse, WorkerExecuteOptions, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
|