request-iframe 0.1.0 → 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 (71) hide show
  1. package/README.CN.md +174 -22
  2. package/README.md +166 -22
  3. package/esm/api/client.js +6 -5
  4. package/esm/api/server.js +3 -1
  5. package/esm/constants/index.js +38 -6
  6. package/esm/constants/messages.js +3 -1
  7. package/esm/core/client-server.js +8 -43
  8. package/esm/core/client.js +7 -7
  9. package/esm/core/response.js +16 -8
  10. package/esm/core/server.js +18 -9
  11. package/esm/message/channel.js +2 -1
  12. package/esm/message/dispatcher.js +63 -25
  13. package/esm/stream/readable-stream.js +57 -18
  14. package/esm/stream/stream-core.js +115 -2
  15. package/esm/stream/writable-stream.js +281 -27
  16. package/esm/utils/ack.js +36 -0
  17. package/esm/utils/debug.js +1 -1
  18. package/esm/utils/index.js +5 -3
  19. package/esm/utils/origin.js +3 -1
  20. package/library/api/client.d.ts.map +1 -1
  21. package/library/api/client.js +5 -4
  22. package/library/api/server.d.ts.map +1 -1
  23. package/library/api/server.js +3 -1
  24. package/library/constants/index.d.ts +36 -5
  25. package/library/constants/index.d.ts.map +1 -1
  26. package/library/constants/index.js +41 -8
  27. package/library/constants/messages.d.ts +3 -1
  28. package/library/constants/messages.d.ts.map +1 -1
  29. package/library/constants/messages.js +3 -1
  30. package/library/core/client-server.d.ts +4 -13
  31. package/library/core/client-server.d.ts.map +1 -1
  32. package/library/core/client-server.js +7 -42
  33. package/library/core/client.d.ts.map +1 -1
  34. package/library/core/client.js +6 -6
  35. package/library/core/response.d.ts +2 -2
  36. package/library/core/response.d.ts.map +1 -1
  37. package/library/core/response.js +15 -7
  38. package/library/core/server.d.ts +6 -2
  39. package/library/core/server.d.ts.map +1 -1
  40. package/library/core/server.js +18 -9
  41. package/library/message/channel.d.ts +1 -1
  42. package/library/message/channel.d.ts.map +1 -1
  43. package/library/message/channel.js +2 -1
  44. package/library/message/dispatcher.d.ts +10 -0
  45. package/library/message/dispatcher.d.ts.map +1 -1
  46. package/library/message/dispatcher.js +62 -24
  47. package/library/stream/readable-stream.d.ts.map +1 -1
  48. package/library/stream/readable-stream.js +56 -17
  49. package/library/stream/stream-core.d.ts +22 -1
  50. package/library/stream/stream-core.d.ts.map +1 -1
  51. package/library/stream/stream-core.js +114 -1
  52. package/library/stream/types.d.ts +115 -2
  53. package/library/stream/types.d.ts.map +1 -1
  54. package/library/stream/writable-stream.d.ts +20 -2
  55. package/library/stream/writable-stream.d.ts.map +1 -1
  56. package/library/stream/writable-stream.js +277 -23
  57. package/library/types/index.d.ts +3 -4
  58. package/library/types/index.d.ts.map +1 -1
  59. package/library/utils/ack.d.ts +2 -0
  60. package/library/utils/ack.d.ts.map +1 -0
  61. package/library/utils/ack.js +44 -0
  62. package/library/utils/debug.js +1 -1
  63. package/library/utils/index.d.ts.map +1 -1
  64. package/library/utils/index.js +4 -3
  65. package/library/utils/origin.d.ts.map +1 -1
  66. package/library/utils/origin.js +2 -1
  67. package/package.json +1 -1
  68. package/esm/utils/ack-meta.js +0 -53
  69. package/library/utils/ack-meta.d.ts +0 -2
  70. package/library/utils/ack-meta.d.ts.map +0 -1
  71. package/library/utils/ack-meta.js +0 -59
package/README.CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # request-iframe
2
2
 
3
- 像发送 HTTP 请求一样与 iframe 通信!基于 `postMessage` 实现的 iframe 跨域通信库。
3
+ 像发送 HTTP 请求一样与 iframe / Window 通信!基于 `postMessage` 实现的浏览器跨页面通信库。
4
4
 
5
5
  > 🌐 **Languages**: [English](./README.md) | [中文](./README.CN.md)
