request-iframe 0.0.3 → 0.0.5

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 (69) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +177 -24
  4. package/README.md +237 -19
  5. package/library/__tests__/channel.test.ts +16 -4
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +22 -0
  8. package/library/__tests__/dispatcher.test.ts +8 -4
  9. package/library/__tests__/requestIframe.test.ts +1243 -87
  10. package/library/__tests__/stream.test.ts +92 -16
  11. package/library/__tests__/utils.test.ts +41 -1
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +1 -0
  14. package/library/constants/index.d.ts +2 -0
  15. package/library/constants/index.d.ts.map +1 -1
  16. package/library/constants/index.js +3 -1
  17. package/library/constants/messages.d.ts +3 -0
  18. package/library/constants/messages.d.ts.map +1 -1
  19. package/library/constants/messages.js +3 -0
  20. package/library/core/client-server.d.ts +4 -0
  21. package/library/core/client-server.d.ts.map +1 -1
  22. package/library/core/client-server.js +45 -22
  23. package/library/core/client.d.ts +36 -4
  24. package/library/core/client.d.ts.map +1 -1
  25. package/library/core/client.js +508 -285
  26. package/library/core/request.d.ts +3 -1
  27. package/library/core/request.d.ts.map +1 -1
  28. package/library/core/request.js +2 -1
  29. package/library/core/response.d.ts +26 -4
  30. package/library/core/response.d.ts.map +1 -1
  31. package/library/core/response.js +192 -112
  32. package/library/core/server.d.ts +13 -0
  33. package/library/core/server.d.ts.map +1 -1
  34. package/library/core/server.js +221 -6
  35. package/library/index.d.ts +2 -1
  36. package/library/index.d.ts.map +1 -1
  37. package/library/index.js +39 -3
  38. package/library/message/channel.d.ts +2 -2
  39. package/library/message/channel.d.ts.map +1 -1
  40. package/library/message/channel.js +5 -1
  41. package/library/message/dispatcher.d.ts +2 -2
  42. package/library/message/dispatcher.d.ts.map +1 -1
  43. package/library/message/dispatcher.js +6 -5
  44. package/library/stream/index.d.ts +11 -1
  45. package/library/stream/index.d.ts.map +1 -1
  46. package/library/stream/index.js +21 -3
  47. package/library/stream/types.d.ts +2 -2
  48. package/library/stream/types.d.ts.map +1 -1
  49. package/library/stream/writable-stream.d.ts +1 -1
  50. package/library/stream/writable-stream.d.ts.map +1 -1
  51. package/library/stream/writable-stream.js +87 -47
  52. package/library/types/index.d.ts +29 -5
  53. package/library/types/index.d.ts.map +1 -1
  54. package/library/utils/debug.d.ts.map +1 -1
  55. package/library/utils/debug.js +6 -2
  56. package/library/utils/error.d.ts +21 -0
  57. package/library/utils/error.d.ts.map +1 -0
  58. package/library/utils/error.js +34 -0
  59. package/library/utils/index.d.ts +21 -0
  60. package/library/utils/index.d.ts.map +1 -1
  61. package/library/utils/index.js +141 -2
  62. package/library/utils/path-match.d.ts +16 -0
  63. package/library/utils/path-match.d.ts.map +1 -1
  64. package/library/utils/path-match.js +65 -0
  65. package/package.json +2 -1
  66. package/react/library/__tests__/index.test.tsx +44 -22
  67. package/react/library/index.d.ts.map +1 -1
  68. package/react/library/index.js +81 -23
  69. package/react/package.json +7 -0
package/README.md CHANGED
@@ -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 base64-encoded file sending
77
+ - 📁 **File Transfer** - Support for file sending via stream (client→server)
78
78
  - 🌊 **Streaming** - Support for large file chunked transfer, supports async iterators
79
79
  - 🌍 **Internationalization** - Error messages can be customized for i18n
