@angular-helpers/worker-http 0.6.0 → 1.0.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 +134 -9
- package/fesm2022/angular-helpers-worker-http-backend.mjs +42 -1
- package/fesm2022/angular-helpers-worker-http-crypto.mjs +5 -1
- package/fesm2022/angular-helpers-worker-http-esbuild-plugin.mjs +170 -0
- package/fesm2022/angular-helpers-worker-http-interceptors.mjs +13 -6
- package/fesm2022/angular-helpers-worker-http-streams-polyfill.mjs +125 -0
- package/fesm2022/angular-helpers-worker-http-transport.mjs +178 -10
- package/fesm2022/angular-helpers-worker-http.mjs +0 -17
- package/package.json +53 -30
- package/types/angular-helpers-worker-http-backend.d.ts +36 -2
- package/types/angular-helpers-worker-http-esbuild-plugin.d.ts +46 -0
- package/types/angular-helpers-worker-http-streams-polyfill.d.ts +34 -0
- package/types/angular-helpers-worker-http-transport.d.ts +75 -5
- package/types/angular-helpers-worker-http.d.ts +1 -15
package/README.md
CHANGED
|
@@ -16,13 +16,15 @@ On top of that, workers provide a natural isolation boundary for security-sensit
|
|
|
16
16
|
|
|
17
17
|
## Package map
|
|
18
18
|
|
|
19
|
-
| Entry point
|
|
20
|
-
|
|
|
21
|
-
| `@angular-helpers/worker-http/transport`
|
|
22
|
-
| `@angular-helpers/worker-http/serializer`
|
|
23
|
-
| `@angular-helpers/worker-http/interceptors`
|
|
24
|
-
| `@angular-helpers/worker-http/crypto`
|
|
25
|
-
| `@angular-helpers/worker-http/backend`
|
|
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
|
+
| `@angular-helpers/worker-http/esbuild-plugin` | esbuild plugin for auto-bundling interceptors into workers | ✅ Available |
|
|
27
|
+
| `@angular-helpers/worker-http/streams-polyfill` | Safari streams ponyfill for transferable stream support | ✅ Available |
|
|
26
28
|
|
|
27
29
|
---
|
|
28
30
|
|
|
@@ -43,6 +45,43 @@ Angular HttpClient createWorkerPipeline([
|
|
|
43
45
|
|
|
44
46
|
---
|
|
45
47
|
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### Quick setup with ng-add
|
|
51
|
+
|
|
52
|
+
The easiest way to get started is using the Angular CLI schematic:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ng add @angular-helpers/worker-http
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This will:
|
|
59
|
+
|
|
60
|
+
1. Install the package
|
|
61
|
+
2. Create a worker file at `src/app/workers/http-api.worker.ts`
|
|
62
|
+
3. Update `tsconfig.json` with webworker lib
|
|
63
|
+
4. Add `provideWorkerHttpClient()` to your `app.config.ts`
|
|
64
|
+
|
|
65
|
+
**Options:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Custom worker path
|
|
69
|
+
ng add @angular-helpers/worker-http --workerPath=src/workers/api.worker.ts
|
|
70
|
+
|
|
71
|
+
# Configure esbuild plugin (for custom build setups)
|
|
72
|
+
ng add @angular-helpers/worker-http --installEsbuildPlugin=true
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Manual installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @angular-helpers/worker-http
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then follow the setup in the `/backend` section below.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
46
85
|
## Entry points
|
|
47
86
|
|
|
48
87
|
### `/transport` — Typed RPC bridge
|
|
@@ -67,9 +106,40 @@ transport.terminate();
|
|
|
67
106
|
**Features:**
|
|
68
107
|
|
|
69
108
|
- 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
109
|
- Lazy worker instantiation — no worker created until first request
|
|
110
|
+
- **Cancellation that actually aborts `fetch()`** — unsubscribing posts a
|
|
111
|
+
cancel message; the worker-side message loop threads an `AbortSignal` all
|
|
112
|
+
the way into `fetch()` so the in-flight HTTP request is truly aborted
|
|
113
|
+
- **Per-request timeout** (default `30_000` ms) via `requestTimeout`; errors
|
|
114
|
+
with `WorkerHttpTimeoutError` and sends a cancel message to the worker.
|
|
115
|
+
Set to `0` to disable.
|
|
116
|
+
- **Opt-in transferable detection** via `transferDetection: 'auto'` — passes
|
|
117
|
+
detected `ArrayBuffer` / `MessagePort` / `ImageBitmap` /
|
|
118
|
+
`OffscreenCanvas` / streams as the transfer list of `postMessage`, enabling
|
|
119
|
+
zero-copy transfer of large buffers. Default is `'none'` to preserve the
|
|
120
|
+
caller's access to the original data after post.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import {
|
|
124
|
+
createWorkerTransport,
|
|
125
|
+
WorkerHttpTimeoutError,
|
|
126
|
+
} from '@angular-helpers/worker-http/transport';
|
|
127
|
+
|
|
128
|
+
const transport = createWorkerTransport({
|
|
129
|
+
workerUrl: new URL('./workers/api.worker', import.meta.url),
|
|
130
|
+
maxInstances: 2,
|
|
131
|
+
requestTimeout: 10_000, // override default 30 s
|
|
132
|
+
transferDetection: 'auto', // zero-copy ArrayBuffer at postMessage
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
transport.execute(request).subscribe({
|
|
136
|
+
error: (err) => {
|
|
137
|
+
if (err instanceof WorkerHttpTimeoutError) {
|
|
138
|
+
// dedicated timeout handling
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
73
143
|
|
|
74
144
|
---
|
|
75
145
|
|
|
@@ -479,6 +549,61 @@ interface WorkerHttpTelemetryEventBase {
|
|
|
479
549
|
|
|
480
550
|
---
|
|
481
551
|
|
|
552
|
+
### `/esbuild-plugin` — Interceptor auto-bundling
|
|
553
|
+
|
|
554
|
+
An esbuild plugin that automatically discovers and bundles interceptor files into your worker builds. When using Angular with a custom webpack/esbuild configuration, this ensures your interceptors are included in the worker bundle without manual imports.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
// esbuild.config.ts
|
|
558
|
+
import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
559
|
+
|
|
560
|
+
export default {
|
|
561
|
+
plugins: [
|
|
562
|
+
workerHttpPlugin({
|
|
563
|
+
// Explicit interceptors (relative to project root)
|
|
564
|
+
interceptors: ['./src/interceptors/auth.ts', './src/interceptors/logging.ts'],
|
|
565
|
+
|
|
566
|
+
// Or auto-discover all files matching interceptor naming pattern
|
|
567
|
+
autoDiscover: true,
|
|
568
|
+
}),
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Options:**
|
|
574
|
+
|
|
575
|
+
| Option | Type | Default | Description |
|
|
576
|
+
| -------------- | ---------- | ------- | ------------------------------------------------------ |
|
|
577
|
+
| `interceptors` | `string[]` | `[]` | Explicit list of interceptor paths to bundle |
|
|
578
|
+
| `autoDiscover` | `boolean` | `false` | Scan `src/` for files matching `*interceptor*` pattern |
|
|
579
|
+
|
|
580
|
+
Discovered interceptors are merged with explicit ones. Test files (`.spec.ts`, `.test.ts`) are automatically excluded.
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
### `/streams-polyfill` — Safari transferable streams
|
|
585
|
+
|
|
586
|
+
Safari 16-17 lack native transferable `ReadableStream`/`TransformStream` support. This ponyfill enables stream transfer in workers for those browsers, loaded lazily only when needed.
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// Enable in your app config (main thread)
|
|
590
|
+
import { withWorkerStreamsPolyfill } from '@angular-helpers/worker-http/backend';
|
|
591
|
+
|
|
592
|
+
provideWorkerHttpClient(
|
|
593
|
+
withWorkerConfigs([...]),
|
|
594
|
+
withWorkerStreamsPolyfill(), // Enable Safari 16-17 compatibility
|
|
595
|
+
);
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**When to use:**
|
|
599
|
+
|
|
600
|
+
- Your app uses `responseType: 'stream'` and targets Safari 16-17
|
|
601
|
+
- You see `DataCloneError` when transferring streams to/from workers
|
|
602
|
+
|
|
603
|
+
**Bundle impact:** Zero for modern browsers. The polyfill is lazy-loaded only on affected Safari versions when streams are actually used.
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
482
607
|
## SSR + hydration
|
|
483
608
|
|
|
484
609
|
Worker HTTP integrates transparently with Angular SSR. The two problems SSR
|
|
@@ -62,6 +62,15 @@ const WORKER_HTTP_INTERCEPTORS_TOKEN = new InjectionToken('WorkerHttpInterceptor
|
|
|
62
62
|
* affects the HTTP request).
|
|
63
63
|
*/
|
|
64
64
|
const WORKER_HTTP_TELEMETRY_TOKEN = new InjectionToken('WorkerHttpTelemetry', { factory: () => [] });
|
|
65
|
+
/**
|
|
66
|
+
* Enable Safari streams polyfill for transferable ReadableStream/TransformStream
|
|
67
|
+
* support in workers. Provided via `withWorkerStreamsPolyfill()`.
|
|
68
|
+
* Defaults to `false` (native streams only).
|
|
69
|
+
*
|
|
70
|
+
* When enabled, the transport dynamically imports `@angular-helpers/worker-http/streams-polyfill`
|
|
71
|
+
* on Safari 16-17 to enable stream transfer via postMessage.
|
|
72
|
+
*/
|
|
73
|
+
const WORKER_HTTP_STREAMS_POLYFILL_TOKEN = new InjectionToken('WorkerHttpStreamsPolyfill', { factory: () => false });
|
|
65
74
|
|
|
66
75
|
/**
|
|
67
76
|
* Converts an Angular `HttpRequest` into a structured-clone-safe POJO
|
|
@@ -156,6 +165,7 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
156
165
|
interceptorSpecs = inject(WORKER_HTTP_INTERCEPTORS_TOKEN);
|
|
157
166
|
fetchBackend = inject(FetchBackend, { optional: true });
|
|
158
167
|
telemetry = inject(WORKER_HTTP_TELEMETRY_TOKEN);
|
|
168
|
+
streamsPolyfill = inject(WORKER_HTTP_STREAMS_POLYFILL_TOKEN);
|
|
159
169
|
transports = new Map();
|
|
160
170
|
handle(req) {
|
|
161
171
|
if (typeof Worker === 'undefined') {
|
|
@@ -208,6 +218,7 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
208
218
|
workerFactory: () => new Worker(config.workerUrl, { type: 'module' }),
|
|
209
219
|
maxInstances: config.maxInstances ?? 1,
|
|
210
220
|
initMessage: specs.length > 0 ? { type: 'init-interceptors', specs } : undefined,
|
|
221
|
+
streamsPolyfill: this.streamsPolyfill,
|
|
211
222
|
});
|
|
212
223
|
this.transports.set(config.id, transport);
|
|
213
224
|
return transport;
|
|
@@ -285,6 +296,7 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
285
296
|
}
|
|
286
297
|
catch (telemetryError) {
|
|
287
298
|
// A throwing telemetry subscriber must never affect the HTTP request.
|
|
299
|
+
// oxlint-disable-next-line no-console -- defensive log when user-provided telemetry throws
|
|
288
300
|
console.error('[WorkerHttpBackend] telemetry subscriber threw:', telemetryError);
|
|
289
301
|
}
|
|
290
302
|
}
|
|
@@ -557,9 +569,38 @@ function withTelemetry(telemetry) {
|
|
|
557
569
|
providers: [{ provide: WORKER_HTTP_TELEMETRY_TOKEN, useValue: telemetry, multi: true }],
|
|
558
570
|
};
|
|
559
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Enables the Safari streams polyfill for transferable ReadableStream/TransformStream
|
|
574
|
+
* support in Web Workers.
|
|
575
|
+
*
|
|
576
|
+
* Safari 16-17 lacks native transferable streams. When this feature is enabled,
|
|
577
|
+
* the transport layer dynamically loads a ponyfill that enables stream transfer
|
|
578
|
+
* via postMessage on affected browsers.
|
|
579
|
+
*
|
|
580
|
+
* **When to use:**
|
|
581
|
+
* - Your application uses `responseType: 'stream'` and targets Safari 16-17
|
|
582
|
+
* - You see "DataCloneError" when transferring streams to/from workers
|
|
583
|
+
*
|
|
584
|
+
* **Bundle impact:** The polyfill is lazy-loaded only on Safari 16-17 when
|
|
585
|
+
* streams are actually used. Non-Safari browsers and modern Safari pay 0 bytes.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* provideWorkerHttpClient(
|
|
590
|
+
* withWorkerConfigs([...]),
|
|
591
|
+
* withWorkerStreamsPolyfill(), // Enable for Safari compatibility
|
|
592
|
+
* )
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
function withWorkerStreamsPolyfill() {
|
|
596
|
+
return {
|
|
597
|
+
kind: 'StreamsPolyfill',
|
|
598
|
+
providers: [{ provide: WORKER_HTTP_STREAMS_POLYFILL_TOKEN, useValue: true }],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
560
601
|
|
|
561
602
|
/**
|
|
562
603
|
* Generated bundle index. Do not edit.
|
|
563
604
|
*/
|
|
564
605
|
|
|
565
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
606
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
@@ -80,9 +80,13 @@ async function createAesEncryptor(config) {
|
|
|
80
80
|
? new Uint8Array(config.keyMaterial.buffer.slice(0))
|
|
81
81
|
: new Uint8Array(config.keyMaterial);
|
|
82
82
|
const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: algorithm, length: keyLength }, false, ['encrypt', 'decrypt']);
|
|
83
|
+
// AES-GCM uses a 96-bit (12-byte) IV per NIST SP 800-38D recommendation.
|
|
84
|
+
// AES-CBC and AES-CTR require exactly one block (16 bytes) for their IV /
|
|
85
|
+
// counter respectively; passing 12 bytes causes `OperationError`.
|
|
86
|
+
const ivLength = algorithm === 'AES-GCM' ? 12 : 16;
|
|
83
87
|
return {
|
|
84
88
|
async encrypt(data) {
|
|
85
|
-
const iv = crypto.getRandomValues(new Uint8Array(
|
|
89
|
+
const iv = crypto.getRandomValues(new Uint8Array(ivLength));
|
|
86
90
|
const params = algorithm === 'AES-GCM'
|
|
87
91
|
? { name: algorithm, iv }
|
|
88
92
|
: algorithm === 'AES-CBC'
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* esbuild plugin for worker-http that bundles interceptors into worker files.
|
|
6
|
+
*
|
|
7
|
+
* This plugin:
|
|
8
|
+
* 1. Intercepts worker file builds
|
|
9
|
+
* 2. Discovers interceptor files if autoDiscover is true
|
|
10
|
+
* 3. Injects interceptor imports into the worker bootstrap
|
|
11
|
+
* 4. Ensures interceptors are available in the worker's interceptor pipeline
|
|
12
|
+
*
|
|
13
|
+
* @param options - Plugin configuration
|
|
14
|
+
* @returns esbuild Plugin
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
19
|
+
*
|
|
20
|
+
* const config = {
|
|
21
|
+
* plugins: [
|
|
22
|
+
* workerHttpPlugin({
|
|
23
|
+
* autoDiscover: true,
|
|
24
|
+
* })
|
|
25
|
+
* ]
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function workerHttpPlugin(options = {}) {
|
|
30
|
+
const { interceptors = [], autoDiscover = false } = options;
|
|
31
|
+
return {
|
|
32
|
+
name: 'worker-http',
|
|
33
|
+
setup(build) {
|
|
34
|
+
const filter = /\.worker\.(ts|js|mjs)$/;
|
|
35
|
+
const discoveredInterceptors = [];
|
|
36
|
+
// Auto-discover interceptors from src directory
|
|
37
|
+
if (autoDiscover) {
|
|
38
|
+
const rootDir = build.initialOptions.absWorkingDir ?? process.cwd();
|
|
39
|
+
discoveredInterceptors.push(...discoverInterceptors(rootDir));
|
|
40
|
+
}
|
|
41
|
+
const allInterceptors = [...new Set([...interceptors, ...discoveredInterceptors])];
|
|
42
|
+
// Intercept worker file loads to inject interceptor imports
|
|
43
|
+
build.onLoad({ filter }, async (args) => {
|
|
44
|
+
let contents = await fs.promises.readFile(args.path, 'utf-8');
|
|
45
|
+
if (allInterceptors.length > 0) {
|
|
46
|
+
const importStatements = generateInterceptorImports(allInterceptors, args.path);
|
|
47
|
+
contents = injectImports(contents, importStatements);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
contents,
|
|
51
|
+
loader: 'ts',
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Discovers interceptor files by scanning src directories.
|
|
59
|
+
* Looks for files matching interceptor naming patterns
|
|
60
|
+
* in TypeScript or JavaScript files.
|
|
61
|
+
*/
|
|
62
|
+
function discoverInterceptors(rootDir) {
|
|
63
|
+
const interceptors = [];
|
|
64
|
+
const srcDir = path.join(rootDir, 'src');
|
|
65
|
+
if (!fs.existsSync(srcDir)) {
|
|
66
|
+
return interceptors;
|
|
67
|
+
}
|
|
68
|
+
scanDirectory(srcDir, interceptors, rootDir);
|
|
69
|
+
return interceptors;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Recursively scans a directory for interceptor files.
|
|
73
|
+
*/
|
|
74
|
+
function scanDirectory(dir, interceptors, rootDir) {
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
// Skip node_modules and dist
|
|
80
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
scanDirectory(fullPath, interceptors, rootDir);
|
|
84
|
+
}
|
|
85
|
+
else if (entry.isFile()) {
|
|
86
|
+
const isInterceptorFile = /interceptor/i.test(entry.name) &&
|
|
87
|
+
(entry.name.endsWith('.ts') || entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) &&
|
|
88
|
+
!entry.name.endsWith('.spec.ts') &&
|
|
89
|
+
!entry.name.endsWith('.test.ts') &&
|
|
90
|
+
!entry.name.endsWith('.d.ts');
|
|
91
|
+
if (isInterceptorFile) {
|
|
92
|
+
// Get relative path from project root
|
|
93
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
94
|
+
interceptors.push('./' + relativePath.replace(/\\/g, '/'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generates import statements for discovered interceptors.
|
|
101
|
+
* Converts relative paths to valid import specifiers.
|
|
102
|
+
*/
|
|
103
|
+
function generateInterceptorImports(interceptorPaths, workerFilePath) {
|
|
104
|
+
return interceptorPaths.map((interceptorPath, index) => {
|
|
105
|
+
// Create a valid identifier from the path
|
|
106
|
+
const identifier = `interceptor_${index}_${interceptorPath.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
107
|
+
return `import ${identifier} from '${interceptorPath}';`;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Injects import statements at the top of the worker file.
|
|
112
|
+
* Preserves any existing imports.
|
|
113
|
+
*/
|
|
114
|
+
function injectImports(contents, importStatements) {
|
|
115
|
+
if (importStatements.length === 0) {
|
|
116
|
+
return contents;
|
|
117
|
+
}
|
|
118
|
+
const imports = importStatements.join('\n');
|
|
119
|
+
const marker = '// Auto-injected by worker-http esbuild plugin';
|
|
120
|
+
// Check if already injected
|
|
121
|
+
if (contents.includes(marker)) {
|
|
122
|
+
return contents;
|
|
123
|
+
}
|
|
124
|
+
const injection = `${marker}\n${imports}\n`;
|
|
125
|
+
// Find the first non-comment, non-whitespace line to insert before
|
|
126
|
+
const lines = contents.split('\n');
|
|
127
|
+
let insertIndex = 0;
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i].trim();
|
|
130
|
+
// Skip shebang, comments, and empty lines
|
|
131
|
+
if (line.startsWith('#!') ||
|
|
132
|
+
line.startsWith('//') ||
|
|
133
|
+
line.startsWith('/*') ||
|
|
134
|
+
line.startsWith('*') ||
|
|
135
|
+
line === '') {
|
|
136
|
+
insertIndex = i + 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
lines.splice(insertIndex, 0, injection);
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* esbuild plugin for @angular-helpers/worker-http
|
|
147
|
+
*
|
|
148
|
+
* Automatically bundles worker interceptors and injects them into the worker bootstrap.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* // angular.json custom webpack config
|
|
153
|
+
* import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
154
|
+
*
|
|
155
|
+
* export default {
|
|
156
|
+
* plugins: [
|
|
157
|
+
* workerHttpPlugin({
|
|
158
|
+
* interceptors: ['./src/interceptors/auth.ts'],
|
|
159
|
+
* autoDiscover: true
|
|
160
|
+
* })
|
|
161
|
+
* ]
|
|
162
|
+
* };
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generated bundle index. Do not edit.
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
export { workerHttpPlugin };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composes interceptor functions around a final handler, producing a single
|
|
3
|
-
* `(req) => Promise<resp>` chain. Pure — no side effects.
|
|
3
|
+
* `(req, signal?) => Promise<resp>` chain. Pure — no side effects.
|
|
4
|
+
*
|
|
5
|
+
* The optional `signal` is threaded through interceptor boundaries automatically
|
|
6
|
+
* via a wrapper around `next`, so legacy `WorkerInterceptorFn` implementations
|
|
7
|
+
* (which take only `req, next`) keep working and still propagate cancellation
|
|
8
|
+
* when they invoke `next(req)`.
|
|
4
9
|
*/
|
|
5
10
|
function buildChain(fns, finalHandler) {
|
|
6
|
-
return fns.reduceRight((next, interceptor) => (req) => interceptor(req, next), finalHandler);
|
|
11
|
+
return fns.reduceRight((next, interceptor) => (req, signal) => interceptor(req, (r) => next(r, signal)), finalHandler);
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* Performs the actual `fetch()` call inside the worker, translating the
|
|
@@ -98,7 +103,7 @@ function attachRequestLoop(chain) {
|
|
|
98
103
|
const controller = new AbortController();
|
|
99
104
|
controllers.set(requestId, controller);
|
|
100
105
|
try {
|
|
101
|
-
const response = await chain(payload);
|
|
106
|
+
const response = await chain(payload, controller.signal);
|
|
102
107
|
self.postMessage({ type: 'response', requestId, result: response });
|
|
103
108
|
}
|
|
104
109
|
catch (error) {
|
|
@@ -145,7 +150,7 @@ function attachRequestLoop(chain) {
|
|
|
145
150
|
* ```
|
|
146
151
|
*/
|
|
147
152
|
function createWorkerPipeline(interceptors) {
|
|
148
|
-
const chain = buildChain(interceptors, (req) => executeFetch(req));
|
|
153
|
+
const chain = buildChain(interceptors, (req, signal) => executeFetch(req, signal));
|
|
149
154
|
attachRequestLoop(chain);
|
|
150
155
|
}
|
|
151
156
|
|
|
@@ -325,7 +330,9 @@ function hmacSigningInterceptor(config) {
|
|
|
325
330
|
* ```
|
|
326
331
|
*/
|
|
327
332
|
function loggingInterceptor(config) {
|
|
328
|
-
const logger = config?.logger ??
|
|
333
|
+
const logger = config?.logger ??
|
|
334
|
+
// oxlint-disable-next-line no-console -- default logger of a logging interceptor; consumers inject their own via config.logger
|
|
335
|
+
((msg, data) => console.log(msg, data));
|
|
329
336
|
const includeHeaders = config?.includeHeaders ?? false;
|
|
330
337
|
function safeLog(message, data) {
|
|
331
338
|
try {
|
|
@@ -538,7 +545,7 @@ function createConfigurableWorkerPipeline() {
|
|
|
538
545
|
if (data.type === INIT_MESSAGE_TYPE) {
|
|
539
546
|
const specs = data.specs ?? [];
|
|
540
547
|
const fns = specs.map((spec) => resolveSpec(spec));
|
|
541
|
-
const chain = buildChain(fns, (req) => executeFetch(req));
|
|
548
|
+
const chain = buildChain(fns, (req, signal) => executeFetch(req, signal));
|
|
542
549
|
// Swap to the regular request loop and replay any buffered messages.
|
|
543
550
|
attachRequestLoop(chain);
|
|
544
551
|
const handler = self.onmessage;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if the current browser needs the streams ponyfill.
|
|
3
|
+
*
|
|
4
|
+
* Safari 16-17 fails to transfer `ReadableStream`/`TransformStream`
|
|
5
|
+
* to/from Web Workers via `structuredClone` because they lack
|
|
6
|
+
* the transferable streams implementation.
|
|
7
|
+
*
|
|
8
|
+
* @param userAgent - Optional user agent string for testing (defaults to feature detection)
|
|
9
|
+
* @returns `true` if ponyfill is needed, `false` if native works
|
|
10
|
+
*/
|
|
11
|
+
function needsPolyfill(userAgent) {
|
|
12
|
+
// If userAgent provided (testing mode), use UA-based detection
|
|
13
|
+
if (userAgent !== undefined) {
|
|
14
|
+
return isLegacySafari(userAgent);
|
|
15
|
+
}
|
|
16
|
+
// Quick check: if we're not in a browser context, assume no polyfill needed
|
|
17
|
+
if (typeof ReadableStream === 'undefined') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const rs = new ReadableStream({
|
|
22
|
+
start(controller) {
|
|
23
|
+
controller.close();
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Safari < 18 lacks the transferable streams spec extension
|
|
27
|
+
// The presence of 'getReader' alone isn't enough — we need to verify
|
|
28
|
+
// that the stream can be transferred via postMessage
|
|
29
|
+
const channel = new MessageChannel();
|
|
30
|
+
let transferable = true;
|
|
31
|
+
// Try to transfer — this throws on Safari 16-17
|
|
32
|
+
try {
|
|
33
|
+
channel.port1.postMessage(rs, [rs]);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
transferable = false;
|
|
37
|
+
}
|
|
38
|
+
channel.port1.close();
|
|
39
|
+
channel.port2.close();
|
|
40
|
+
return !transferable;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Any error means we should try polyfill
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* User-agent based detection for Safari.
|
|
49
|
+
* Used as a fast-path before the more expensive transfer test.
|
|
50
|
+
*
|
|
51
|
+
* @param userAgent - Optional user agent string (defaults to navigator.userAgent)
|
|
52
|
+
*/
|
|
53
|
+
function isSafari(userAgent) {
|
|
54
|
+
const ua = userAgent ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '');
|
|
55
|
+
return /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if Safari version is known to need polyfill (< 18).
|
|
59
|
+
*
|
|
60
|
+
* @param userAgent - Optional user agent string (defaults to navigator.userAgent)
|
|
61
|
+
*/
|
|
62
|
+
function isLegacySafari(userAgent) {
|
|
63
|
+
if (!isSafari(userAgent)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const ua = userAgent ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '');
|
|
67
|
+
const match = /Version\/(\d+)/.exec(ua);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const version = Number.parseInt(match[1], 10);
|
|
72
|
+
return version < 18;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ponyfill for Web Streams API that supports transfer to/from workers.
|
|
77
|
+
*
|
|
78
|
+
* Uses `web-streams-polyfill` internally but only loads it when needed.
|
|
79
|
+
* This keeps bundle size small for non-Safari browsers.
|
|
80
|
+
*
|
|
81
|
+
* @see {@link needsPolyfill} for detection
|
|
82
|
+
*/
|
|
83
|
+
let cachedPonyfill = null;
|
|
84
|
+
/**
|
|
85
|
+
* Lazily loads the web-streams-polyfill ponyfill.
|
|
86
|
+
*
|
|
87
|
+
* @returns Ponyfilled streams or native if not needed
|
|
88
|
+
*/
|
|
89
|
+
async function ponyfillStreams() {
|
|
90
|
+
if (cachedPonyfill) {
|
|
91
|
+
return cachedPonyfill;
|
|
92
|
+
}
|
|
93
|
+
// Dynamic import to avoid bundling polyfill for non-Safari users
|
|
94
|
+
const { ReadableStream, TransformStream, WritableStream } = await import('web-streams-polyfill');
|
|
95
|
+
cachedPonyfill = {
|
|
96
|
+
ReadableStream,
|
|
97
|
+
TransformStream,
|
|
98
|
+
WritableStream,
|
|
99
|
+
};
|
|
100
|
+
return cachedPonyfill;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Safari Transferable Streams Ponyfill
|
|
105
|
+
*
|
|
106
|
+
* Provides `ReadableStream` and `TransformStream` implementations that support
|
|
107
|
+
* `structuredClone` transfer to/from Web Workers on Safari 16-17.
|
|
108
|
+
*
|
|
109
|
+
* This is a **ponyfill** (not a polyfill) — it does not modify global scope.
|
|
110
|
+
* Native implementations are re-exported when available.
|
|
111
|
+
*
|
|
112
|
+
* Usage:
|
|
113
|
+
* ```typescript
|
|
114
|
+
* import '@angular-helpers/worker-http/streams-polyfill';
|
|
115
|
+
* ```
|
|
116
|
+
* Or let the transport auto-inject when `safariPolyfill: true` is configured.
|
|
117
|
+
*
|
|
118
|
+
* @see {@link detect} for feature detection
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generated bundle index. Do not edit.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
export { needsPolyfill, ponyfillStreams };
|
|
@@ -1,14 +1,118 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Scans a payload one level deep and collects every `Transferable` instance
|
|
5
|
+
* (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
|
|
6
|
+
* WritableStream, TransformStream) found in its own enumerable properties.
|
|
7
|
+
*
|
|
8
|
+
* Used by `createWorkerTransport` when `transferDetection === 'auto'` to build
|
|
9
|
+
* the second argument of `worker.postMessage(data, transfer)` so large buffers
|
|
10
|
+
* move zero-copy instead of being structured-cloned.
|
|
11
|
+
*
|
|
12
|
+
* Design notes:
|
|
13
|
+
* - Only one level deep by design: deep traversal has quadratic cost on heavy
|
|
14
|
+
* graphs and makes the transfer list surprising. Real payloads that care
|
|
15
|
+
* about zero-copy put the buffer at the top level.
|
|
16
|
+
* - Duplicates are filtered — the same buffer referenced twice is transferred
|
|
17
|
+
* only once (required by the structured-clone algorithm).
|
|
18
|
+
* - Returns an empty array for primitives, plain serializable values, or when
|
|
19
|
+
* no transferable is found; `postMessage` accepts `[]` safely.
|
|
20
|
+
*/
|
|
21
|
+
function detectTransferables(payload) {
|
|
22
|
+
if (payload === null || payload === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
if (typeof payload !== 'object')
|
|
25
|
+
return [];
|
|
26
|
+
const found = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const collect = (value) => {
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return;
|
|
31
|
+
if (isTransferable(value)) {
|
|
32
|
+
if (!seen.has(value)) {
|
|
33
|
+
seen.add(value);
|
|
34
|
+
found.push(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (isTransferable(payload)) {
|
|
39
|
+
collect(payload);
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(payload)) {
|
|
43
|
+
for (const item of payload)
|
|
44
|
+
collect(item);
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(payload)) {
|
|
48
|
+
collect(payload[key]);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
function isTransferable(value) {
|
|
53
|
+
if (value === null || value === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
if (typeof value !== 'object')
|
|
56
|
+
return false;
|
|
57
|
+
// ArrayBuffer is the common case; check first.
|
|
58
|
+
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer)
|
|
59
|
+
return true;
|
|
60
|
+
// Typed-array views carry an underlying buffer but are NOT Transferable —
|
|
61
|
+
// only the buffer is. Caller must pass `.buffer` explicitly if they want it.
|
|
62
|
+
if (typeof MessagePort !== 'undefined' && value instanceof MessagePort)
|
|
63
|
+
return true;
|
|
64
|
+
if (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap)
|
|
65
|
+
return true;
|
|
66
|
+
if (typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)
|
|
67
|
+
return true;
|
|
68
|
+
if (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream)
|
|
69
|
+
return true;
|
|
70
|
+
if (typeof WritableStream !== 'undefined' && value instanceof WritableStream)
|
|
71
|
+
return true;
|
|
72
|
+
if (typeof TransformStream !== 'undefined' && value instanceof TransformStream)
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown by `createWorkerTransport` when a request exceeds its configured
|
|
79
|
+
* `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
|
|
80
|
+
* timeout rejections from transport/worker errors.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* transport.execute(req).subscribe({
|
|
85
|
+
* error: (err) => {
|
|
86
|
+
* if (err instanceof WorkerHttpTimeoutError) {
|
|
87
|
+
* // dedicated timeout handling
|
|
88
|
+
* }
|
|
89
|
+
* },
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
class WorkerHttpTimeoutError extends Error {
|
|
94
|
+
name = 'WorkerHttpTimeoutError';
|
|
95
|
+
timeoutMs;
|
|
96
|
+
constructor(timeoutMs) {
|
|
97
|
+
super(`Worker request timed out after ${timeoutMs} ms`);
|
|
98
|
+
this.timeoutMs = timeoutMs;
|
|
99
|
+
// Maintain a proper prototype chain across TS transpile targets.
|
|
100
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
3
105
|
/**
|
|
4
106
|
* Creates a typed, Observable-based transport for communicating with a web worker.
|
|
5
107
|
*
|
|
6
108
|
* Features:
|
|
7
109
|
* - Request/response correlation via `requestId`
|
|
8
|
-
* -
|
|
110
|
+
* - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
|
|
111
|
+
* - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
|
|
9
112
|
* - Optional worker pool with round-robin dispatch
|
|
10
113
|
* - Lazy worker creation (default)
|
|
11
|
-
* -
|
|
114
|
+
* - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
|
|
115
|
+
* `ArrayBuffer` / stream payloads
|
|
12
116
|
*
|
|
13
117
|
* @example
|
|
14
118
|
* ```typescript
|
|
@@ -28,6 +132,10 @@ function createWorkerTransport(config) {
|
|
|
28
132
|
let roundRobinIndex = 0;
|
|
29
133
|
let terminated = false;
|
|
30
134
|
const maxInstances = Math.min(config.maxInstances ?? 1, typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 1);
|
|
135
|
+
const requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
136
|
+
const transferDetection = config.transferDetection ?? 'none';
|
|
137
|
+
const streamsPolyfill = config.streamsPolyfill ?? false;
|
|
138
|
+
let polyfillLoaded = false;
|
|
31
139
|
function createWorker() {
|
|
32
140
|
if (config.workerFactory) {
|
|
33
141
|
return config.workerFactory();
|
|
@@ -61,13 +169,31 @@ function createWorkerTransport(config) {
|
|
|
61
169
|
}
|
|
62
170
|
const requestId = crypto.randomUUID();
|
|
63
171
|
return new Observable((subscriber) => {
|
|
172
|
+
// Lazy-load polyfill on first request if enabled
|
|
173
|
+
if (streamsPolyfill && !polyfillLoaded) {
|
|
174
|
+
loadStreamsPolyfill().catch((err) => {
|
|
175
|
+
console.warn('[worker-http] Streams polyfill failed to load:', err);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
64
178
|
const worker = getOrCreateWorker();
|
|
179
|
+
let settled = false;
|
|
180
|
+
let timeoutHandle;
|
|
181
|
+
const cleanup = () => {
|
|
182
|
+
worker.removeEventListener('message', messageHandler);
|
|
183
|
+
worker.removeEventListener('error', errorHandler);
|
|
184
|
+
if (timeoutHandle !== undefined) {
|
|
185
|
+
clearTimeout(timeoutHandle);
|
|
186
|
+
timeoutHandle = undefined;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
65
189
|
const messageHandler = (event) => {
|
|
66
190
|
const data = event.data;
|
|
67
191
|
if (data.requestId !== requestId)
|
|
68
192
|
return;
|
|
69
|
-
|
|
70
|
-
|
|
193
|
+
if (settled)
|
|
194
|
+
return;
|
|
195
|
+
settled = true;
|
|
196
|
+
cleanup();
|
|
71
197
|
if (data.type === 'error') {
|
|
72
198
|
const err = data.error;
|
|
73
199
|
subscriber.error(new Error(err.message));
|
|
@@ -78,21 +204,63 @@ function createWorkerTransport(config) {
|
|
|
78
204
|
}
|
|
79
205
|
};
|
|
80
206
|
const errorHandler = (event) => {
|
|
81
|
-
|
|
82
|
-
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
210
|
+
cleanup();
|
|
83
211
|
subscriber.error(new Error(event.message ?? 'Worker error'));
|
|
84
212
|
};
|
|
85
213
|
worker.addEventListener('message', messageHandler);
|
|
86
214
|
worker.addEventListener('error', errorHandler);
|
|
87
|
-
|
|
215
|
+
if (transferDetection === 'auto') {
|
|
216
|
+
const transferables = detectTransferables(request);
|
|
217
|
+
worker.postMessage({ type: 'request', requestId, payload: request }, transferables);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
worker.postMessage({ type: 'request', requestId, payload: request });
|
|
221
|
+
}
|
|
222
|
+
if (requestTimeout > 0 && Number.isFinite(requestTimeout)) {
|
|
223
|
+
timeoutHandle = setTimeout(() => {
|
|
224
|
+
if (settled)
|
|
225
|
+
return;
|
|
226
|
+
settled = true;
|
|
227
|
+
cleanup();
|
|
228
|
+
// Ask the worker to abort any in-flight work for this id. The
|
|
229
|
+
// cancellation fix wires this through to `fetch()`.
|
|
230
|
+
worker.postMessage({ type: 'cancel', requestId });
|
|
231
|
+
subscriber.error(new WorkerHttpTimeoutError(requestTimeout));
|
|
232
|
+
}, requestTimeout);
|
|
233
|
+
}
|
|
88
234
|
// Teardown: send cancel message on unsubscribe
|
|
89
235
|
return () => {
|
|
90
|
-
|
|
91
|
-
|
|
236
|
+
if (settled)
|
|
237
|
+
return;
|
|
238
|
+
settled = true;
|
|
239
|
+
cleanup();
|
|
92
240
|
worker.postMessage({ type: 'cancel', requestId });
|
|
93
241
|
};
|
|
94
242
|
});
|
|
95
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Lazy-loads the streams polyfill if enabled and not already loaded.
|
|
246
|
+
* Called before operations that might involve stream transfer.
|
|
247
|
+
*/
|
|
248
|
+
async function loadStreamsPolyfill() {
|
|
249
|
+
if (!streamsPolyfill || polyfillLoaded) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const { needsPolyfill, ponyfillStreams } = await import('@angular-helpers/worker-http/streams-polyfill');
|
|
254
|
+
if (needsPolyfill()) {
|
|
255
|
+
await ponyfillStreams();
|
|
256
|
+
}
|
|
257
|
+
polyfillLoaded = true;
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
// Log warning but don't block request — streams will fail naturally
|
|
261
|
+
console.warn('[worker-http] Failed to load streams polyfill:', err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
96
264
|
function terminate() {
|
|
97
265
|
terminated = true;
|
|
98
266
|
for (const worker of workers) {
|
|
@@ -116,4 +284,4 @@ function createWorkerTransport(config) {
|
|
|
116
284
|
* Generated bundle index. Do not edit.
|
|
117
285
|
*/
|
|
118
286
|
|
|
119
|
-
export { createWorkerTransport };
|
|
287
|
+
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @angular-helpers/worker-http
|
|
3
|
-
*
|
|
4
|
-
* Angular HTTP over Web Workers — off-main-thread HTTP pipelines
|
|
5
|
-
* with configurable interceptors, WebCrypto security, and pluggable serialization.
|
|
6
|
-
*
|
|
7
|
-
* Sub-entry points:
|
|
8
|
-
* - @angular-helpers/worker-http/transport (P1: typed RPC bridge)
|
|
9
|
-
* - @angular-helpers/worker-http/serializer (P2: TOON, seroval, auto-detect)
|
|
10
|
-
* - @angular-helpers/worker-http/backend (P3: Angular HttpBackend replacement)
|
|
11
|
-
* - @angular-helpers/worker-http/interceptors (P4: pure-fn interceptors for workers)
|
|
12
|
-
* - @angular-helpers/worker-http/crypto (P5: WebCrypto primitives)
|
|
13
|
-
*/
|
|
14
|
-
const WORKER_HTTP_VERSION = '0.0.1';
|
|
15
|
-
|
|
16
1
|
/**
|
|
17
2
|
* Generated bundle index. Do not edit.
|
|
18
3
|
*/
|
|
19
|
-
|
|
20
|
-
export { WORKER_HTTP_VERSION };
|
package/package.json
CHANGED
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/worker-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Angular HTTP over Web Workers — off-main-thread HTTP pipelines with configurable interceptors, WebCrypto security, and pluggable serialization",
|
|
5
|
+
"schematics": "./schematics/collection.json",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types/angular-helpers-worker-http.d.ts",
|
|
9
|
+
"esm": "./esm/src/index.js",
|
|
10
|
+
"default": "./fesm2022/angular-helpers-worker-http.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./backend": {
|
|
13
|
+
"types": "./types/angular-helpers-worker-http-backend.d.ts",
|
|
14
|
+
"esm": "./esm/backend/src/index.js",
|
|
15
|
+
"default": "./fesm2022/angular-helpers-worker-http-backend.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./transport": {
|
|
18
|
+
"types": "./types/angular-helpers-worker-http-transport.d.ts",
|
|
19
|
+
"esm": "./esm/transport/src/index.js",
|
|
20
|
+
"default": "./fesm2022/angular-helpers-worker-http-transport.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./interceptors": {
|
|
23
|
+
"types": "./types/angular-helpers-worker-http-interceptors.d.ts",
|
|
24
|
+
"esm": "./esm/interceptors/src/index.js",
|
|
25
|
+
"default": "./fesm2022/angular-helpers-worker-http-interceptors.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./crypto": {
|
|
28
|
+
"types": "./types/angular-helpers-worker-http-crypto.d.ts",
|
|
29
|
+
"esm": "./esm/crypto/src/index.js",
|
|
30
|
+
"default": "./fesm2022/angular-helpers-worker-http-crypto.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./serializer": {
|
|
33
|
+
"types": "./types/angular-helpers-worker-http-serializer.d.ts",
|
|
34
|
+
"esm": "./esm/serializer/src/index.js",
|
|
35
|
+
"default": "./fesm2022/angular-helpers-worker-http-serializer.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./esbuild-plugin": {
|
|
38
|
+
"types": "./types/angular-helpers-worker-http-esbuild-plugin.d.ts",
|
|
39
|
+
"esm": "./esm/esbuild-plugin/src/index.js",
|
|
40
|
+
"default": "./fesm2022/angular-helpers-worker-http-esbuild-plugin.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./streams-polyfill": {
|
|
43
|
+
"types": "./types/angular-helpers-worker-http-streams-polyfill.d.ts",
|
|
44
|
+
"esm": "./esm/streams-polyfill/src/index.js",
|
|
45
|
+
"default": "./fesm2022/angular-helpers-worker-http-streams-polyfill.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": {
|
|
48
|
+
"default": "./package.json"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
5
51
|
"keywords": [
|
|
6
52
|
"angular",
|
|
7
53
|
"web-worker",
|
|
@@ -41,39 +87,16 @@
|
|
|
41
87
|
},
|
|
42
88
|
"seroval": {
|
|
43
89
|
"optional": true
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"module": "fesm2022/angular-helpers-worker-http.mjs",
|
|
47
|
-
"typings": "types/angular-helpers-worker-http.d.ts",
|
|
48
|
-
"exports": {
|
|
49
|
-
"./package.json": {
|
|
50
|
-
"default": "./package.json"
|
|
51
|
-
},
|
|
52
|
-
".": {
|
|
53
|
-
"types": "./types/angular-helpers-worker-http.d.ts",
|
|
54
|
-
"default": "./fesm2022/angular-helpers-worker-http.mjs"
|
|
55
|
-
},
|
|
56
|
-
"./backend": {
|
|
57
|
-
"types": "./types/angular-helpers-worker-http-backend.d.ts",
|
|
58
|
-
"default": "./fesm2022/angular-helpers-worker-http-backend.mjs"
|
|
59
|
-
},
|
|
60
|
-
"./crypto": {
|
|
61
|
-
"types": "./types/angular-helpers-worker-http-crypto.d.ts",
|
|
62
|
-
"default": "./fesm2022/angular-helpers-worker-http-crypto.mjs"
|
|
63
|
-
},
|
|
64
|
-
"./interceptors": {
|
|
65
|
-
"types": "./types/angular-helpers-worker-http-interceptors.d.ts",
|
|
66
|
-
"default": "./fesm2022/angular-helpers-worker-http-interceptors.mjs"
|
|
67
90
|
},
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"default": "./fesm2022/angular-helpers-worker-http-serializer.mjs"
|
|
91
|
+
"esbuild": {
|
|
92
|
+
"optional": true
|
|
71
93
|
},
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"default": "./fesm2022/angular-helpers-worker-http-transport.mjs"
|
|
94
|
+
"web-streams-polyfill": {
|
|
95
|
+
"optional": true
|
|
75
96
|
}
|
|
76
97
|
},
|
|
98
|
+
"module": "fesm2022/angular-helpers-worker-http.mjs",
|
|
99
|
+
"typings": "types/angular-helpers-worker-http.d.ts",
|
|
77
100
|
"sideEffects": false,
|
|
78
101
|
"dependencies": {
|
|
79
102
|
"tslib": "^2.3.0"
|
|
@@ -9,7 +9,7 @@ import { Observable } from 'rxjs';
|
|
|
9
9
|
* Discriminated union for worker HTTP feature kinds.
|
|
10
10
|
* Mirrors Angular's HttpFeatureKind pattern.
|
|
11
11
|
*/
|
|
12
|
-
type WorkerHttpFeatureKind = 'WorkerConfigs' | 'WorkerRoutes' | 'WorkerFallback' | 'WorkerSerialization' | 'WorkerInterceptors' | 'Telemetry';
|
|
12
|
+
type WorkerHttpFeatureKind = 'WorkerConfigs' | 'WorkerRoutes' | 'WorkerFallback' | 'WorkerSerialization' | 'WorkerInterceptors' | 'Telemetry' | 'StreamsPolyfill';
|
|
13
13
|
/**
|
|
14
14
|
* Feature object — mirrors Angular's HttpFeature<K> shape.
|
|
15
15
|
*/
|
|
@@ -182,6 +182,15 @@ declare const WORKER_HTTP_SERIALIZER_TOKEN: InjectionToken<WorkerSerializer>;
|
|
|
182
182
|
* Defaults to an empty map (no interceptors).
|
|
183
183
|
*/
|
|
184
184
|
declare const WORKER_HTTP_INTERCEPTORS_TOKEN: InjectionToken<Readonly<Record<string, readonly WorkerInterceptorSpec[]>>>;
|
|
185
|
+
/**
|
|
186
|
+
* Enable Safari streams polyfill for transferable ReadableStream/TransformStream
|
|
187
|
+
* support in workers. Provided via `withWorkerStreamsPolyfill()`.
|
|
188
|
+
* Defaults to `false` (native streams only).
|
|
189
|
+
*
|
|
190
|
+
* When enabled, the transport dynamically imports `@angular-helpers/worker-http/streams-polyfill`
|
|
191
|
+
* on Safari 16-17 to enable stream transfer via postMessage.
|
|
192
|
+
*/
|
|
193
|
+
declare const WORKER_HTTP_STREAMS_POLYFILL_TOKEN: InjectionToken<boolean>;
|
|
185
194
|
|
|
186
195
|
/**
|
|
187
196
|
* Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
|
|
@@ -345,6 +354,30 @@ declare function withWorkerInterceptors(specs: readonly WorkerInterceptorSpec[]
|
|
|
345
354
|
* ```
|
|
346
355
|
*/
|
|
347
356
|
declare function withTelemetry(telemetry: WorkerHttpTelemetry): WorkerHttpFeature<'Telemetry'>;
|
|
357
|
+
/**
|
|
358
|
+
* Enables the Safari streams polyfill for transferable ReadableStream/TransformStream
|
|
359
|
+
* support in Web Workers.
|
|
360
|
+
*
|
|
361
|
+
* Safari 16-17 lacks native transferable streams. When this feature is enabled,
|
|
362
|
+
* the transport layer dynamically loads a ponyfill that enables stream transfer
|
|
363
|
+
* via postMessage on affected browsers.
|
|
364
|
+
*
|
|
365
|
+
* **When to use:**
|
|
366
|
+
* - Your application uses `responseType: 'stream'` and targets Safari 16-17
|
|
367
|
+
* - You see "DataCloneError" when transferring streams to/from workers
|
|
368
|
+
*
|
|
369
|
+
* **Bundle impact:** The polyfill is lazy-loaded only on Safari 16-17 when
|
|
370
|
+
* streams are actually used. Non-Safari browsers and modern Safari pay 0 bytes.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* provideWorkerHttpClient(
|
|
375
|
+
* withWorkerConfigs([...]),
|
|
376
|
+
* withWorkerStreamsPolyfill(), // Enable for Safari compatibility
|
|
377
|
+
* )
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
declare function withWorkerStreamsPolyfill(): WorkerHttpFeature<'StreamsPolyfill'>;
|
|
348
381
|
|
|
349
382
|
/**
|
|
350
383
|
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
@@ -366,6 +399,7 @@ declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
|
|
|
366
399
|
private readonly interceptorSpecs;
|
|
367
400
|
private readonly fetchBackend;
|
|
368
401
|
private readonly telemetry;
|
|
402
|
+
private readonly streamsPolyfill;
|
|
369
403
|
private readonly transports;
|
|
370
404
|
handle(req: HttpRequest<unknown>): Observable<HttpEvent<unknown>>;
|
|
371
405
|
ngOnDestroy(): void;
|
|
@@ -460,5 +494,5 @@ declare function matchWorkerRoute(url: string, routes: Array<{
|
|
|
460
494
|
priority?: number;
|
|
461
495
|
}>): string | null;
|
|
462
496
|
|
|
463
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
497
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
|
|
464
498
|
export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpErrorEvent, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerHttpRequestEvent, WorkerHttpResponseEvent, WorkerHttpTelemetry, WorkerHttpTelemetryEventBase, WorkerHttpTransportKind, WorkerInterceptorSpecsMap, WorkerRequestOptions, WorkerRoute };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Plugin } from 'esbuild';
|
|
2
|
+
|
|
3
|
+
interface WorkerHttpPluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Explicit list of interceptor file paths to bundle into the worker.
|
|
6
|
+
* Paths are relative to project root.
|
|
7
|
+
* @example ['./src/interceptors/auth.ts', './src/interceptors/logging.ts']
|
|
8
|
+
*/
|
|
9
|
+
interceptors?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Auto-discover interceptors by scanning src folders for files
|
|
12
|
+
* matching the interceptor naming pattern.
|
|
13
|
+
* Discovered interceptors are merged with explicit interceptors list.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
autoDiscover?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* esbuild plugin for worker-http that bundles interceptors into worker files.
|
|
20
|
+
*
|
|
21
|
+
* This plugin:
|
|
22
|
+
* 1. Intercepts worker file builds
|
|
23
|
+
* 2. Discovers interceptor files if autoDiscover is true
|
|
24
|
+
* 3. Injects interceptor imports into the worker bootstrap
|
|
25
|
+
* 4. Ensures interceptors are available in the worker's interceptor pipeline
|
|
26
|
+
*
|
|
27
|
+
* @param options - Plugin configuration
|
|
28
|
+
* @returns esbuild Plugin
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { workerHttpPlugin } from '@angular-helpers/worker-http/esbuild-plugin';
|
|
33
|
+
*
|
|
34
|
+
* const config = {
|
|
35
|
+
* plugins: [
|
|
36
|
+
* workerHttpPlugin({
|
|
37
|
+
* autoDiscover: true,
|
|
38
|
+
* })
|
|
39
|
+
* ]
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function workerHttpPlugin(options?: WorkerHttpPluginOptions): Plugin;
|
|
44
|
+
|
|
45
|
+
export { workerHttpPlugin };
|
|
46
|
+
export type { WorkerHttpPluginOptions };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if the current browser needs the streams ponyfill.
|
|
3
|
+
*
|
|
4
|
+
* Safari 16-17 fails to transfer `ReadableStream`/`TransformStream`
|
|
5
|
+
* to/from Web Workers via `structuredClone` because they lack
|
|
6
|
+
* the transferable streams implementation.
|
|
7
|
+
*
|
|
8
|
+
* @param userAgent - Optional user agent string for testing (defaults to feature detection)
|
|
9
|
+
* @returns `true` if ponyfill is needed, `false` if native works
|
|
10
|
+
*/
|
|
11
|
+
declare function needsPolyfill(userAgent?: string): boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ponyfill for Web Streams API that supports transfer to/from workers.
|
|
15
|
+
*
|
|
16
|
+
* Uses `web-streams-polyfill` internally but only loads it when needed.
|
|
17
|
+
* This keeps bundle size small for non-Safari browsers.
|
|
18
|
+
*
|
|
19
|
+
* @see {@link needsPolyfill} for detection
|
|
20
|
+
*/
|
|
21
|
+
interface StreamPonyfillExports {
|
|
22
|
+
ReadableStream: typeof ReadableStream;
|
|
23
|
+
TransformStream: typeof TransformStream;
|
|
24
|
+
WritableStream: typeof WritableStream;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lazily loads the web-streams-polyfill ponyfill.
|
|
28
|
+
*
|
|
29
|
+
* @returns Ponyfilled streams or native if not needed
|
|
30
|
+
*/
|
|
31
|
+
declare function ponyfillStreams(): Promise<StreamPonyfillExports>;
|
|
32
|
+
|
|
33
|
+
export { needsPolyfill, ponyfillStreams };
|
|
34
|
+
export type { StreamPonyfillExports };
|
|
@@ -34,9 +34,27 @@ interface WorkerTransportConfig {
|
|
|
34
34
|
workerUrl?: string | URL;
|
|
35
35
|
/** Maximum number of worker instances in the pool (default: 1) */
|
|
36
36
|
maxInstances?: number;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Transfer strategy for `postMessage` payloads.
|
|
39
|
+
*
|
|
40
|
+
* - `'none'` (default) — payloads are always structured-cloned, preserving
|
|
41
|
+
* the caller's access to the original data after post.
|
|
42
|
+
* - `'auto'` — shallowly walks the payload and passes every detected
|
|
43
|
+
* `Transferable` (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas,
|
|
44
|
+
* ReadableStream, WritableStream, TransformStream) in the transfer list
|
|
45
|
+
* of `postMessage`. Large buffers move zero-copy; their `byteLength`
|
|
46
|
+
* becomes `0` in the main thread after post.
|
|
47
|
+
*
|
|
48
|
+
* The `'manual'` value is reserved for a future API where callers supply
|
|
49
|
+
* their own transfer list per request. It currently behaves like `'none'`.
|
|
50
|
+
*/
|
|
38
51
|
transferDetection?: 'auto' | 'manual' | 'none';
|
|
39
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Per-request timeout in milliseconds. If the worker does not respond
|
|
54
|
+
* within this window, `execute()` errors with `WorkerHttpTimeoutError`
|
|
55
|
+
* and a cancel message is posted to the worker. Set to `0` or
|
|
56
|
+
* non-finite to disable the timeout entirely. Default: `30000` (30 s).
|
|
57
|
+
*/
|
|
40
58
|
requestTimeout?: number;
|
|
41
59
|
/**
|
|
42
60
|
* Optional handshake message posted to every worker as soon as it is
|
|
@@ -50,6 +68,14 @@ interface WorkerTransportConfig {
|
|
|
50
68
|
type: string;
|
|
51
69
|
[key: string]: unknown;
|
|
52
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Enable Safari streams polyfill for transferable ReadableStream/TransformStream
|
|
73
|
+
* support. When `true`, the transport lazy-loads the ponyfill on Safari 16-17
|
|
74
|
+
* to enable stream transfer via postMessage.
|
|
75
|
+
*
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
streamsPolyfill?: boolean;
|
|
53
79
|
}
|
|
54
80
|
/**
|
|
55
81
|
* Message sent from main thread to worker.
|
|
@@ -103,10 +129,12 @@ interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
|
|
|
103
129
|
*
|
|
104
130
|
* Features:
|
|
105
131
|
* - Request/response correlation via `requestId`
|
|
106
|
-
* -
|
|
132
|
+
* - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
|
|
133
|
+
* - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
|
|
107
134
|
* - Optional worker pool with round-robin dispatch
|
|
108
135
|
* - Lazy worker creation (default)
|
|
109
|
-
* -
|
|
136
|
+
* - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
|
|
137
|
+
* `ArrayBuffer` / stream payloads
|
|
110
138
|
*
|
|
111
139
|
* @example
|
|
112
140
|
* ```typescript
|
|
@@ -123,5 +151,47 @@ interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
|
|
|
123
151
|
*/
|
|
124
152
|
declare function createWorkerTransport<TRequest = unknown, TResponse = unknown>(config: WorkerTransportConfig): WorkerTransport<TRequest, TResponse>;
|
|
125
153
|
|
|
126
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Thrown by `createWorkerTransport` when a request exceeds its configured
|
|
156
|
+
* `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
|
|
157
|
+
* timeout rejections from transport/worker errors.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* transport.execute(req).subscribe({
|
|
162
|
+
* error: (err) => {
|
|
163
|
+
* if (err instanceof WorkerHttpTimeoutError) {
|
|
164
|
+
* // dedicated timeout handling
|
|
165
|
+
* }
|
|
166
|
+
* },
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare class WorkerHttpTimeoutError extends Error {
|
|
171
|
+
readonly name = "WorkerHttpTimeoutError";
|
|
172
|
+
readonly timeoutMs: number;
|
|
173
|
+
constructor(timeoutMs: number);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Scans a payload one level deep and collects every `Transferable` instance
|
|
178
|
+
* (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
|
|
179
|
+
* WritableStream, TransformStream) found in its own enumerable properties.
|
|
180
|
+
*
|
|
181
|
+
* Used by `createWorkerTransport` when `transferDetection === 'auto'` to build
|
|
182
|
+
* the second argument of `worker.postMessage(data, transfer)` so large buffers
|
|
183
|
+
* move zero-copy instead of being structured-cloned.
|
|
184
|
+
*
|
|
185
|
+
* Design notes:
|
|
186
|
+
* - Only one level deep by design: deep traversal has quadratic cost on heavy
|
|
187
|
+
* graphs and makes the transfer list surprising. Real payloads that care
|
|
188
|
+
* about zero-copy put the buffer at the top level.
|
|
189
|
+
* - Duplicates are filtered — the same buffer referenced twice is transferred
|
|
190
|
+
* only once (required by the structured-clone algorithm).
|
|
191
|
+
* - Returns an empty array for primitives, plain serializable values, or when
|
|
192
|
+
* no transferable is found; `postMessage` accepts `[]` safely.
|
|
193
|
+
*/
|
|
194
|
+
declare function detectTransferables(payload: unknown): Transferable[];
|
|
195
|
+
|
|
196
|
+
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
127
197
|
export type { WorkerErrorResponse, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
|
|
@@ -1,16 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @angular-helpers/worker-http
|
|
3
|
-
*
|
|
4
|
-
* Angular HTTP over Web Workers — off-main-thread HTTP pipelines
|
|
5
|
-
* with configurable interceptors, WebCrypto security, and pluggable serialization.
|
|
6
|
-
*
|
|
7
|
-
* Sub-entry points:
|
|
8
|
-
* - @angular-helpers/worker-http/transport (P1: typed RPC bridge)
|
|
9
|
-
* - @angular-helpers/worker-http/serializer (P2: TOON, seroval, auto-detect)
|
|
10
|
-
* - @angular-helpers/worker-http/backend (P3: Angular HttpBackend replacement)
|
|
11
|
-
* - @angular-helpers/worker-http/interceptors (P4: pure-fn interceptors for workers)
|
|
12
|
-
* - @angular-helpers/worker-http/crypto (P5: WebCrypto primitives)
|
|
13
|
-
*/
|
|
14
|
-
declare const WORKER_HTTP_VERSION = "0.0.1";
|
|
15
1
|
|
|
16
|
-
export {
|
|
2
|
+
export { };
|