request-iframe 0.0.5 → 0.1.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.
Files changed (142) hide show
  1. package/README.CN.md +54 -7
  2. package/README.md +64 -11
  3. package/esm/api/client.js +79 -0
  4. package/esm/api/server.js +59 -0
  5. package/esm/constants/index.js +257 -0
  6. package/esm/constants/messages.js +155 -0
  7. package/esm/core/client-server.js +329 -0
  8. package/esm/core/client.js +873 -0
  9. package/esm/core/request.js +27 -0
  10. package/esm/core/response.js +451 -0
  11. package/esm/core/server.js +767 -0
  12. package/esm/index.js +21 -0
  13. package/esm/interceptors/index.js +122 -0
  14. package/esm/message/channel.js +181 -0
  15. package/esm/message/dispatcher.js +380 -0
  16. package/esm/message/index.js +2 -0
  17. package/esm/stream/file-stream.js +289 -0
  18. package/esm/stream/index.js +44 -0
  19. package/esm/stream/readable-stream.js +500 -0
  20. package/esm/stream/stream-core.js +91 -0
  21. package/esm/stream/types.js +1 -0
  22. package/esm/stream/writable-stream.js +582 -0
  23. package/esm/types/index.js +1 -0
  24. package/esm/utils/ack-meta.js +53 -0
  25. package/esm/utils/cache.js +147 -0
  26. package/esm/utils/cookie.js +352 -0
  27. package/esm/utils/debug.js +521 -0
  28. package/esm/utils/error.js +27 -0
  29. package/esm/utils/index.js +178 -0
  30. package/esm/utils/origin.js +28 -0
  31. package/esm/utils/path-match.js +148 -0
  32. package/esm/utils/protocol.js +157 -0
  33. package/library/api/client.d.ts.map +1 -1
  34. package/library/api/client.js +8 -1
  35. package/library/api/server.d.ts.map +1 -1
  36. package/library/api/server.js +4 -1
  37. package/library/constants/index.d.ts +25 -1
  38. package/library/constants/index.d.ts.map +1 -1
  39. package/library/constants/index.js +30 -5
  40. package/library/constants/messages.d.ts +5 -0
  41. package/library/constants/messages.d.ts.map +1 -1
  42. package/library/constants/messages.js +5 -0
  43. package/library/core/client-server.d.ts +3 -2
  44. package/library/core/client-server.d.ts.map +1 -1
  45. package/library/core/client-server.js +51 -4
  46. package/library/core/client.d.ts +4 -1
  47. package/library/core/client.d.ts.map +1 -1
  48. package/library/core/client.js +74 -31
  49. package/library/core/response.d.ts +21 -3
  50. package/library/core/response.d.ts.map +1 -1
  51. package/library/core/response.js +46 -6
  52. package/library/core/server.d.ts +28 -1
  53. package/library/core/server.d.ts.map +1 -1
  54. package/library/core/server.js +180 -19
  55. package/library/message/channel.d.ts +6 -0
  56. package/library/message/channel.d.ts.map +1 -1
  57. package/library/message/dispatcher.d.ts +22 -0
  58. package/library/message/dispatcher.d.ts.map +1 -1
  59. package/library/message/dispatcher.js +92 -0
  60. package/library/stream/file-stream.d.ts +4 -0
  61. package/library/stream/file-stream.d.ts.map +1 -1
  62. package/library/stream/file-stream.js +61 -33
  63. package/library/stream/index.d.ts.map +1 -1
  64. package/library/stream/index.js +2 -0
  65. package/library/stream/readable-stream.d.ts +30 -11
  66. package/library/stream/readable-stream.d.ts.map +1 -1
  67. package/library/stream/readable-stream.js +329 -73
  68. package/library/stream/stream-core.d.ts +44 -0
  69. package/library/stream/stream-core.d.ts.map +1 -0
  70. package/library/stream/stream-core.js +98 -0
  71. package/library/stream/types.d.ts +90 -3
  72. package/library/stream/types.d.ts.map +1 -1
  73. package/library/stream/writable-stream.d.ts +40 -12
  74. package/library/stream/writable-stream.d.ts.map +1 -1
  75. package/library/stream/writable-stream.js +391 -195
  76. package/library/types/index.d.ts +70 -3
  77. package/library/types/index.d.ts.map +1 -1
  78. package/library/utils/ack-meta.d.ts +2 -0
  79. package/library/utils/ack-meta.d.ts.map +1 -0
  80. package/library/utils/ack-meta.js +59 -0
  81. package/library/utils/index.d.ts +1 -0
  82. package/library/utils/index.d.ts.map +1 -1
  83. package/library/utils/index.js +16 -0
  84. package/library/utils/origin.d.ts +14 -0
  85. package/library/utils/origin.d.ts.map +1 -0
  86. package/library/utils/origin.js +34 -0
  87. package/package.json +31 -7
  88. package/react/README.md +16 -0
  89. package/react/esm/index.js +284 -0
  90. package/react/library/index.d.ts +1 -1
  91. package/react/library/index.d.ts.map +1 -1
  92. package/react/library/index.js +7 -4
  93. package/react/package.json +24 -2
  94. package/library/__tests__/channel.test.ts +0 -432
  95. package/library/__tests__/coverage-branches.test.ts +0 -356
  96. package/library/__tests__/debug.test.ts +0 -610
  97. package/library/__tests__/dispatcher.test.ts +0 -485
  98. package/library/__tests__/interceptors.test.ts +0 -146
  99. package/library/__tests__/requestIframe.test.ts +0 -5590
  100. package/library/__tests__/server.test.ts +0 -738
  101. package/library/__tests__/stream.test.ts +0 -726
  102. package/library/__tests__/utils.test.ts +0 -473
  103. package/library/api/client.d.js +0 -5
  104. package/library/api/server.d.js +0 -5
  105. package/library/constants/index.d.js +0 -36
  106. package/library/constants/messages.d.js +0 -5
  107. package/library/core/client.d.js +0 -5
  108. package/library/core/message-handler.d.ts +0 -110
  109. package/library/core/message-handler.d.ts.map +0 -1
  110. package/library/core/message-handler.js +0 -320
  111. package/library/core/request-response.d.ts +0 -59
  112. package/library/core/request-response.d.ts.map +0 -1
  113. package/library/core/request-response.js +0 -337
  114. package/library/core/request.d.js +0 -5
  115. package/library/core/response.d.js +0 -5
  116. package/library/core/server-base.d.ts +0 -86
  117. package/library/core/server-base.d.ts.map +0 -1
  118. package/library/core/server-base.js +0 -257
  119. package/library/core/server-client.d.js +0 -5
  120. package/library/core/server-client.d.ts +0 -101
  121. package/library/core/server-client.d.ts.map +0 -1
  122. package/library/core/server-client.js +0 -266
  123. package/library/core/server.d.js +0 -5
  124. package/library/interceptors/index.d.js +0 -5
  125. package/library/message/channel.d.js +0 -5
  126. package/library/message/dispatcher.d.js +0 -5
  127. package/library/message/index.d.js +0 -25
  128. package/library/stream/file-stream.d.js +0 -4
  129. package/library/stream/index.d.js +0 -58
  130. package/library/stream/readable-stream.d.js +0 -5
  131. package/library/stream/types.d.js +0 -5
  132. package/library/stream/writable-stream.d.js +0 -5
  133. package/library/types/index.d.js +0 -5
  134. package/library/utils/cache.d.js +0 -5
  135. package/library/utils/cookie.d.js +0 -5
  136. package/library/utils/debug.d.js +0 -5
  137. package/library/utils/index.d.js +0 -94
  138. package/library/utils/path-match.d.js +0 -5
  139. package/library/utils/protocol.d.js +0 -5
  140. package/react/library/__tests__/index.test.d.ts +0 -2
  141. package/react/library/__tests__/index.test.d.ts.map +0 -1
  142. package/react/library/__tests__/index.test.tsx +0 -792
