request-iframe 0.0.6 → 0.1.1

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.
Files changed (95) hide show
  1. package/README.CN.md +220 -21
  2. package/README.md +221 -24
  3. package/esm/api/client.js +80 -0
  4. package/esm/api/server.js +61 -0
  5. package/esm/constants/index.js +289 -0
  6. package/esm/constants/messages.js +157 -0
  7. package/esm/core/client-server.js +294 -0
  8. package/esm/core/client.js +873 -0
  9. package/esm/core/request.js +27 -0
  10. package/esm/core/response.js +459 -0
  11. package/esm/core/server.js +776 -0
  12. package/esm/index.js +21 -0
  13. package/esm/interceptors/index.js +122 -0
  14. package/esm/message/channel.js +182 -0
  15. package/esm/message/dispatcher.js +418 -0
  16. package/esm/message/index.js +2 -0
  17. package/esm/stream/file-stream.js +289 -0
  18. package/esm/stream/index.js +44 -0
  19. package/esm/stream/readable-stream.js +539 -0
  20. package/esm/stream/stream-core.js +204 -0
  21. package/esm/stream/types.js +1 -0
  22. package/esm/stream/writable-stream.js +836 -0
  23. package/esm/types/index.js +1 -0
  24. package/esm/utils/ack.js +36 -0
  25. package/esm/utils/cache.js +147 -0
  26. package/esm/utils/cookie.js +352 -0
  27. package/esm/utils/debug.js +521 -0
  28. package/esm/utils/error.js +27 -0
  29. package/esm/utils/index.js +180 -0
  30. package/esm/utils/origin.js +30 -0
  31. package/esm/utils/path-match.js +148 -0
  32. package/esm/utils/protocol.js +157 -0
  33. package/library/api/client.d.ts.map +1 -1
  34. package/library/api/client.js +13 -5
  35. package/library/api/server.d.ts.map +1 -1
  36. package/library/api/server.js +6 -1
  37. package/library/constants/index.d.ts +59 -4
  38. package/library/constants/index.d.ts.map +1 -1
  39. package/library/constants/index.js +67 -9
  40. package/library/constants/messages.d.ts +8 -1
  41. package/library/constants/messages.d.ts.map +1 -1
  42. package/library/constants/messages.js +8 -1
  43. package/library/core/client-server.d.ts +7 -15
  44. package/library/core/client-server.d.ts.map +1 -1
  45. package/library/core/client-server.js +56 -44
  46. package/library/core/client.d.ts +4 -1
  47. package/library/core/client.d.ts.map +1 -1
  48. package/library/core/client.js +74 -31
  49. package/library/core/response.d.ts +21 -3
  50. package/library/core/response.d.ts.map +1 -1
  51. package/library/core/response.js +55 -7
  52. package/library/core/server.d.ts +34 -3
  53. package/library/core/server.d.ts.map +1 -1
  54. package/library/core/server.js +191 -21
  55. package/library/message/channel.d.ts +6 -0
  56. package/library/message/channel.d.ts.map +1 -1
  57. package/library/message/channel.js +2 -1
  58. package/library/message/dispatcher.d.ts +32 -0
  59. package/library/message/dispatcher.d.ts.map +1 -1
  60. package/library/message/dispatcher.js +131 -1
  61. package/library/stream/file-stream.d.ts +4 -0
  62. package/library/stream/file-stream.d.ts.map +1 -1
  63. package/library/stream/file-stream.js +61 -33
  64. package/library/stream/index.d.ts.map +1 -1
  65. package/library/stream/index.js +2 -0
  66. package/library/stream/readable-stream.d.ts +30 -11
  67. package/library/stream/readable-stream.d.ts.map +1 -1
  68. package/library/stream/readable-stream.js +368 -73
  69. package/library/stream/stream-core.d.ts +65 -0
  70. package/library/stream/stream-core.d.ts.map +1 -0
  71. package/library/stream/stream-core.js +211 -0
  72. package/library/stream/types.d.ts +203 -3
  73. package/library/stream/types.d.ts.map +1 -1
  74. package/library/stream/writable-stream.d.ts +59 -13
  75. package/library/stream/writable-stream.d.ts.map +1 -1
  76. package/library/stream/writable-stream.js +647 -197
  77. package/library/types/index.d.ts +70 -4
  78. package/library/types/index.d.ts.map +1 -1
  79. package/library/utils/ack.d.ts +2 -0
  80. package/library/utils/ack.d.ts.map +1 -0
  81. package/library/utils/ack.js +44 -0
  82. package/library/utils/debug.js +1 -1
  83. package/library/utils/index.d.ts +1 -0
  84. package/library/utils/index.d.ts.map +1 -1
  85. package/library/utils/index.js +19 -2
  86. package/library/utils/origin.d.ts +14 -0
  87. package/library/utils/origin.d.ts.map +1 -0
  88. package/library/utils/origin.js +35 -0
  89. package/package.json +30 -7
  90. package/react/README.md +16 -0
  91. package/react/esm/index.js +284 -0
  92. package/react/library/index.d.ts +1 -1
  93. package/react/library/index.d.ts.map +1 -1
  94. package/react/library/index.js +3 -3
  95. package/react/package.json +24 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # request-iframe