6
6
 
@@ -8,7 +8,7 @@
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
  ## 📑 目录
@@ -48,7 +48,7 @@
48
48
 
49
49
  ## 为什么选择 request-iframe?
50
50
 
51
- 在微前端、iframe 嵌套等场景下,父子页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
51
+ 在微前端、iframe 嵌套、弹窗(window.open)等场景下,跨页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
52
52
 
53
53
  | 痛点 | 传统方式 | request-iframe |
54
54
  |------|----------|----------------|
@@ -74,7 +74,7 @@
74
74
  - ⏱️ **智能超时** - 三阶段超时(连接/同步/异步),自动识别长任务
75
75
  - 📦 **TypeScript** - 完整的类型定义和智能提示
76
76
  - 🔒 **消息隔离** - secretKey 机制避免多实例消息串线
77
- - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
77
+ - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
78
78
  - 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
79
79
  - 🌍 **多语言** - 错误消息可自定义,便于国际化
80
80
  - ✅ **协议版本** - 内置版本控制,便于升级兼容
@@ -151,7 +151,7 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
151
151
  │ │
152
152
  │ <──── RESPONSE ──────────────────────── │ 返回结果
153
153
  │ │
154
- │ ──── RECEIVED (可选) ──────────────────> │ 确认收到响应
154
+ │ ──── ACK (可选) ───────────────────────> │ 确认收到响应
155
155
  │ │
156
156
  ```
157
157
 
@@ -160,11 +160,10 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
160
160
  | 类型 | 方向 | 说明 |
161
161
  |------|------|------|
162
162
  | `request` | Client → Server | 客户端发起请求 |
163
- | `ack` | ServerClient | 服务端确认收到请求(当请求 `requireAck` 开启时) |
163
+ | `ack` | 接收方发送方 | 回执确认(当消息 `requireAck: true` 且被接管/处理时发送;ACK-only) |
164
164
  | `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
165
165
  | `response` | Server → Client | 返回响应数据 |
166
166
  | `error` | Server → Client | 返回错误信息 |
167
- | `received` | Client → Server | 客户端确认收到响应/错误(可选,由响应的 `requireAck` 控制) |
168
167
  | `ping` | Client → Server | 连接检测(`isConnect()`;可使用 `requireAck` 确认投递) |
169
168
  | `pong` | Server → Client | 连接检测响应 |
170
169
 
@@ -286,6 +285,29 @@ server.on('/event', (req, res) => {
286
285
  });
287
286
  ```
288
287
 
288
+ ### 弹窗 / 新标签页(Window 通信)
289
+
290
+ `request-iframe` 不仅可以与 iframe 通信,也可以把 `target` 直接传 `Window`(例如弹窗/新标签页)。
291
+
292
+ **重要前提**:你必须拿到对方页面的 `Window` 引用(例如 `window.open()` 的返回值,或通过 `window.opener` / `MessageEvent.source` 获取)。**无法**通过 URL 给“任意标签页”发消息。
293
+
294
+ ```typescript
295
+ // 父页面:打开新标签页/弹窗
296
+ const child = window.open('https://child.example.com/page.html', '_blank');
297
+ if (!child) throw new Error('弹窗被拦截');
298
+
299
+ // 父 -> 子
300
+ const client = requestIframeClient(child, {
301
+ secretKey: 'popup-demo',
302
+ targetOrigin: 'https://child.example.com' // 强烈建议不要用 '*'
303
+ });
304
+ await client.send('/api/ping', { from: 'parent' });
305
+
306
+ // 子页面:创建 server
307
+ const server = requestIframeServer({ secretKey: 'popup-demo' });
308
+ server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
309
+ ```
310
+
289
311
  ### 跨域数据获取
290
312
 
291
313
  当 iframe 与父页面不同域时,使用 request-iframe 安全地获取数据:
@@ -583,6 +605,8 @@ server.on('/api/data', (req, res) => {
583
605
 
584
606
  ### 文件传输
585
607
 
608
+ > 说明:文件传输(无论 Client→Server 还是 Server→Client)底层都会通过 stream 协议承载;你只需要使用 `client.sendFile()` / `res.sendFile()` 这一层 API 即可。
609
+
586
610
  ```typescript
587
611
  // Server 端发送文件