package/README.CN.md CHANGED
@@ -160,12 +160,12 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
160
160
  | 类型 | 方向 | 说明 |
161
161
  |------|------|------|
162
162
  | `request` | Client → Server | 客户端发起请求 |
163
- | `ack` | Server → Client | 服务端确认收到请求 |
163
+ | `ack` | Server → Client | 服务端确认收到请求(当请求 `requireAck` 开启时) |
164
164
  | `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
165
165
  | `response` | Server → Client | 返回响应数据 |
166
166
  | `error` | Server → Client | 返回错误信息 |
167
- | `received` | Client → Server | 客户端确认收到响应(可选,由 `requireAck` 控制) |
168
- | `ping` | Client → Server | 连接检测(`isConnect()` 方法) |
167
+ | `received` | Client → Server | 客户端确认收到响应/错误(可选,由响应的 `requireAck` 控制) |
168
+ | `ping` | Client → Server | 连接检测(`isConnect()`;可使用 `requireAck` 确认投递) |
169
169
  | `pong` | Server → Client | 连接检测响应 |
170
170
 
171
171
  ### 超时机制
@@ -176,7 +176,8 @@ request-iframe 采用三阶段超时策略,智能适应不同场景:
176
176
  client.send('/api/getData', data, {
177
177
  ackTimeout: 1000, // 阶段1:等待 ACK 的超时时间(默认 1000ms)
178
178
  timeout: 5000, // 阶段2:请求超时时间(默认 5s)
179
- asyncTimeout: 120000 // 阶段3:异步请求超时时间(默认 120s)
179
+ asyncTimeout: 120000, // 阶段3:异步请求超时时间(默认 120s)
180
+ requireAck: true // 是否需要服务端 ACK(默认 true;为 false 则跳过 ACK 阶段,直接进入 timeout)
180
181
  });
181
182
  ```