2
2
 
3
- Communicate with iframes like sending HTTP requests! A cross-origin iframe communication library based on `postMessage`.
3
+ Communicate with iframes/windows like sending HTTP requests! A cross-origin browser communication library based on `postMessage`.
4
4
 
5
5
  > 🌐 **Languages**: [English](./README.md) | [中文](./README.CN.md)
6
6
 
@@ -8,7 +8,7 @@ Communicate with iframes like sending HTTP requests! A cross-origin iframe commu
8
8
  <img src="https://img.shields.io/badge/TypeScript-Ready-blue" alt="TypeScript Ready">
9
9
  <img src="https://img.shields.io/badge/API-Express%20Like-green" alt="Express Like API">
10
10
  <img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
11
- <img src="https://img.shields.io/badge/Test%20Coverage-76%25-brightgreen" alt="Test Coverage">
11
+ <img src="https://coveralls.io/repos/github/gxlmyacc/request-iframe/badge.svg?branch=main" alt="Coverage Status">
12
12
  </p>
13
13
 
14
14
  ## 📑 Table of Contents
@@ -48,7 +48,7 @@ Communicate with iframes like sending HTTP requests! A cross-origin iframe commu
48
48
 
49
49
  ## Why request-iframe?
50
50
 
51
- In micro-frontend and iframe nesting scenarios, parent-child page communication is a common requirement. Traditional `postMessage` communication has the following pain points:
51
+ In micro-frontend, iframe nesting, and popup window scenarios, cross-page communication is a common requirement. Traditional `postMessage` communication has the following pain points:
52
52
 
53
53
  | Pain Point | Traditional Way | request-iframe |
54
54
  |------------|----------------|----------------|
@@ -74,7 +74,7 @@ In micro-frontend and iframe nesting scenarios, parent-child page communication
74
74
  - ⏱️ **Smart Timeout** - Three-stage timeout (connection/sync/async), automatically detects long tasks
75
75
  - 📦 **TypeScript** - Complete type definitions and IntelliSense
76
76
  - 🔒 **Message Isolation** - secretKey mechanism prevents message cross-talk between multiple instances
77
- - 📁 **File Transfer** - Support for file sending via stream (clientserver)
77
+ - 📁 **File Transfer** - File transfer via streams (clientserver)
78
78
  - 🌊 **Streaming** - Support for large file chunked transfer, supports async iterators
79
79
  - 🌍 **Internationalization** - Error messages can be customized for i18n
80
80
  - ✅ **Protocol Versioning** - Built-in version control for upgrade compatibility
@@ -172,6 +172,29 @@ server.on('/event', (req, res) => {
172
172
  });
173
173
  ```
174
174
 
175
+ ### Popup / New Window (Window Communication)
176
+
177
+ `request-iframe` also works with a `Window` target (not only an iframe).
178
+
179
+ **Important**: you must have a real `Window` reference (e.g. returned by `window.open()`, or available via `window.opener` / `event.source`). You cannot send to an arbitrary browser tab by URL.
180
+
181
+ ```typescript
182
+ // Parent page: open a new tab/window
183
+ const child = window.open('https://child.example.com/page.html', '_blank');
184
+ if (!child) throw new Error('Popup blocked');
185
+
186
+ // Parent -> child
187
+ const client = requestIframeClient(child, {
188
+ secretKey: 'popup-demo',
189
+ targetOrigin: 'https://child.example.com' // strongly recommended (avoid '*')
190
+ });
191
+ await client.send('/api/ping', { from: 'parent' });
192
+
193
+ // Child page: create server
194
+ const server = requestIframeServer({ secretKey: 'popup-demo' });
195
+ server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
196
+ ```
197
+
175
198
  ### Cross-Origin Data Fetching
176
199
 
177
200
  When iframe and parent page are on different origins, use request-iframe to securely fetch data:
@@ -229,7 +252,7 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
229
252
  │ │
230
253
  │ ──── REQUEST ────────────────────────> │ Send request
231
254
  │ │
232
- │ <──── ACK ─────────────────────────── │ Acknowledge receipt
255
+ │ <──── ACK (optional) ──────────────── │ Acknowledge receipt (controlled by request `requireAck`, default true)
233
256
  │ │
234
257
  │ │ Execute handler
235
258
  │ │
@@ -237,7 +260,7 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
237
260
  │ │
238
261
  │ <──── RESPONSE ────────────────────── │ Return result
239
262
  │ │
240
- │ ──── RECEIVED (optional) ────────────> │ Acknowledge receipt of response
263
+ │ ──── ACK (optional) ─────────────────> │ Acknowledge receipt of response/error (ACK-only requireAck)
241
264
  │ │
242
265
  ```