588
612
  server.on('/api/download', async (req, res) => {
@@ -615,7 +639,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
615
639
 
616
640
  #### Client → Server(Client 向 Server 发送文件)
617
641
 
618
- Client 端发送文件**仅走流式**。使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
642
+ Client 端发送文件使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
619
643
 
620
644
  ```typescript
621
645
  // Client 端:发送文件(stream,autoResolve 默认 true)
@@ -639,14 +663,74 @@ server.on('/api/upload', async (req, res) => {
639
663
  });
640
664
  ```
641
665
 
642
- **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`(走流式)。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
666
+ **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
643
667
 
644
668
  ### 流式传输(Stream)
645
669
 
646
- 对于大文件或需要分块传输的场景,可以使用流式传输:
670
+ Stream 除了用于大文件/分块传输,也可以用于“长连接 / 订阅式交互”(类似 SSE / WebSocket,但基于 `postMessage`)。常见用法有两类:
671
+
672
+ - **长连接/订阅**:Client 发起一次请求拿到 `response.stream`,然后用 `for await` 持续消费事件;需要结束时调用 `stream.cancel()`。
673
+ - **分块/文件流**:按 chunk 传输数据或文件(下方示例)。
674
+
675
+ > 长连接注意事项:
676
+ > - `IframeWritableStream` 默认会使用 `asyncTimeout` 作为 `expireTimeout`(避免泄露)。如果你的订阅需要更久,请显式设置更大的 `expireTimeout`,或设置 `expireTimeout: 0` 关闭自动过期(建议配合业务侧取消/重连,避免泄露)。
677
+ > - Server 端的 `res.sendStream(stream)` 会一直等待到流结束;如果你需要在后续主动 `write()` 推送数据,请不要直接 `await` 它,可以用 `void res.sendStream(stream)` 或保存返回的 Promise。
678
+ > - 如果启用了 `maxConcurrentRequestsPerClient`,一个长连接 stream 会占用一个并发槽,需要按业务场景调整。
679
+ > - **事件订阅**:stream 支持 `stream.on(event, listener)`(返回取消订阅函数),可用于埋点/进度/调试(如监听 `start/data/read/write/cancel/end/error/timeout/expired`)。主消费仍建议使用 `for await`。
680
+
681
+ #### 长连接 / 订阅式交互(push 模式示例)
647
682
 
648
683
  ```typescript
649
- import {
684
+ /**
685
+ * Server 端:订阅(长连接)
686
+ * - mode: 'push':由写侧主动 write()
687
+ * - expireTimeout: 0:关闭自动过期(谨慎使用,建议结合业务取消/重连)
688
+ */
689
+ server.on('/api/subscribe', (req, res) => {
690
+ const stream = new IframeWritableStream({
691
+ type: 'data',
692
+ chunked: true,
693
+ mode: 'push',
694
+ expireTimeout: 0,
695
+ /** 可选:写侧空闲检测(等待 pull/ack 太久会做心跳并失败) */
696
+ streamTimeout: 15000
697
+ });
698
+
699
+ /** 注意:不要 await,否则会一直等到流结束 */
700
+ void res.sendStream(stream);
701
+
702
+ const timer = setInterval(() => {
703
+ try {
704
+ stream.write({ type: 'tick', ts: Date.now() });
705
+ } catch {
706
+ clearInterval(timer);
707
+ }
708
+ }, 1000);
709
+ });
710
+
711
+ /**
712
+ * Client 端:持续读取(长连接建议用 for await,而不是 readAll())
713
+ */
714
+ const resp = await client.send('/api/subscribe', {});
715
+ if (isIframeReadableStream(resp.stream)) {
716
+ /** 事件订阅示例(可选) */
717
+ const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
718
+ console.error('stream error:', error);
719
+ });
720
+
721
+ for await (const evt of resp.stream) {
722
+ console.log('event:', evt);
723
+ }
724
+
725
+ off();
726
+ }
727
+ ```
728
+
729
+ #### 分块 / 文件流示例
730
+
731
+ ```typescript
732
+ import {
733
+ StreamEvent,
650
734
  IframeWritableStream,
651
735
  IframeFileWritableStream,
652
736
  isIframeReadableStream,
@@ -757,10 +841,10 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
757
841
 
758
842
  | 类型 | 说明 |
759
843
  |------|------|
760
- | `IframeWritableStream` | 服务端可写流,用于发送普通数据 |
761
- | `IframeFileWritableStream` | 服务端文件可写流(文件流) |
762
- | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
763
- | `IframeFileReadableStream` | 客户端文件可读流(文件流) |
844
+ | `IframeWritableStream` | 写侧(生产者)流:**谁要发送 stream,谁就创建它**;可用于 Server→Client 的响应流,也可用于 Client→Server 的请求流 |
845
+ | `IframeFileWritableStream` | 文件写侧(生产者)流:用于发送文件(底层会做 Base64 编码) |
846
+ | `IframeReadableStream` | 读侧(消费者)流:用于接收普通数据(无论来自 Server 还是 Client) |
847
+ | `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(底层会做 Base64 解码) |
764
848
 
765
849
  > **注意**:文件流内部会进行 Base64 编/解码。Base64 会带来约 33% 的体积膨胀,并且在超大文件场景下可能会有较高的内存/CPU 开销。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
766
850
 
@@ -775,18 +859,22 @@ interface WritableStreamOptions {
775
859
  streamTimeout?: number; // 写侧空闲超时(ms,可选):长时间未收到对端 pull/ack 时会做心跳确认并失败
776
860
  iterator?: () => AsyncGenerator; // 数据生成迭代器
777
861
  next?: () => Promise<{ data: any; done: boolean }>; // 数据生成函数
862
+ maxPendingChunks?: number; // 写侧待发送缓冲上限(可选;push/长连接场景建议配置,避免 pendingQueue 无限增长)
863
+ maxPendingBytes?: number; // 写侧待发送字节上限(可选;避免单次 write 超大 chunk 导致内存暴涨)
778
864
  metadata?: Record<string, any>; // 自定义元数据
779
865
  }
780
866
  ```
781
867
 
782
868
  **流超时/保活:**
783
- - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认,失败则认为流已断开并报错。
784
- - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull/ack 协议下,若长时间未收到对端 `pull/ack`,会做心跳确认并失败(避免长时间无效占用)。
869
+ - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认(默认使用 `client.isConnect()`),失败则认为流已断开并报错。
870
+ - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull 协议下,若长时间未收到对端 `pull`,会做心跳确认并失败(避免长时间无效占用)。
785
871
  - `expireTimeout`(流参数):写侧有效期;过期后会发送 `stream_error`,读侧会收到明确的“已过期”错误。
872
+ - `maxPendingChunks`(流参数):写侧待发送缓冲上限(可选)。对 `push` / 长连接场景很重要:当对端停止 pull 时,继续 `write()` 会在写侧积压,建议设置上限防止内存无限增长。
873
+ - `maxPendingBytes`(流参数):写侧待发送字节上限(可选)。用于防止单次写入超大 chunk(例如大字符串/大 Blob 包装)导致内存占用过高。
786
874
 
787
875
  **pull/ack 协议(新增,默认启用):**
788
- - 读侧会自动发送 `stream_pull` 请求更多 chunk,并对每个收到的 chunk 自动发送 `stream_ack`。
789
- - 写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
876
+ - 读侧会自动发送 `stream_pull` 请求更多 chunk;写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
877
+ - 断连检测不依赖 `stream_ack`,而是通过 `streamTimeout + 心跳(isConnect)` 来实现。
790
878
 
791
879
  **consume 默认行为(变更):**
792
880
  - `for await (const chunk of stream)` 默认会 **消费并丢弃已迭代过的 chunk**(`consume: true`),避免长流场景内存无限增长。
@@ -811,9 +899,9 @@ Server 可以要求 Client 确认收到响应:
811
899
  ```typescript
812
900
  server.on('/api/important', async (req, res) => {
813
901
  // requireAck: true 表示需要客户端确认
814
- const received = await res.send(data, { requireAck: true });
902
+ const acked = await res.send(data, { requireAck: true });
815
903
 
816
- if (received) {
904
+ if (acked) {
817
905
  console.log('客户端已确认收到');
818
906
  } else {
819
907
  console.log('客户端未确认(超时)');
@@ -821,7 +909,7 @@ server.on('/api/important', async (req, res) => {
821
909
  });
822
910
  ```
823
911
 
824
- > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `received`,无需业务侧手动发送。
912
+ > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `ack`,无需业务侧手动发送。
825
913
 
826
914
  ### 追踪模式
827
915
 
@@ -886,6 +974,70 @@ setMessages({
886
974
 
887
975
  **返回值:** `RequestIframeClient`
888
976
 
977
+ **关于 `target: Window` 的说明:**
978
+ - **必须持有对方页面的 `Window` 引用**(例如 `window.open()` 返回值、`window.opener`、或 `MessageEvent.source`)。
979
+ - **无法**通过 URL 给“任意标签页”发消息。
980
+ - 安全起见,建议显式设置 `targetOrigin`,并配置 `allowedOrigins` / `validateOrigin`。
981
+
982
+ **生产环境推荐配置(模板):**
983
+
984
+ ```typescript
985
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
986
+
987
+ /**
988
+ * 建议:明确限定 3 件事
989
+ * - secretKey:隔离不同业务/不同实例,避免消息串线
990
+ * - targetOrigin:发送时的 targetOrigin(Window 场景强烈建议不要用 '*')
991
+ * - allowedOrigins / validateOrigin:接收时的 origin 白名单校验
992
+ */
993
+ const secretKey = 'my-app';
994
+ const targetOrigin = 'https://child.example.com';
995
+ const allowedOrigins = [targetOrigin];
996
+
997
+ // Client(父页)
998
+ const client = requestIframeClient(window.open(targetOrigin)!, {
999
+ secretKey,
1000
+ targetOrigin,
1001
+ allowedOrigins
1002
+ });
1003
+
1004
+ // Server(子页/iframe 内)
1005
+ const server = requestIframeServer({
1006
+ secretKey,
1007
+ allowedOrigins,
1008
+ // 防止异常/攻击导致消息爆炸(按需设置)
1009
+ maxConcurrentRequestsPerClient: 50
1010
+ });
1011
+ ```
1012
+
1013
+ **生产环境推荐配置(iframe 场景模板):**
1014
+
1015
+ ```typescript
1016
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
1017
+
1018
+ /**
1019
+ * iframe 场景通常可以从 iframe.src 推导 targetOrigin,并用它作为 allowedOrigins 白名单。
1020
+ */
1021
+ const iframe = document.querySelector('iframe')!;
1022
+ const targetOrigin = new URL(iframe.src).origin;
1023
+ const secretKey = 'my-app';
1024
+
1025
+ // Client(父页)
1026
+ const client = requestIframeClient(iframe, {
1027
+ secretKey,
1028
+ // 可显式写出来(即使库内部也会默认推导),便于审计/避免误用 '*'
1029
+ targetOrigin,
1030
+ allowedOrigins: [targetOrigin]
1031
+ });
1032
+
1033
+ // Server(iframe 内)
1034
+ const server = requestIframeServer({
1035
+ secretKey,
1036
+ allowedOrigins: [targetOrigin],
1037
+ maxConcurrentRequestsPerClient: 50
1038
+ });
1039
+ ```
1040
+
889
1041
  **示例:**
890
1042
 
891
1043
  ```typescript
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:
@@ -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/error (controlled by response `requireAck`)
263
+ │ ──── ACK (optional) ─────────────────> │ Acknowledge receipt of response/error (ACK-only requireAck)
241
264
  │ │
242
265
  ```
243
266
 
@@ -246,15 +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 (when request `requireAck` is enabled) |
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/error (optional, controlled by response `requireAck`) |
254
276
  | `ping` | Client → Server | Connection detection (`isConnect()` method, may use `requireAck` to confirm delivery) |
255
277
  | `pong` | Server → Client | Connection detection response |
256
278
  | `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
279
 
259
280
  ### Timeout Mechanism
260
281
 
@@ -558,6 +579,8 @@ server.on('/api/data', (req, res) => {
558
579
 
559
580
  ### File Transfer
560
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
+
561
584
  #### Server → Client (Server sends file to client)
562
585
 
563
586
  ```typescript
@@ -592,7 +615,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
592
615
 
593
616
  #### Client → Server (Client sends file to server)
594
617
 
595
- 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`.
596
619
 
597
620
  ```typescript
598
621
  // Client side: Send file (stream, autoResolve defaults to true)
@@ -616,16 +639,71 @@ server.on('/api/upload', async (req, res) => {
616
639
  });
617
640
  ```
618
641
 
619
- **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.
620
643
 
621
644
  ### Streaming
622
645
 
623
- 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
+ ```
624
701
 
625
702
  #### Server → Client (Server sends stream to client)
626
703
 
627
704
  ```typescript
628
- import {
705
+ import {
706
+ StreamEvent,
629
707
  IframeWritableStream,
630
708
  IframeFileWritableStream,
631
709
  isIframeReadableStream,
@@ -740,21 +818,23 @@ server.on('/api/uploadStream', async (req, res) => {
740
818
 
741
819
  | Type | Description |
742
820
  |------|-------------|
743
- | `IframeWritableStream` | Server-side writable stream for sending regular data |
744
- | `IframeFileWritableStream` | Server-side file writable stream, automatically handles base64 encoding |
745
- | `IframeReadableStream` | Client-side readable stream for receiving regular data |
746
- | `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) |
747
825
 
748
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).
749
827
 
750
828
  **Stream timeouts:**
751
- - `options.streamTimeout` (request option): client-side stream idle timeout while consuming `response.stream` (data/file streams). When triggered, the client will attempt a heartbeat check and fail the stream if the connection is not alive.
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.
752
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`.
753
- - `streamTimeout` (writable stream option): writer-side idle timeout. If the writer does not receive `stream_pull/stream_ack` for a long time, it will heartbeat-check and fail to avoid wasting resources.
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.
754
834
 
755
835
  **Pull/Ack protocol (default):**
756
- - Reader automatically sends `stream_pull` to request chunks and sends `stream_ack` for each received chunk.
757
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)`.
758
838
 
759
839
  **consume default change:**
760
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.
@@ -778,9 +858,9 @@ Server can require Client to acknowledge receipt of response:
778
858
  ```typescript
779
859
  server.on('/api/important', async (req, res) => {
780
860
  // requireAck: true means client needs to acknowledge
781
- const received = await res.send(data, { requireAck: true });
861
+ const acked = await res.send(data, { requireAck: true });
782
862
 
783
- if (received) {
863
+ if (acked) {
784
864
  console.log('Client acknowledged receipt');
785
865
  } else {
786
866
  console.log('Client did not acknowledge (timeout)');
@@ -788,7 +868,7 @@ server.on('/api/important', async (req, res) => {
788
868
  });
789
869
  ```
790
870
 
791
- > **Note**: Client acknowledgment (`received`) 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 `received`.
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`.
792
872
 
793
873
  ### Trace Mode
794
874
 
@@ -853,6 +933,70 @@ Create a Client instance.
853
933
 
854
934
  **Returns:** `RequestIframeClient`
855
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
+
856
1000
  ### requestIframeServer(options?)
857
1001
 
858
1002
  Create a Server instance.
package/esm/api/client.js CHANGED
@@ -2,7 +2,7 @@ import { getIframeTargetOrigin, generateInstanceId } from '../utils';
2
2
  import { RequestIframeClientServer } from '../core/client-server';
3
3
  import { RequestIframeClientImpl } from '../core/client';
4
4
  import { setupClientDebugInterceptors } from '../utils/debug';
5
- import { Messages, ErrorCode } from '../constants';
5
+ import { Messages, ErrorCode, OriginConstant } from '../constants';
6
6
 
7
7
  /**
8
8
  * Create a client (for sending requests)
@@ -14,7 +14,7 @@ import { Messages, ErrorCode } from '../constants';
14
14
  */
15
15
  export function requestIframeClient(target, options) {
16
16
  var targetWindow = null;
17
- var targetOrigin = '*';
17
+ var targetOrigin = OriginConstant.ANY;
18
18
  if (target.tagName === 'IFRAME') {
19
19
  var iframe = target;
20
20
  targetWindow = iframe.contentWindow;
@@ -27,7 +27,7 @@ export function requestIframeClient(target, options) {
27
27
  }
28
28
  } else {
29
29
  targetWindow = target;
30
- targetOrigin = '*';
30
+ targetOrigin = OriginConstant.ANY;
31
31
  }
32
32
 
33
33
  // Allow user to override targetOrigin explicitly
@@ -44,8 +44,9 @@ export function requestIframeClient(target, options) {
44
44
  // Create ClientServer (internally obtains or creates a shared MessageChannel)
45
45
  var server = new RequestIframeClientServer({
46
46
  secretKey,
47
- ackTimeout: options === null || options === void 0 ? void 0 : options.ackTimeout,
48
- autoOpen: options === null || options === void 0 ? void 0 : options.autoOpen
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
49
50
  }, instanceId);
50
51
 
51
52
  // Create client instance