182
183
 
@@ -218,13 +219,17 @@ client.send('/api/getData', data, {
218
219
  | timeout | 中等(5s) | 适用于简单的同步处理,如读取数据、参数校验等 |
219
220
  | asyncTimeout | 较长(120s) | 适用于复杂异步操作,如文件处理、批量操作、第三方 API 调用等 |
220
221
 
222
+ **补充说明:**
223
+ - `requireAck: false` 会跳过 ACK 阶段,直接以 `timeout` 作为第一阶段计时。
224
+ - 流(Stream)有独立的可选空闲超时:`streamTimeout`(见「流式传输(Stream)」)。
225
+
221
226
  ### 协议版本
222
227
 
223
228
  每条消息都包含 `__requestIframe__` 字段标识协议版本,以及 `timestamp` 字段记录消息创建时间:
224
229
 
225
230
  ```typescript
226
231
  {
227
- __requestIframe__: 1, // 协议版本号
232
+ __requestIframe__: 2, // 协议版本号
228
233
  timestamp: 1704067200000, // 消息创建时间戳(毫秒)
229
234
  type: 'request',
230
235
  requestId: 'req_xxx',
@@ -235,7 +240,7 @@ client.send('/api/getData', data, {
235
240
 
236
241
  这使得:
237
242
  - 不同版本的库可以做兼容处理
238
- - 新版本 Server 可兼容旧版本 Client
243
+ - 当前协议版本为 `2`;对于新的 stream pull/ack 流程,建议双方保持一致版本
239
244
  - 版本过低时会返回明确的错误信息
240
245
  - `timestamp` 便于调试消息延迟、分析通信性能
241
246
 
@@ -274,7 +279,7 @@ await client.send('/config', {
274
279
  });
275
280
 
276
281
  // 监听组件事件(通过反向通信)
277
- const server = requestIframeServer({ secretKey: 'widget-events' });
282
+ const server = requestIframeServer({ secretKey: 'widget' });
278
283
  server.on('/event', (req, res) => {
279
284
  console.log('组件事件:', req.body);
280
285
  res.send({ received: true });
@@ -706,6 +711,8 @@ const response = await client.send('/api/stream', {});
706
711
  if (isIframeReadableStream(response.stream)) {
707
712
  // 方式1:一次性读取所有数据
708
713
  const allData = await response.stream.read();
714
+ // 如果希望返回类型稳定(始终是 chunk 数组),可使用 readAll()
715
+ const allChunks = await response.stream.readAll();
709
716
 
710
717
  // 方式2:使用异步迭代器逐块读取
711
718
  for await (const chunk of response.stream) {
@@ -755,18 +762,36 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
755
762
  | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
756
763
  | `IframeFileReadableStream` | 客户端文件可读流(文件流) |
757
764
 
765
+ > **注意**:文件流内部会进行 Base64 编/解码。Base64 会带来约 33% 的体积膨胀,并且在超大文件场景下可能会有较高的内存/CPU 开销。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
766
+
758
767
  **流选项:**
759
768
 
760
769
  ```typescript
761
770
  interface WritableStreamOptions {
762
771
  type?: 'data' | 'file'; // 流类型
763
772
  chunked?: boolean; // 是否分块传输(默认 true)
773
+ mode?: 'pull' | 'push'; // 流模式:pull(默认,按需拉取) / push(主动写入)
774
+ expireTimeout?: number; // 流过期时间(ms,可选;默认约等于 asyncTimeout)
775
+ streamTimeout?: number; // 写侧空闲超时(ms,可选):长时间未收到对端 pull/ack 时会做心跳确认并失败
764
776
  iterator?: () => AsyncGenerator; // 数据生成迭代器
765
777
  next?: () => Promise<{ data: any; done: boolean }>; // 数据生成函数
766
778
  metadata?: Record<string, any>; // 自定义元数据
767
779
  }
768
780
  ```
769
781
 
782
+ **流超时/保活:**
783
+ - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认,失败则认为流已断开并报错。
784
+ - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull/ack 协议下,若长时间未收到对端 `pull/ack`,会做心跳确认并失败(避免长时间无效占用)。
785
+ - `expireTimeout`(流参数):写侧有效期;过期后会发送 `stream_error`,读侧会收到明确的“已过期”错误。
786
+
787
+ **pull/ack 协议(新增,默认启用):**
788
+ - 读侧会自动发送 `stream_pull` 请求更多 chunk,并对每个收到的 chunk 自动发送 `stream_ack`。
789
+ - 写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
790
+
791
+ **consume 默认行为(变更):**
792
+ - `for await (const chunk of stream)` 默认会 **消费并丢弃已迭代过的 chunk**(`consume: true`),避免长流场景内存无限增长。
793
+ - 如果你希望后续还能 `read()/readAll()` 拿到历史数据,可在创建流时传 `consume: false`(或在业务上自行缓存)。
794
+
770
795
  ### 连接检测
771
796
 
772
797
  ```typescript
@@ -796,6 +821,8 @@ server.on('/api/important', async (req, res) => {
796
821
  });
797
822
  ```
798
823
 
824
+ > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `received`,无需业务侧手动发送。
825
+
799
826
  ### 追踪模式
800
827
 
801
828
  开启追踪模式可以在控制台查看详细的通信日志:
@@ -848,9 +875,14 @@ setMessages({
848
875
  | `target` | `HTMLIFrameElement \| Window` | 目标 iframe 元素或 window 对象 |
849
876
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
850
877
  | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
878
+ | `options.targetOrigin` | `string` | 覆盖 postMessage 的 targetOrigin(可选)。当 `target` 是 `Window` 时默认 `*`;当 `target` 是 iframe 时默认取 `iframe.src` 的 origin。 |
851
879
  | `options.ackTimeout` | `number` | 全局默认 ACK 确认超时(ms),默认 1000 |
852
880
  | `options.timeout` | `number` | 全局默认请求超时(ms),默认 5000 |
853
881
  | `options.asyncTimeout` | `number` | 全局默认异步超时(ms),默认 120000 |
882
+ | `options.requireAck` | `boolean` | 全局默认请求投递 ACK(默认 true)。为 false 时请求跳过 ACK 阶段,直接进入 timeout |
883
+ | `options.streamTimeout` | `number` | 全局默认流空闲超时(ms,可选),用于消费 `response.stream` |
884
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
885
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | 自定义 origin 校验函数(可选,优先级高于 `allowedOrigins`) |
854
886
 
855
887
  **返回值:** `RequestIframeClient`
856
888
 
@@ -882,6 +914,9 @@ await client.send('/api/longTask', {}, {
882
914
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
883
915
  | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
884
916
  | `options.ackTimeout` | `number` | 等待客户端确认超时(ms),默认 1000 |
917
+ | `options.maxConcurrentRequestsPerClient` | `number` | 每个客户端的最大并发 in-flight 请求数(按 origin + creatorId 维度),默认 Infinity |
918
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
919
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | 自定义 origin 校验函数(可选,优先级高于 `allowedOrigins`) |
885
920
 
886
921
  **返回值:** `RequestIframeServer`
887
922
 
@@ -902,6 +937,8 @@ await client.send('/api/longTask', {}, {
902
937
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
903
938
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
904
939
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
940
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
941
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
905
942
  | `options.headers` | `object` | 请求 headers(可选) |
906
943
  | `options.cookies` | `object` | 请求 cookies(可选,会与内部存储的 cookies 合并,传入的优先级更高) |
907
944
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -953,6 +990,8 @@ await client.send('/api/uploadStream', stream);
953
990
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
954
991
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
955
992
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
993
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
994
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
956
995
  | `options.headers` | `object` | 请求 headers(可选) |
957
996
  | `options.cookies` | `object` | 请求 cookies(可选) |
958
997
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -974,6 +1013,8 @@ await client.send('/api/uploadStream', stream);
974
1013
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
975
1014
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
976
1015
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
1016
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
1017
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
977
1018
  | `options.headers` | `object` | 请求 headers(可选) |
978
1019
  | `options.cookies` | `object` | 请求 cookies(可选) |
979
1020
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -1127,6 +1168,8 @@ server.use(['/a', '/b'], (req, res, next) => { ... });
1127
1168
 
1128
1169
  request-iframe 提供了 React hooks,方便在 React 应用中使用。从 `request-iframe/react` 导入 hooks:
1129
1170
 
1171
+ > 注意:只有在使用 `request-iframe/react` 时才需要安装 React;单独安装 `request-iframe` 不依赖 React。
1172
+
1130
1173
  ```typescript
1131
1174
  import { useClient, useServer, useServerHandler, useServerHandlerMap } from 'request-iframe/react';
1132
1175
  ```
@@ -1498,8 +1541,12 @@ import {
1498
1541
  | `PROTOCOL_UNSUPPORTED` | 协议版本不支持 |
1499
1542
  | `IFRAME_NOT_READY` | iframe 未就绪 |
1500
1543
  | `STREAM_ERROR` | 流传输错误 |
1544
+ | `STREAM_TIMEOUT` | 流空闲超时 |
1545
+ | `STREAM_EXPIRED` | 流已过期(可写流超过有效期) |
1501
1546
  | `STREAM_CANCELLED` | 流被取消 |
1502
1547
  | `STREAM_NOT_BOUND` | 流未绑定到请求上下文 |
1548
+ | `STREAM_START_TIMEOUT` | 流启动超时(请求体 stream_start 未按时到达) |
1549
+ | `TOO_MANY_REQUESTS` | 请求过多(服务端并发限制) |
1503
1550
 
1504
1551
  ### 错误处理示例
1505
1552
 
package/README.md CHANGED
@@ -165,7 +165,7 @@ await client.send('/config', {
165
165
  });
166
166
 
167
167
  // Listen to component events (via reverse communication)
168
- const server = requestIframeServer({ secretKey: 'widget-events' });
168
+ const server = requestIframeServer({ secretKey: 'widget' });
169
169
  server.on('/event', (req, res) => {
170
170
  console.log('Component event:', req.body);
171
171
  res.send({ received: true });
@@ -229,7 +229,7 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
229
229
  │ │
230
230
  │ ──── REQUEST ────────────────────────> │ Send request
231
231
  │ │
232
- │ <──── ACK ─────────────────────────── │ Acknowledge receipt
232
+ │ <──── ACK (optional) ──────────────── │ Acknowledge receipt (controlled by request `requireAck`, default true)
233
233
  │ │
234
234
  │ │ Execute handler
235
235
  │ │
@@ -237,7 +237,7 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
237
237
  │ │
238
238
  │ <──── RESPONSE ────────────────────── │ Return result
239
239
  │ │
240
- │ ──── RECEIVED (optional) ────────────> │ Acknowledge receipt of response
240
+ │ ──── RECEIVED (optional) ────────────> │ Acknowledge receipt of response/error (controlled by response `requireAck`)
241
241
  │ │
242
242
  ```
243
243
 
@@ -246,13 +246,15 @@ request-iframe implements an HTTP-like communication protocol on top of `postMes
246
246
  | Type | Direction | Description |
247
247
  |------|-----------|-------------|
248
248
  | `request` | Client → Server | Client initiates request |
249
- | `ack` | Server → Client | Server acknowledges receipt of request |
249
+ | `ack` | Server → Client | Server acknowledges receipt of request (when request `requireAck` is enabled) |
250
250
  | `async` | Server → Client | Notifies client this is an async task (sent when handler returns Promise) |
251
251
  | `response` | Server → Client | Returns response data |
252
252
  | `error` | Server → Client | Returns error information |
253
- | `received` | Client → Server | Client acknowledges receipt of response (optional, controlled by `requireAck`) |
254
- | `ping` | Client → Server | Connection detection (`isConnect()` method) |
253
+ | `received` | Client → Server | Client acknowledges receipt of response/error (optional, controlled by response `requireAck`) |
254
+ | `ping` | Client → Server | Connection detection (`isConnect()` method, may use `requireAck` to confirm delivery) |
255
255
  | `pong` | Server → Client | Connection detection response |
256
+ | `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) |
256
258
 
257
259
  ### Timeout Mechanism
258
260
 
@@ -262,7 +264,8 @@ request-iframe uses a three-stage timeout strategy to intelligently adapt to dif
262
264
  client.send('/api/getData', data, {
263
265
  ackTimeout: 1000, // Stage 1: ACK timeout (default 1000ms)
264
266
  timeout: 5000, // Stage 2: Request timeout (default 5s)
265
- asyncTimeout: 120000 // Stage 3: Async request timeout (default 120s)
267
+ asyncTimeout: 120000, // Stage 3: Async request timeout (default 120s)
268
+ requireAck: true // Whether to wait for server ACK before switching to stage 2 (default true)
266
269
  });
267
270
  ```
268
271
 
@@ -304,13 +307,17 @@ Send REQUEST
304
307
  | timeout | Medium (5s) | Suitable for simple synchronous processing, like reading data, parameter validation |
305
308
  | asyncTimeout | Long (120s) | Suitable for complex async operations, like file processing, batch operations, third-party API calls |
306
309
 
310
+ **Notes:**
311
+ - If you set `requireAck: false`, the request will **skip** the ACK stage and start `timeout` immediately.
312
+ - Stream transfer has its own optional idle timeout: use `streamTimeout` (see [Streaming](#streaming)).
313
+
307
314
  ### Protocol Version
308
315
 
309
316
  Each message contains a `__requestIframe__` field identifying the protocol version, and a `timestamp` field recording message creation time:
310
317
 
311
318
  ```typescript
312
319
  {
313
- __requestIframe__: 1, // Protocol version number
320
+ __requestIframe__: 2, // Protocol version number
314
321
  timestamp: 1704067200000, // Message creation timestamp (milliseconds)
315
322
  type: 'request',
316
323
  requestId: 'req_xxx',
@@ -321,7 +328,7 @@ Each message contains a `__requestIframe__` field identifying the protocol versi
321
328
 
322
329
  This enables:
323
330
  - Different library versions can handle compatibility
324
- - New version Server can be compatible with old version Client
331
+ - Current protocol version is `2`. For the new stream pull/ack flow, both sides should use the same version.
325
332
  - Clear error messages when version is too low
326
333
  - `timestamp` facilitates debugging message delays and analyzing communication performance
327
334
 
@@ -630,6 +637,11 @@ server.on('/api/stream', async (req, res) => {
630
637
  const stream = new IframeWritableStream({
631
638
  type: 'data',
632
639
  chunked: true,
640
+ mode: 'pull', // default: pull/ack protocol (backpressure)
641
+ // Optional: auto-expire stream to avoid leaking resources (default: asyncTimeout)
642
+ // expireTimeout: 120000,
643
+ // Optional: writer-side idle timeout while waiting for pull/ack
644
+ // streamTimeout: 10000,
633
645
  // Generate data using async iterator
634
646
  iterator: async function* () {
635
647
  for (let i = 0; i < 10; i++) {
@@ -643,14 +655,19 @@ server.on('/api/stream', async (req, res) => {
643
655
  });
644
656
 
645
657
  // Client side: Receive stream data
646
- const response = await client.send('/api/stream', {});
658
+ const response = await client.send('/api/stream', {}, { streamTimeout: 10000 });
647
659
 
648
660
  // Check if it's a stream response
649
661
  if (isIframeReadableStream(response.stream)) {
662
+ // Sender stream mode (from stream_start)
663
+ console.log('Stream mode:', response.stream.mode); // 'pull' | 'push' | undefined
664
+
650
665
  // Method 1: Read all data at once
651
666
  const allData = await response.stream.read();
667
+ // If you want a consistent return type (always an array of chunks), use readAll()
668
+ const allChunks = await response.stream.readAll();
652
669
 
653
- // Method 2: Read chunk by chunk using async iterator
670
+ // Method 2: Read chunk by chunk using async iterator (consume defaults to true)
654
671
  for await (const chunk of response.stream) {
655
672
  console.log('Received chunk:', chunk);
656
673
  }
@@ -728,6 +745,20 @@ server.on('/api/uploadStream', async (req, res) => {
728
745
  | `IframeReadableStream` | Client-side readable stream for receiving regular data |
729
746
  | `IframeFileReadableStream` | Client-side file readable stream, automatically handles base64 decoding |
730
747
 
748
+ > **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
+
750
+ **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.
752
+ - `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.
754
+
755
+ **Pull/Ack protocol (default):**
756
+ - Reader automatically sends `stream_pull` to request chunks and sends `stream_ack` for each received chunk.
757
+ - Writer only sends `stream_data` when it has received `stream_pull`, enabling real backpressure.
758
+
759
+ **consume default change:**
760
+ - `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.
761
+
731
762
  ### Connection Detection
732
763
 
733
764
  ```typescript
@@ -757,6 +788,8 @@ server.on('/api/important', async (req, res) => {
757
788
  });
758
789
  ```
759
790
 
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`.
792
+
760
793
  ### Trace Mode
761
794
 
762
795
  Enable trace mode to view detailed communication logs in console:
@@ -809,9 +842,14 @@ Create a Client instance.
809
842
  | `target` | `HTMLIFrameElement \| Window` | Target iframe element or window object |
810
843
  | `options.secretKey` | `string` | Message isolation identifier (optional) |
811
844
  | `options.trace` | `boolean` | Whether to enable trace mode (optional) |
845
+ | `options.targetOrigin` | `string` | Override postMessage targetOrigin for sending (optional). If `target` is a `Window`, default is `*`. |
812
846
  | `options.ackTimeout` | `number` | Global default ACK acknowledgment timeout (ms), default 1000 |
813
847
  | `options.timeout` | `number` | Global default request timeout (ms), default 5000 |
814
848
  | `options.asyncTimeout` | `number` | Global default async timeout (ms), default 120000 |
849
+ | `options.requireAck` | `boolean` | Global default for request delivery ACK (default true). If false, requests skip the ACK stage and start `timeout` immediately |
850
+ | `options.streamTimeout` | `number` | Global default stream idle timeout (ms) when consuming `response.stream` (optional) |
851
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | Allowlist for incoming message origins (optional, recommended for production) |
852
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | Custom origin validator (optional, higher priority than `allowedOrigins`) |
815
853
 
816
854
  **Returns:** `RequestIframeClient`
817
855
 
@@ -826,6 +864,9 @@ Create a Server instance.
826
864
  | `options.secretKey` | `string` | Message isolation identifier (optional) |
827
865
  | `options.trace` | `boolean` | Whether to enable trace mode (optional) |
828
866
  | `options.ackTimeout` | `number` | Wait for client acknowledgment timeout (ms), default 1000 |
867
+ | `options.maxConcurrentRequestsPerClient` | `number` | Max concurrent in-flight requests per client (per origin + creatorId). Default Infinity |
868
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | Allowlist for incoming message origins (optional, recommended for production) |
869
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | Custom origin validator (optional, higher priority than `allowedOrigins`) |
829
870
 
830
871
  **Returns:** `RequestIframeServer`
831
872
 
@@ -844,6 +885,8 @@ Send a request. Automatically dispatches to `sendFile()` or `sendStream()` based
844
885
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
845
886
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
846
887
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
888
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
889
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
847
890
  | `options.headers` | `object` | Request headers (optional) |
848
891
  | `options.cookies` | `object` | Request cookies (optional, merged with internally stored cookies, passed-in takes priority) |
849
892
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -895,6 +938,8 @@ Send file as request body (via stream; server receives File/Blob when autoResolv
895
938
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
896
939
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
897
940
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
941
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
942
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
898
943
  | `options.headers` | `object` | Request headers (optional) |
899
944
  | `options.cookies` | `object` | Request cookies (optional) |
900
945
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -916,6 +961,8 @@ Send stream as request body (server receives readable stream).
916
961
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
917
962
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
918
963
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
964
+ | `options.requireAck` | `boolean` | Whether to require server delivery ACK (default true). If false, skips ACK stage |
965
+ | `options.streamTimeout` | `number` | Stream idle timeout (ms) while consuming `response.stream` (optional) |
919
966
  | `options.headers` | `object` | Request headers (optional) |
920
967
  | `options.cookies` | `object` | Request cookies (optional) |
921
968
  | `options.requestId` | `string` | Custom request ID (optional) |
@@ -1064,6 +1111,8 @@ Destroy Server instance, remove all listeners.
1064
1111
 
1065
1112
  request-iframe provides React hooks for easy integration in React applications. Import hooks from `request-iframe/react`:
1066
1113
 
1114
+ > Note: React is only required if you use `request-iframe/react`. Installing `request-iframe` alone does not require React.
1115
+
1067
1116
  ```typescript
1068
1117
  import { useClient, useServer, useServerHandler, useServerHandlerMap } from 'request-iframe/react';
1069
1118
  ```
@@ -1328,8 +1377,12 @@ const IframeComponent = () => {
1328
1377
  | `PROTOCOL_UNSUPPORTED` | Protocol version not supported |
1329
1378
  | `IFRAME_NOT_READY` | iframe not ready |
1330
1379
  | `STREAM_ERROR` | Stream transfer error |
1380
+ | `STREAM_TIMEOUT` | Stream idle timeout |
1381
+ | `STREAM_EXPIRED` | Stream expired (writable stream lifetime exceeded) |
1331
1382
  | `STREAM_CANCELLED` | Stream cancelled |
1332
1383
  | `STREAM_NOT_BOUND` | Stream not bound to request context |
1384
+ | `STREAM_START_TIMEOUT` | Stream start timeout (request body stream_start not received in time) |
1385
+ | `TOO_MANY_REQUESTS` | Too many concurrent requests (server-side limit) |
1333
1386
 
1334
1387
  ### Error Handling Example
1335
1388
 
@@ -0,0 +1,79 @@
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 } 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 = '*';
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 = '*';
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
+ ackTimeout: options === null || options === void 0 ? void 0 : options.ackTimeout,
48
+ autoOpen: options === null || options === void 0 ? void 0 : options.autoOpen
49
+ }, instanceId);
50
+
51
+ // Create client instance
52
+ var client = new RequestIframeClientImpl(targetWindow, targetOrigin, server, {
53
+ secretKey,
54
+ ackTimeout: options === null || options === void 0 ? void 0 : options.ackTimeout,
55
+ timeout: options === null || options === void 0 ? void 0 : options.timeout,
56
+ asyncTimeout: options === null || options === void 0 ? void 0 : options.asyncTimeout,
57
+ returnData: options === null || options === void 0 ? void 0 : options.returnData,
58
+ headers: options === null || options === void 0 ? void 0 : options.headers,
59
+ allowedOrigins: options === null || options === void 0 ? void 0 : options.allowedOrigins,
60
+ validateOrigin: options === null || options === void 0 ? void 0 : options.validateOrigin
61
+ }, instanceId);
62
+
63
+ // If trace mode is enabled, register debug interceptors
64
+ if (options !== null && options !== void 0 && options.trace) {
65
+ setupClientDebugInterceptors(client);
66
+ }
67
+ return client;
68
+ }
69
+
70
+ /**
71
+ * Clear MessageChannel cache (for testing or reset)
72
+ * Note: This clears the shared message channel for the specified secretKey
73
+ */
74
+ export function clearRequestIframeClientCache(secretKey) {
75
+ // Now client is no longer cached, only need to clear MessageChannel cache
76
+ // MessageChannel cleanup is handled by clearMessageChannelCache in cache.ts
77
+ // Empty implementation kept here to maintain API compatibility
78
+ void secretKey;
79
+ }
@@ -0,0 +1,59 @@
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
+ });
37
+
38
+ // If trace mode is enabled, register debug listeners
39
+ if (options !== null && options !== void 0 && options.trace) {
40
+ setupServerDebugListeners(server);
41
+ }
42
+
43
+ // Cache server if id is specified
44
+ if (id) {
45
+ cacheServer(server, secretKey, id);
46
+ }
47
+ return server;
48
+ }
49
+
50
+ /**
51
+ * Clear server cache (for testing or reset)
52
+ * Note: This clears the cached server instances
53
+ */
54
+ export function clearRequestIframeServerCache(secretKey) {
55
+ // Clear server cache
56
+ clearServerCache();
57
+ // MessageChannel cleanup is handled by clearMessageChannelCache in cache.ts
58
+ void secretKey;
59
+ }