243
266
 
@@ -246,13 +269,13 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
246
269
  | Type | Direction | Description |
247
270
  |------|-----------|-------------|
248
271
  | `request` | Client → Server | Client initiates request |
249
- | `ack` | ServerClient | Server acknowledges receipt of request |
272
+ | `ack` | ReceiverSender | Acknowledgment when `requireAck: true` and the message is accepted/handled (ACK-only) |
250
273
  | `async` | Server → Client | Notifies client this is an async task (sent when handler returns Promise) |
251
274
  | `response` | Server → Client | Returns response data |
252
275
  | `error` | Server → Client | Returns error information |
253
- | `received` | Client → Server | Client acknowledges receipt of response (optional, controlled by `requireAck`) |
254
- | `ping` | Client → Server | Connection detection (`isConnect()` method) |
276
+ | `ping` | Client → Server | Connection detection (`isConnect()` method, may use `requireAck` to confirm delivery) |
255
277
  | `pong` | Server → Client | Connection detection response |
278
+ | `stream_pull` | Receiver → Sender | Stream pull: receiver requests next chunks (pull/ack protocol) |
256
279
 
257
280
  ### Timeout Mechanism
258
281
 
@@ -262,7 +285,8 @@ request-iframe uses a three-stage timeout strategy to intelligently adapt to dif
262
285
  client.send('/api/getData', data, {
263
286
  ackTimeout: 1000, // Stage 1: ACK timeout (default 1000ms)
264
287
  timeout: 5000, // Stage 2: Request timeout (default 5s)
265
- asyncTimeout: 120000 // Stage 3: Async request timeout (default 120s)
288
+ asyncTimeout: 120000, // Stage 3: Async request timeout (default 120s)
289
+ requireAck: true // Whether to wait for server ACK before switching to stage 2 (default true)
266
290
  });
267
291
  ```
268
292
 
@@ -304,13 +328,17 @@ Send REQUEST
304
328
  | timeout | Medium (5s) | Suitable for simple synchronous processing, like reading data, parameter validation |
305
329
  | asyncTimeout | Long (120s) | Suitable for complex async operations, like file processing, batch operations, third-party API calls |
306
330
 
331
+ **Notes:**
332
+ - If you set `requireAck: false`, the request will **skip** the ACK stage and start `timeout` immediately.
333
+ - Stream transfer has its own optional idle timeout: use `streamTimeout` (see [Streaming](#streaming)).
334
+
307
335
  ### Protocol Version
308
336
 
309
337
  Each message contains a `__requestIframe__` field identifying the protocol version, and a `timestamp` field recording message creation time:
310
338
 
311
339
  ```typescript
