@angular-helpers/worker-http 0.2.0 → 0.4.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 +112 -22
- package/fesm2022/angular-helpers-worker-http-backend.mjs +428 -0
- package/fesm2022/angular-helpers-worker-http-interceptors.mjs +377 -196
- package/fesm2022/angular-helpers-worker-http-transport.mjs +3 -0
- package/package.json +1 -1
- package/types/angular-helpers-worker-http-backend.d.ts +306 -3
- package/types/angular-helpers-worker-http-interceptors.d.ts +127 -5
- package/types/angular-helpers-worker-http-transport.d.ts +12 -0
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()` |
|
|
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
|
|
316
|
+
### `/backend` — Angular `HttpBackend` replacement
|
|
317
317
|
|
|
318
|
-
|
|
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,86 @@ import {
|
|
|
328
324
|
withWorkerConfigs,
|
|
329
325
|
withWorkerRoutes,
|
|
330
326
|
withWorkerFallback,
|
|
327
|
+
withWorkerSerialization,
|
|
328
|
+
withWorkerInterceptors,
|
|
331
329
|
} from '@angular-helpers/worker-http/backend';
|
|
330
|
+
import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
331
|
+
import { workerLogging, workerRetry, workerCache } from '@angular-helpers/worker-http/interceptors';
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
export const appConfig: ApplicationConfig = {
|
|
334
334
|
providers: [
|
|
335
335
|
provideWorkerHttpClient(
|
|
336
336
|
withWorkerConfigs([
|
|
337
|
+
{
|
|
338
|
+
id: 'api',
|
|
339
|
+
workerUrl: new URL('./workers/api.worker', import.meta.url),
|
|
340
|
+
maxInstances: 2, // round-robin pool
|
|
341
|
+
},
|
|
337
342
|
{
|
|
338
343
|
id: 'secure',
|
|
339
344
|
workerUrl: new URL('./workers/secure.worker', import.meta.url),
|
|
340
|
-
maxInstances: 2,
|
|
341
345
|
},
|
|
342
346
|
]),
|
|
343
|
-
withWorkerRoutes([
|
|
344
|
-
|
|
347
|
+
withWorkerRoutes([
|
|
348
|
+
{ pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
|
|
349
|
+
{ pattern: /\/api\//, worker: 'api', priority: 5 },
|
|
350
|
+
]),
|
|
351
|
+
withWorkerFallback('main-thread'), // SSR-safe
|
|
352
|
+
withWorkerSerialization(createSerovalSerializer()), // optional: complex bodies
|
|
353
|
+
withWorkerInterceptors([
|
|
354
|
+
workerLogging(),
|
|
355
|
+
workerRetry({ maxRetries: 3 }),
|
|
356
|
+
workerCache({ ttl: 60000 }),
|
|
357
|
+
]),
|
|
345
358
|
),
|
|
346
359
|
],
|
|
347
|
-
}
|
|
360
|
+
};
|
|
348
361
|
|
|
349
|
-
// data.service.ts —
|
|
362
|
+
// data.service.ts — WorkerHttpClient is a drop-in for HttpClient
|
|
350
363
|
export class DataService {
|
|
351
|
-
private http = inject(
|
|
364
|
+
private http = inject(WorkerHttpClient);
|
|
365
|
+
|
|
366
|
+
getUsers() {
|
|
367
|
+
return this.http.get<User[]>('/api/users'); // auto-routed to 'api' worker
|
|
368
|
+
}
|
|
352
369
|
|
|
353
|
-
|
|
354
|
-
|
|
370
|
+
getSecureData() {
|
|
371
|
+
// per-request override via { worker } option or WORKER_TARGET context token
|
|
372
|
+
return this.http.get('/api/secure/payments', { worker: 'secure' });
|
|
355
373
|
}
|
|
356
374
|
}
|
|
375
|
+
|
|
376
|
+
// workers/api.worker.ts — runs on a separate OS thread
|
|
377
|
+
import { createConfigurableWorkerPipeline } from '@angular-helpers/worker-http/interceptors';
|
|
378
|
+
|
|
379
|
+
// The pipeline is built at runtime from the specs configured via
|
|
380
|
+
// `withWorkerInterceptors([...])` in `app.config.ts`. Custom interceptors
|
|
381
|
+
// not covered by the built-in catalogue can be wired in here:
|
|
382
|
+
//
|
|
383
|
+
// import { registerInterceptor } from '@angular-helpers/worker-http/interceptors';
|
|
384
|
+
// registerInterceptor('auth-token', (config) => async (req, next) => { ... });
|
|
385
|
+
//
|
|
386
|
+
// then referenced from the main thread with `workerCustom('auth-token', config)`.
|
|
387
|
+
createConfigurableWorkerPipeline();
|
|
357
388
|
```
|
|
358
389
|
|
|
390
|
+
If you prefer to keep the pipeline composition inside the worker file, use
|
|
391
|
+
`createWorkerPipeline([interceptors])` instead — it stays available for full
|
|
392
|
+
manual control.
|
|
393
|
+
|
|
394
|
+
**Features:**
|
|
395
|
+
|
|
396
|
+
- `provideWorkerHttpClient(...features)` — replaces `provideHttpClient()`; do not use both
|
|
397
|
+
- `withWorkerConfigs(configs)` — register named workers with optional pool size
|
|
398
|
+
- `withWorkerRoutes(routes)` — URL-pattern routing with priority ordering
|
|
399
|
+
- `withWorkerFallback(strategy)` — `'main-thread'` (SSR-safe) or `'error'`
|
|
400
|
+
- `withWorkerSerialization(serializer)` — plug in `createSerovalSerializer()` for complex request bodies (`Date`, `Map`, `Set`)
|
|
401
|
+
- `withWorkerInterceptors(specs | specsByWorker)` — configure the worker-side pipeline from Angular DI; pairs with `createConfigurableWorkerPipeline()` in the worker file
|
|
402
|
+
- `WORKER_TARGET` — `HttpContextToken<string | null>` for per-request worker routing via `HttpContext`
|
|
403
|
+
- `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field
|
|
404
|
+
- `WorkerHttpBackend` — the `HttpBackend` implementation (injectable for advanced use)
|
|
405
|
+
- `matchWorkerRoute(url, routes)` — pure utility to test routing rules
|
|
406
|
+
|
|
359
407
|
---
|
|
360
408
|
|
|
361
409
|
## Design principles
|
|
@@ -392,6 +440,48 @@ Server-Side Rendering (SSR) is supported via automatic fallback to the main thre
|
|
|
392
440
|
|
|
393
441
|
---
|
|
394
442
|
|
|
443
|
+
## Benchmarks
|
|
444
|
+
|
|
445
|
+
A reproducible benchmark suite ships with the demo app at
|
|
446
|
+
[`/demo/worker-http-benchmark`](../../src/app/demo/worker-http-benchmark) and compares three
|
|
447
|
+
transport modes across four workloads:
|
|
448
|
+
|
|
449
|
+
| Mode | What it measures |
|
|
450
|
+
| --------------- | ---------------------------------------------------------- |
|
|
451
|
+
| `main-thread` | Baseline — the same simulated work runs on the main thread |
|
|
452
|
+
| `worker-pool-1` | Single worker — measures pure transport overhead |
|
|
453
|
+
| `worker-pool-4` | Four workers — measures the benefit of parallel dispatch |
|
|
454
|
+
|
|
455
|
+
Each scenario simulates identical "server" work (synchronous CPU burn + async delay + payload
|
|
456
|
+
generation), so the only variable being compared is **where** the work runs.
|
|
457
|
+
|
|
458
|
+
**Workloads**:
|
|
459
|
+
|
|
460
|
+
- 100 small sequential requests — pure transport overhead
|
|
461
|
+
- 1 large response (10MB) — serialization / structured clone cost
|
|
462
|
+
- 50 parallel requests — pool benefit
|
|
463
|
+
- 20 parallel requests + 500ms main-thread CPU burn — real-world jank case
|
|
464
|
+
|
|
465
|
+
**Metrics collected**:
|
|
466
|
+
|
|
467
|
+
- **Long Tasks** (`PerformanceObserver` / `longtask`) — count and total duration (Chromium-only)
|
|
468
|
+
- **Dropped frames** (`requestAnimationFrame` deltas > 25 ms) — visible jank proxy, works in every browser
|
|
469
|
+
- **Wall-clock total** for the scenario
|
|
470
|
+
- **Success / failure** counts
|
|
471
|
+
|
|
472
|
+
To run locally:
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
npm run build:workers
|
|
476
|
+
npm start
|
|
477
|
+
# open https://localhost:4200/demo/worker-http-benchmark
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Numbers vary by hardware, browser, and current system load — always run a scenario several times
|
|
481
|
+
and watch the trend, not a single value.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
395
485
|
## Related documentation
|
|
396
486
|
|
|
397
487
|
- [Architecture & Feasibility Study](../../docs/sdd-angular-http-web-workers.md)
|
|
@@ -1,3 +1,431 @@
|
|
|
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
|
+
* Per-worker interceptor specs provided via `withWorkerInterceptors()`.
|
|
53
|
+
* Defaults to an empty map (no interceptors).
|
|
54
|
+
*/
|
|
55
|
+
const WORKER_HTTP_INTERCEPTORS_TOKEN = new InjectionToken('WorkerHttpInterceptors', { factory: () => ({}) });
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts an Angular `HttpRequest` into a structured-clone-safe POJO
|
|
59
|
+
* that can be sent to a web worker via `postMessage`.
|
|
60
|
+
*
|
|
61
|
+
* Notes:
|
|
62
|
+
* - `urlWithParams` is used so query params embedded via `HttpParams` are included.
|
|
63
|
+
* - The `context` field is intentionally left empty: `HttpContext` uses class references
|
|
64
|
+
* as keys which cannot cross the worker boundary.
|
|
65
|
+
*/
|
|
66
|
+
function toSerializableRequest(req) {
|
|
67
|
+
const headers = {};
|
|
68
|
+
for (const name of req.headers.keys()) {
|
|
69
|
+
headers[name.toLowerCase()] = req.headers.getAll(name) ?? [];
|
|
70
|
+
}
|
|
71
|
+
const params = {};
|
|
72
|
+
for (const name of req.params.keys()) {
|
|
73
|
+
params[name] = req.params.getAll(name) ?? [];
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
method: req.method,
|
|
77
|
+
url: req.urlWithParams,
|
|
78
|
+
headers,
|
|
79
|
+
params,
|
|
80
|
+
body: req.body,
|
|
81
|
+
responseType: req.responseType,
|
|
82
|
+
withCredentials: req.withCredentials,
|
|
83
|
+
context: {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Converts a worker `SerializableResponse` back into an Angular `HttpResponse`.
|
|
88
|
+
*/
|
|
89
|
+
function toHttpResponse(res, req) {
|
|
90
|
+
let headers = new HttpHeaders();
|
|
91
|
+
for (const [key, values] of Object.entries(res.headers)) {
|
|
92
|
+
for (const value of values) {
|
|
93
|
+
headers = headers.append(key, value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return new HttpResponse({
|
|
97
|
+
body: res.body,
|
|
98
|
+
headers,
|
|
99
|
+
status: res.status,
|
|
100
|
+
statusText: res.statusText,
|
|
101
|
+
url: res.url || req.urlWithParams,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Matches a URL against a sorted list of `WorkerRoute` rules.
|
|
106
|
+
* Rules with higher `priority` are evaluated first.
|
|
107
|
+
* Returns the matched worker ID or `null` if no rule matches.
|
|
108
|
+
*/
|
|
109
|
+
function matchWorkerRoute(url, routes) {
|
|
110
|
+
const sorted = [...routes].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
111
|
+
for (const route of sorted) {
|
|
112
|
+
const pattern = typeof route.pattern === 'string' ? new RegExp(route.pattern) : route.pattern;
|
|
113
|
+
if (pattern.test(url)) {
|
|
114
|
+
return route.worker;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
122
|
+
*
|
|
123
|
+
* Registered via `provideWorkerHttpClient()`. Not meant to be used directly.
|
|
124
|
+
*
|
|
125
|
+
* Flow per request:
|
|
126
|
+
* 1. Check SSR: if `Worker` is undefined → fallback strategy
|
|
127
|
+
* 2. Resolve target worker ID from `WORKER_TARGET` context or URL-pattern routing
|
|
128
|
+
* 3. Serialize `HttpRequest` → `SerializableRequest` (structured-clone safe)
|
|
129
|
+
* 4. Dispatch to the worker's `WorkerTransport`
|
|
130
|
+
* 5. Deserialize `SerializableResponse` → `HttpResponse`
|
|
131
|
+
*/
|
|
132
|
+
class WorkerHttpBackend extends HttpBackend {
|
|
133
|
+
configs = inject(WORKER_HTTP_CONFIGS_TOKEN);
|
|
134
|
+
routes = inject(WORKER_HTTP_ROUTES_TOKEN);
|
|
135
|
+
fallback = inject(WORKER_HTTP_FALLBACK_TOKEN);
|
|
136
|
+
serializer = inject(WORKER_HTTP_SERIALIZER_TOKEN);
|
|
137
|
+
interceptorSpecs = inject(WORKER_HTTP_INTERCEPTORS_TOKEN);
|
|
138
|
+
fetchBackend = inject(FetchBackend, { optional: true });
|
|
139
|
+
transports = new Map();
|
|
140
|
+
handle(req) {
|
|
141
|
+
if (typeof Worker === 'undefined') {
|
|
142
|
+
return this.handleFallback(req, 'Web Workers are not available in this environment (SSR)');
|
|
143
|
+
}
|
|
144
|
+
const workerId = req.context.get(WORKER_TARGET) ?? matchWorkerRoute(req.url, this.routes);
|
|
145
|
+
if (!workerId) {
|
|
146
|
+
return this.handleFallback(req, `No worker route matched for URL: ${req.url}`);
|
|
147
|
+
}
|
|
148
|
+
const config = this.configs.find((c) => c.id === workerId);
|
|
149
|
+
if (!config) {
|
|
150
|
+
return throwError(() => new Error(`[WorkerHttpBackend] Unknown worker id: "${workerId}". ` +
|
|
151
|
+
`Register it via withWorkerConfigs([{ id: "${workerId}", workerUrl: ... }]).`));
|
|
152
|
+
}
|
|
153
|
+
const transport = this.getOrCreateTransport(config);
|
|
154
|
+
const serializable = toSerializableRequest(req);
|
|
155
|
+
const body = this.serializer !== null && serializable.body !== null && serializable.body !== undefined
|
|
156
|
+
? this.serializer.serialize(serializable.body).data
|
|
157
|
+
: serializable.body;
|
|
158
|
+
const payload = body !== serializable.body ? { ...serializable, body } : serializable;
|
|
159
|
+
return transport.execute(payload).pipe(map((res) => toHttpResponse(res, req)), catchError((err) => throwError(() => new HttpErrorResponse({
|
|
160
|
+
error: err,
|
|
161
|
+
status: 0,
|
|
162
|
+
statusText: 'Worker Error',
|
|
163
|
+
url: req.urlWithParams,
|
|
164
|
+
}))));
|
|
165
|
+
}
|
|
166
|
+
ngOnDestroy() {
|
|
167
|
+
for (const transport of this.transports.values()) {
|
|
168
|
+
transport.terminate();
|
|
169
|
+
}
|
|
170
|
+
this.transports.clear();
|
|
171
|
+
}
|
|
172
|
+
getOrCreateTransport(config) {
|
|
173
|
+
const existing = this.transports.get(config.id);
|
|
174
|
+
if (existing)
|
|
175
|
+
return existing;
|
|
176
|
+
const specs = this.resolveSpecsFor(config.id);
|
|
177
|
+
const transport = createWorkerTransport({
|
|
178
|
+
workerFactory: () => new Worker(config.workerUrl, { type: 'module' }),
|
|
179
|
+
maxInstances: config.maxInstances ?? 1,
|
|
180
|
+
initMessage: specs.length > 0 ? { type: 'init-interceptors', specs } : undefined,
|
|
181
|
+
});
|
|
182
|
+
this.transports.set(config.id, transport);
|
|
183
|
+
return transport;
|
|
184
|
+
}
|
|
185
|
+
resolveSpecsFor(workerId) {
|
|
186
|
+
const wildcard = this.interceptorSpecs['*'] ?? [];
|
|
187
|
+
const specific = this.interceptorSpecs[workerId] ?? [];
|
|
188
|
+
if (wildcard.length === 0)
|
|
189
|
+
return specific;
|
|
190
|
+
if (specific.length === 0)
|
|
191
|
+
return wildcard;
|
|
192
|
+
return [...wildcard, ...specific];
|
|
193
|
+
}
|
|
194
|
+
handleFallback(req, reason) {
|
|
195
|
+
if (this.fallback === 'error' || !this.fetchBackend) {
|
|
196
|
+
return throwError(() => new Error(`[WorkerHttpBackend] ${reason}`));
|
|
197
|
+
}
|
|
198
|
+
return this.fetchBackend.handle(req);
|
|
199
|
+
}
|
|
200
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
201
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend });
|
|
202
|
+
}
|
|
203
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, decorators: [{
|
|
204
|
+
type: Injectable
|
|
205
|
+
}] });
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Convenience wrapper over `HttpClient` that adds an optional `{ worker }` field
|
|
209
|
+
* to every method. Under the hood it sets `WORKER_TARGET` on the `HttpContext` —
|
|
210
|
+
* the caller never has to touch the context manually.
|
|
211
|
+
*
|
|
212
|
+
* Usage is identical to `HttpClient` — just inject `WorkerHttpClient` instead.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* @Injectable({ providedIn: 'root' })
|
|
217
|
+
* export class DataService {
|
|
218
|
+
* private readonly http = inject(WorkerHttpClient);
|
|
219
|
+
*
|
|
220
|
+
* getUsers(): Observable<User[]> {
|
|
221
|
+
* return this.http.get<User[]>('/api/users'); // auto-routed by URL pattern
|
|
222
|
+
* }
|
|
223
|
+
*
|
|
224
|
+
* getSensitiveReport(): Observable<Report> {
|
|
225
|
+
* return this.http.get<Report>('/api/secure/reports', { worker: 'secure' });
|
|
226
|
+
* }
|
|
227
|
+
* }
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
class WorkerHttpClient {
|
|
231
|
+
http = inject(HttpClient);
|
|
232
|
+
get(url, options) {
|
|
233
|
+
return this.http.get(url, this.withWorker(options));
|
|
234
|
+
}
|
|
235
|
+
post(url, body, options) {
|
|
236
|
+
return this.http.post(url, body, this.withWorker(options));
|
|
237
|
+
}
|
|
238
|
+
put(url, body, options) {
|
|
239
|
+
return this.http.put(url, body, this.withWorker(options));
|
|
240
|
+
}
|
|
241
|
+
patch(url, body, options) {
|
|
242
|
+
return this.http.patch(url, body, this.withWorker(options));
|
|
243
|
+
}
|
|
244
|
+
delete(url, options) {
|
|
245
|
+
return this.http.delete(url, this.withWorker(options));
|
|
246
|
+
}
|
|
247
|
+
head(url, options) {
|
|
248
|
+
return this.http.head(url, this.withWorker(options));
|
|
249
|
+
}
|
|
250
|
+
withWorker(options) {
|
|
251
|
+
const { worker = null, context, ...rest } = options ?? {};
|
|
252
|
+
return {
|
|
253
|
+
...rest,
|
|
254
|
+
context: (context ?? new HttpContext()).set(WORKER_TARGET, worker),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
258
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient });
|
|
259
|
+
}
|
|
260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, decorators: [{
|
|
261
|
+
type: Injectable
|
|
262
|
+
}] });
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
|
|
266
|
+
* with `WorkerHttpBackend`.
|
|
267
|
+
*
|
|
268
|
+
* Drop-in companion to `provideHttpClient()`. Can be used INSTEAD of it —
|
|
269
|
+
* `HttpClient` and the full interceptor chain are included automatically.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* // app.config.ts
|
|
274
|
+
* export const appConfig: ApplicationConfig = {
|
|
275
|
+
* providers: [
|
|
276
|
+
* provideWorkerHttpClient(
|
|
277
|
+
* withWorkerConfigs([
|
|
278
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
279
|
+
* ]),
|
|
280
|
+
* withWorkerRoutes([
|
|
281
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
282
|
+
* ]),
|
|
283
|
+
* withWorkerFallback('main-thread'),
|
|
284
|
+
* ),
|
|
285
|
+
* ],
|
|
286
|
+
* };
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
function provideWorkerHttpClient(...features) {
|
|
290
|
+
const featureProviders = features.flatMap((f) => f.providers);
|
|
291
|
+
return makeEnvironmentProviders([
|
|
292
|
+
provideHttpClient(withFetch()),
|
|
293
|
+
FetchBackend,
|
|
294
|
+
{ provide: HttpBackend, useClass: WorkerHttpBackend },
|
|
295
|
+
WorkerHttpClient,
|
|
296
|
+
...featureProviders,
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Registers worker definitions (id + workerUrl + optional pool size).
|
|
301
|
+
*
|
|
302
|
+
* At least one config is required for any request to reach a worker.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```typescript
|
|
306
|
+
* withWorkerConfigs([
|
|
307
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
308
|
+
* { id: 'secure', workerUrl: new URL('./workers/secure.worker', import.meta.url), maxInstances: 2 },
|
|
309
|
+
* ])
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
function withWorkerConfigs(configs) {
|
|
313
|
+
return {
|
|
314
|
+
kind: 'WorkerConfigs',
|
|
315
|
+
providers: [{ provide: WORKER_HTTP_CONFIGS_TOKEN, useValue: configs }],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Declares URL-pattern → worker routing rules evaluated in priority order.
|
|
320
|
+
*
|
|
321
|
+
* When a request URL matches a pattern, the associated worker handles it.
|
|
322
|
+
* Explicit `WORKER_TARGET` context always takes precedence over routes.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* withWorkerRoutes([
|
|
327
|
+
* { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
|
|
328
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
329
|
+
* ])
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
function withWorkerRoutes(routes) {
|
|
333
|
+
return {
|
|
334
|
+
kind: 'WorkerRoutes',
|
|
335
|
+
providers: [{ provide: WORKER_HTTP_ROUTES_TOKEN, useValue: routes }],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Sets the fallback strategy when workers are unavailable (SSR, old browsers,
|
|
340
|
+
* or when no route matches).
|
|
341
|
+
*
|
|
342
|
+
* - `'main-thread'` (default) — silently delegates to `FetchBackend`
|
|
343
|
+
* - `'error'` — throws, forcing explicit handling in the application
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```typescript
|
|
347
|
+
* withWorkerFallback('main-thread') // SSR-safe
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
function withWorkerFallback(strategy) {
|
|
351
|
+
return {
|
|
352
|
+
kind: 'WorkerFallback',
|
|
353
|
+
providers: [{ provide: WORKER_HTTP_FALLBACK_TOKEN, useValue: strategy }],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Configures a custom serializer for crossing the worker boundary.
|
|
358
|
+
*
|
|
359
|
+
* By default `WorkerHttpBackend` relies on the browser's structured clone algorithm
|
|
360
|
+
* (safe for plain objects, arrays, primitives, `Date`, `ArrayBuffer`).
|
|
361
|
+
* Use `withWorkerSerialization` when your request bodies contain types that
|
|
362
|
+
* structured clone cannot handle (e.g. class instances, circular references, `Map`, `Set`).
|
|
363
|
+
*
|
|
364
|
+
* **Worker-side note:** The serialized form is what the worker receives as `req.body`.
|
|
365
|
+
* If you use `createSerovalSerializer` or similar, add a worker-side interceptor
|
|
366
|
+
* to deserialize the body before calling `fetch()`.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
371
|
+
*
|
|
372
|
+
* provideWorkerHttpClient(
|
|
373
|
+
* withWorkerConfigs([...]),
|
|
374
|
+
* withWorkerSerialization(createSerovalSerializer()),
|
|
375
|
+
* )
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
function withWorkerSerialization(serializer) {
|
|
379
|
+
return {
|
|
380
|
+
kind: 'WorkerSerialization',
|
|
381
|
+
providers: [{ provide: WORKER_HTTP_SERIALIZER_TOKEN, useValue: serializer }],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Configures the worker-side interceptor pipeline from Angular DI.
|
|
386
|
+
*
|
|
387
|
+
* Specs are forwarded to each worker via an `init-interceptors` handshake
|
|
388
|
+
* message posted before any HTTP request. Workers must call
|
|
389
|
+
* `createConfigurableWorkerPipeline()` to receive and act on the handshake.
|
|
390
|
+
*
|
|
391
|
+
* Two shapes are accepted:
|
|
392
|
+
* - `WorkerInterceptorSpec[]` — applied to every registered worker
|
|
393
|
+
* - `Record<workerId, WorkerInterceptorSpec[]>` — per-worker, with the
|
|
394
|
+
* special `'*'` key applied to all workers in addition to the
|
|
395
|
+
* worker-specific specs
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* provideWorkerHttpClient(
|
|
400
|
+
* withWorkerConfigs([{ id: 'api', workerUrl: ... }]),
|
|
401
|
+
* withWorkerInterceptors([
|
|
402
|
+
* workerLogging(),
|
|
403
|
+
* workerRetry({ maxRetries: 3 }),
|
|
404
|
+
* workerCache({ ttl: 30_000 }),
|
|
405
|
+
* ]),
|
|
406
|
+
* );
|
|
407
|
+
* ```
|
|
408
|
+
*
|
|
409
|
+
* @example Per-worker specs
|
|
410
|
+
* ```typescript
|
|
411
|
+
* withWorkerInterceptors({
|
|
412
|
+
* '*': [workerLogging()],
|
|
413
|
+
* 'secure': [workerHmacSigning({ keyMaterial })],
|
|
414
|
+
* });
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
function withWorkerInterceptors(specs) {
|
|
418
|
+
const map = Array.isArray(specs)
|
|
419
|
+
? { '*': [...specs] }
|
|
420
|
+
: specs;
|
|
421
|
+
return {
|
|
422
|
+
kind: 'WorkerInterceptors',
|
|
423
|
+
providers: [{ provide: WORKER_HTTP_INTERCEPTORS_TOKEN, useValue: map }],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
1
427
|
/**
|
|
2
428
|
* Generated bundle index. Do not edit.
|
|
3
429
|
*/
|
|
430
|
+
|
|
431
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|