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.
Files changed (223) hide show
  1. package/QUICKSTART.CN.md +4 -2
  2. package/QUICKSTART.md +4 -2
  3. package/README.CN.md +302 -54
  4. package/README.md +281 -36
  5. package/cdn/request-iframe-react.umd.js +3354 -0
  6. package/cdn/request-iframe-react.umd.js.map +1 -0
  7. package/cdn/request-iframe-react.umd.min.js +2 -0
  8. package/cdn/request-iframe-react.umd.min.js.map +1 -0
  9. package/cdn/request-iframe.umd.js +19735 -0
  10. package/cdn/request-iframe.umd.js.map +1 -0
  11. package/cdn/request-iframe.umd.min.js +4 -0
  12. package/cdn/request-iframe.umd.min.js.map +1 -0
  13. package/esm/api/client.js +31 -22
  14. package/esm/api/endpoint.js +229 -0
  15. package/esm/api/server.js +19 -9
  16. package/esm/constants/debug.js +17 -0
  17. package/esm/constants/index.js +115 -66
  18. package/esm/constants/log.js +11 -0
  19. package/esm/constants/messages.js +6 -1
  20. package/esm/constants/warn-once.js +15 -0
  21. package/esm/endpoint/facade.js +390 -0
  22. package/esm/endpoint/heartbeat/heartbeat.js +60 -0
  23. package/esm/endpoint/heartbeat/ping.js +20 -0
  24. package/esm/endpoint/index.js +13 -0
  25. package/esm/endpoint/infra/hub.js +316 -0
  26. package/esm/endpoint/infra/inbox.js +232 -0
  27. package/esm/endpoint/infra/outbox.js +408 -0
  28. package/esm/endpoint/stream/dispatcher.js +58 -0
  29. package/esm/endpoint/stream/errors.js +27 -0
  30. package/esm/endpoint/stream/factory.js +76 -0
  31. package/esm/endpoint/stream/file-auto-resolve.js +34 -0
  32. package/esm/endpoint/stream/file-writable.js +105 -0
  33. package/esm/endpoint/stream/handler.js +26 -0
  34. package/esm/{core → impl}/client.js +243 -320
  35. package/esm/{core → impl}/response.js +120 -154
  36. package/esm/impl/server.js +568 -0
  37. package/esm/index.js +13 -6
  38. package/esm/message/ack.js +27 -0
  39. package/esm/message/channel-cache.js +108 -0
  40. package/esm/message/channel.js +92 -5
  41. package/esm/message/dispatcher.js +149 -98
  42. package/esm/stream/error.js +22 -0
  43. package/esm/stream/index.js +3 -1
  44. package/esm/stream/readable-stream.js +101 -26
  45. package/esm/stream/stream-core.js +121 -3
  46. package/esm/stream/writable-stream.js +368 -43
  47. package/esm/utils/ack.js +36 -0
  48. package/esm/utils/blob.js +16 -0
  49. package/esm/utils/cache.js +25 -76
  50. package/esm/utils/content-type.js +81 -0
  51. package/esm/utils/debug.js +157 -180
  52. package/esm/utils/hooks.js +130 -0
  53. package/esm/utils/id.js +14 -0
  54. package/esm/utils/iframe.js +20 -0
  55. package/esm/utils/index.js +12 -162
  56. package/esm/utils/is.js +3 -0
  57. package/esm/utils/logger.js +55 -0
  58. package/esm/utils/origin.js +3 -1
  59. package/esm/utils/promise.js +3 -0
  60. package/esm/utils/window.js +31 -0
  61. package/library/api/client.d.ts.map +1 -1
  62. package/library/api/client.js +32 -23
  63. package/library/api/endpoint.d.ts +23 -0
  64. package/library/api/endpoint.d.ts.map +1 -0
  65. package/library/api/endpoint.js +235 -0
  66. package/library/api/server.d.ts +4 -1
  67. package/library/api/server.d.ts.map +1 -1
  68. package/library/api/server.js +19 -9
  69. package/library/constants/debug.d.ts +18 -0
  70. package/library/constants/debug.d.ts.map +1 -0
  71. package/library/constants/debug.js +23 -0
  72. package/library/constants/index.d.ts +58 -7
  73. package/library/constants/index.d.ts.map +1 -1
  74. package/library/constants/index.js +143 -67
  75. package/library/constants/log.d.ts +12 -0
  76. package/library/constants/log.d.ts.map +1 -0
  77. package/library/constants/log.js +17 -0
  78. package/library/constants/messages.d.ts +6 -1
  79. package/library/constants/messages.d.ts.map +1 -1
  80. package/library/constants/messages.js +6 -1
  81. package/library/constants/warn-once.d.ts +12 -0
  82. package/library/constants/warn-once.d.ts.map +1 -0
  83. package/library/constants/warn-once.js +22 -0
  84. package/library/endpoint/facade.d.ts +238 -0
  85. package/library/endpoint/facade.d.ts.map +1 -0
  86. package/library/endpoint/facade.js +398 -0
  87. package/library/endpoint/heartbeat/heartbeat.d.ts +34 -0
  88. package/library/endpoint/heartbeat/heartbeat.d.ts.map +1 -0
  89. package/library/endpoint/heartbeat/heartbeat.js +67 -0
  90. package/library/endpoint/heartbeat/ping.d.ts +18 -0
  91. package/library/endpoint/heartbeat/ping.d.ts.map +1 -0
  92. package/library/endpoint/heartbeat/ping.js +26 -0
  93. package/library/endpoint/index.d.ts +16 -0
  94. package/library/endpoint/index.d.ts.map +1 -0
  95. package/library/endpoint/index.js +114 -0
  96. package/library/endpoint/infra/hub.d.ts +170 -0
  97. package/library/endpoint/infra/hub.d.ts.map +1 -0
  98. package/library/endpoint/infra/hub.js +323 -0
  99. package/library/endpoint/infra/inbox.d.ts +73 -0
  100. package/library/endpoint/infra/inbox.d.ts.map +1 -0
  101. package/library/endpoint/infra/inbox.js +239 -0
  102. package/library/endpoint/infra/outbox.d.ts +149 -0
  103. package/library/endpoint/infra/outbox.d.ts.map +1 -0
  104. package/library/endpoint/infra/outbox.js +415 -0
  105. package/library/endpoint/stream/dispatcher.d.ts +33 -0
  106. package/library/endpoint/stream/dispatcher.d.ts.map +1 -0
  107. package/library/endpoint/stream/dispatcher.js +66 -0
  108. package/library/endpoint/stream/errors.d.ts +20 -0
  109. package/library/endpoint/stream/errors.d.ts.map +1 -0
  110. package/library/endpoint/stream/errors.js +32 -0
  111. package/library/endpoint/stream/factory.d.ts +44 -0
  112. package/library/endpoint/stream/factory.d.ts.map +1 -0
  113. package/library/endpoint/stream/factory.js +82 -0
  114. package/library/endpoint/stream/file-auto-resolve.d.ts +26 -0
  115. package/library/endpoint/stream/file-auto-resolve.d.ts.map +1 -0
  116. package/library/endpoint/stream/file-auto-resolve.js +41 -0
  117. package/library/endpoint/stream/file-writable.d.ts +33 -0
  118. package/library/endpoint/stream/file-writable.d.ts.map +1 -0
  119. package/library/endpoint/stream/file-writable.js +115 -0
  120. package/library/endpoint/stream/handler.d.ts +20 -0
  121. package/library/endpoint/stream/handler.d.ts.map +1 -0
  122. package/library/endpoint/stream/handler.js +32 -0
  123. package/library/{core → impl}/client.d.ts +16 -13
  124. package/library/impl/client.d.ts.map +1 -0
  125. package/library/{core → impl}/client.js +254 -333
  126. package/library/{core → impl}/request.d.ts.map +1 -1
  127. package/library/{core → impl}/response.d.ts +7 -12
  128. package/library/impl/response.d.ts.map +1 -0
  129. package/library/{core → impl}/response.js +120 -154
  130. package/library/{core → impl}/server.d.ts +26 -55
  131. package/library/impl/server.d.ts.map +1 -0
  132. package/library/impl/server.js +575 -0
  133. package/library/index.d.ts +13 -6
  134. package/library/index.d.ts.map +1 -1
  135. package/library/index.js +16 -16
  136. package/library/message/ack.d.ts +15 -0
  137. package/library/message/ack.d.ts.map +1 -0
  138. package/library/message/ack.js +33 -0
  139. package/library/message/channel-cache.d.ts +26 -0
  140. package/library/message/channel-cache.d.ts.map +1 -0
  141. package/library/message/channel-cache.js +115 -0
  142. package/library/message/channel.d.ts +53 -6
  143. package/library/message/channel.d.ts.map +1 -1
  144. package/library/message/channel.js +96 -9
  145. package/library/message/dispatcher.d.ts +17 -0
  146. package/library/message/dispatcher.d.ts.map +1 -1
  147. package/library/message/dispatcher.js +149 -98
  148. package/library/stream/error.d.ts +24 -0
  149. package/library/stream/error.d.ts.map +1 -0
  150. package/library/stream/error.js +29 -0
  151. package/library/stream/index.d.ts +4 -1
  152. package/library/stream/index.d.ts.map +1 -1
  153. package/library/stream/index.js +7 -4
  154. package/library/stream/readable-stream.d.ts.map +1 -1
  155. package/library/stream/readable-stream.js +102 -27
  156. package/library/stream/stream-core.d.ts +22 -1
  157. package/library/stream/stream-core.d.ts.map +1 -1
  158. package/library/stream/stream-core.js +120 -2
  159. package/library/stream/types.d.ts +115 -2
  160. package/library/stream/types.d.ts.map +1 -1
  161. package/library/stream/writable-stream.d.ts +20 -2
  162. package/library/stream/writable-stream.d.ts.map +1 -1
  163. package/library/stream/writable-stream.js +366 -41
  164. package/library/types/index.d.ts +17 -22
  165. package/library/types/index.d.ts.map +1 -1
  166. package/library/utils/ack.d.ts +2 -0
  167. package/library/utils/ack.d.ts.map +1 -0
  168. package/library/utils/ack.js +44 -0
  169. package/library/utils/blob.d.ts +3 -0
  170. package/library/utils/blob.d.ts.map +1 -0
  171. package/library/utils/blob.js +22 -0
  172. package/library/utils/cache.d.ts +10 -20
  173. package/library/utils/cache.d.ts.map +1 -1
  174. package/library/utils/cache.js +25 -79
  175. package/library/utils/content-type.d.ts +13 -0
  176. package/library/utils/content-type.d.ts.map +1 -0
  177. package/library/utils/content-type.js +87 -0
  178. package/library/utils/debug.d.ts.map +1 -1
  179. package/library/utils/debug.js +156 -178
  180. package/library/utils/hooks.d.ts +30 -0
  181. package/library/utils/hooks.d.ts.map +1 -0
  182. package/library/utils/hooks.js +139 -0
  183. package/library/utils/id.d.ts +9 -0
  184. package/library/utils/id.d.ts.map +1 -0
  185. package/library/utils/id.js +21 -0
  186. package/library/utils/iframe.d.ts +5 -0
  187. package/library/utils/iframe.d.ts.map +1 -0
  188. package/library/utils/iframe.js +25 -0
  189. package/library/utils/index.d.ts +7 -34
  190. package/library/utils/index.d.ts.map +1 -1
  191. package/library/utils/index.js +58 -193
  192. package/library/utils/is.d.ts +2 -0
  193. package/library/utils/is.d.ts.map +1 -0
  194. package/library/utils/is.js +9 -0
  195. package/library/utils/logger.d.ts +13 -0
  196. package/library/utils/logger.d.ts.map +1 -0
  197. package/library/utils/logger.js +63 -0
  198. package/library/utils/origin.d.ts.map +1 -1
  199. package/library/utils/origin.js +2 -1
  200. package/library/utils/promise.d.ts +2 -0
  201. package/library/utils/promise.d.ts.map +1 -0
  202. package/library/utils/promise.js +9 -0
  203. package/library/utils/window.d.ts +2 -0
  204. package/library/utils/window.d.ts.map +1 -0
  205. package/library/utils/window.js +38 -0
  206. package/package.json +49 -2
  207. package/react/package.json +2 -1
  208. package/esm/core/client-server.js +0 -329
  209. package/esm/core/server.js +0 -767
  210. package/esm/utils/ack-meta.js +0 -53
  211. package/library/core/client-server.d.ts +0 -106
  212. package/library/core/client-server.d.ts.map +0 -1
  213. package/library/core/client-server.js +0 -336
  214. package/library/core/client.d.ts.map +0 -1
  215. package/library/core/response.d.ts.map +0 -1
  216. package/library/core/server.d.ts.map +0 -1
  217. package/library/core/server.js +0 -772
  218. package/library/utils/ack-meta.d.ts +0 -2
  219. package/library/utils/ack-meta.d.ts.map +0 -1
  220. package/library/utils/ack-meta.js +0 -59
  221. /package/esm/{core → impl}/request.js +0 -0
  222. /package/library/{core → impl}/request.d.ts +0 -0
  223. /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 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,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** - 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
+ - 🧾 **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
- │ ──── RECEIVED (optional) ────────────> │ Acknowledge receipt of response/error (controlled by response `requireAck`)
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` | ServerClient | Server acknowledges receipt of request (when request `requireAck` is enabled) |
302
+ | `ack` | ReceiverSender | 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 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`.
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()`, 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.
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
- For large files or scenarios requiring chunked transfer, you can use streaming:
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` | 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 |
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 will attempt a heartbeat check and fail the stream if the connection is not alive.
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/stream_ack` for a long time, it will heartbeat-check and fail to avoid wasting resources.
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 received = await res.send(data, { requireAck: true });
891
+ const acked = await res.send(data, { requireAck: true });
782
892
 
783
- if (received) {
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 (`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`.
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
- Enable trace mode to view detailed communication logs in console:
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: true
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` | Whether to enable trace mode (optional) |
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` | Whether to enable trace mode (optional) |
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. For bidirectional communication, you can create a Client inside the iframe:
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 trace mode**: View detailed communication logs
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 console**: Check for cross-origin errors
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 currently has **76.88%** test coverage, meeting production requirements:
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