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.
- package/README.CN.md +220 -21
- package/README.md +221 -24
- package/esm/api/client.js +80 -0
- package/esm/api/server.js +61 -0
- package/esm/constants/index.js +289 -0
- package/esm/constants/messages.js +157 -0
- package/esm/core/client-server.js +294 -0
- package/esm/core/client.js +873 -0
- package/esm/core/request.js +27 -0
- package/esm/core/response.js +459 -0
- package/esm/core/server.js +776 -0
- package/esm/index.js +21 -0
- package/esm/interceptors/index.js +122 -0
- package/esm/message/channel.js +182 -0
- package/esm/message/dispatcher.js +418 -0
- package/esm/message/index.js +2 -0
- package/esm/stream/file-stream.js +289 -0
- package/esm/stream/index.js +44 -0
- package/esm/stream/readable-stream.js +539 -0
- package/esm/stream/stream-core.js +204 -0
- package/esm/stream/types.js +1 -0
- package/esm/stream/writable-stream.js +836 -0
- package/esm/types/index.js +1 -0
- package/esm/utils/ack.js +36 -0
- package/esm/utils/cache.js +147 -0
- package/esm/utils/cookie.js +352 -0
- package/esm/utils/debug.js +521 -0
- package/esm/utils/error.js +27 -0
- package/esm/utils/index.js +180 -0
- package/esm/utils/origin.js +30 -0
- package/esm/utils/path-match.js +148 -0
- package/esm/utils/protocol.js +157 -0
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +13 -5
- package/library/api/server.d.ts.map +1 -1
- package/library/api/server.js +6 -1
- package/library/constants/index.d.ts +59 -4
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +67 -9
- package/library/constants/messages.d.ts +8 -1
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +8 -1
- package/library/core/client-server.d.ts +7 -15
- package/library/core/client-server.d.ts.map +1 -1
- package/library/core/client-server.js +56 -44
- package/library/core/client.d.ts +4 -1
- package/library/core/client.d.ts.map +1 -1
- package/library/core/client.js +74 -31
- package/library/core/response.d.ts +21 -3
- package/library/core/response.d.ts.map +1 -1
- package/library/core/response.js +55 -7
- package/library/core/server.d.ts +34 -3
- package/library/core/server.d.ts.map +1 -1
- package/library/core/server.js +191 -21
- package/library/message/channel.d.ts +6 -0
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +2 -1
- package/library/message/dispatcher.d.ts +32 -0
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +131 -1
- package/library/stream/file-stream.d.ts +4 -0
- package/library/stream/file-stream.d.ts.map +1 -1
- package/library/stream/file-stream.js +61 -33
- package/library/stream/index.d.ts.map +1 -1
- package/library/stream/index.js +2 -0
- package/library/stream/readable-stream.d.ts +30 -11
- package/library/stream/readable-stream.d.ts.map +1 -1
- package/library/stream/readable-stream.js +368 -73
- package/library/stream/stream-core.d.ts +65 -0
- package/library/stream/stream-core.d.ts.map +1 -0
- package/library/stream/stream-core.js +211 -0
- package/library/stream/types.d.ts +203 -3
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts +59 -13
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +647 -197
- package/library/types/index.d.ts +70 -4
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/ack.d.ts +2 -0
- package/library/utils/ack.d.ts.map +1 -0
- package/library/utils/ack.js +44 -0
- package/library/utils/debug.js +1 -1
- package/library/utils/index.d.ts +1 -0
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +19 -2
- package/library/utils/origin.d.ts +14 -0
- package/library/utils/origin.d.ts.map +1 -0
- package/library/utils/origin.js +35 -0
- package/package.json +30 -7
- package/react/README.md +16 -0
- package/react/esm/index.js +284 -0
- package/react/library/index.d.ts +1 -1
- package/react/library/index.d.ts.map +1 -1
- package/react/library/index.js +3 -3
- 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
|
|
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://
|
|
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
|
|
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** -
|
|
77
|
+
- 📁 **File Transfer** - File transfer via streams (client↔server)
|
|
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
|
|
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
|
-
│ ────
|
|
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` |
|
|
272
|
+
| `ack` | Receiver → Sender | 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
|
-
| `
|
|
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
|
|
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__:
|
|
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
|
-
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
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` |
|
|
727
|
-
| `IframeFileWritableStream` |
|
|
728
|
-
| `IframeReadableStream` |
|
|
729
|
-
| `IframeFileReadableStream` |
|
|
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
|
|
861
|
+
const acked = await res.send(data, { requireAck: true });
|
|
751
862
|
|
|
752
|
-
if (
|
|
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
|
+
}
|