80
80
  - ✅ **Protocol Versioning** - Built-in version control for upgrade compatibility
@@ -143,7 +143,8 @@ In micro-frontend architecture, the main application needs to communicate with c
143
143
  const client = requestIframeClient(iframe, { secretKey: 'main-app' });
144
144
 
145
145
  // Get user info from child application
146
- const userInfo = await client.send('/api/user/info', {});
146
+ const userInfoResponse = await client.send('/api/user/info', {});
147
+ console.log(userInfoResponse.data); // User info data
147
148
 
148
149
  // Notify child application to refresh data
149
150
  await client.send('/api/data/refresh', { timestamp: Date.now() });
@@ -187,7 +188,8 @@ server.on('/api/data', async (req, res) => {
187
188
 
188
189
  // Parent page (cross-origin)
189
190
  const client = requestIframeClient(iframe, { secretKey: 'data-api' });
190
- const data = await client.send('/api/data', {}); // Successfully fetch cross-origin data
191
+ const response = await client.send('/api/data', {});
192
+ const data = response.data; // Successfully fetch cross-origin data
191
193
  ```
192
194
 
193
195
  ### File Preview and Download
@@ -209,8 +211,8 @@ server.on('/api/processFile', async (req, res) => {
209
211
 
210
212
  // Parent page: download file
211
213
  const response = await client.send('/api/processFile', { fileId: '123' });
212
- if (response.fileData) {
213
- downloadFile(response.fileData);
214
+ if (response.data instanceof File || response.data instanceof Blob) {
215
+ downloadFile(response.data);
214
216
  }
215
217
  ```
216
218
 
@@ -439,6 +441,13 @@ server.use(['/user', '/profile'], (req, res, next) => {
439
441
 
440
442
  request-iframe simulates HTTP's automatic cookie management mechanism:
441
443
 
444
+ **Cookie Lifetime (Important):**
445
+
446
+ - **In-memory only**: Cookies are stored in the Client instance's internal `CookieStore` (not the browser's real cookies).
447
+ - **Lifecycle**: By default, cookies live **from `requestIframeClient()` creation until `client.destroy()`**.
448
+ - **`open()` / `close()`**: These only enable/disable message handling; they **do not clear** the internal cookies.
449
+ - **Expiration**: `Expires` / `Max-Age` are respected. Expired cookies are automatically filtered out when reading/sending (and can be removed via `client.clearCookies()` / `client.removeCookie()`).
450
+
442
451
  **How It Works (Similar to HTTP Set-Cookie):**
443
452
 
444
453
  1. **When Server sets cookie**: Generate `Set-Cookie` string via `res.cookie(name, value, options)`
@@ -478,10 +487,12 @@ server.on('/api/logout', (req, res) => {
478
487
  await client.send('/api/login', { username: 'tom', password: '123' });
479
488
 
480
489
  // Client side: Subsequent request to /api/getUserInfo (automatically carries authToken and userId)
481
- const userInfo = await client.send('/api/getUserInfo', {});
490
+ const userInfoResponse = await client.send('/api/getUserInfo', {});
491
+ const userInfo = userInfoResponse.data;
482
492
 
483
493
  // Client side: Request root path (only carries userId, because authToken's path is /api)
484
- const rootData = await client.send('/other', {});
494
+ const rootResponse = await client.send('/other', {});
495
+ const rootData = rootResponse.data;
485
496
  ```
486
497
 
487
498
  #### Client Cookie Management API
@@ -540,6 +551,8 @@ server.on('/api/data', (req, res) => {
540
551
 
541
552
  ### File Transfer
542
553
 
554
+ #### Server → Client (Server sends file to client)
555
+
543
556
  ```typescript
544
557
  // Server side: Send file
545
558
  server.on('/api/download', async (req, res) => {
@@ -556,32 +569,60 @@ server.on('/api/download', async (req, res) => {
556
569
 
557
570
  // Client side: Receive
558
571
  const response = await client.send('/api/download', {});
559
- if (response.fileData) {
560
- const { content, mimeType, fileName } = response.fileData;
572
+ if (response.data instanceof File || response.data instanceof Blob) {
573
+ const file = response.data instanceof File ? response.data : null;
574
+ const fileName = file?.name || 'download';
561
575
 
562
- // content is base64-encoded string
563
- const binaryString = atob(content);
564
- const blob = new Blob([binaryString], { type: mimeType });
565
-
566
- // Download file
567
- const url = URL.createObjectURL(blob);
576
+ // Download file directly using File/Blob
577
+ const url = URL.createObjectURL(response.data);
568
578
  const a = document.createElement('a');
569
579
  a.href = url;
570
- a.download = fileName || 'download';
580
+ a.download = fileName;
571
581
  a.click();
582
+ URL.revokeObjectURL(url);
572
583
  }
573
584
  ```
574
585
 
586
+ #### Client → Server (Client sends file to server)
587
+
588
+ 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`.
589
+
590
+ ```typescript
591
+ // Client side: Send file (stream, autoResolve defaults to true)
592
+ const file = new File(['Hello Upload'], 'upload.txt', { type: 'text/plain' });
593
+ const response = await client.send('/api/upload', file);
594
+
595
+ // Or use sendFile explicitly
596
+ const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
597
+ const response2 = await client.sendFile('/api/upload', blob, {
598
+ fileName: 'data.bin',
599
+ mimeType: 'application/octet-stream',
600
+ autoResolve: true // optional, default true: server gets File/Blob in req.body
601
+ });
602
+
603
+ // Server side: Receive file (autoResolve true → req.body is File/Blob)
604
+ server.on('/api/upload', async (req, res) => {
605
+ const blob = req.body as Blob; // or File when client sent File
606
+ const text = await blob.text();
607
+ console.log('Received file content:', text);
608
+ res.send({ success: true, size: blob.size });
609
+ });
610
+ ```
611
+
612
+ **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.
613
+
575
614
  ### Streaming
576
615
 
577
616
  For large files or scenarios requiring chunked transfer, you can use streaming:
578
617
 
618
+ #### Server → Client (Server sends stream to client)
619
+
579
620
  ```typescript
580
621
  import {
581
622
  IframeWritableStream,
582
623
  IframeFileWritableStream,
583
624
  isIframeReadableStream,
584
- isIframeFileStream
625
+ isIframeFileReadableStream
585
626
  } from 'request-iframe';
586
627
 
587
628
  // Server side: Send data stream using iterator
@@ -629,6 +670,55 @@ if (isIframeReadableStream(response.stream)) {
629
670
  }
630
671
  ```
631
672
 
673
+ #### Client → Server (Client sends stream to server)
674
+
675
+ ```typescript
676
+ import { IframeWritableStream } from 'request-iframe';
677
+
678
+ // Client side: Send stream to server
679
+ const stream = new IframeWritableStream({
680
+ chunked: true,
681
+ iterator: async function* () {
682
+ for (let i = 0; i < 5; i++) {
683
+ yield `Chunk ${i}`;
684
+ await new Promise(r => setTimeout(r, 50));
685
+ }
686
+ }
687
+ });
688
+
689
+ // Use sendStream to send stream as request body
690
+ const response = await client.sendStream('/api/uploadStream', stream);
691
+ console.log('Upload result:', response.data);
692
+
693
+ // Or use send() - it automatically dispatches to sendStream for IframeWritableStream
694
+ const stream2 = new IframeWritableStream({
695
+ next: async () => ({ data: 'single chunk', done: true })
696
+ });
697
+ const response2 = await client.send('/api/uploadStream', stream2);
698
+
699
+ // Server side: Receive stream
700
+ server.on('/api/uploadStream', async (req, res) => {
701
+ // req.stream is available when client sends stream
702
+ if (req.stream) {
703
+ const chunks: string[] = [];
704
+
705
+ // Read stream chunk by chunk
706
+ for await (const chunk of req.stream) {
707
+ chunks.push(chunk);
708
+ console.log('Received chunk:', chunk);
709
+ }
710
+
711
+ res.send({
712
+ success: true,
713
+ chunkCount: chunks.length,
714
+ chunks
715
+ });
716
+ } else {
717
+ res.status(400).send({ error: 'Expected stream body' });
718
+ }
719
+ });
720
+ ```
721
+
632
722
  **Stream Types:**
633
723
 
634
724
  | Type | Description |
@@ -743,14 +833,14 @@ Create a Server instance.
743
833
 
744
834
  #### client.send(path, body?, options?)
745
835
 
746
- Send a request.
836
+ Send a request. Automatically dispatches to `sendFile()` or `sendStream()` based on body type.
747
837
 
748
838
  **Parameters:**
749
839
 
750
840
  | Parameter | Type | Description |
751
841
  |-----------|------|-------------|
752
842
  | `path` | `string` | Request path |
753
- | `body` | `object` | Request data (optional) |
843
+ | `body` | `any` | Request data (optional). Can be plain object, File, Blob, or IframeWritableStream. Automatically dispatches: File/Blob → `sendFile()`, IframeWritableStream → `sendStream()` |
754
844
  | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
755
845
  | `options.timeout` | `number` | Request timeout (ms), default 5000 |
756
846
  | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
@@ -771,6 +861,69 @@ interface Response<T = any> {
771
861
  }
772
862
  ```
773
863
 
864
+ **Examples:**
865
+
866
+ ```typescript
867
+ // Send plain object (auto Content-Type: application/json)
868
+ await client.send('/api/data', { name: 'test' });
869
+
870
+ // Send string (auto Content-Type: text/plain)
871
+ await client.send('/api/text', 'Hello');
872
+
873
+ // Send File/Blob (auto-dispatches to sendFile)
874
+ const file = new File(['content'], 'test.txt');
875
+ await client.send('/api/upload', file);
876
+
877
+ // Send stream (auto-dispatches to sendStream)
878
+ const stream = new IframeWritableStream({ iterator: async function* () { yield 'data'; } });
879
+ await client.send('/api/uploadStream', stream);
880
+ ```
881
+
882
+ #### client.sendFile(path, content, options?)
883
+
884
+ Send file as request body (via stream; server receives File/Blob when autoResolve is true).
885
+
886
+ **Parameters:**
887
+
888
+ | Parameter | Type | Description |
889
+ |-----------|------|-------------|
890
+ | `path` | `string` | Request path |
891
+ | `content` | `string \| Blob \| File` | File content to send |
892
+ | `options.mimeType` | `string` | File MIME type (optional, uses content.type if available) |
893
+ | `options.fileName` | `string` | File name (optional) |
894
+ | `options.autoResolve` | `boolean` | If true (default), server receives File/Blob in `req.body`; if false, server gets `req.stream` / `req.body` as `IframeFileReadableStream` |
895
+ | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
896
+ | `options.timeout` | `number` | Request timeout (ms), default 5000 |
897
+ | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
898
+ | `options.headers` | `object` | Request headers (optional) |
899
+ | `options.cookies` | `object` | Request cookies (optional) |
900
+ | `options.requestId` | `string` | Custom request ID (optional) |
901
+
902
+ **Returns:** `Promise<Response>`
903
+
904
+ **Note:** The file is sent via stream. When `autoResolve` is true (default), the server receives `req.body` as File/Blob; when false, the server receives `req.stream` / `req.body` as `IframeFileReadableStream`.
905
+
906
+ #### client.sendStream(path, stream, options?)
907
+
908
+ Send stream as request body (server receives readable stream).
909
+
910
+ **Parameters:**
911
+
912
+ | Parameter | Type | Description |
913
+ |-----------|------|-------------|
914
+ | `path` | `string` | Request path |
915
+ | `stream` | `IframeWritableStream` | Writable stream to send |
916
+ | `options.ackTimeout` | `number` | ACK acknowledgment timeout (ms), default 1000 |
917
+ | `options.timeout` | `number` | Request timeout (ms), default 5000 |
918
+ | `options.asyncTimeout` | `number` | Async timeout (ms), default 120000 |
919
+ | `options.headers` | `object` | Request headers (optional) |
920
+ | `options.cookies` | `object` | Request cookies (optional) |
921
+ | `options.requestId` | `string` | Custom request ID (optional) |
922
+
923
+ **Returns:** `Promise<Response>`
924
+
925
+ **Note:** On the server side, the stream is available as `req.stream` (an `IIframeReadableStream`). You can iterate over it using `for await (const chunk of req.stream)`.
926
+
774
927
  #### client.isConnect()
775
928
 
776
929
  Detect if Server is reachable.
@@ -806,6 +959,71 @@ Register route handler.
806
959
  type ServerHandler = (req: ServerRequest, res: ServerResponse) => any | Promise<any>;
807
960
  ```
808
961
 
962
+ **ServerRequest interface:**
963
+
964
+ ```typescript
965
+ interface ServerRequest {
966
+ body: any; // Request body (plain data, or File/Blob when client sendFile with autoResolve true)
967
+ stream?: IIframeReadableStream; // Request stream (when client sends via sendStream or sendFile with autoResolve false)
968
+ headers: Record<string, string>; // Request headers
969
+ cookies: Record<string, string>; // Request cookies
970
+ path: string; // Request path
971
+ params: Record<string, string>; // Path parameters extracted from route pattern (e.g., { id: '123' } for '/api/users/:id' and '/api/users/123')
972
+ requestId: string; // Request ID
973
+ origin: string; // Sender origin
974
+ source: Window; // Sender window
975
+ res: ServerResponse; // Response object
976
+ }
977
+ ```
978
+
979
+ **Note:**
980
+ - When client sends a file via `sendFile()` (or `send(path, file)`), the file is sent via stream. If `autoResolve` is true (default), `req.body` is the resolved File/Blob; if false, `req.stream` / `req.body` is an `IIframeReadableStream` (e.g. `IframeFileReadableStream`).
981
+ - When client sends a stream via `sendStream()`, `req.stream` is available as an `IIframeReadableStream`. You can iterate over it using `for await (const chunk of req.stream)`.
982
+ - **Path parameters**: You can use Express-style route parameters (e.g., `/api/users/:id`) to extract path segments. The extracted parameters are available in `req.params`. For example, registering `/api/users/:id` and receiving `/api/users/123` will set `req.params.id` to `'123'`.
983
+
984
+ **Path Parameters Example:**
985
+
986
+ ```typescript
987
+ // Register route with parameter
988
+ server.on('/api/users/:id', (req, res) => {
989
+ const userId = req.params.id; // '123' when path is '/api/users/123'
990
+ res.send({ userId });
991
+ });
992
+
993
+ // Multiple parameters
994
+ server.on('/api/users/:userId/posts/:postId', (req, res) => {
995
+ const { userId, postId } = req.params;
996
+ res.send({ userId, postId });
997
+ });
998
+ ```
999
+
1000
+ **Handler return value behavior**
1001
+
1002
+ - If your handler **does not call** `res.send()` / `res.json()` / `res.sendFile()` / `res.sendStream()`, but it **returns a value that is not `undefined`**, then the server will treat it as a successful result and automatically send it back to the client (equivalent to `res.send(returnValue)`).
1003
+ - For **async handlers** (`Promise`): if the promise **resolves to a value that is not `undefined`** and no response has been sent yet, it will also be auto-sent.
1004
+ - If the handler (or resolved promise) returns `undefined` **and** no response method was called, the server will respond with error code `NO_RESPONSE`.
1005
+
1006
+ Examples:
1007
+
1008
+ ```typescript
1009
+ // Sync: auto-send return value
1010
+ server.on('/api/hello', () => {
1011
+ return { message: 'hello' };
1012
+ });
1013
+
1014
+ // Async: auto-send resolved value
1015
+ server.on('/api/user', async (req) => {
1016
+ const user = await getUser(req.body.userId);
1017
+ return user; // auto-send if not undefined
1018
+ });
1019
+
1020
+ // If you manually send, return value is ignored
1021
+ server.on('/api/manual', (req, res) => {
1022
+ res.send({ ok: true });
1023
+ return { ignored: true };
1024
+ });
1025
+ ```
1026
+
809
1027
  #### server.off(path)
810
1028
 
811
1029
  Remove route handler.
@@ -227,6 +227,8 @@ describe('MessageChannel', () => {
227
227
  describe('send', () => {
228
228
  it('should send message to target window', () => {
229
229
  const targetWindow = {
230
+ closed: false,
231
+ document: {},
230
232
  postMessage: jest.fn()
231
233
  } as any;
232
234
 
@@ -234,8 +236,9 @@ describe('MessageChannel', () => {
234
236
  path: 'test'
235
237
  });
236
238
 
237
- channel.send(targetWindow, message, 'https://example.com');
239
+ const ok = channel.send(targetWindow, message, 'https://example.com');
238
240
 
241
+ expect(ok).toBe(true);
239
242
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
240
243
  message,
241
244
  'https://example.com'
@@ -244,6 +247,8 @@ describe('MessageChannel', () => {
244
247
 
245
248
  it('should use default origin * when not specified', () => {
246
249
  const targetWindow = {
250
+ closed: false,
251
+ document: {},
247
252
  postMessage: jest.fn()
248
253
  } as any;
249
254
 
@@ -251,8 +256,9 @@ describe('MessageChannel', () => {
251
256
  path: 'test'
252
257
  });
253
258
 
254
- channel.send(targetWindow, message);
259
+ const ok = channel.send(targetWindow, message);
255
260
 
261
+ expect(ok).toBe(true);
256
262
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
257
263
  message,
258
264
  '*'
@@ -263,10 +269,12 @@ describe('MessageChannel', () => {
263
269
  describe('sendMessage', () => {
264
270
  it('should create and send message', () => {
265
271
  const targetWindow = {
272
+ closed: false,
273
+ document: {},
266
274
  postMessage: jest.fn()
267
275
  } as any;
268
276
 
269
- channel.sendMessage(
277
+ const ok = channel.sendMessage(
270
278
  targetWindow,
271
279
  'https://example.com',
272
280
  MessageType.REQUEST,
@@ -277,6 +285,7 @@ describe('MessageChannel', () => {
277
285
  }
278
286
  );
279
287
 
288
+ expect(ok).toBe(true);
280
289
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
281
290
  expect.objectContaining({
282
291
  __requestIframe__: 1,
@@ -293,10 +302,12 @@ describe('MessageChannel', () => {
293
302
  it('should include secretKey in message', () => {
294
303
  const channelWithKey = new MessageChannel('test-key');
295
304
  const targetWindow = {
305
+ closed: false,
306
+ document: {},
296
307
  postMessage: jest.fn()
297
308
  } as any;
298
309
 
299
- channelWithKey.sendMessage(
310
+ const ok = channelWithKey.sendMessage(
300
311
  targetWindow,
301
312
  'https://example.com',
302
313
  MessageType.REQUEST,
@@ -304,6 +315,7 @@ describe('MessageChannel', () => {
304
315
  { path: 'test' }
305
316
  );
306
317
 
318
+ expect(ok).toBe(true);
307
319
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
308
320
  expect.objectContaining({
309
321
  secretKey: 'test-key'