@angular-helpers/worker-http 21.1.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
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
|
|
|
@@ -146,12 +149,17 @@ transport.execute(request).subscribe({
|
|
|
146
149
|
});
|
|
147
150
|
```
|
|
148
151
|
|
|
152
|
+
</details>
|
|
153
|
+
|
|
149
154
|
---
|
|
150
155
|
|
|
151
156
|
### `/interceptors` — Worker-side pipeline
|
|
152
157
|
|
|
153
158
|
Pure-function interceptors that run inside the worker. No Angular DI, no DOM access — just `(req, next) => Promise<response>`.
|
|
154
159
|
|
|
160
|
+
<details>
|
|
161
|
+
<summary><strong>Setup, built-in interceptors, and custom interceptors</strong></summary>
|
|
162
|
+
|
|
155
163
|
#### Setup in your worker file
|
|
156
164
|
|
|
157
165
|
```typescript
|
|
@@ -283,11 +291,22 @@ export const authTokenInterceptor: WorkerInterceptorFn = (req, next) => {
|
|
|
283
291
|
};
|
|
284
292
|
```
|
|
285
293
|
|
|
294
|
+
</details>
|
|
295
|
+
|
|
286
296
|
---
|
|
287
297
|
|
|
288
298
|
### `/serializer` — Pluggable serialization
|
|
289
299
|
|
|
290
|
-
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>
|
|
291
310
|
|
|
292
311
|
#### `structuredCloneSerializer` (default)
|
|
293
312
|
|
|
@@ -316,14 +335,49 @@ const original = serializer.deserialize(payload);
|
|
|
316
335
|
// original.tags instanceof Set → true
|
|
317
336
|
```
|
|
318
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
|
+
|
|
319
370
|
#### `createAutoSerializer()` — Smart auto-detection
|
|
320
371
|
|
|
321
|
-
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.
|
|
322
373
|
|
|
323
|
-
**Detection logic (depth-1):**
|
|
374
|
+
**Detection logic (depth-1, top-down, first match wins):**
|
|
324
375
|
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
327
381
|
|
|
328
382
|
Payloads larger than `transferThreshold` (default: 100 KiB) are encoded to `ArrayBuffer` and transferred zero-copy.
|
|
329
383
|
|
|
@@ -346,12 +400,17 @@ auto.serialize(hugeDataset); // transferables: [ArrayBuffer]
|
|
|
346
400
|
|
|
347
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.
|
|
348
402
|
|
|
403
|
+
</details>
|
|
404
|
+
|
|
349
405
|
---
|
|
350
406
|
|
|
351
407
|
### `/crypto` — WebCrypto primitives
|
|
352
408
|
|
|
353
409
|
Standalone WebCrypto utilities. Useful in both workers and the main thread, but workers provide memory isolation for key material.
|
|
354
410
|
|
|
411
|
+
<details>
|
|
412
|
+
<summary><strong>HMAC, AES, hashing examples</strong></summary>
|
|
413
|
+
|
|
355
414
|
#### `createHmacSigner(config)`
|
|
356
415
|
|
|
357
416
|
```typescript
|
|
@@ -386,12 +445,17 @@ const hasher = createContentHasher();
|
|
|
386
445
|
const hash = await hasher.hash('SHA-256', data); // → hex string
|
|
387
446
|
```
|
|
388
447
|
|
|
448
|
+
</details>
|
|
449
|
+
|
|
389
450
|
---
|
|
390
451
|
|
|
391
452
|
### `/backend` — Angular `HttpBackend` replacement
|
|
392
453
|
|
|
393
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.
|
|
394
455
|
|
|
456
|
+
<details>
|
|
457
|
+
<summary><strong>Configuration, providers, and consumer code</strong></summary>
|
|
458
|
+
|
|
395
459
|
```typescript
|
|
396
460
|
// app.config.ts
|
|
397
461
|
import {
|
|
@@ -480,13 +544,18 @@ manual control.
|
|
|
480
544
|
- `matchWorkerRoute(url, routes)` — pure utility to test routing rules
|
|
481
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.
|
|
482
546
|
|
|
547
|
+
</details>
|
|
548
|
+
|
|
483
549
|
---
|
|
484
550
|
|
|
485
551
|
## Telemetry
|
|
486
552
|
|
|
487
|
-
Main-thread extension point for APM / metrics. `withTelemetry(...)` registers
|
|
488
|
-
|
|
489
|
-
|
|
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:
|
|
490
559
|
|
|
491
560
|
- **`onRequest`** — after worker resolution, before dispatch
|
|
492
561
|
- **`onResponse`** — when a successful response is emitted
|
|
@@ -553,12 +622,17 @@ interface WorkerHttpTelemetryEventBase {
|
|
|
553
622
|
// onError adds: kind: 'error', error, durationMs
|
|
554
623
|
```
|
|
555
624
|
|
|
625
|
+
</details>
|
|
626
|
+
|
|
556
627
|
---
|
|
557
628
|
|
|
558
629
|
### `/esbuild-plugin` — Interceptor auto-bundling
|
|
559
630
|
|
|
560
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.
|
|
561
632
|
|
|
633
|
+
<details>
|
|
634
|
+
<summary><strong>Plugin options and example</strong></summary>
|
|
635
|
+
|
|
562
636
|
```typescript
|
|
563
637
|
// esbuild.config.ts
|
|
564
638
|
import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
@@ -585,12 +659,17 @@ export default {
|
|
|
585
659
|
|
|
586
660
|
Discovered interceptors are merged with explicit ones. Test files (`.spec.ts`, `.test.ts`) are automatically excluded.
|
|
587
661
|
|
|
662
|
+
</details>
|
|
663
|
+
|
|
588
664
|
---
|
|
589
665
|
|
|
590
666
|
### `/streams-polyfill` — Safari transferable streams
|
|
591
667
|
|
|
592
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.
|
|
593
669
|
|
|
670
|
+
<details>
|
|
671
|
+
<summary><strong>Setup and bundle impact</strong></summary>
|
|
672
|
+
|
|
594
673
|
```typescript
|
|
595
674
|
// Enable in your app config (main thread)
|
|
596
675
|
import { withWorkerStreamsPolyfill } from '@angular-helpers/worker-http/backend';
|
|
@@ -608,12 +687,16 @@ provideWorkerHttpClient(
|
|
|
608
687
|
|
|
609
688
|
**Bundle impact:** Zero for modern browsers. The polyfill is lazy-loaded only on affected Safari versions when streams are actually used.
|
|
610
689
|
|
|
690
|
+
</details>
|
|
691
|
+
|
|
611
692
|
---
|
|
612
693
|
|
|
613
694
|
## SSR + hydration
|
|
614
695
|
|
|
615
|
-
Worker HTTP integrates transparently with Angular SSR.
|
|
616
|
-
|
|
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>
|
|
617
700
|
|
|
618
701
|
**1. Workers do not exist on the server.**
|
|
619
702
|
During SSR, `typeof Worker === 'undefined'`. `WorkerHttpBackend` detects this
|
|
@@ -663,6 +746,8 @@ To customise which headers are captured or to cache `POST` requests, pass
|
|
|
663
746
|
`withHttpTransferCacheOptions(...)` to `provideClientHydration()` — both are
|
|
664
747
|
re-exported from `@angular/platform-browser`.
|
|
665
748
|
|
|
749
|
+
</details>
|
|
750
|
+
|
|
666
751
|
---
|
|
667
752
|
|
|
668
753
|
## Design principles
|
|
@@ -677,13 +762,14 @@ re-exported from `@angular/platform-browser`.
|
|
|
677
762
|
|
|
678
763
|
## Serialization strategy decision guide
|
|
679
764
|
|
|
680
|
-
| Payload type
|
|
681
|
-
|
|
|
682
|
-
| Simple objects, arrays of primitives
|
|
683
|
-
|
|
|
684
|
-
|
|
|
685
|
-
|
|
|
686
|
-
|
|
|
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 |
|
|
687
773
|
|
|
688
774
|
---
|
|
689
775
|
|
|
@@ -701,9 +787,12 @@ Server-Side Rendering (SSR) is supported via automatic fallback to the main thre
|
|
|
701
787
|
|
|
702
788
|
## Benchmarks
|
|
703
789
|
|
|
704
|
-
A reproducible benchmark suite ships with the demo app at
|
|
705
|
-
|
|
706
|
-
|
|
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:
|
|
707
796
|
|
|
708
797
|
| Mode | What it measures |
|
|
709
798
|
| --------------- | ---------------------------------------------------------- |
|
|
@@ -739,6 +828,8 @@ npm start
|
|
|
739
828
|
Numbers vary by hardware, browser, and current system load — always run a scenario several times
|
|
740
829
|
and watch the trend, not a single value.
|
|
741
830
|
|
|
831
|
+
</details>
|
|
832
|
+
|
|
742
833
|
---
|
|
743
834
|
|
|
744
835
|
## Related documentation
|
|
@@ -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 };
|
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
|
},
|
|
@@ -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 };
|