@angular-helpers/worker-http 0.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 ADDED
@@ -0,0 +1,399 @@
1
+ [Leer en Español](./README.es.md)
2
+
3
+ # @angular-helpers/worker-http
4
+
5
+ Move HTTP requests off the main thread. A composable toolkit for Angular that runs `fetch()` inside Web Workers, protecting your UI from network latency while adding security primitives (HMAC signing, content integrity, rate limiting) that live entirely in the worker's isolated scope.
6
+
7
+ ---
8
+
9
+ ## Why?
10
+
11
+ Every `HttpClient` request blocks a shared thread budget. When a slow API stalls, your animations stutter and user interactions queue up. Web Workers solve this at the architecture level: they run on a separate OS thread, so a 2-second network call costs the main thread nothing.
12
+
13
+ On top of that, workers provide a natural isolation boundary for security-sensitive logic: HMAC keys, request signing, and content verification never touch the main thread's memory.
14
+
15
+ ---
16
+
17
+ ## Package map
18
+
19
+ | Entry point | Description | Status |
20
+ | ------------------------------------------- | ---------------------------------------------------------------- | -------------- |
21
+ | `@angular-helpers/worker-http/transport` | Typed RPC bridge, round-robin pool, cancellation | ✅ Available |
22
+ | `@angular-helpers/worker-http/serializer` | Pluggable serialization (structured clone, seroval, auto-detect) | ✅ Available |
23
+ | `@angular-helpers/worker-http/interceptors` | Pure-function interceptor pipeline for workers | ✅ Available |
24
+ | `@angular-helpers/worker-http/crypto` | WebCrypto primitives (HMAC, AES-GCM, SHA hashing) | ✅ Available |
25
+ | `@angular-helpers/worker-http/backend` | Angular `HttpBackend` replacement — `provideWorkerHttpClient()` | 🔧 In progress |
26
+
27
+ ---
28
+
29
+ ## Architecture at a glance
30
+
31
+ ```
32
+ Main thread Web Worker
33
+ ──────────────────────────── ──────────────────────────────────
34
+ Angular HttpClient createWorkerPipeline([
35
+ └─ WorkerHttpBackend loggingInterceptor(),
36
+ └─ WorkerTransport retryInterceptor({ maxRetries: 3 }),
37
+ └─ postMessage ───────► hmacSigningInterceptor({ keyMaterial }),
38
+ ◄─────── cacheInterceptor({ ttl: 60000 }),
39
+ transfer ])
40
+ (zero-copy)
41
+ fetch() ──► API Server
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Entry points
47
+
48
+ ### `/transport` — Typed RPC bridge
49
+
50
+ 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.
51
+
52
+ ```typescript
53
+ import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
54
+
55
+ const transport = createWorkerTransport({
56
+ workerUrl: new URL('./workers/api.worker', import.meta.url),
57
+ maxInstances: 2,
58
+ });
59
+
60
+ // Returns Observable — unsubscribe sends a cancel message to the worker
61
+ const response$ = transport.execute(request);
62
+
63
+ // Clean up
64
+ transport.terminate();
65
+ ```
66
+
67
+ **Features:**
68
+
69
+ - Round-robin pool (`maxInstances`) for parallel request handling
70
+ - Request cancellation via `AbortController` in the worker
71
+ - Automatic `Transferable` detection for zero-copy `ArrayBuffer` transfer
72
+ - Lazy worker instantiation — no worker created until first request
73
+
74
+ ---
75
+
76
+ ### `/interceptors` — Worker-side pipeline
77
+
78
+ Pure-function interceptors that run inside the worker. No Angular DI, no DOM access — just `(req, next) => Promise<response>`.
79
+
80
+ #### Setup in your worker file
81
+
82
+ ```typescript
83
+ // workers/secure.worker.ts
84
+ import { createWorkerPipeline } from '@angular-helpers/worker-http/interceptors';
85
+ import {
86
+ hmacSigningInterceptor,
87
+ retryInterceptor,
88
+ loggingInterceptor,
89
+ } from '@angular-helpers/worker-http/interceptors';
90
+
91
+ createWorkerPipeline([
92
+ loggingInterceptor(),
93
+ retryInterceptor({ maxRetries: 3, initialDelay: 500 }),
94
+ hmacSigningInterceptor({
95
+ keyMaterial: new TextEncoder().encode(self.HMAC_SECRET),
96
+ headerName: 'X-HMAC-Signature',
97
+ }),
98
+ ]);
99
+ ```
100
+
101
+ #### Available interceptors
102
+
103
+ ##### `retryInterceptor(config?)`
104
+
105
+ Retries failed requests with exponential backoff. Respects the `Retry-After` header.
106
+
107
+ ```typescript
108
+ retryInterceptor({
109
+ maxRetries: 3, // default: 3 (0 = disabled, returns response as-is)
110
+ initialDelay: 1000, // ms, default: 1000
111
+ backoffMultiplier: 2, // default: 2 → delays: 1s, 2s, 4s
112
+ retryStatusCodes: [408, 429, 500, 502, 503, 504], // default list
113
+ retryOnNetworkError: true, // retry on fetch() throws (default: true)
114
+ });
115
+ ```
116
+
117
+ ##### `cacheInterceptor(config?)`
118
+
119
+ In-worker response cache. State is per-factory-instance and resets when the worker is terminated.
120
+
121
+ ```typescript
122
+ cacheInterceptor({
123
+ ttl: 60000, // ms, default: 60000 (1 min). 0 = never cache
124
+ maxEntries: 100, // default: 100. Eviction: FIFO (insertion order)
125
+ methods: ['GET'], // default: ['GET']
126
+ });
127
+ ```
128
+
129
+ ##### `hmacSigningInterceptor(config)`
130
+
131
+ Signs outgoing requests with HMAC-SHA256/384/512 via the native WebCrypto API. The `CryptoKey` is imported once per factory instance and reused across requests.
132
+
133
+ ```typescript
134
+ hmacSigningInterceptor({
135
+ keyMaterial: rawKeyBytes, // ArrayBuffer | Uint8Array
136
+ algorithm: 'SHA-256', // default: 'SHA-256'
137
+ headerName: 'X-HMAC-Signature', // default: 'X-HMAC-Signature'
138
+ payloadBuilder: (
139
+ req, // optional: what to sign
140
+ ) => `${req.method}:${req.url}:${JSON.stringify(req.body)}`,
141
+ });
142
+ ```
143
+
144
+ ##### `loggingInterceptor(config?)`
145
+
146
+ Logs request/response to `console.log` (or a custom logger). Logger exceptions are swallowed — a logging failure never interrupts the pipeline.
147
+
148
+ ```typescript
149
+ loggingInterceptor({
150
+ logger: (msg, data) => myMonitoring.log(msg, data), // default: console.log
151
+ includeHeaders: false, // default: false
152
+ });
153
+ // Output: [worker] → GET https://api.example.com (0ms)
154
+ // [worker] ← 200 https://api.example.com (47ms)
155
+ ```
156
+
157
+ ##### `rateLimitInterceptor(config?)`
158
+
159
+ Client-side sliding-window rate limiter. Throws `{ status: 429 }` when the limit is exceeded.
160
+
161
+ ```typescript
162
+ rateLimitInterceptor({
163
+ maxRequests: 100, // default: 100
164
+ windowMs: 60000, // default: 60000 (1 min)
165
+ });
166
+ ```
167
+
168
+ ##### `contentIntegrityInterceptor(config?)`
169
+
170
+ Verifies the SHA-256 hash of the response body against a server-provided header. Useful when the server signs responses.
171
+
172
+ ```typescript
173
+ contentIntegrityInterceptor({
174
+ algorithm: 'SHA-256', // default: 'SHA-256'
175
+ headerName: 'X-Content-Hash', // default: 'X-Content-Hash'
176
+ requireHash: false, // default: false. true = throw if header absent
177
+ });
178
+ ```
179
+
180
+ ##### `composeInterceptors(...fns)`
181
+
182
+ Composes multiple interceptors into a single `WorkerInterceptorFn`. Interceptors run left-to-right.
183
+
184
+ ```typescript
185
+ import { composeInterceptors } from '@angular-helpers/worker-http/interceptors';
186
+
187
+ const securityLayer = composeInterceptors(
188
+ rateLimitInterceptor({ maxRequests: 50 }),
189
+ hmacSigningInterceptor({ keyMaterial }),
190
+ contentIntegrityInterceptor({ requireHash: true }),
191
+ );
192
+
193
+ createWorkerPipeline([loggingInterceptor(), securityLayer]);
194
+ ```
195
+
196
+ #### Custom interceptors
197
+
198
+ Implement `WorkerInterceptorFn` — a pure function with no external dependencies:
199
+
200
+ ```typescript
201
+ import type { WorkerInterceptorFn } from '@angular-helpers/worker-http/interceptors';
202
+
203
+ export const authTokenInterceptor: WorkerInterceptorFn = (req, next) => {
204
+ return next({
205
+ ...req,
206
+ headers: { ...req.headers, Authorization: [`Bearer ${TOKEN}`] },
207
+ });
208
+ };
209
+ ```
210
+
211
+ ---
212
+
213
+ ### `/serializer` — Pluggable serialization
214
+
215
+ Handles the `postMessage` serialization boundary. Structured clone is zero-overhead but loses `Date`, `Map`, `Set` fidelity. `seroval` preserves full type fidelity. The auto-serializer picks the best strategy per payload.
216
+
217
+ #### `structuredCloneSerializer` (default)
218
+
219
+ Zero-overhead. Uses the browser's native structured clone algorithm. Best for simple objects and primitives.
220
+
221
+ ```typescript
222
+ import { structuredCloneSerializer } from '@angular-helpers/worker-http/serializer';
223
+ // No setup needed — this is the default when no serializer is configured.
224
+ ```
225
+
226
+ #### `createSerovalSerializer()` — Full type fidelity
227
+
228
+ Requires `seroval` as an optional peer dependency (`npm install seroval`).
229
+
230
+ Supports: `Date`, `Map`, `Set`, `BigInt`, `RegExp`, circular references, and more.
231
+
232
+ ```typescript
233
+ import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
234
+
235
+ // Factory is async — pre-loads the seroval module
236
+ const serializer = await createSerovalSerializer();
237
+
238
+ const payload = serializer.serialize({ date: new Date(), tags: new Set(['a', 'b']) });
239
+ const original = serializer.deserialize(payload);
240
+ // original.date instanceof Date → true
241
+ // original.tags instanceof Set → true
242
+ ```
243
+
244
+ #### `createAutoSerializer()` — Smart auto-detection
245
+
246
+ Automatically picks the best strategy per payload. The factory is async (pre-loads `seroval` during initialization), but the returned serializer is fully synchronous.
247
+
248
+ **Detection logic (depth-1):**
249
+
250
+ - Contains `Date`, `Map`, `Set`, or `RegExp` at the top level or as direct array/object values → `seroval`
251
+ - Otherwise → structured clone (zero overhead)
252
+
253
+ Payloads larger than `transferThreshold` (default: 100 KiB) are encoded to `ArrayBuffer` and transferred zero-copy.
254
+
255
+ ```typescript
256
+ import { createAutoSerializer } from '@angular-helpers/worker-http/serializer';
257
+
258
+ const auto = await createAutoSerializer({
259
+ transferThreshold: 102400, // bytes, default: 100 KiB
260
+ });
261
+
262
+ // Simple object → structured-clone (no overhead)
263
+ auto.serialize({ id: 1, name: 'Alice' }); // format: 'structured-clone'
264
+
265
+ // Object with Date → seroval (type fidelity)
266
+ auto.serialize({ createdAt: new Date() }); // format: 'seroval'
267
+
268
+ // Large payload → ArrayBuffer transfer (zero-copy)
269
+ auto.serialize(hugeDataset); // transferables: [ArrayBuffer]
270
+ ```
271
+
272
+ > **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.
273
+
274
+ ---
275
+
276
+ ### `/crypto` — WebCrypto primitives
277
+
278
+ Standalone WebCrypto utilities. Useful in both workers and the main thread, but workers provide memory isolation for key material.
279
+
280
+ #### `createHmacSigner(config)`
281
+
282
+ ```typescript
283
+ import { createHmacSigner } from '@angular-helpers/worker-http/crypto';
284
+
285
+ const signer = await createHmacSigner({
286
+ keyMaterial: rawKeyBytes,
287
+ algorithm: 'SHA-256', // default
288
+ });
289
+
290
+ const signature = await signer.sign('GET:/api/users:');
291
+ const isValid = await signer.verify('GET:/api/users:', signature);
292
+ ```
293
+
294
+ #### `createAesEncryptor(config)`
295
+
296
+ ```typescript
297
+ import { createAesEncryptor } from '@angular-helpers/worker-http/crypto';
298
+
299
+ const encryptor = await createAesEncryptor({ keyLength: 256 });
300
+
301
+ const { ciphertext, iv } = await encryptor.encrypt('sensitive data');
302
+ const plaintext = await encryptor.decrypt(ciphertext, iv);
303
+ ```
304
+
305
+ #### `createContentHasher()`
306
+
307
+ ```typescript
308
+ import { createContentHasher } from '@angular-helpers/worker-http/crypto';
309
+
310
+ const hasher = createContentHasher();
311
+ const hash = await hasher.hash('SHA-256', data); // → hex string
312
+ ```
313
+
314
+ ---
315
+
316
+ ### `/backend` — Angular `HttpBackend` replacement (in progress)
317
+
318
+ > 🔧 **This entry point is currently in development.**
319
+
320
+ The goal is a drop-in replacement for Angular's `HttpBackend` that transparently routes requests to the appropriate worker.
321
+
322
+ **Planned API:**
323
+
324
+ ```typescript
325
+ // app.config.ts
326
+ import {
327
+ provideWorkerHttpClient,
328
+ withWorkerConfigs,
329
+ withWorkerRoutes,
330
+ withWorkerFallback,
331
+ } from '@angular-helpers/worker-http/backend';
332
+
333
+ bootstrapApplication(AppComponent, {
334
+ providers: [
335
+ provideWorkerHttpClient(
336
+ withWorkerConfigs([
337
+ {
338
+ id: 'secure',
339
+ workerUrl: new URL('./workers/secure.worker', import.meta.url),
340
+ maxInstances: 2,
341
+ },
342
+ ]),
343
+ withWorkerRoutes([{ pattern: /\/api\/secure\//, worker: 'secure', priority: 10 }]),
344
+ withWorkerFallback('main-thread'), // SSR-safe fallback
345
+ ),
346
+ ],
347
+ });
348
+
349
+ // data.service.ts — identical to normal HttpClient usage
350
+ export class DataService {
351
+ private http = inject(HttpClient);
352
+
353
+ getReports() {
354
+ return this.http.get<Report[]>('/api/secure/reports');
355
+ }
356
+ }
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Design principles
362
+
363
+ - **Zero main-thread cost** — `fetch()` runs entirely in the worker; the main thread only handles the `postMessage` handoff
364
+ - **Black box** — developers use `HttpClient` as normal; workers are an implementation detail
365
+ - **Pure-function interceptors** — no Angular DI, no DOM, no closures over mutable state; fully testable without a browser
366
+ - **Composable** — use only the sub-packages you need; each is independently useful
367
+ - **SSR-safe** — `typeof Worker === 'undefined'` falls back to a standard `FetchBackend` automatically
368
+
369
+ ---
370
+
371
+ ## Serialization strategy decision guide
372
+
373
+ | Payload type | Recommended serializer | Reason |
374
+ | ------------------------------------ | -------------------------------------- | --------------------------- |
375
+ | Simple objects, arrays of primitives | `structuredCloneSerializer` (default) | Zero overhead |
376
+ | Objects with `Date`, `Map`, `Set` | `createSerovalSerializer()` | Full type fidelity |
377
+ | Unknown payload shape | `createAutoSerializer()` | Depth-1 auto-detect |
378
+ | Large arrays (> 100 KiB) | `createAutoSerializer()` | Auto ArrayBuffer transfer |
379
+ | Deeply nested complex types | `createSerovalSerializer()` explicitly | Auto-detect is depth-1 only |
380
+
381
+ ---
382
+
383
+ ## Browser support
384
+
385
+ All features require a browser that supports:
386
+
387
+ - **Web Workers** — all modern browsers (Chrome 4+, Firefox 3.5+, Safari 4+)
388
+ - **WebCrypto (`crypto.subtle`)** — requires HTTPS (or `localhost`)
389
+ - **`Transferable` objects** — `ArrayBuffer` transfer supported in all modern browsers
390
+
391
+ Server-Side Rendering (SSR) is supported via automatic fallback to the main thread.
392
+
393
+ ---
394
+
395
+ ## Related documentation
396
+
397
+ - [Architecture & Feasibility Study](../../docs/sdd-angular-http-web-workers.md)
398
+ - [Deep Research: Serialization, Transferables, Benchmarks](../../docs/research/http-worker-deep-research.md)
399
+ - [Product Breakdown](../../docs/research/http-worker-product-breakdown.md)
@@ -0,0 +1,3 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
@@ -0,0 +1,146 @@
1
+ function toArrayBuffer$2(data) {
2
+ if (typeof data === 'string') {
3
+ return new TextEncoder().encode(data).buffer;
4
+ }
5
+ if (data instanceof Uint8Array) {
6
+ return data.buffer;
7
+ }
8
+ return data;
9
+ }
10
+ function arrayBufferToHex$1(buffer) {
11
+ return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
12
+ }
13
+ /**
14
+ * Creates an HMAC signer using the Web Crypto API.
15
+ *
16
+ * Works in both main thread and web workers.
17
+ * Requires a secure context (HTTPS).
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const signer = await createHmacSigner({
22
+ * keyMaterial: new TextEncoder().encode('my-secret-key'),
23
+ * algorithm: 'SHA-256',
24
+ * });
25
+ *
26
+ * const signature = await signer.signHex('request-payload');
27
+ * const isValid = await signer.verify('request-payload', signatureBuffer);
28
+ * ```
29
+ */
30
+ async function createHmacSigner(config) {
31
+ const algorithm = config.algorithm ?? 'SHA-256';
32
+ const keyMaterial = config.keyMaterial instanceof Uint8Array
33
+ ? new Uint8Array(config.keyMaterial.buffer.slice(0))
34
+ : new Uint8Array(config.keyMaterial);
35
+ const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: 'HMAC', hash: algorithm }, false, ['sign', 'verify']);
36
+ return {
37
+ async sign(data) {
38
+ return crypto.subtle.sign('HMAC', cryptoKey, toArrayBuffer$2(data));
39
+ },
40
+ async signHex(data) {
41
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, toArrayBuffer$2(data));
42
+ return arrayBufferToHex$1(signature);
43
+ },
44
+ async verify(data, signature) {
45
+ return crypto.subtle.verify('HMAC', cryptoKey, signature, toArrayBuffer$2(data));
46
+ },
47
+ };
48
+ }
49
+
50
+ function toArrayBuffer$1(data) {
51
+ if (typeof data === 'string') {
52
+ return new TextEncoder().encode(data).buffer;
53
+ }
54
+ if (data instanceof Uint8Array) {
55
+ return data.buffer;
56
+ }
57
+ return data;
58
+ }
59
+ /**
60
+ * Creates an AES encryptor using the Web Crypto API.
61
+ *
62
+ * Works in both main thread and web workers.
63
+ * Requires a secure context (HTTPS).
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const encryptor = await createAesEncryptor({
68
+ * keyMaterial: new TextEncoder().encode('32-byte-secret-key-for-aes-256!'),
69
+ * algorithm: 'AES-GCM',
70
+ * });
71
+ *
72
+ * const encrypted = await encryptor.encrypt('sensitive data');
73
+ * const decrypted = await encryptor.decrypt(encrypted);
74
+ * ```
75
+ */
76
+ async function createAesEncryptor(config) {
77
+ const algorithm = config.algorithm ?? 'AES-GCM';
78
+ const keyLength = config.keyLength ?? 256;
79
+ const keyMaterial = config.keyMaterial instanceof Uint8Array
80
+ ? new Uint8Array(config.keyMaterial.buffer.slice(0))
81
+ : new Uint8Array(config.keyMaterial);
82
+ const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: algorithm, length: keyLength }, false, ['encrypt', 'decrypt']);
83
+ return {
84
+ async encrypt(data) {
85
+ const iv = crypto.getRandomValues(new Uint8Array(12));
86
+ const params = algorithm === 'AES-GCM'
87
+ ? { name: algorithm, iv }
88
+ : algorithm === 'AES-CBC'
89
+ ? { name: algorithm, iv }
90
+ : { name: algorithm, counter: iv, length: 64 };
91
+ const ciphertext = await crypto.subtle.encrypt(params, cryptoKey, toArrayBuffer$1(data));
92
+ return { ciphertext, iv, algorithm };
93
+ },
94
+ async decrypt(payload) {
95
+ const iv = payload.iv;
96
+ const params = payload.algorithm === 'AES-GCM'
97
+ ? { name: payload.algorithm, iv }
98
+ : payload.algorithm === 'AES-CBC'
99
+ ? { name: payload.algorithm, iv }
100
+ : { name: payload.algorithm, counter: iv, length: 64 };
101
+ return crypto.subtle.decrypt(params, cryptoKey, payload.ciphertext);
102
+ },
103
+ };
104
+ }
105
+
106
+ function toArrayBuffer(data) {
107
+ if (typeof data === 'string') {
108
+ return new TextEncoder().encode(data).buffer;
109
+ }
110
+ if (data instanceof Uint8Array) {
111
+ return data.buffer;
112
+ }
113
+ return data;
114
+ }
115
+ function arrayBufferToHex(buffer) {
116
+ return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
117
+ }
118
+ /**
119
+ * Creates a content hasher using the Web Crypto API.
120
+ *
121
+ * Works in both main thread and web workers.
122
+ * Useful for content integrity verification (e.g., response body hashing).
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * const hasher = createContentHasher('SHA-256');
127
+ * const hex = await hasher.hashHex('response-body-content');
128
+ * ```
129
+ */
130
+ function createContentHasher(algorithm = 'SHA-256') {
131
+ return {
132
+ async hash(data) {
133
+ return crypto.subtle.digest(algorithm, toArrayBuffer(data));
134
+ },
135
+ async hashHex(data) {
136
+ const hash = await crypto.subtle.digest(algorithm, toArrayBuffer(data));
137
+ return arrayBufferToHex(hash);
138
+ },
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Generated bundle index. Do not edit.
144
+ */
145
+
146
+ export { createAesEncryptor, createContentHasher, createHmacSigner };