@angular-helpers/worker-http 0.2.0 → 0.3.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
@@ -16,13 +16,13 @@ On top of that, workers provide a natural isolation boundary for security-sensit
16
16
 
17
17
  ## Package map
18
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 |
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()` | Available |
26
26
 
27
27
  ---
28
28
 
@@ -313,13 +313,9 @@ const hash = await hasher.hash('SHA-256', data); // → hex string
313
313
 
314
314
  ---
315
315
 
316
- ### `/backend` — Angular `HttpBackend` replacement (in progress)
316
+ ### `/backend` — Angular `HttpBackend` replacement
317
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:**
318
+ 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.
323
319
 
324
320
  ```typescript
325
321
  // app.config.ts
@@ -328,34 +324,75 @@ import {
328
324
  withWorkerConfigs,
329
325
  withWorkerRoutes,
330
326
  withWorkerFallback,
327
+ withWorkerSerialization,
331
328
  } from '@angular-helpers/worker-http/backend';
329
+ import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
332
330
 
333
- bootstrapApplication(AppComponent, {
331
+ export const appConfig: ApplicationConfig = {
334
332
  providers: [
335
333
  provideWorkerHttpClient(
336
334
  withWorkerConfigs([
335
+ {
336
+ id: 'api',
337
+ workerUrl: new URL('./workers/api.worker', import.meta.url),
338
+ maxInstances: 2, // round-robin pool
339
+ },
337
340
  {
338
341
  id: 'secure',
339
342
  workerUrl: new URL('./workers/secure.worker', import.meta.url),
340
- maxInstances: 2,
341
343
  },
342
344
  ]),
343
- withWorkerRoutes([{ pattern: /\/api\/secure\//, worker: 'secure', priority: 10 }]),
344
- withWorkerFallback('main-thread'), // SSR-safe fallback
345
+ withWorkerRoutes([
346
+ { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
347
+ { pattern: /\/api\//, worker: 'api', priority: 5 },
348
+ ]),
349
+ withWorkerFallback('main-thread'), // SSR-safe
350
+ withWorkerSerialization(createSerovalSerializer()), // optional: complex bodies
345
351
  ),
346
352
  ],
347
- });
353
+ };
348
354
 
349
- // data.service.ts — identical to normal HttpClient usage
355
+ // data.service.ts — WorkerHttpClient is a drop-in for HttpClient
350
356
  export class DataService {
351
- private http = inject(HttpClient);
357
+ private http = inject(WorkerHttpClient);
352
358
 
353
- getReports() {
354
- return this.http.get<Report[]>('/api/secure/reports');
359
+ getUsers() {
360
+ return this.http.get<User[]>('/api/users'); // auto-routed to 'api' worker
361
+ }
362
+
363
+ getSecureData() {
364
+ // per-request override via { worker } option or WORKER_TARGET context token
365
+ return this.http.get('/api/secure/payments', { worker: 'secure' });
355
366
  }
356
367
  }
368
+
369
+ // workers/api.worker.ts — runs on a separate OS thread
370
+ import {
371
+ createWorkerPipeline,
372
+ loggingInterceptor,
373
+ retryInterceptor,
374
+ cacheInterceptor,
375
+ } from '@angular-helpers/worker-http/interceptors';
376
+
377
+ createWorkerPipeline([
378
+ loggingInterceptor(),
379
+ retryInterceptor({ maxRetries: 3 }),
380
+ cacheInterceptor({ ttl: 60000 }),
381
+ ]);
357
382
  ```
358
383
 
384
+ **Features:**
385
+
386
+ - `provideWorkerHttpClient(...features)` — replaces `provideHttpClient()`; do not use both
387
+ - `withWorkerConfigs(configs)` — register named workers with optional pool size
388
+ - `withWorkerRoutes(routes)` — URL-pattern routing with priority ordering
389
+ - `withWorkerFallback(strategy)` — `'main-thread'` (SSR-safe) or `'error'`
390
+ - `withWorkerSerialization(serializer)` — plug in `createSerovalSerializer()` for complex request bodies (`Date`, `Map`, `Set`)
391
+ - `WORKER_TARGET` — `HttpContextToken<string | null>` for per-request worker routing via `HttpContext`
392
+ - `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field
393
+ - `WorkerHttpBackend` — the `HttpBackend` implementation (injectable for advanced use)
394
+ - `matchWorkerRoute(url, routes)` — pure utility to test routing rules
395
+
359
396
  ---
360
397
 
361
398
  ## Design principles
@@ -1,3 +1,372 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, makeEnvironmentProviders } from '@angular/core';
3
+ import { HttpContextToken, HttpHeaders, HttpResponse, HttpBackend, FetchBackend, HttpErrorResponse, HttpClient, HttpContext, provideHttpClient, withFetch } from '@angular/common/http';
4
+ import { throwError } from 'rxjs';
5
+ import { map, catchError } from 'rxjs/operators';
6
+ import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
7
+
8
+ /**
9
+ * Per-request HttpContextToken that carries the target worker ID.
10
+ *
11
+ * `null` → use URL-pattern auto-routing (or main-thread fallback if no route matches).
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // With WorkerHttpClient (recommended)
16
+ * this.http.get('/api/data', { worker: 'secure' });
17
+ *
18
+ * // With standard HttpClient (power user)
19
+ * this.http.get('/api/data', {
20
+ * context: new HttpContext().set(WORKER_TARGET, 'secure'),
21
+ * });
22
+ * ```
23
+ */
24
+ const WORKER_TARGET = new HttpContextToken(() => null);
25
+ /**
26
+ * Registered worker definitions provided via `withWorkerConfigs()`.
27
+ */
28
+ const WORKER_HTTP_CONFIGS_TOKEN = new InjectionToken('WorkerHttpConfigs', {
29
+ factory: () => [],
30
+ });
31
+ /**
32
+ * URL-pattern routing rules provided via `withWorkerRoutes()`.
33
+ */
34
+ const WORKER_HTTP_ROUTES_TOKEN = new InjectionToken('WorkerHttpRoutes', {
35
+ factory: () => [],
36
+ });
37
+ /**
38
+ * Fallback strategy provided via `withWorkerFallback()`.
39
+ * Defaults to `'main-thread'` (safe for SSR / unsupported environments).
40
+ */
41
+ const WORKER_HTTP_FALLBACK_TOKEN = new InjectionToken('WorkerHttpFallback', { factory: () => 'main-thread' });
42
+ /**
43
+ * Optional serializer for crossing the worker boundary.
44
+ * Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
45
+ *
46
+ * When set, `WorkerHttpBackend` serializes the request body before `postMessage`
47
+ * using this serializer. The worker-side `createWorkerPipeline()` receives the
48
+ * serialized form — add a worker interceptor to deserialize it if needed.
49
+ */
50
+ const WORKER_HTTP_SERIALIZER_TOKEN = new InjectionToken('WorkerHttpSerializer', { factory: () => null });
51
+
52
+ /**
53
+ * Converts an Angular `HttpRequest` into a structured-clone-safe POJO
54
+ * that can be sent to a web worker via `postMessage`.
55
+ *
56
+ * Notes:
57
+ * - `urlWithParams` is used so query params embedded via `HttpParams` are included.
58
+ * - The `context` field is intentionally left empty: `HttpContext` uses class references
59
+ * as keys which cannot cross the worker boundary.
60
+ */
61
+ function toSerializableRequest(req) {
62
+ const headers = {};
63
+ for (const name of req.headers.keys()) {
64
+ headers[name.toLowerCase()] = req.headers.getAll(name) ?? [];
65
+ }
66
+ const params = {};
67
+ for (const name of req.params.keys()) {
68
+ params[name] = req.params.getAll(name) ?? [];
69
+ }
70
+ return {
71
+ method: req.method,
72
+ url: req.urlWithParams,
73
+ headers,
74
+ params,
75
+ body: req.body,
76
+ responseType: req.responseType,
77
+ withCredentials: req.withCredentials,
78
+ context: {},
79
+ };
80
+ }
81
+ /**
82
+ * Converts a worker `SerializableResponse` back into an Angular `HttpResponse`.
83
+ */
84
+ function toHttpResponse(res, req) {
85
+ let headers = new HttpHeaders();
86
+ for (const [key, values] of Object.entries(res.headers)) {
87
+ for (const value of values) {
88
+ headers = headers.append(key, value);
89
+ }
90
+ }
91
+ return new HttpResponse({
92
+ body: res.body,
93
+ headers,
94
+ status: res.status,
95
+ statusText: res.statusText,
96
+ url: res.url || req.urlWithParams,
97
+ });
98
+ }
99
+ /**
100
+ * Matches a URL against a sorted list of `WorkerRoute` rules.
101
+ * Rules with higher `priority` are evaluated first.
102
+ * Returns the matched worker ID or `null` if no rule matches.
103
+ */
104
+ function matchWorkerRoute(url, routes) {
105
+ const sorted = [...routes].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
106
+ for (const route of sorted) {
107
+ const pattern = typeof route.pattern === 'string' ? new RegExp(route.pattern) : route.pattern;
108
+ if (pattern.test(url)) {
109
+ return route.worker;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Angular `HttpBackend` replacement that routes HTTP requests to web workers.
117
+ *
118
+ * Registered via `provideWorkerHttpClient()`. Not meant to be used directly.
119
+ *
120
+ * Flow per request:
121
+ * 1. Check SSR: if `Worker` is undefined → fallback strategy
122
+ * 2. Resolve target worker ID from `WORKER_TARGET` context or URL-pattern routing
123
+ * 3. Serialize `HttpRequest` → `SerializableRequest` (structured-clone safe)
124
+ * 4. Dispatch to the worker's `WorkerTransport`
125
+ * 5. Deserialize `SerializableResponse` → `HttpResponse`
126
+ */
127
+ class WorkerHttpBackend extends HttpBackend {
128
+ configs = inject(WORKER_HTTP_CONFIGS_TOKEN);
129
+ routes = inject(WORKER_HTTP_ROUTES_TOKEN);
130
+ fallback = inject(WORKER_HTTP_FALLBACK_TOKEN);
131
+ serializer = inject(WORKER_HTTP_SERIALIZER_TOKEN);
132
+ fetchBackend = inject(FetchBackend, { optional: true });
133
+ transports = new Map();
134
+ handle(req) {
135
+ if (typeof Worker === 'undefined') {
136
+ return this.handleFallback(req, 'Web Workers are not available in this environment (SSR)');
137
+ }
138
+ const workerId = req.context.get(WORKER_TARGET) ?? matchWorkerRoute(req.url, this.routes);
139
+ if (!workerId) {
140
+ return this.handleFallback(req, `No worker route matched for URL: ${req.url}`);
141
+ }
142
+ const config = this.configs.find((c) => c.id === workerId);
143
+ if (!config) {
144
+ return throwError(() => new Error(`[WorkerHttpBackend] Unknown worker id: "${workerId}". ` +
145
+ `Register it via withWorkerConfigs([{ id: "${workerId}", workerUrl: ... }]).`));
146
+ }
147
+ const transport = this.getOrCreateTransport(config);
148
+ const serializable = toSerializableRequest(req);
149
+ const body = this.serializer !== null && serializable.body !== null && serializable.body !== undefined
150
+ ? this.serializer.serialize(serializable.body).data
151
+ : serializable.body;
152
+ const payload = body !== serializable.body ? { ...serializable, body } : serializable;
153
+ return transport.execute(payload).pipe(map((res) => toHttpResponse(res, req)), catchError((err) => throwError(() => new HttpErrorResponse({
154
+ error: err,
155
+ status: 0,
156
+ statusText: 'Worker Error',
157
+ url: req.urlWithParams,
158
+ }))));
159
+ }
160
+ ngOnDestroy() {
161
+ for (const transport of this.transports.values()) {
162
+ transport.terminate();
163
+ }
164
+ this.transports.clear();
165
+ }
166
+ getOrCreateTransport(config) {
167
+ const existing = this.transports.get(config.id);
168
+ if (existing)
169
+ return existing;
170
+ const transport = createWorkerTransport({
171
+ workerFactory: () => new Worker(config.workerUrl, { type: 'module' }),
172
+ maxInstances: config.maxInstances ?? 1,
173
+ });
174
+ this.transports.set(config.id, transport);
175
+ return transport;
176
+ }
177
+ handleFallback(req, reason) {
178
+ if (this.fallback === 'error' || !this.fetchBackend) {
179
+ return throwError(() => new Error(`[WorkerHttpBackend] ${reason}`));
180
+ }
181
+ return this.fetchBackend.handle(req);
182
+ }
183
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
184
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend });
185
+ }
186
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, decorators: [{
187
+ type: Injectable
188
+ }] });
189
+
190
+ /**
191
+ * Convenience wrapper over `HttpClient` that adds an optional `{ worker }` field
192
+ * to every method. Under the hood it sets `WORKER_TARGET` on the `HttpContext` —
193
+ * the caller never has to touch the context manually.
194
+ *
195
+ * Usage is identical to `HttpClient` — just inject `WorkerHttpClient` instead.
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * @Injectable({ providedIn: 'root' })
200
+ * export class DataService {
201
+ * private readonly http = inject(WorkerHttpClient);
202
+ *
203
+ * getUsers(): Observable<User[]> {
204
+ * return this.http.get<User[]>('/api/users'); // auto-routed by URL pattern
205
+ * }
206
+ *
207
+ * getSensitiveReport(): Observable<Report> {
208
+ * return this.http.get<Report>('/api/secure/reports', { worker: 'secure' });
209
+ * }
210
+ * }
211
+ * ```
212
+ */
213
+ class WorkerHttpClient {
214
+ http = inject(HttpClient);
215
+ get(url, options) {
216
+ return this.http.get(url, this.withWorker(options));
217
+ }
218
+ post(url, body, options) {
219
+ return this.http.post(url, body, this.withWorker(options));
220
+ }
221
+ put(url, body, options) {
222
+ return this.http.put(url, body, this.withWorker(options));
223
+ }
224
+ patch(url, body, options) {
225
+ return this.http.patch(url, body, this.withWorker(options));
226
+ }
227
+ delete(url, options) {
228
+ return this.http.delete(url, this.withWorker(options));
229
+ }
230
+ head(url, options) {
231
+ return this.http.head(url, this.withWorker(options));
232
+ }
233
+ withWorker(options) {
234
+ const { worker = null, context, ...rest } = options ?? {};
235
+ return {
236
+ ...rest,
237
+ context: (context ?? new HttpContext()).set(WORKER_TARGET, worker),
238
+ };
239
+ }
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
241
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient });
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, decorators: [{
244
+ type: Injectable
245
+ }] });
246
+
247
+ /**
248
+ * Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
249
+ * with `WorkerHttpBackend`.
250
+ *
251
+ * Drop-in companion to `provideHttpClient()`. Can be used INSTEAD of it —
252
+ * `HttpClient` and the full interceptor chain are included automatically.
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * // app.config.ts
257
+ * export const appConfig: ApplicationConfig = {
258
+ * providers: [
259
+ * provideWorkerHttpClient(
260
+ * withWorkerConfigs([
261
+ * { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
262
+ * ]),
263
+ * withWorkerRoutes([
264
+ * { pattern: /\/api\//, worker: 'public', priority: 1 },
265
+ * ]),
266
+ * withWorkerFallback('main-thread'),
267
+ * ),
268
+ * ],
269
+ * };
270
+ * ```
271
+ */
272
+ function provideWorkerHttpClient(...features) {
273
+ const featureProviders = features.flatMap((f) => f.providers);
274
+ return makeEnvironmentProviders([
275
+ provideHttpClient(withFetch()),
276
+ FetchBackend,
277
+ { provide: HttpBackend, useClass: WorkerHttpBackend },
278
+ WorkerHttpClient,
279
+ ...featureProviders,
280
+ ]);
281
+ }
282
+ /**
283
+ * Registers worker definitions (id + workerUrl + optional pool size).
284
+ *
285
+ * At least one config is required for any request to reach a worker.
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * withWorkerConfigs([
290
+ * { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
291
+ * { id: 'secure', workerUrl: new URL('./workers/secure.worker', import.meta.url), maxInstances: 2 },
292
+ * ])
293
+ * ```
294
+ */
295
+ function withWorkerConfigs(configs) {
296
+ return {
297
+ kind: 'WorkerConfigs',
298
+ providers: [{ provide: WORKER_HTTP_CONFIGS_TOKEN, useValue: configs }],
299
+ };
300
+ }
301
+ /**
302
+ * Declares URL-pattern → worker routing rules evaluated in priority order.
303
+ *
304
+ * When a request URL matches a pattern, the associated worker handles it.
305
+ * Explicit `WORKER_TARGET` context always takes precedence over routes.
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * withWorkerRoutes([
310
+ * { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
311
+ * { pattern: /\/api\//, worker: 'public', priority: 1 },
312
+ * ])
313
+ * ```
314
+ */
315
+ function withWorkerRoutes(routes) {
316
+ return {
317
+ kind: 'WorkerRoutes',
318
+ providers: [{ provide: WORKER_HTTP_ROUTES_TOKEN, useValue: routes }],
319
+ };
320
+ }
321
+ /**
322
+ * Sets the fallback strategy when workers are unavailable (SSR, old browsers,
323
+ * or when no route matches).
324
+ *
325
+ * - `'main-thread'` (default) — silently delegates to `FetchBackend`
326
+ * - `'error'` — throws, forcing explicit handling in the application
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * withWorkerFallback('main-thread') // SSR-safe
331
+ * ```
332
+ */
333
+ function withWorkerFallback(strategy) {
334
+ return {
335
+ kind: 'WorkerFallback',
336
+ providers: [{ provide: WORKER_HTTP_FALLBACK_TOKEN, useValue: strategy }],
337
+ };
338
+ }
339
+ /**
340
+ * Configures a custom serializer for crossing the worker boundary.
341
+ *
342
+ * By default `WorkerHttpBackend` relies on the browser's structured clone algorithm
343
+ * (safe for plain objects, arrays, primitives, `Date`, `ArrayBuffer`).
344
+ * Use `withWorkerSerialization` when your request bodies contain types that
345
+ * structured clone cannot handle (e.g. class instances, circular references, `Map`, `Set`).
346
+ *
347
+ * **Worker-side note:** The serialized form is what the worker receives as `req.body`.
348
+ * If you use `createSerovalSerializer` or similar, add a worker-side interceptor
349
+ * to deserialize the body before calling `fetch()`.
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
354
+ *
355
+ * provideWorkerHttpClient(
356
+ * withWorkerConfigs([...]),
357
+ * withWorkerSerialization(createSerovalSerializer()),
358
+ * )
359
+ * ```
360
+ */
361
+ function withWorkerSerialization(serializer) {
362
+ return {
363
+ kind: 'WorkerSerialization',
364
+ providers: [{ provide: WORKER_HTTP_SERIALIZER_TOKEN, useValue: serializer }],
365
+ };
366
+ }
367
+
1
368
  /**
2
369
  * Generated bundle index. Do not edit.
3
370
  */
371
+
372
+ export { WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerRoutes, withWorkerSerialization };
@@ -228,7 +228,7 @@ function arrayBufferToHex$1(buffer) {
228
228
  return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
229
229
  }
230
230
  function defaultPayloadBuilder(req) {
231
- const body = req.body != null ? JSON.stringify(req.body) : '';
231
+ const body = req.body !== null && req.body !== undefined ? JSON.stringify(req.body) : '';
232
232
  return `${req.method}:${req.url}:${body}`;
233
233
  }
234
234
  /**
@@ -315,7 +315,7 @@ function loggingInterceptor(config) {
315
315
  catch (error) {
316
316
  const elapsedMs = Date.now() - startMs;
317
317
  const status = error.status;
318
- const label = status != null ? String(status) : 'NETWORK_ERROR';
318
+ const label = status !== null && status !== undefined ? String(status) : 'NETWORK_ERROR';
319
319
  safeLog(`[worker] ✕ ${label} ${req.url} (${elapsedMs}ms)`, error);
320
320
  throw error;
321
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/worker-http",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Angular HTTP over Web Workers — off-main-thread HTTP pipelines with configurable interceptors, WebCrypto security, and pluggable serialization",
5
5
  "keywords": [
6
6
  "angular",
@@ -1,4 +1,8 @@
1
- import { Provider } from '@angular/core';
1
+ import * as i0 from '@angular/core';
2
+ import { Provider, InjectionToken, EnvironmentProviders, OnDestroy } from '@angular/core';
3
+ import { HttpContextToken, HttpBackend, HttpRequest, HttpEvent, HttpContext, HttpResponse } from '@angular/common/http';
4
+ import { WorkerSerializer } from '@angular-helpers/worker-http/serializer';
5
+ import { Observable } from 'rxjs';
2
6
 
3
7
  /**
4
8
  * Discriminated union for worker HTTP feature kinds.
@@ -38,5 +42,255 @@ interface WorkerRoute {
38
42
  * Fallback strategy when workers are unavailable (SSR, old browsers).
39
43
  */
40
44
  type WorkerFallbackStrategy = 'main-thread' | 'error';
45
+ /**
46
+ * Serializable HTTP request — POJO version of Angular's HttpRequest.
47
+ * Structured-clone safe: no classes, no functions, no prototype chains.
48
+ */
49
+ interface SerializableRequest {
50
+ method: string;
51
+ url: string;
52
+ headers: Record<string, string[]>;
53
+ params: Record<string, string[]>;
54
+ body: unknown;
55
+ responseType: 'json' | 'text' | 'blob' | 'arraybuffer';
56
+ withCredentials: boolean;
57
+ context: Record<string, unknown>;
58
+ }
59
+ /**
60
+ * Serializable HTTP response — POJO version of Angular's HttpResponse.
61
+ */
62
+ interface SerializableResponse {
63
+ status: number;
64
+ statusText: string;
65
+ headers: Record<string, string[]>;
66
+ body: unknown;
67
+ url: string;
68
+ }
69
+
70
+ /**
71
+ * Per-request HttpContextToken that carries the target worker ID.
72
+ *
73
+ * `null` → use URL-pattern auto-routing (or main-thread fallback if no route matches).
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * // With WorkerHttpClient (recommended)
78
+ * this.http.get('/api/data', { worker: 'secure' });
79
+ *
80
+ * // With standard HttpClient (power user)
81
+ * this.http.get('/api/data', {
82
+ * context: new HttpContext().set(WORKER_TARGET, 'secure'),
83
+ * });
84
+ * ```
85
+ */
86
+ declare const WORKER_TARGET: HttpContextToken<string>;
87
+ /**
88
+ * Optional serializer for crossing the worker boundary.
89
+ * Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
90
+ *
91
+ * When set, `WorkerHttpBackend` serializes the request body before `postMessage`
92
+ * using this serializer. The worker-side `createWorkerPipeline()` receives the
93
+ * serialized form — add a worker interceptor to deserialize it if needed.
94
+ */
95
+ declare const WORKER_HTTP_SERIALIZER_TOKEN: InjectionToken<WorkerSerializer>;
96
+
97
+ /**
98
+ * Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
99
+ * with `WorkerHttpBackend`.
100
+ *
101
+ * Drop-in companion to `provideHttpClient()`. Can be used INSTEAD of it —
102
+ * `HttpClient` and the full interceptor chain are included automatically.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // app.config.ts
107
+ * export const appConfig: ApplicationConfig = {
108
+ * providers: [
109
+ * provideWorkerHttpClient(
110
+ * withWorkerConfigs([
111
+ * { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
112
+ * ]),
113
+ * withWorkerRoutes([
114
+ * { pattern: /\/api\//, worker: 'public', priority: 1 },
115
+ * ]),
116
+ * withWorkerFallback('main-thread'),
117
+ * ),
118
+ * ],
119
+ * };
120
+ * ```
121
+ */
122
+ declare function provideWorkerHttpClient(...features: WorkerHttpFeature<WorkerHttpFeatureKind>[]): EnvironmentProviders;
123
+ /**
124
+ * Registers worker definitions (id + workerUrl + optional pool size).
125
+ *
126
+ * At least one config is required for any request to reach a worker.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * withWorkerConfigs([
131
+ * { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
132
+ * { id: 'secure', workerUrl: new URL('./workers/secure.worker', import.meta.url), maxInstances: 2 },
133
+ * ])
134
+ * ```
135
+ */
136
+ declare function withWorkerConfigs(configs: WorkerConfig[]): WorkerHttpFeature<'WorkerConfigs'>;
137
+ /**
138
+ * Declares URL-pattern → worker routing rules evaluated in priority order.
139
+ *
140
+ * When a request URL matches a pattern, the associated worker handles it.
141
+ * Explicit `WORKER_TARGET` context always takes precedence over routes.
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * withWorkerRoutes([
146
+ * { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
147
+ * { pattern: /\/api\//, worker: 'public', priority: 1 },
148
+ * ])
149
+ * ```
150
+ */
151
+ declare function withWorkerRoutes(routes: WorkerRoute[]): WorkerHttpFeature<'WorkerRoutes'>;
152
+ /**
153
+ * Sets the fallback strategy when workers are unavailable (SSR, old browsers,
154
+ * or when no route matches).
155
+ *
156
+ * - `'main-thread'` (default) — silently delegates to `FetchBackend`
157
+ * - `'error'` — throws, forcing explicit handling in the application
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * withWorkerFallback('main-thread') // SSR-safe
162
+ * ```
163
+ */
164
+ declare function withWorkerFallback(strategy: WorkerFallbackStrategy): WorkerHttpFeature<'WorkerFallback'>;
165
+ /**
166
+ * Configures a custom serializer for crossing the worker boundary.
167
+ *
168
+ * By default `WorkerHttpBackend` relies on the browser's structured clone algorithm
169
+ * (safe for plain objects, arrays, primitives, `Date`, `ArrayBuffer`).
170
+ * Use `withWorkerSerialization` when your request bodies contain types that
171
+ * structured clone cannot handle (e.g. class instances, circular references, `Map`, `Set`).
172
+ *
173
+ * **Worker-side note:** The serialized form is what the worker receives as `req.body`.
174
+ * If you use `createSerovalSerializer` or similar, add a worker-side interceptor
175
+ * to deserialize the body before calling `fetch()`.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
180
+ *
181
+ * provideWorkerHttpClient(
182
+ * withWorkerConfigs([...]),
183
+ * withWorkerSerialization(createSerovalSerializer()),
184
+ * )
185
+ * ```
186
+ */
187
+ declare function withWorkerSerialization(serializer: WorkerSerializer): WorkerHttpFeature<'WorkerSerialization'>;
188
+
189
+ /**
190
+ * Angular `HttpBackend` replacement that routes HTTP requests to web workers.
191
+ *
192
+ * Registered via `provideWorkerHttpClient()`. Not meant to be used directly.
193
+ *
194
+ * Flow per request:
195
+ * 1. Check SSR: if `Worker` is undefined → fallback strategy
196
+ * 2. Resolve target worker ID from `WORKER_TARGET` context or URL-pattern routing
197
+ * 3. Serialize `HttpRequest` → `SerializableRequest` (structured-clone safe)
198
+ * 4. Dispatch to the worker's `WorkerTransport`
199
+ * 5. Deserialize `SerializableResponse` → `HttpResponse`
200
+ */
201
+ declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
202
+ private readonly configs;
203
+ private readonly routes;
204
+ private readonly fallback;
205
+ private readonly serializer;
206
+ private readonly fetchBackend;
207
+ private readonly transports;
208
+ handle(req: HttpRequest<unknown>): Observable<HttpEvent<unknown>>;
209
+ ngOnDestroy(): void;
210
+ private getOrCreateTransport;
211
+ private handleFallback;
212
+ static ɵfac: i0.ɵɵFactoryDeclaration<WorkerHttpBackend, never>;
213
+ static ɵprov: i0.ɵɵInjectableDeclaration<WorkerHttpBackend>;
214
+ }
215
+
216
+ /**
217
+ * Options accepted by `WorkerHttpClient` methods.
218
+ * Identical to `HttpClient` options with an optional `worker` field added.
219
+ */
220
+ interface WorkerRequestOptions {
221
+ /** Target worker ID. Overrides URL-pattern routing for this specific request. */
222
+ worker?: string | null;
223
+ context?: HttpContext;
224
+ headers?: Record<string, string | string[]>;
225
+ params?: Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;
226
+ responseType?: 'json';
227
+ withCredentials?: boolean;
228
+ observe?: 'body';
229
+ reportProgress?: boolean;
230
+ transferCache?: {
231
+ includeHeaders?: string[];
232
+ } | boolean;
233
+ }
234
+ /**
235
+ * Convenience wrapper over `HttpClient` that adds an optional `{ worker }` field
236
+ * to every method. Under the hood it sets `WORKER_TARGET` on the `HttpContext` —
237
+ * the caller never has to touch the context manually.
238
+ *
239
+ * Usage is identical to `HttpClient` — just inject `WorkerHttpClient` instead.
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * @Injectable({ providedIn: 'root' })
244
+ * export class DataService {
245
+ * private readonly http = inject(WorkerHttpClient);
246
+ *
247
+ * getUsers(): Observable<User[]> {
248
+ * return this.http.get<User[]>('/api/users'); // auto-routed by URL pattern
249
+ * }
250
+ *
251
+ * getSensitiveReport(): Observable<Report> {
252
+ * return this.http.get<Report>('/api/secure/reports', { worker: 'secure' });
253
+ * }
254
+ * }
255
+ * ```
256
+ */
257
+ declare class WorkerHttpClient {
258
+ private readonly http;
259
+ get<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
260
+ post<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
261
+ put<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
262
+ patch<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
263
+ delete<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
264
+ head<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
265
+ private withWorker;
266
+ static ɵfac: i0.ɵɵFactoryDeclaration<WorkerHttpClient, never>;
267
+ static ɵprov: i0.ɵɵInjectableDeclaration<WorkerHttpClient>;
268
+ }
269
+
270
+ /**
271
+ * Converts an Angular `HttpRequest` into a structured-clone-safe POJO
272
+ * that can be sent to a web worker via `postMessage`.
273
+ *
274
+ * Notes:
275
+ * - `urlWithParams` is used so query params embedded via `HttpParams` are included.
276
+ * - The `context` field is intentionally left empty: `HttpContext` uses class references
277
+ * as keys which cannot cross the worker boundary.
278
+ */
279
+ declare function toSerializableRequest(req: HttpRequest<unknown>): SerializableRequest;
280
+ /**
281
+ * Converts a worker `SerializableResponse` back into an Angular `HttpResponse`.
282
+ */
283
+ declare function toHttpResponse(res: SerializableResponse, req: HttpRequest<unknown>): HttpResponse<unknown>;
284
+ /**
285
+ * Matches a URL against a sorted list of `WorkerRoute` rules.
286
+ * Rules with higher `priority` are evaluated first.
287
+ * Returns the matched worker ID or `null` if no rule matches.
288
+ */
289
+ declare function matchWorkerRoute(url: string, routes: Array<{
290
+ pattern: RegExp | string;
291
+ worker: string;
292
+ priority?: number;
293
+ }>): string | null;
41
294
 
42
- export type { WorkerConfig, WorkerFallbackStrategy, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerRoute };
295
+ export { WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerRoutes, withWorkerSerialization };
296
+ export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerRequestOptions, WorkerRoute };