request-iframe 0.1.0 → 0.2.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/QUICKSTART.CN.md +4 -2
- package/QUICKSTART.md +4 -2
- package/README.CN.md +302 -54
- package/README.md +281 -36
- package/cdn/request-iframe-react.umd.js +3354 -0
- package/cdn/request-iframe-react.umd.js.map +1 -0
- package/cdn/request-iframe-react.umd.min.js +2 -0
- package/cdn/request-iframe-react.umd.min.js.map +1 -0
- package/cdn/request-iframe.umd.js +19735 -0
- package/cdn/request-iframe.umd.js.map +1 -0
- package/cdn/request-iframe.umd.min.js +4 -0
- package/cdn/request-iframe.umd.min.js.map +1 -0
- package/esm/api/client.js +31 -22
- package/esm/api/endpoint.js +229 -0
- package/esm/api/server.js +19 -9
- package/esm/constants/debug.js +17 -0
- package/esm/constants/index.js +115 -66
- package/esm/constants/log.js +11 -0
- package/esm/constants/messages.js +6 -1
- package/esm/constants/warn-once.js +15 -0
- package/esm/endpoint/facade.js +390 -0
- package/esm/endpoint/heartbeat/heartbeat.js +60 -0
- package/esm/endpoint/heartbeat/ping.js +20 -0
- package/esm/endpoint/index.js +13 -0
- package/esm/endpoint/infra/hub.js +316 -0
- package/esm/endpoint/infra/inbox.js +232 -0
- package/esm/endpoint/infra/outbox.js +408 -0
- package/esm/endpoint/stream/dispatcher.js +58 -0
- package/esm/endpoint/stream/errors.js +27 -0
- package/esm/endpoint/stream/factory.js +76 -0
- package/esm/endpoint/stream/file-auto-resolve.js +34 -0
- package/esm/endpoint/stream/file-writable.js +105 -0
- package/esm/endpoint/stream/handler.js +26 -0
- package/esm/{core → impl}/client.js +243 -320
- package/esm/{core → impl}/response.js +120 -154
- package/esm/impl/server.js +568 -0
- package/esm/index.js +13 -6
- package/esm/message/ack.js +27 -0
- package/esm/message/channel-cache.js +108 -0
- package/esm/message/channel.js +92 -5
- package/esm/message/dispatcher.js +149 -98
- package/esm/stream/error.js +22 -0
- package/esm/stream/index.js +3 -1
- package/esm/stream/readable-stream.js +101 -26
- package/esm/stream/stream-core.js +121 -3
- package/esm/stream/writable-stream.js +368 -43
- package/esm/utils/ack.js +36 -0
- package/esm/utils/blob.js +16 -0
- package/esm/utils/cache.js +25 -76
- package/esm/utils/content-type.js +81 -0
- package/esm/utils/debug.js +157 -180
- package/esm/utils/hooks.js +130 -0
- package/esm/utils/id.js +14 -0
- package/esm/utils/iframe.js +20 -0
- package/esm/utils/index.js +12 -162
- package/esm/utils/is.js +3 -0
- package/esm/utils/logger.js +55 -0
- package/esm/utils/origin.js +3 -1
- package/esm/utils/promise.js +3 -0
- package/esm/utils/window.js +31 -0
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +32 -23
- package/library/api/endpoint.d.ts +23 -0
- package/library/api/endpoint.d.ts.map +1 -0
- package/library/api/endpoint.js +235 -0
- package/library/api/server.d.ts +4 -1
- package/library/api/server.d.ts.map +1 -1
- package/library/api/server.js +19 -9
- package/library/constants/debug.d.ts +18 -0
- package/library/constants/debug.d.ts.map +1 -0
- package/library/constants/debug.js +23 -0
- package/library/constants/index.d.ts +58 -7
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +143 -67
- package/library/constants/log.d.ts +12 -0
- package/library/constants/log.d.ts.map +1 -0
- package/library/constants/log.js +17 -0
- package/library/constants/messages.d.ts +6 -1
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +6 -1
- package/library/constants/warn-once.d.ts +12 -0
- package/library/constants/warn-once.d.ts.map +1 -0
- package/library/constants/warn-once.js +22 -0
- package/library/endpoint/facade.d.ts +238 -0
- package/library/endpoint/facade.d.ts.map +1 -0
- package/library/endpoint/facade.js +398 -0
- package/library/endpoint/heartbeat/heartbeat.d.ts +34 -0
- package/library/endpoint/heartbeat/heartbeat.d.ts.map +1 -0
- package/library/endpoint/heartbeat/heartbeat.js +67 -0
- package/library/endpoint/heartbeat/ping.d.ts +18 -0
- package/library/endpoint/heartbeat/ping.d.ts.map +1 -0
- package/library/endpoint/heartbeat/ping.js +26 -0
- package/library/endpoint/index.d.ts +16 -0
- package/library/endpoint/index.d.ts.map +1 -0
- package/library/endpoint/index.js +114 -0
- package/library/endpoint/infra/hub.d.ts +170 -0
- package/library/endpoint/infra/hub.d.ts.map +1 -0
- package/library/endpoint/infra/hub.js +323 -0
- package/library/endpoint/infra/inbox.d.ts +73 -0
- package/library/endpoint/infra/inbox.d.ts.map +1 -0
- package/library/endpoint/infra/inbox.js +239 -0
- package/library/endpoint/infra/outbox.d.ts +149 -0
- package/library/endpoint/infra/outbox.d.ts.map +1 -0
- package/library/endpoint/infra/outbox.js +415 -0
- package/library/endpoint/stream/dispatcher.d.ts +33 -0
- package/library/endpoint/stream/dispatcher.d.ts.map +1 -0
- package/library/endpoint/stream/dispatcher.js +66 -0
- package/library/endpoint/stream/errors.d.ts +20 -0
- package/library/endpoint/stream/errors.d.ts.map +1 -0
- package/library/endpoint/stream/errors.js +32 -0
- package/library/endpoint/stream/factory.d.ts +44 -0
- package/library/endpoint/stream/factory.d.ts.map +1 -0
- package/library/endpoint/stream/factory.js +82 -0
- package/library/endpoint/stream/file-auto-resolve.d.ts +26 -0
- package/library/endpoint/stream/file-auto-resolve.d.ts.map +1 -0
- package/library/endpoint/stream/file-auto-resolve.js +41 -0
- package/library/endpoint/stream/file-writable.d.ts +33 -0
- package/library/endpoint/stream/file-writable.d.ts.map +1 -0
- package/library/endpoint/stream/file-writable.js +115 -0
- package/library/endpoint/stream/handler.d.ts +20 -0
- package/library/endpoint/stream/handler.d.ts.map +1 -0
- package/library/endpoint/stream/handler.js +32 -0
- package/library/{core → impl}/client.d.ts +16 -13
- package/library/impl/client.d.ts.map +1 -0
- package/library/{core → impl}/client.js +254 -333
- package/library/{core → impl}/request.d.ts.map +1 -1
- package/library/{core → impl}/response.d.ts +7 -12
- package/library/impl/response.d.ts.map +1 -0
- package/library/{core → impl}/response.js +120 -154
- package/library/{core → impl}/server.d.ts +26 -55
- package/library/impl/server.d.ts.map +1 -0
- package/library/impl/server.js +575 -0
- package/library/index.d.ts +13 -6
- package/library/index.d.ts.map +1 -1
- package/library/index.js +16 -16
- package/library/message/ack.d.ts +15 -0
- package/library/message/ack.d.ts.map +1 -0
- package/library/message/ack.js +33 -0
- package/library/message/channel-cache.d.ts +26 -0
- package/library/message/channel-cache.d.ts.map +1 -0
- package/library/message/channel-cache.js +115 -0
- package/library/message/channel.d.ts +53 -6
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +96 -9
- package/library/message/dispatcher.d.ts +17 -0
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +149 -98
- package/library/stream/error.d.ts +24 -0
- package/library/stream/error.d.ts.map +1 -0
- package/library/stream/error.js +29 -0
- package/library/stream/index.d.ts +4 -1
- package/library/stream/index.d.ts.map +1 -1
- package/library/stream/index.js +7 -4
- package/library/stream/readable-stream.d.ts.map +1 -1
- package/library/stream/readable-stream.js +102 -27
- package/library/stream/stream-core.d.ts +22 -1
- package/library/stream/stream-core.d.ts.map +1 -1
- package/library/stream/stream-core.js +120 -2
- package/library/stream/types.d.ts +115 -2
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts +20 -2
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +366 -41
- package/library/types/index.d.ts +17 -22
- 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/blob.d.ts +3 -0
- package/library/utils/blob.d.ts.map +1 -0
- package/library/utils/blob.js +22 -0
- package/library/utils/cache.d.ts +10 -20
- package/library/utils/cache.d.ts.map +1 -1
- package/library/utils/cache.js +25 -79
- package/library/utils/content-type.d.ts +13 -0
- package/library/utils/content-type.d.ts.map +1 -0
- package/library/utils/content-type.js +87 -0
- package/library/utils/debug.d.ts.map +1 -1
- package/library/utils/debug.js +156 -178
- package/library/utils/hooks.d.ts +30 -0
- package/library/utils/hooks.d.ts.map +1 -0
- package/library/utils/hooks.js +139 -0
- package/library/utils/id.d.ts +9 -0
- package/library/utils/id.d.ts.map +1 -0
- package/library/utils/id.js +21 -0
- package/library/utils/iframe.d.ts +5 -0
- package/library/utils/iframe.d.ts.map +1 -0
- package/library/utils/iframe.js +25 -0
- package/library/utils/index.d.ts +7 -34
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +58 -193
- package/library/utils/is.d.ts +2 -0
- package/library/utils/is.d.ts.map +1 -0
- package/library/utils/is.js +9 -0
- package/library/utils/logger.d.ts +13 -0
- package/library/utils/logger.d.ts.map +1 -0
- package/library/utils/logger.js +63 -0
- package/library/utils/origin.d.ts.map +1 -1
- package/library/utils/origin.js +2 -1
- package/library/utils/promise.d.ts +2 -0
- package/library/utils/promise.d.ts.map +1 -0
- package/library/utils/promise.js +9 -0
- package/library/utils/window.d.ts +2 -0
- package/library/utils/window.d.ts.map +1 -0
- package/library/utils/window.js +38 -0
- package/package.json +49 -2
- package/react/package.json +2 -1
- package/esm/core/client-server.js +0 -329
- package/esm/core/server.js +0 -767
- package/esm/utils/ack-meta.js +0 -53
- package/library/core/client-server.d.ts +0 -106
- package/library/core/client-server.d.ts.map +0 -1
- package/library/core/client-server.js +0 -336
- package/library/core/client.d.ts.map +0 -1
- package/library/core/response.d.ts.map +0 -1
- package/library/core/server.d.ts.map +0 -1
- package/library/core/server.js +0 -772
- package/library/utils/ack-meta.d.ts +0 -2
- package/library/utils/ack-meta.d.ts.map +0 -1
- package/library/utils/ack-meta.js +0 -59
- /package/esm/{core → impl}/request.js +0 -0
- /package/library/{core → impl}/request.d.ts +0 -0
- /package/library/{core → impl}/request.js +0 -0
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,8 +74,9 @@ 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
|
+
- 🧾 **Leveled Logging** - Warn/Error logs enabled by default; can be configured via `trace` level
|
|
79
80
|
- 🌍 **Internationalization** - Error messages can be customized for i18n
|
|
80
81
|
- ✅ **Protocol Versioning** - Built-in version control for upgrade compatibility
|
|
81
82
|
|
|
@@ -93,6 +94,30 @@ pnpm add request-iframe
|
|
|
93
94
|
|
|
94
95
|
**TypeScript**: Built-in complete type definitions, no need to install `@types/request-iframe`
|
|
95
96
|
|
|
97
|
+
## CDN (UMD bundles)
|
|
98
|
+
|
|
99
|
+
This repo also builds **script-tag friendly UMD bundles** (core + react hooks) that can be hosted on a CDN.
|
|
100
|
+
|
|
101
|
+
- Core bundle output: `cdn/request-iframe.umd(.min).js` → global `RequestIframe`
|
|
102
|
+
- React bundle output: `cdn/request-iframe-react.umd(.min).js` → global `RequestIframeReact` (requires `React` global and `RequestIframe` global)
|
|
103
|
+
|
|
104
|
+
Example (using unpkg):
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<!-- Core -->
|
|
108
|
+
<script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe.umd.min.js"></script>
|
|
109
|
+
|
|
110
|
+
<!-- React (optional) -->
|
|
111
|
+
<script src="https://unpkg.com/react@latest/umd/react.production.min.js"></script>
|
|
112
|
+
<script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe-react.umd.min.js"></script>
|
|
113
|
+
|
|
114
|
+
<script>
|
|
115
|
+
const { requestIframeClient, requestIframeServer, requestIframeEndpoint } = RequestIframe;
|
|
116
|
+
// React hooks are available on RequestIframeReact (e.g. RequestIframeReact.useClient)
|
|
117
|
+
console.log(!!requestIframeClient, !!requestIframeServer, !!requestIframeEndpoint);
|
|
118
|
+
<\/script>
|
|
119
|
+
```
|
|
120
|
+
|
|
96
121
|
## Quick Start
|
|
97
122
|
|
|
98
123
|
### 1. Parent Page (Client Side)
|
|
@@ -130,6 +155,11 @@ That's it! 🎉
|
|
|
130
155
|
|
|
131
156
|
> 💡 **Tip**: For more quick start guides, see [QUICKSTART.md](./QUICKSTART.md) or [QUICKSTART.CN.md](./QUICKSTART.CN.md) (中文)
|
|
132
157
|
|
|
158
|
+
## Which API should I use?
|
|
159
|
+
|
|
160
|
+
- **Use `requestIframeClient()` + `requestIframeServer()`** when communication is mostly one-way (parent → iframe), and you prefer a clear separation between request sender and handler.
|
|
161
|
+
- **Use `requestIframeEndpoint()`** when you need **bidirectional** communication (both sides need `send()` + `on()/use()/map()`), or when you want a single façade object to debug a full flow more easily.
|
|
162
|
+
|
|
133
163
|
---
|
|
134
164
|
|
|
135
165
|
## Use Cases
|
|
@@ -172,6 +202,29 @@ server.on('/event', (req, res) => {
|
|
|
172
202
|
});
|
|
173
203
|
```
|
|
174
204
|
|
|
205
|
+
### Popup / New Window (Window Communication)
|
|
206
|
+
|
|
207
|
+
`request-iframe` also works with a `Window` target (not only an iframe).
|
|
208
|
+
|
|
209
|
+
**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.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Parent page: open a new tab/window
|
|
213
|
+
const child = window.open('https://child.example.com/page.html', '_blank');
|
|
214
|
+
if (!child) throw new Error('Popup blocked');
|
|
215
|
+
|
|
216
|
+
// Parent -> child
|
|
217
|
+
const client = requestIframeClient(child, {
|
|
218
|
+
secretKey: 'popup-demo',
|
|
219
|
+
targetOrigin: 'https://child.example.com' // strongly recommended (avoid '*')
|
|
220
|
+
});
|
|
221
|
+
await client.send('/api/ping', { from: 'parent' });
|
|
222
|
+
|
|
223
|
+
// Child page: create server
|
|
224
|
+
const server = requestIframeServer({ secretKey: 'popup-demo' });
|
|
225
|
+
server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
|
|
226
|
+
```
|
|
227
|
+
|
|
175
228
|
### Cross-Origin Data Fetching
|
|
176
229
|
|
|
177
230
|
When iframe and parent page are on different origins, use request-iframe to securely fetch data:
|
|
@@ -237,7 +290,7 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
|
|
|
237
290
|
│ │
|
|
238
291
|
│ <──── RESPONSE ────────────────────── │ Return result
|
|
239
292
|
│ │
|
|
240
|
-
│ ────
|
|
293
|
+
│ ──── ACK (optional) ─────────────────> │ Acknowledge receipt of response/error (ACK-only requireAck)
|
|
241
294
|
│ │
|
|
242
295
|
```
|
|
243
296
|
|
|
@@ -246,15 +299,13 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
|
|
|
246
299
|
| Type | Direction | Description |
|
|
247
300
|
|------|-----------|-------------|
|
|
248
301
|
| `request` | Client → Server | Client initiates request |
|
|
249
|
-
| `ack` |
|
|
302
|
+
| `ack` | Receiver → Sender | Acknowledgment when `requireAck: true` and the message is accepted/handled (ACK-only) |
|
|
250
303
|
| `async` | Server → Client | Notifies client this is an async task (sent when handler returns Promise) |
|
|
251
304
|
| `response` | Server → Client | Returns response data |
|
|
252
305
|
| `error` | Server → Client | Returns error information |
|
|
253
|
-
| `received` | Client → Server | Client acknowledges receipt of response/error (optional, controlled by response `requireAck`) |
|
|
254
306
|
| `ping` | Client → Server | Connection detection (`isConnect()` method, may use `requireAck` to confirm delivery) |
|
|
255
307
|
| `pong` | Server → Client | Connection detection response |
|
|
256
308
|
| `stream_pull` | Receiver → Sender | Stream pull: receiver requests next chunks (pull/ack protocol) |
|
|
257
|
-
| `stream_ack` | Receiver → Sender | Stream ack: receiver acknowledges a chunk (pull/ack protocol) |
|
|
258
309
|
|
|
259
310
|
### Timeout Mechanism
|
|
260
311
|
|
|
@@ -558,6 +609,8 @@ server.on('/api/data', (req, res) => {
|
|
|
558
609
|
|
|
559
610
|
### File Transfer
|
|
560
611
|
|
|
612
|
+
> 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()`.
|
|
613
|
+
|
|
561
614
|
#### Server → Client (Server sends file to client)
|
|
562
615
|
|
|
563
616
|
```typescript
|
|
@@ -592,7 +645,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
|
|
|
592
645
|
|
|
593
646
|
#### Client → Server (Client sends file to server)
|
|
594
647
|
|
|
595
|
-
Client sends file
|
|
648
|
+
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`.
|
|
596
649
|
|
|
597
650
|
```typescript
|
|
598
651
|
// Client side: Send file (stream, autoResolve defaults to true)
|
|
@@ -616,16 +669,71 @@ server.on('/api/upload', async (req, res) => {
|
|
|
616
669
|
});
|
|
617
670
|
```
|
|
618
671
|
|
|
619
|
-
**Note:** When using `client.send()` with a `File` or `Blob`, it automatically dispatches to `client.sendFile()
|
|
672
|
+
**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.
|
|
620
673
|
|
|
621
674
|
### Streaming
|
|
622
675
|
|
|
623
|
-
|
|
676
|
+
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`).
|
|
677
|
+
|
|
678
|
+
#### Long-lived subscription (push mode)
|
|
679
|
+
|
|
680
|
+
> Notes:
|
|
681
|
+
> - `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).
|
|
682
|
+
> - `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.
|
|
683
|
+
> - If `maxConcurrentRequestsPerClient` is enabled, a long-lived stream occupies one in-flight request slot.
|
|
684
|
+
> - **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`.
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
/**
|
|
688
|
+
* Server side: subscribe (long-lived)
|
|
689
|
+
* - mode: 'push': writer calls write()
|
|
690
|
+
* - expireTimeout: 0: disable auto-expire (use with care)
|
|
691
|
+
*/
|
|
692
|
+
server.on('/api/subscribe', (req, res) => {
|
|
693
|
+
const stream = new IframeWritableStream({
|
|
694
|
+
type: 'data',
|
|
695
|
+
chunked: true,
|
|
696
|
+
mode: 'push',
|
|
697
|
+
expireTimeout: 0,
|
|
698
|
+
/** optional: writer-side idle timeout while waiting for pull/ack */
|
|
699
|
+
streamTimeout: 15000
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
/** do not await, otherwise it blocks until stream ends */
|
|
703
|
+
void res.sendStream(stream);
|
|
704
|
+
|
|
705
|
+
const timer = setInterval(() => {
|
|
706
|
+
try {
|
|
707
|
+
stream.write({ type: 'tick', ts: Date.now() });
|
|
708
|
+
} catch {
|
|
709
|
+
clearInterval(timer);
|
|
710
|
+
}
|
|
711
|
+
}, 1000);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Client side: consume continuously (prefer for-await for long-lived streams)
|
|
716
|
+
*/
|
|
717
|
+
const resp = await client.send('/api/subscribe', {});
|
|
718
|
+
if (isIframeReadableStream(resp.stream)) {
|
|
719
|
+
/** Optional: observe events */
|
|
720
|
+
const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
|
|
721
|
+
console.error('stream error:', error);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
for await (const evt of resp.stream) {
|
|
725
|
+
console.log('event:', evt);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
off();
|
|
729
|
+
}
|
|
730
|
+
```
|
|
624
731
|
|
|
625
732
|
#### Server → Client (Server sends stream to client)
|
|
626
733
|
|
|
627
734
|
```typescript
|
|
628
|
-
import {
|
|
735
|
+
import {
|
|
736
|
+
StreamEvent,
|
|
629
737
|
IframeWritableStream,
|
|
630
738
|
IframeFileWritableStream,
|
|
631
739
|
isIframeReadableStream,
|
|
@@ -740,21 +848,23 @@ server.on('/api/uploadStream', async (req, res) => {
|
|
|
740
848
|
|
|
741
849
|
| Type | Description |
|
|
742
850
|
|------|-------------|
|
|
743
|
-
| `IframeWritableStream` |
|
|
744
|
-
| `IframeFileWritableStream` |
|
|
745
|
-
| `IframeReadableStream` |
|
|
746
|
-
| `IframeFileReadableStream` |
|
|
851
|
+
| `IframeWritableStream` | Writer/producer stream: **created by whichever side is sending the stream** (server→client response stream, or client→server request stream) |
|
|
852
|
+
| `IframeFileWritableStream` | File writer/producer stream (base64-encodes internally) |
|
|
853
|
+
| `IframeReadableStream` | Reader/consumer stream for receiving regular data (regardless of which side sent it) |
|
|
854
|
+
| `IframeFileReadableStream` | File reader/consumer stream (base64-decodes internally) |
|
|
747
855
|
|
|
748
856
|
> **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).
|
|
749
857
|
|
|
750
858
|
**Stream timeouts:**
|
|
751
|
-
- `options.streamTimeout` (request option): client-side stream idle timeout while consuming `response.stream` (data/file streams). When triggered, the client
|
|
859
|
+
- `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.
|
|
752
860
|
- `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`.
|
|
753
|
-
- `streamTimeout` (writable stream option): writer-side idle timeout. If the writer does not receive `stream_pull
|
|
861
|
+
- `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.
|
|
862
|
+
- `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.
|
|
863
|
+
- `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.
|
|
754
864
|
|
|
755
865
|
**Pull/Ack protocol (default):**
|
|
756
|
-
- Reader automatically sends `stream_pull` to request chunks and sends `stream_ack` for each received chunk.
|
|
757
866
|
- Writer only sends `stream_data` when it has received `stream_pull`, enabling real backpressure.
|
|
867
|
+
- Disconnect detection does not rely on a dedicated per-frame ack message type, but uses `streamTimeout + heartbeat(isConnect)`.
|
|
758
868
|
|
|
759
869
|
**consume default change:**
|
|
760
870
|
- `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.
|
|
@@ -778,9 +888,9 @@ Server can require Client to acknowledge receipt of response:
|
|
|
778
888
|
```typescript
|
|
779
889
|
server.on('/api/important', async (req, res) => {
|
|
780
890
|
// requireAck: true means client needs to acknowledge
|
|
781
|
-
const
|
|
891
|
+
const acked = await res.send(data, { requireAck: true });
|
|
782
892
|
|
|
783
|
-
if (
|
|
893
|
+
if (acked) {
|
|
784
894
|
console.log('Client acknowledged receipt');
|
|
785
895
|
} else {
|
|
786
896
|
console.log('Client did not acknowledge (timeout)');
|
|
@@ -788,21 +898,25 @@ server.on('/api/important', async (req, res) => {
|
|
|
788
898
|
});
|
|
789
899
|
```
|
|
790
900
|
|
|
791
|
-
> **Note**: Client acknowledgment (`
|
|
901
|
+
> **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`.
|
|
792
902
|
|
|
793
903
|
### Trace Mode
|
|
794
904
|
|
|
795
|
-
|
|
905
|
+
By default, request-iframe only prints **warn/error** logs (to avoid noisy console in production).
|
|
906
|
+
|
|
907
|
+
Enable trace mode (or set a log level) to view detailed communication logs:
|
|
796
908
|
|
|
797
909
|
```typescript
|
|
910
|
+
import { LogLevel } from 'request-iframe';
|
|
911
|
+
|
|
798
912
|
const client = requestIframeClient(iframe, {
|
|
799
913
|
secretKey: 'demo',
|
|
800
|
-
trace: true
|
|
914
|
+
trace: true // equivalent to LogLevel.TRACE
|
|
801
915
|
});
|
|
802
916
|
|
|
803
917
|
const server = requestIframeServer({
|
|
804
918
|
secretKey: 'demo',
|
|
805
|
-
trace:
|
|
919
|
+
trace: LogLevel.INFO // enable info/warn/error logs (less verbose than trace)
|
|
806
920
|
});
|
|
807
921
|
|
|
808
922
|
// Console output:
|
|
@@ -811,6 +925,14 @@ const server = requestIframeServer({
|
|
|
811
925
|
// [request-iframe] [INFO] ✅ Request Success { status: 200, data: {...} }
|
|
812
926
|
```
|
|
813
927
|
|
|
928
|
+
`trace` supports:
|
|
929
|
+
- `true` / `false`
|
|
930
|
+
- `'trace' | 'info' | 'warn' | 'error' | 'silent'` (or `LogLevel.*`)
|
|
931
|
+
|
|
932
|
+
Notes:
|
|
933
|
+
- When `trace` is `LogLevel.TRACE` or `LogLevel.INFO`, the library will also attach built-in debug interceptors/listeners for richer request/response logs.
|
|
934
|
+
- When `trace` is `LogLevel.WARN` / `LogLevel.ERROR` / `LogLevel.SILENT`, it only affects log output level (no extra debug interceptors attached).
|
|
935
|
+
|
|
814
936
|
### Internationalization
|
|
815
937
|
|
|
816
938
|
```typescript
|
|
@@ -841,7 +963,7 @@ Create a Client instance.
|
|
|
841
963
|
|-----------|------|-------------|
|
|
842
964
|
| `target` | `HTMLIFrameElement \| Window` | Target iframe element or window object |
|
|
843
965
|
| `options.secretKey` | `string` | Message isolation identifier (optional) |
|
|
844
|
-
| `options.trace` | `boolean
|
|
966
|
+
| `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | Trace/log level (optional). Default logs are warn/error only |
|
|
845
967
|
| `options.targetOrigin` | `string` | Override postMessage targetOrigin for sending (optional). If `target` is a `Window`, default is `*`. |
|
|
846
968
|
| `options.ackTimeout` | `number` | Global default ACK acknowledgment timeout (ms), default 1000 |
|
|
847
969
|
| `options.timeout` | `number` | Global default request timeout (ms), default 5000 |
|
|
@@ -853,6 +975,70 @@ Create a Client instance.
|
|
|
853
975
|
|
|
854
976
|
**Returns:** `RequestIframeClient`
|
|
855
977
|
|
|
978
|
+
**Notes about `target: Window`:**
|
|
979
|
+
- **You must have a `Window` reference** (e.g. from `window.open()`, `window.opener`, or `MessageEvent.source`).
|
|
980
|
+
- You **cannot** communicate with an arbitrary browser tab by URL.
|
|
981
|
+
- For security, prefer setting a strict `targetOrigin` and configure `allowedOrigins` / `validateOrigin`.
|
|
982
|
+
|
|
983
|
+
**Production configuration template:**
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
import { requestIframeClient, requestIframeServer } from 'request-iframe';
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Recommended: explicitly constrain 3 things
|
|
990
|
+
* - secretKey: isolate different apps/instances (avoid cross-talk)
|
|
991
|
+
* - targetOrigin: postMessage targetOrigin (strongly avoid '*' for Window targets)
|
|
992
|
+
* - allowedOrigins / validateOrigin: incoming origin allowlist validation
|
|
993
|
+
*/
|
|
994
|
+
const secretKey = 'my-app';
|
|
995
|
+
const targetOrigin = 'https://child.example.com';
|
|
996
|
+
const allowedOrigins = [targetOrigin];
|
|
997
|
+
|
|
998
|
+
// Client (parent)
|
|
999
|
+
const client = requestIframeClient(window.open(targetOrigin)!, {
|
|
1000
|
+
secretKey,
|
|
1001
|
+
targetOrigin,
|
|
1002
|
+
allowedOrigins
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Server (child/iframe)
|
|
1006
|
+
const server = requestIframeServer({
|
|
1007
|
+
secretKey,
|
|
1008
|
+
allowedOrigins,
|
|
1009
|
+
// Mitigate message explosion (tune as needed)
|
|
1010
|
+
maxConcurrentRequestsPerClient: 50
|
|
1011
|
+
});
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**Production configuration template (iframe target):**
|
|
1015
|
+
|
|
1016
|
+
```typescript
|
|
1017
|
+
import { requestIframeClient, requestIframeServer } from 'request-iframe';
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* For iframe targets, you can derive targetOrigin from iframe.src, and use it as the allowedOrigins allowlist.
|
|
1021
|
+
*/
|
|
1022
|
+
const iframe = document.querySelector('iframe')!;
|
|
1023
|
+
const targetOrigin = new URL(iframe.src).origin;
|
|
1024
|
+
const secretKey = 'my-app';
|
|
1025
|
+
|
|
1026
|
+
// Client (parent)
|
|
1027
|
+
const client = requestIframeClient(iframe, {
|
|
1028
|
+
secretKey,
|
|
1029
|
+
// Explicitly set it (even though the library can derive it) to avoid accidentally using '*'
|
|
1030
|
+
targetOrigin,
|
|
1031
|
+
allowedOrigins: [targetOrigin]
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// Server (inside iframe)
|
|
1035
|
+
const server = requestIframeServer({
|
|
1036
|
+
secretKey,
|
|
1037
|
+
allowedOrigins: [targetOrigin],
|
|
1038
|
+
maxConcurrentRequestsPerClient: 50
|
|
1039
|
+
});
|
|
1040
|
+
```
|
|
1041
|
+
|
|
856
1042
|
### requestIframeServer(options?)
|
|
857
1043
|
|
|
858
1044
|
Create a Server instance.
|
|
@@ -862,7 +1048,7 @@ Create a Server instance.
|
|
|
862
1048
|
| Parameter | Type | Description |
|
|
863
1049
|
|-----------|------|-------------|
|
|
864
1050
|
| `options.secretKey` | `string` | Message isolation identifier (optional) |
|
|
865
|
-
| `options.trace` | `boolean
|
|
1051
|
+
| `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | Trace/log level (optional). Default logs are warn/error only |
|
|
866
1052
|
| `options.ackTimeout` | `number` | Wait for client acknowledgment timeout (ms), default 1000 |
|
|
867
1053
|
| `options.maxConcurrentRequestsPerClient` | `number` | Max concurrent in-flight requests per client (per origin + creatorId). Default Infinity |
|
|
868
1054
|
| `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | Allowlist for incoming message origins (optional, recommended for production) |
|
|
@@ -870,6 +1056,65 @@ Create a Server instance.
|
|
|
870
1056
|
|
|
871
1057
|
**Returns:** `RequestIframeServer`
|
|
872
1058
|
|
|
1059
|
+
### requestIframeEndpoint(target, options?)
|
|
1060
|
+
|
|
1061
|
+
Create an **endpoint facade** (client + server) for a peer window/iframe.
|
|
1062
|
+
|
|
1063
|
+
It can:
|
|
1064
|
+
- **send requests** to the peer: `endpoint.send(...)`
|
|
1065
|
+
- **handle requests** from the peer: `endpoint.on(...)`, `endpoint.use(...)`, `endpoint.map(...)`
|
|
1066
|
+
|
|
1067
|
+
Notes:
|
|
1068
|
+
- Client and server instances are **lazily created** (only when you first access send/handlers APIs).
|
|
1069
|
+
- `options.id` (if provided) becomes the shared ID for both sides (client + server); otherwise a random one is generated.
|
|
1070
|
+
- `options.trace` follows the same leveled logging rules as client/server (`LogLevel.*` recommended).
|
|
1071
|
+
|
|
1072
|
+
Example (bidirectional via endpoint, recommended):
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
import { requestIframeEndpoint, LogLevel } from 'request-iframe';
|
|
1076
|
+
|
|
1077
|
+
// Parent page (has iframe element)
|
|
1078
|
+
const iframe = document.querySelector('iframe')!;
|
|
1079
|
+
const parentEndpoint = requestIframeEndpoint(iframe, {
|
|
1080
|
+
secretKey: 'demo',
|
|
1081
|
+
trace: LogLevel.INFO
|
|
1082
|
+
});
|
|
1083
|
+
parentEndpoint.on('/notify', (req, res) => res.send({ ok: true, echo: req.body }));
|
|
1084
|
+
|
|
1085
|
+
// Iframe page (has window.parent)
|
|
1086
|
+
const iframeEndpoint = requestIframeEndpoint(window.parent, {
|
|
1087
|
+
secretKey: 'demo',
|
|
1088
|
+
targetOrigin: 'https://parent.example.com',
|
|
1089
|
+
trace: true
|
|
1090
|
+
});
|
|
1091
|
+
iframeEndpoint.on('/api/ping', (req, res) => res.send({ ok: true }));
|
|
1092
|
+
|
|
1093
|
+
// Either side can now send + handle
|
|
1094
|
+
await parentEndpoint.send('/api/ping', { from: 'parent' });
|
|
1095
|
+
await iframeEndpoint.send('/notify', { from: 'iframe' });
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
Production configuration template (recommended):
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
import { requestIframeEndpoint, LogLevel } from 'request-iframe';
|
|
1102
|
+
|
|
1103
|
+
const secretKey = 'my-app';
|
|
1104
|
+
const iframe = document.querySelector('iframe')!;
|
|
1105
|
+
const targetOrigin = new URL(iframe.src).origin;
|
|
1106
|
+
|
|
1107
|
+
const endpoint = requestIframeEndpoint(iframe, {
|
|
1108
|
+
secretKey,
|
|
1109
|
+
targetOrigin,
|
|
1110
|
+
allowedOrigins: [targetOrigin],
|
|
1111
|
+
// Mitigate message explosion (tune as needed)
|
|
1112
|
+
maxConcurrentRequestsPerClient: 50,
|
|
1113
|
+
// Logs: default is warn/error; choose info for debugging
|
|
1114
|
+
trace: LogLevel.WARN
|
|
1115
|
+
});
|
|
1116
|
+
```
|
|
1117
|
+
|
|
873
1118
|
### Client API
|
|
874
1119
|
|
|
875
1120
|
#### client.send(path, body?, options?)
|
|
@@ -1447,7 +1692,11 @@ Just ensure both sides use the same `secretKey`.
|
|
|
1447
1692
|
|
|
1448
1693
|
### 4. Can Server actively push messages?
|
|
1449
1694
|
|
|
1450
|
-
request-iframe is request-response mode, Server cannot actively push
|
|
1695
|
+
request-iframe is request-response mode, Server cannot actively push by itself.
|
|
1696
|
+
|
|
1697
|
+
For bidirectional communication, you have 2 options:
|
|
1698
|
+
- Create a reverse `client` inside the iframe (traditional way)
|
|
1699
|
+
- Use `requestIframeEndpoint()` on both sides (recommended) so each side has **send + handle** in one object
|
|
1451
1700
|
|
|
1452
1701
|
```typescript
|
|
1453
1702
|
// Inside iframe
|
|
@@ -1460,10 +1709,11 @@ await client.send('/notify', { event: 'data-changed' });
|
|
|
1460
1709
|
|
|
1461
1710
|
### 5. How to debug communication issues?
|
|
1462
1711
|
|
|
1463
|
-
1. **Enable
|
|
1712
|
+
1. **Enable logs with levels**: by default only warn/error logs are printed; enable `trace: LogLevel.INFO` (or `trace: true`) to see detailed logs
|
|
1464
1713
|
2. **Check secretKey**: Ensure Client and Server use the same secretKey
|
|
1465
1714
|
3. **Check iframe loading**: Ensure iframe is fully loaded
|
|
1466
|
-
4. **Check
|
|
1715
|
+
4. **Check origin constraints**: prefer setting strict `targetOrigin` and configure `allowedOrigins` / `validateOrigin`
|
|
1716
|
+
5. **Consider using `requestIframeEndpoint()`**: it unifies both directions (send + handle) and makes it easier to debug flows in one place
|
|
1467
1717
|
|
|
1468
1718
|
---
|
|
1469
1719
|
|
|
@@ -1515,12 +1765,7 @@ yarn build
|
|
|
1515
1765
|
|
|
1516
1766
|
### Test Coverage
|
|
1517
1767
|
|
|
1518
|
-
The project
|
|
1519
|
-
|
|
1520
|
-
- **Statement Coverage**: 76.88%
|
|
1521
|
-
- **Branch Coverage**: 64.13%
|
|
1522
|
-
- **Function Coverage**: 75%
|
|
1523
|
-
- **Line Coverage**: 78.71%
|
|
1768
|
+
The project maintains **high test coverage** and CI coverage reports (see badges at the top of this README).
|
|
1524
1769
|
|
|
1525
1770
|
Coverage reports are generated in the `coverage/` directory, view detailed coverage report via `coverage/index.html`.
|
|
1526
1771
|
|