312
340
  {
313
- __requestIframe__: 1, // Protocol version number
341
+ __requestIframe__: 2, // Protocol version number
314
342
  timestamp: 1704067200000, // Message creation timestamp (milliseconds)
315
343
  type: 'request',
316
344
  requestId: 'req_xxx',
@@ -321,7 +349,7 @@ Each message contains a `__requestIframe__` field identifying the protocol versi
321
349
 
322
350
  This enables:
323
351
  - Different library versions can handle compatibility
324
- - New version Server can be compatible with old version Client
352
+ - Current protocol version is `2`. For the new stream pull/ack flow, both sides should use the same version.
325
353
  - Clear error messages when version is too low
326
354
  - `timestamp` facilitates debugging message delays and analyzing communication performance
327
355
 
@@ -551,6 +579,8 @@ server.on('/api/data', (req, res) => {
551
579
 
552
580
  ### File Transfer
553
581
 
582
+ > Note: File transfer (both Client→Server and Server→Client) is carried by the stream protocol under the hood. You normally only need to use `client.sendFile()` / `res.sendFile()`.
583
+
554
584
  #### Server → Client (Server sends file to client)
555
585
 
556
586
  ```typescript
@@ -585,7 +615,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
585
615
 
586
616
  #### Client → Server (Client sends file to server)
587
617
 
588
- Client sends file via stream only. Use `sendFile()` (or `send(path, file)`); server receives either `req.body` as File/Blob when `autoResolve: true` (default), or `req.stream` as `IframeFileReadableStream` when `autoResolve: false`.
618
+ Client sends file using `sendFile()` (or `send(path, file)`); server receives either `req.body` as File/Blob when `autoResolve: true` (default), or `req.stream` as `IframeFileReadableStream` when `autoResolve: false`.
589
619
 
590
620
  ```typescript
591
621
  // Client side: Send file (stream, autoResolve defaults to true)
@@ -609,16 +639,71 @@ server.on('/api/upload', async (req, res) => {
609
639
  });
610
640
  ```
611
641
 
612
- **Note:** When using `client.send()` with a `File` or `Blob`, it automatically dispatches to `client.sendFile()`, which sends the file via stream. Server gets `req.body` as File/Blob when `autoResolve` is true (default), or `req.stream` / `req.body` as `IframeFileReadableStream` when `autoResolve` is false.
642
+ **Note:** When using `client.send()` with a `File` or `Blob`, it automatically dispatches to `client.sendFile()`. Server gets `req.body` as File/Blob when `autoResolve` is true (default), or `req.stream` / `req.body` as `IframeFileReadableStream` when `autoResolve` is false.
613
643
 
614
644
  ### Streaming
615
645
 
616
- For large files or scenarios requiring chunked transfer, you can use streaming:
646
+ Streaming is not only for large/chunked transfers, but also works well for **long-lived subscription-style interactions** (similar to SSE/WebSocket, but built on top of `postMessage`).
647
+
648
+ #### Long-lived subscription (push mode)
649
+
650
+ > Notes:
651
+ > - `IframeWritableStream` defaults `expireTimeout` to `asyncTimeout` to avoid leaking long-lived streams. For real subscriptions, set a larger `expireTimeout`, or set `expireTimeout: 0` to disable auto-expire (use with care and pair with cancel/reconnect).
652
+ > - `res.sendStream(stream)` waits until the stream ends. If you want to keep pushing via `write()`, **do not** `await` it; use `void res.sendStream(stream)` or keep the returned Promise.
653
+ > - If `maxConcurrentRequestsPerClient` is enabled, a long-lived stream occupies one in-flight request slot.
654
+ > - **Event subscription**: streams support `stream.on(event, listener)` (returns an unsubscribe function) for observability (e.g. `start/data/read/write/cancel/end/error/timeout/expired`). For consuming data, prefer `for await`.
655
+
656
+ ```typescript
657
+ /**
658
+ * Server side: subscribe (long-lived)
659
+ * - mode: 'push': writer calls write()
660
+ * - expireTimeout: 0: disable auto-expire (use with care)
661
+ */
662
+ server.on('/api/subscribe', (req, res) => {
663
+ const stream = new IframeWritableStream({
664
+ type: 'data',
665
+ chunked: true,
666
+ mode: 'push',
667
+ expireTimeout: 0,
668
+ /** optional: writer-side idle timeout while waiting for pull/ack */
669
+ streamTimeout: 15000
670
+ });
671
+
672
+ /** do not await, otherwise it blocks until stream ends */
673
+ void res.sendStream(stream);
674
+
675
+ const timer = setInterval(() => {
676
+ try {
677
+ stream.write({ type: 'tick', ts: Date.now() });
678
+ } catch {
679
+ clearInterval(timer);
680
+ }
681
+ }, 1000);
682
+ });
683
+
684
+ /**
685
+ * Client side: consume continuously (prefer for-await for long-lived streams)
686
+ */
687
+ const resp = await client.send('/api/subscribe', {});
688
+ if (isIframeReadableStream(resp.stream)) {
689
+ /** Optional: observe events */
690
+ const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
691
+ console.error('stream error:', error);
692
+ });
693
+
694
+ for await (const evt of resp.stream) {
695
+ console.log('event:', evt);
696
+ }
697
+
698
+ off();
699
+ }
700
+ ```
617
701
 
618
702
  #### Server → Client (Server sends stream to client)
619
703
 
620
704
  ```typescript
621
- import {
705
+ import {
706
+ StreamEvent,
622
707
  IframeWritableStream,
623
708
  IframeFileWritableStream,
624
709
  isIframeReadableStream,
@@ -630,6 +715,11 @@ server.on('/api/stream', async (req, res) => {
630
715
  const stream = new IframeWritableStream({
631
716
  type: 'data',
632
717
  chunked: true,
718
+ mode: 'pull', // default: pull/ack protocol (backpressure)
719
+ // Optional: auto-expire stream to avoid leaking resources (default: asyncTimeout)
720
+ // expireTimeout: 120000,
721
+ // Optional: writer-side idle timeout while waiting for pull/ack
722
+ // streamTimeout: 10000,
633
723
  // Generate data using async iterator
634
724
  iterator: async function* () {
635
725
  for (let i = 0; i < 10; i++) {
@@ -643,14 +733,19 @@ server.on('/api/stream', async (req, res) => {
643
733
  });
644
734
 
645
735
  // Client side: Receive stream data
646
- const response = await client.send('/api/stream', {});
736
+ const response = await client.send('/api/stream', {}, { streamTimeout: 10000 });
647
737
 
648
738
  // Check if it's a stream response
649
739
  if (isIframeReadableStream(response.stream)) {
740
+ // Sender stream mode (from stream_start)
741
+ console.log('Stream mode:', response.stream.mode); // 'pull' | 'push' | undefined
742
+
650
743
  // Method 1: Read all data at once
651
744
  const allData = await response.stream.read();
745
+ // If you want a consistent return type (always an array of chunks), use readAll()
746
+ const allChunks = await response.stream.readAll();
652
747
 
653
- // Method 2: Read chunk by chunk using async iterator
748
+ // Method 2: Read chunk by chunk using async iterator (consume defaults to true)
654
749
  for await (const chunk of response.stream) {
655
750
  console.log('Received chunk:', chunk);
656
751
  }
@@ -723,10 +818,26 @@ server.on('/api/uploadStream', async (req, res) => {
723
818
 
724
819
  | Type | Description |
725
820
  |------|-------------|
726
- | `IframeWritableStream` | Server-side writable stream for sending regular data |
727
- | `IframeFileWritableStream` | Server-side file writable stream, automatically handles base64 encoding |
728
- | `IframeReadableStream` | Client-side readable stream for receiving regular data |
729
- | `IframeFileReadableStream` | Client-side file readable stream, automatically handles base64 decoding |
821
+ | `IframeWritableStream` | Writer/producer stream: **created by whichever side is sending the stream** (server→client response stream, or client→server request stream) |
822
+ | `IframeFileWritableStream` | File writer/producer stream (base64-encodes internally) |
823
+ | `IframeReadableStream` | Reader/consumer stream for receiving regular data (regardless of which side sent it) |
824
+ | `IframeFileReadableStream` | File reader/consumer stream (base64-decodes internally) |
825
+
826
+ > **Note**: File streams are base64-encoded internally. Base64 introduces ~33% size overhead and can be memory/CPU heavy for very large files. For large files, prefer **chunked** file streams (`chunked: true`) and keep chunk sizes moderate (e.g. 256KB–1MB).
827
+
828
+ **Stream timeouts:**
829
+ - `options.streamTimeout` (request option): client-side stream idle timeout while consuming `response.stream` (data/file streams). When triggered, the client performs a heartbeat check (by default `client.isConnect()`); if not alive, the stream fails as disconnected.
830
+ - `expireTimeout` (writable stream option): writer-side stream lifetime. When expired, the writer sends `stream_error` and the reader will fail the stream with `STREAM_EXPIRED`.
831
+ - `streamTimeout` (writable stream option): writer-side idle timeout. If the writer does not receive `stream_pull` for a long time, it will heartbeat-check and fail to avoid wasting resources.
832
+ - `maxPendingChunks` (writable stream option): max number of pending (unsent) chunks kept in memory on the writer side (optional). Important for long-lived `push` streams: if the receiver stops pulling, continued `write()` calls will accumulate in the writer queue.
833
+ - `maxPendingBytes` (writable stream option): max bytes of pending (unsent) chunks kept in memory on the writer side (optional). Useful when a single `write()` may enqueue a very large chunk.
834
+
835
+ **Pull/Ack protocol (default):**
836
+ - Writer only sends `stream_data` when it has received `stream_pull`, enabling real backpressure.
837
+ - Disconnect detection does not rely on `stream_ack`, but uses `streamTimeout + heartbeat(isConnect)`.
838
+
839
+ **consume default change:**
840
+ - `for await (const chunk of response.stream)` defaults to **consume and drop** already iterated chunks (`consume: true`) to prevent unbounded memory growth for long streams.
730
841
 
731
842
  ### Connection Detection
732
843
 
@@ -747,9 +858,9 @@ Server can require Client to acknowledge receipt of response:
747
858
  ```typescript
748
859
  server.on('/api/important', async (req, res) => {
749
860
  // requireAck: true means client needs to acknowledge
750
- const received = await res.send(data, { requireAck: true });
861
+ const acked = await res.send(data, { requireAck: true });
751
862
 
752
- if (received) {
863
+ if (acked) {
753
864
  console.log('Client acknowledged receipt');
754
865
  } else {
755
866
  console.log('Client did not acknowledge (timeout)');
@@ -757,6 +868,8 @@ server.on('/api/important', async (req, res) => {
757
868
  });
758
869
  ```
759
870
 
871
+ > **Note**: Client acknowledgment (`ack`) is sent automatically by the library when the response/error is accepted by the client (i.e., there is a matching pending request). You don't need to manually send `ack`.
872
+
760
873
  ### Trace Mode
761
874
 
762
875
  Enable trace mode to view detailed communication logs in console:
@@ -809,12 +922,81 @@ Create a Client instance.
809
922
  | `target` | `HTMLIFrameElement \| Window` | Target iframe element or window object |
810
923
  | `options.secretKey` | `string` | Message isolation identifier (optional) |
811
924
  | `options.trace` | `boolean` | Whether to enable trace mode (optional) |
925
+ | `options.targetOrigin` | `string` | Override postMessage targetOrigin for sending (optional). If `target` is a `Window`, default is `*`. |
812
926
  | `options.ackTimeout` | `number` | Global default ACK acknowledgment timeout (ms), default 1000 |
813
927
  | `options.timeout` | `number` | Global default request timeout (ms), default 5000 |
814
928
  | `options.asyncTimeout` | `number` | Global default async timeout (ms), default 120000 |
929
+ | `options.requireAck` | `boolean` | Global default for request delivery ACK (default true). If false, requests skip the ACK stage and start `timeout` immediately |
930
+ | `options.streamTimeout` | `number` | Global default stream idle timeout (ms) when consuming `response.stream` (optional) |
931
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | Allowlist for incoming message origins (optional, recommended for production) |
932
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | Custom origin validator (optional, higher priority than `allowedOrigins`) |
815
933
 
816
934
  **Returns:** `RequestIframeClient`
817
935
 
936
+ **Notes about `target: Window`:**
937
+ - **You must have a `Window` reference** (e.g. from `window.open()`, `window.opener`, or `MessageEvent.source`).
938
+ - You **cannot** communicate with an arbitrary browser tab by URL.
939
+ - For security, prefer setting a strict `targetOrigin` and configure `allowedOrigins` / `validateOrigin`.
940
+
941
+ **Production configuration template:**
942
+
943
+ ```typescript
944
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
945
+
946
+ /**
947
+ * Recommended: explicitly constrain 3 things
948
+ * - secretKey: isolate different apps/instances (avoid cross-talk)
949
+ * - targetOrigin: postMessage targetOrigin (strongly avoid '*' for Window targets)
950
+ * - allowedOrigins / validateOrigin: incoming origin allowlist validation
951
+ */
952
+ const secretKey = 'my-app';
953
+ const targetOrigin = 'https://child.example.com';
954
+ const allowedOrigins = [targetOrigin];
955
+
956
+ // Client (parent)
957
+ const client = requestIframeClient(window.open(targetOrigin)!, {
958
+ secretKey,
959
+ targetOrigin,
960
+ allowedOrigins
961
+ });
962
+
963
+ // Server (child/iframe)
964
+ const server = requestIframeServer({
965
+ secretKey,
966
+ allowedOrigins,
967
+ // Mitigate message explosion (tune as needed)
968
+ maxConcurrentRequestsPerClient: 50
969
+ });
970
+ ```
971
+
972
+ **Production configuration template (iframe target):**
973
+
974
+ ```typescript
975
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
976
+
977
+ /**
978
+ * For iframe targets, you can derive targetOrigin from iframe.src, and use it as the allowedOrigins allowlist.
979
+ */
980
+ const iframe = document.querySelector('iframe')!;
981
+ const targetOrigin = new URL(iframe.src).origin;
982
+ const secretKey = 'my-app';
983
+
984
+ // Client (parent)
985
+ const client = requestIframeClient(iframe, {
986
+ secretKey,
987
+ // Explicitly set it (even though the library can derive it) to avoid accidentally using '*'
988
+ targetOrigin,
989
+ allowedOrigins: [targetOrigin]
990
+ });
991
+
992
+ // Server (inside iframe)
993
+ const server = requestIframeServer({
994
+ secretKey,
995
+ allowedOrigins: [targetOrigin],
996
+ maxConcurrentRequestsPerClient: 50
997
+ });
998
+ ```
999
+
818
1000
  ### requestIframeServer(options?)
819
1001
 
820
1002
  Create a Server instance.
@@ -826,6 +1008,9 @@ Create a Server instance.
826
1008
  | `options.secretKey` | `string` | Message isolation identifier (optional) |
827
1009
  | `options.trace` | `boolean` | Whether to enable trace mode (optional) |
828
1010
  | `options.ackTimeout` | `number` | Wait for client acknowledgment timeout (ms), default 1000 |
1011
+ | `options.maxConcurrentRequestsPerClient` | `number` | Max concurrent in-flight requests per client (per origin + creatorId). Default Infinity |
1012
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | Allowlist for incoming message origins (optional, recommended for production) |
1013
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | Custom origin validator (optional, higher priority than `allowedOrigins`) |
829
1014
 
830
1015
  **Returns:** `RequestIframeServer`
831
1016
 
@@ -844,6 +1029,8 @@ Send a request. Automatically dispatches to `sendFile()` or `sendStream()` based
844
1029
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
845
1030
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
846
1031
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
1032
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
1033
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
847
1034
  | `options.headers` | `object` | Request headers (optional) |
848
1035
  | `options.cookies` | `object` | Request cookies (optional, merged with internally stored cookies, passed-in takes priority) |
849
1036
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -895,6 +1082,8 @@ Send file as request body (via stream; server receives File/Blob when autoResolv
895
1082
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
896
1083
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
897
1084
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
1085
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
1086
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
898
1087
  | `options.headers` | `object` | Request headers (optional) |
899
1088
  | `options.cookies` | `object` | Request cookies (optional) |
900
1089
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -916,6 +1105,8 @@ Send stream as request body (server receives readable stream).
916
1105
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
917
1106
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
918
1107
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
1108
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
1109
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
919
1110
  | `options.headers` | `object` | Request headers (optional) |
920
1111
  | `options.cookies` | `object` | Request cookies (optional) |
921
1112
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -1064,6 +1255,8 @@ Destroy Server instance, remove all listeners.
1064
1255
 
1065
1256
  request-iframe provides React hooks for easy integration in React applications. Import hooks from `request-iframe/react`:
1066
1257
 
1258
+ > Note: React is only required if you use `request-iframe/react`. Installing `request-iframe` alone does not require React.
1259
+
1067
1260
  ```typescript
1068
1261
  import { useClient, useServer, useServerHandler, useServerHandlerMap } from 'request-iframe/react';
1069
1262
  ```
@@ -1328,8 +1521,12 @@ const IframeComponent = () => {
1328
1521
  | `PROTOCOL_UNSUPPORTED` | Protocol version not supported |
1329
1522
  | `IFRAME_NOT_READY` | iframe not ready |
1330
1523
  | `STREAM_ERROR` | Stream transfer error |
1524
+ | `STREAM_TIMEOUT` | Stream idle timeout |
1525
+ | `STREAM_EXPIRED` | Stream expired (writable stream lifetime exceeded) |
1331
1526
  | `STREAM_CANCELLED` | Stream cancelled |
1332
1527
  | `STREAM_NOT_BOUND` | Stream not bound to request context |
1528
+ | `STREAM_START_TIMEOUT` | Stream start timeout (request body stream_start not received in time) |
1529
+ | `TOO_MANY_REQUESTS` | Too many concurrent requests (server-side limit) |
1333
1530
 
1334
1531
  ### Error Handling Example
1335
1532
 
@@ -0,0 +1,80 @@
1
+ import { getIframeTargetOrigin, generateInstanceId } from '../utils';
2
+ import { RequestIframeClientServer } from '../core/client-server';
3
+ import { RequestIframeClientImpl } from '../core/client';
4
+ import { setupClientDebugInterceptors } from '../utils/debug';
5
+ import { Messages, ErrorCode, OriginConstant } from '../constants';
6
+
7
+ /**
8
+ * Create a client (for sending requests)
9
+ *
10
+ * Note:
11
+ * - MessageChannel is cached at the window level by secretKey (ensures unique message listener)
12
+ * - Client instances are not cached, a new instance is created on each call
13
+ * - This allows different versions of the library to coexist
14
+ */
15
+ export function requestIframeClient(target, options) {
16
+ var targetWindow = null;
17
+ var targetOrigin = OriginConstant.ANY;
18
+ if (target.tagName === 'IFRAME') {
19
+ var iframe = target;
20
+ targetWindow = iframe.contentWindow;
21
+ targetOrigin = getIframeTargetOrigin(iframe);
22
+ if (!targetWindow) {
23
+ throw {
24
+ message: Messages.IFRAME_NOT_READY,
25
+ code: ErrorCode.IFRAME_NOT_READY
26
+ };
27
+ }
28
+ } else {
29
+ targetWindow = target;
30
+ targetOrigin = OriginConstant.ANY;
31
+ }
32
+
33
+ // Allow user to override targetOrigin explicitly
34
+ if (options !== null && options !== void 0 && options.targetOrigin) {
35
+ targetOrigin = options.targetOrigin;
36
+ }
37
+
38
+ // Determine secretKey
39
+ var secretKey = options === null || options === void 0 ? void 0 : options.secretKey;
40
+
41
+ // Generate instance ID first (will be used by both client and server)
42
+ var instanceId = generateInstanceId();
43
+
44
+ // Create ClientServer (internally obtains or creates a shared MessageChannel)
45
+ var server = new RequestIframeClientServer({
46
+ secretKey,
47
+ autoOpen: options === null || options === void 0 ? void 0 : options.autoOpen,
48
+ autoAckMaxMetaLength: options === null || options === void 0 ? void 0 : options.autoAckMaxMetaLength,
49
+ autoAckMaxIdLength: options === null || options === void 0 ? void 0 : options.autoAckMaxIdLength
50
+ }, instanceId);
51
+
52
+ // Create client instance
53
+ var client = new RequestIframeClientImpl(targetWindow, targetOrigin, server, {
54
+ secretKey,
55
+ ackTimeout: options === null || options === void 0 ? void 0 : options.ackTimeout,
56
+ timeout: options === null || options === void 0 ? void 0 : options.timeout,
57
+ asyncTimeout: options === null || options === void 0 ? void 0 : options.asyncTimeout,
58
+ returnData: options === null || options === void 0 ? void 0 : options.returnData,
59
+ headers: options === null || options === void 0 ? void 0 : options.headers,
60
+ allowedOrigins: options === null || options === void 0 ? void 0 : options.allowedOrigins,
61
+ validateOrigin: options === null || options === void 0 ? void 0 : options.validateOrigin
62
+ }, instanceId);
63
+
64
+ // If trace mode is enabled, register debug interceptors
65
+ if (options !== null && options !== void 0 && options.trace) {
66
+ setupClientDebugInterceptors(client);
67
+ }
68
+ return client;
69
+ }
70
+
71
+ /**
72
+ * Clear MessageChannel cache (for testing or reset)
73
+ * Note: This clears the shared message channel for the specified secretKey
74
+ */
75
+ export function clearRequestIframeClientCache(secretKey) {
76
+ // Now client is no longer cached, only need to clear MessageChannel cache
77
+ // MessageChannel cleanup is handled by clearMessageChannelCache in cache.ts
78
+ // Empty implementation kept here to maintain API compatibility
79
+ void secretKey;
80
+ }
@@ -0,0 +1,61 @@
1
+ import { RequestIframeServerImpl } from '../core/server';
2
+ import { setupServerDebugListeners } from '../utils/debug';
3
+ import { getCachedServer, cacheServer, clearServerCache } from '../utils/cache';
4
+
5
+ /**
6
+ * Create a server (for receiving and handling requests)
7
+ *
8
+ * Note:
9
+ * - MessageChannel is cached at the window level by secretKey (ensures unique message listener)
10
+ * - If options.id is specified, the server will be cached and reused (singleton pattern)
11
+ * - If options.id is not specified, a new instance is created on each call
12
+ * - This allows different versions of the library to coexist
13
+ */
14
+ export function requestIframeServer(options) {
15
+ // Determine secretKey and id
16
+ var secretKey = options === null || options === void 0 ? void 0 : options.secretKey;
17
+ var id = options === null || options === void 0 ? void 0 : options.id;
18
+
19
+ // If id is specified, check cache first
20
+ if (id) {
21
+ var cached = getCachedServer(secretKey, id);
22
+ if (cached) {
23
+ return cached;
24
+ }
25
+ }
26
+
27
+ // Create server (internally obtains or creates a shared MessageChannel)
28
+ var server = new RequestIframeServerImpl({
29
+ secretKey,
30
+ id,
31
+ ackTimeout: options === null || options === void 0 ? void 0 : options.ackTimeout,
32
+ autoOpen: options === null || options === void 0 ? void 0 : options.autoOpen,
33
+ allowedOrigins: options === null || options === void 0 ? void 0 : options.allowedOrigins,
34
+ validateOrigin: options === null || options === void 0 ? void 0 : options.validateOrigin,
35
+ maxConcurrentRequestsPerClient: options === null || options === void 0 ? void 0 : options.maxConcurrentRequestsPerClient,
36
+ autoAckMaxMetaLength: options === null || options === void 0 ? void 0 : options.autoAckMaxMetaLength,
37
+ autoAckMaxIdLength: options === null || options === void 0 ? void 0 : options.autoAckMaxIdLength
38
+ });
39
+
40
+ // If trace mode is enabled, register debug listeners
41
+ if (options !== null && options !== void 0 && options.trace) {
42
+ setupServerDebugListeners(server);
43
+ }
44
+
45
+ // Cache server if id is specified
46
+ if (id) {
47
+ cacheServer(server, secretKey, id);
48
+ }
49
+ return server;
50
+ }
51
+
52
+ /**
53
+ * Clear server cache (for testing or reset)
54
+ * Note: This clears the cached server instances
55
+ */
56
+ export function clearRequestIframeServerCache(secretKey) {
57
+ // Clear server cache
58
+ clearServerCache();
59
+ // MessageChannel cleanup is handled by clearMessageChannelCache in cache.ts
60
+ void secretKey;
61
+ }