@angular-helpers/worker-http 0.7.0 → 21.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 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 | 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 |
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
@@ -510,6 +549,61 @@ interface WorkerHttpTelemetryEventBase {
510
549
 
511
550
  ---
512
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
+
513
607
  ## SSR + hydration
514
608
 
515
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;
@@ -558,9 +569,38 @@ function withTelemetry(telemetry) {
558
569
  providers: [{ provide: WORKER_HTTP_TELEMETRY_TOKEN, useValue: telemetry, multi: true }],
559
570
  };
560
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
+ }
561
601
 
562
602
  /**
563
603
  * Generated bundle index. Do not edit.
564
604
  */
565
605
 
566
- 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 };
@@ -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 };
@@ -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 };
@@ -134,6 +134,8 @@ function createWorkerTransport(config) {
134
134
  const maxInstances = Math.min(config.maxInstances ?? 1, typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 1);
135
135
  const requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
136
136
  const transferDetection = config.transferDetection ?? 'none';
137
+ const streamsPolyfill = config.streamsPolyfill ?? false;
138
+ let polyfillLoaded = false;
137
139
  function createWorker() {
138
140
  if (config.workerFactory) {
139
141
  return config.workerFactory();
@@ -167,6 +169,12 @@ function createWorkerTransport(config) {
167
169
  }
168
170
  const requestId = crypto.randomUUID();
169
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
+ }
170
178
  const worker = getOrCreateWorker();
171
179
  let settled = false;
172
180
  let timeoutHandle;
@@ -233,6 +241,26 @@ function createWorkerTransport(config) {
233
241
  };
234
242
  });
235
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
+ }
236
264
  function terminate() {
237
265
  terminated = true;
238
266
  for (const worker of workers) {
package/package.json CHANGED
@@ -1,7 +1,53 @@
1
1
  {
2
2
  "name": "@angular-helpers/worker-http",
3
- "version": "0.7.0",
3
+ "version": "21.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
- "./serializer": {
69
- "types": "./types/angular-helpers-worker-http-serializer.d.ts",
70
- "default": "./fesm2022/angular-helpers-worker-http-serializer.mjs"
91
+ "esbuild": {
92
+ "optional": true
71
93
  },
72
- "./transport": {
73
- "types": "./types/angular-helpers-worker-http-transport.d.ts",
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 };
@@ -68,6 +68,14 @@ interface WorkerTransportConfig {
68
68
  type: string;
69
69
  [key: string]: unknown;
70
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;
71
79
  }
72
80
  /**
73
81
  * Message sent from main thread to worker.