request-iframe 0.0.4 → 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 (43) hide show
  1. package/README.CN.md +7 -0
  2. package/README.md +7 -0
  3. package/library/__tests__/channel.test.ts +16 -4
  4. package/library/__tests__/debug.test.ts +22 -0
  5. package/library/__tests__/dispatcher.test.ts +8 -4
  6. package/library/__tests__/requestIframe.test.ts +207 -1
  7. package/library/__tests__/stream.test.ts +47 -2
  8. package/library/__tests__/utils.test.ts +41 -1
  9. package/library/constants/index.d.ts +2 -0
  10. package/library/constants/index.d.ts.map +1 -1
  11. package/library/constants/index.js +3 -1
  12. package/library/constants/messages.d.ts +1 -0
  13. package/library/constants/messages.d.ts.map +1 -1
  14. package/library/constants/messages.js +1 -0
  15. package/library/core/client.d.ts +5 -0
  16. package/library/core/client.d.ts.map +1 -1
  17. package/library/core/client.js +54 -18
  18. package/library/core/response.d.ts.map +1 -1
  19. package/library/core/response.js +51 -32
  20. package/library/core/server.d.ts.map +1 -1
  21. package/library/core/server.js +10 -0
  22. package/library/index.d.ts +1 -1
  23. package/library/index.d.ts.map +1 -1
  24. package/library/index.js +7 -0
  25. package/library/message/channel.d.ts +2 -2
  26. package/library/message/channel.d.ts.map +1 -1
  27. package/library/message/channel.js +5 -1
  28. package/library/message/dispatcher.d.ts +2 -2
  29. package/library/message/dispatcher.d.ts.map +1 -1
  30. package/library/message/dispatcher.js +2 -2
  31. package/library/stream/writable-stream.d.ts.map +1 -1
  32. package/library/stream/writable-stream.js +81 -39
  33. package/library/types/index.d.ts +3 -1
  34. package/library/types/index.d.ts.map +1 -1
  35. package/library/utils/debug.d.ts.map +1 -1
  36. package/library/utils/debug.js +6 -2
  37. package/library/utils/error.d.ts +21 -0
  38. package/library/utils/error.d.ts.map +1 -0
  39. package/library/utils/error.js +34 -0
  40. package/library/utils/index.d.ts +7 -0
  41. package/library/utils/index.d.ts.map +1 -1
  42. package/library/utils/index.js +42 -1
  43. package/package.json +1 -1
package/README.CN.md CHANGED
@@ -441,6 +441,13 @@ server.use(['/user', '/profile'], (req, res, next) => {
441
441
 
442
442
  request-iframe 模拟了 HTTP 的 cookie 自动管理机制:
443
443
 
444
+ **Cookie 有效期与生命周期(重要):**
445
+
446
+ - **仅内存存储**:cookies 存在于 Client 实例内部的 `CookieStore`(不会写入浏览器真实 Cookie)。
447
+ - **生命周期**:默认从 `requestIframeClient()` 创建开始,直到 `client.destroy()` 为止。
448
+ - **`open()` / `close()`**:只控制消息监听的开启/关闭,**不会清空**内部 cookies。
449
+ - **过期处理**:会遵循 `Expires` / `Max-Age`。已过期的 cookie 在读取/发送时会被自动过滤(也可以用 `client.clearCookies()` / `client.removeCookie()` 手动清理)。
450
+
444
451
  ```
445
452
  ┌─────────────────────────────────────────────────────────────────┐
446
453
  │ Cookies 自动管理流程 │
package/README.md CHANGED
@@ -441,6 +441,13 @@ server.use(['/user', '/profile'], (req, res, next) => {
441
441
 
442
442
  request-iframe simulates HTTP's automatic cookie management mechanism:
443
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
+
444
451
  **How It Works (Similar to HTTP Set-Cookie):**
445
452
 
446
453
  1. **When Server sets cookie**: Generate `Set-Cookie` string via `res.cookie(name, value, options)`
@@ -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'
@@ -101,6 +101,8 @@ describe('debug', () => {
101
101
 
102
102
  expect(console.info).toHaveBeenCalledWith(
103
103
  expect.stringContaining('[Client] Request Start'),
104
+ expect.any(String),
105
+ expect.any(String),
104
106
  expect.objectContaining({
105
107
  path: 'test',
106
108
  body: { param: 'value' }
@@ -109,6 +111,8 @@ describe('debug', () => {
109
111
 
110
112
  expect(console.info).toHaveBeenCalledWith(
111
113
  expect.stringContaining('[Client] Request Success'),
114
+ expect.any(String),
115
+ expect.any(String),
112
116
  expect.objectContaining({
113
117
  requestId: expect.any(String),
114
118
  status: 200
@@ -144,6 +148,8 @@ describe('debug', () => {
144
148
 
145
149
  expect(console.error).toHaveBeenCalledWith(
146
150
  expect.stringContaining('[Client] Request Failed'),
151
+ expect.any(String),
152
+ expect.any(String),
147
153
  expect.objectContaining({
148
154
  code: expect.any(String)
149
155
  })
@@ -258,6 +264,8 @@ describe('debug', () => {
258
264
 
259
265
  expect(console.info).toHaveBeenCalledWith(
260
266
  expect.stringContaining('[Client] Request Success (File)'),
267
+ expect.any(String),
268
+ expect.any(String),
261
269
  expect.objectContaining({
262
270
  fileData: expect.objectContaining({
263
271
  fileName: 'test.txt'
@@ -320,6 +328,8 @@ describe('debug', () => {
320
328
 
321
329
  expect(console.info).toHaveBeenCalledWith(
322
330
  expect.stringContaining('[Client] Received ACK'),
331
+ expect.any(String),
332
+ expect.any(String),
323
333
  expect.objectContaining({
324
334
  requestId: expect.any(String)
325
335
  })
@@ -370,6 +380,8 @@ describe('debug', () => {
370
380
 
371
381
  expect(console.info).toHaveBeenCalledWith(
372
382
  expect.stringContaining('[Server] Received Request'),
383
+ expect.any(String),
384
+ expect.any(String),
373
385
  expect.objectContaining({
374
386
  path: 'test',
375
387
  body: { param: 'value' }
@@ -378,6 +390,8 @@ describe('debug', () => {
378
390
 
379
391
  expect(console.info).toHaveBeenCalledWith(
380
392
  expect.stringContaining('[Server] Sending Response'),
393
+ expect.any(String),
394
+ expect.any(String),
381
395
  expect.objectContaining({
382
396
  status: 200
383
397
  })
@@ -426,6 +440,8 @@ describe('debug', () => {
426
440
 
427
441
  expect(console.info).toHaveBeenCalledWith(
428
442
  expect.stringContaining('[Server] Setting Status Code'),
443
+ expect.any(String),
444
+ expect.any(String),
429
445
  expect.objectContaining({
430
446
  statusCode: 404
431
447
  })
@@ -475,6 +491,8 @@ describe('debug', () => {
475
491
 
476
492
  expect(console.info).toHaveBeenCalledWith(
477
493
  expect.stringContaining('[Server] Setting Header'),
494
+ expect.any(String),
495
+ expect.any(String),
478
496
  expect.objectContaining({
479
497
  header: 'X-Custom',
480
498
  value: 'value'
@@ -527,6 +545,8 @@ describe('debug', () => {
527
545
 
528
546
  expect(console.info).toHaveBeenCalledWith(
529
547
  expect.stringContaining('[Server] Sending File'),
548
+ expect.any(String),
549
+ expect.any(String),
530
550
  expect.objectContaining({
531
551
  fileName: 'test.txt',
532
552
  mimeType: 'text/plain'
@@ -576,6 +596,8 @@ describe('debug', () => {
576
596
 
577
597
  expect(console.info).toHaveBeenCalledWith(
578
598
  expect.stringContaining('[Server] Sending JSON Response'),
599
+ expect.any(String),
600
+ expect.any(String),
579
601
  expect.objectContaining({
580
602
  status: 200
581
603
  })
@@ -373,10 +373,11 @@ describe('MessageDispatcher', () => {
373
373
  delete (message as any).role;
374
374
  delete (message as any).creatorId;
375
375
 
376
- dispatcher.send(targetWindow, message, 'https://example.com');
376
+ const ok = dispatcher.send(targetWindow, message, 'https://example.com');
377
377
 
378
378
  expect(message.role).toBe(MessageRole.CLIENT);
379
379
  expect(message.creatorId).toBe('instance-1');
380
+ expect(ok).toBe(true);
380
381
  expect(targetWindow.postMessage).toHaveBeenCalled();
381
382
  });
382
383
 
@@ -391,10 +392,11 @@ describe('MessageDispatcher', () => {
391
392
  creatorId: 'custom-id'
392
393
  });
393
394
 
394
- dispatcher.send(targetWindow, message, 'https://example.com');
395
+ const ok = dispatcher.send(targetWindow, message, 'https://example.com');
395
396
 
396
397
  expect(message.role).toBe(MessageRole.SERVER);
397
398
  expect(message.creatorId).toBe('custom-id');
399
+ expect(ok).toBe(true);
398
400
  });
399
401
 
400
402
  it('should use default origin * when not specified', () => {
@@ -406,8 +408,9 @@ describe('MessageDispatcher', () => {
406
408
  path: 'test'
407
409
  });
408
410
 
409
- dispatcher.send(targetWindow, message);
411
+ const ok = dispatcher.send(targetWindow, message);
410
412
 
413
+ expect(ok).toBe(true);
411
414
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
412
415
  expect.any(Object),
413
416
  '*'
@@ -421,7 +424,7 @@ describe('MessageDispatcher', () => {
421
424
  postMessage: jest.fn()
422
425
  } as any;
423
426
 
424
- dispatcher.sendMessage(
427
+ const ok = dispatcher.sendMessage(
425
428
  targetWindow,
426
429
  'https://example.com',
427
430
  MessageType.REQUEST,
@@ -432,6 +435,7 @@ describe('MessageDispatcher', () => {
432
435
  }
433
436
  );
434
437
 
438
+ expect(ok).toBe(true);
435
439
  expect(targetWindow.postMessage).toHaveBeenCalledWith(
436
440
  expect.objectContaining({
437
441
  type: 'request',
@@ -1,7 +1,7 @@
1
1
  import { requestIframeClient, clearRequestIframeClientCache } from '../api/client';
2
2
  import { requestIframeServer, clearRequestIframeServerCache } from '../api/server';
3
3
  import { RequestConfig, Response, ErrorResponse, PostMessageData } from '../types';
4
- import { HttpHeader, MessageRole, Messages } from '../constants';
4
+ import { HttpHeader, MessageRole, Messages, ErrorCode } from '../constants';
5
5
  import { IframeWritableStream } from '../stream';
6
6
 
7
7
  /**
@@ -5381,4 +5381,210 @@ describe('requestIframeClient and requestIframeServer', () => {
5381
5381
  cleanupIframe(iframe);
5382
5382
  });
5383
5383
  });
5384
+
5385
+ describe('Target window closed detection', () => {
5386
+ it('should return true when target window is available', () => {
5387
+ const origin = 'https://example.com';
5388
+ const iframe = createTestIframe(origin);
5389
+
5390
+ const mockContentWindow = {
5391
+ closed: false,
5392
+ postMessage: jest.fn()
5393
+ } as any;
5394
+ Object.defineProperty(iframe, 'contentWindow', {
5395
+ value: mockContentWindow,
5396
+ writable: true
5397
+ });
5398
+
5399
+ const client = requestIframeClient(iframe);
5400
+ expect(client.isAvailable()).toBe(true);
5401
+
5402
+ cleanupIframe(iframe);
5403
+ });
5404
+
5405
+ it('should return false when target window is closed', () => {
5406
+ const origin = 'https://example.com';
5407
+ const iframe = createTestIframe(origin);
5408
+
5409
+ // Create a mock window that appears closed
5410
+ const mockContentWindow = {
5411
+ closed: true,
5412
+ postMessage: jest.fn()
5413
+ } as any;
5414
+ Object.defineProperty(iframe, 'contentWindow', {
5415
+ value: mockContentWindow,
5416
+ writable: true
5417
+ });
5418
+
5419
+ const client = requestIframeClient(iframe);
5420
+ expect(client.isAvailable()).toBe(false);
5421
+
5422
+ cleanupIframe(iframe);
5423
+ });
5424
+
5425
+ it('should reject client request when target window is closed', async () => {
5426
+ const origin = 'https://example.com';
5427
+ const iframe = createTestIframe(origin);
5428
+
5429
+ // Create a mock window that appears closed
5430
+ const mockContentWindow = {
5431
+ closed: true,
5432
+ document: null
5433
+ } as any;
5434
+ Object.defineProperty(iframe, 'contentWindow', {
5435
+ value: mockContentWindow,
5436
+ writable: true
5437
+ });
5438
+
5439
+ const client = requestIframeClient(iframe);
5440
+
5441
+ try {
5442
+ await client.send('test', { param: 'value' });
5443
+ throw new Error('Should have thrown');
5444
+ } catch (error: any) {
5445
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5446
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5447
+ }
5448
+
5449
+ cleanupIframe(iframe);
5450
+ });
5451
+
5452
+ it('should reject client ping when target window is closed', async () => {
5453
+ const origin = 'https://example.com';
5454
+ const iframe = createTestIframe(origin);
5455
+
5456
+ // Create a mock window that appears closed
5457
+ const mockContentWindow = {
5458
+ closed: true,
5459
+ document: null
5460
+ } as any;
5461
+ Object.defineProperty(iframe, 'contentWindow', {
5462
+ value: mockContentWindow,
5463
+ writable: true
5464
+ });
5465
+
5466
+ const client = requestIframeClient(iframe);
5467
+
5468
+ try {
5469
+ await client.isConnect();
5470
+ throw new Error('Should have thrown');
5471
+ } catch (error: any) {
5472
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5473
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5474
+ }
5475
+
5476
+ cleanupIframe(iframe);
5477
+ });
5478
+
5479
+ it('should throw error when server sends response to closed window', async () => {
5480
+ const origin = 'https://example.com';
5481
+ const iframe = createTestIframe(origin);
5482
+
5483
+ const mockContentWindow = {
5484
+ postMessage: jest.fn()
5485
+ };
5486
+ Object.defineProperty(iframe, 'contentWindow', {
5487
+ value: mockContentWindow,
5488
+ writable: true
5489
+ });
5490
+
5491
+ const server = requestIframeServer();
5492
+
5493
+ server.on('test', (req, res) => {
5494
+ // Simulate window being closed after request is received
5495
+ const closedWindow = {
5496
+ closed: true,
5497
+ document: null
5498
+ } as any;
5499
+ // Replace targetWindow in response object
5500
+ (res as any).targetWindow = closedWindow;
5501
+
5502
+ try {
5503
+ res.send({ result: 'success' });
5504
+ } catch (error: any) {
5505
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5506
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5507
+ }
5508
+ });
5509
+
5510
+ // Send request
5511
+ window.dispatchEvent(
5512
+ new MessageEvent('message', {
5513
+ data: {
5514
+ __requestIframe__: 1,
5515
+ timestamp: Date.now(),
5516
+ type: 'request',
5517
+ requestId: 'req123',
5518
+ path: 'test',
5519
+ body: { param: 'value' },
5520
+ role: MessageRole.CLIENT,
5521
+ targetId: server.id
5522
+ },
5523
+ origin,
5524
+ source: mockContentWindow as any
5525
+ })
5526
+ );
5527
+
5528
+ await new Promise(resolve => setTimeout(resolve, 50));
5529
+
5530
+ server.destroy();
5531
+ cleanupIframe(iframe);
5532
+ });
5533
+
5534
+ it('should throw error when server sends stream to closed window', async () => {
5535
+ const origin = 'https://example.com';
5536
+ const iframe = createTestIframe(origin);
5537
+
5538
+ const mockContentWindow = {
5539
+ postMessage: jest.fn()
5540
+ };
5541
+ Object.defineProperty(iframe, 'contentWindow', {
5542
+ value: mockContentWindow,
5543
+ writable: true
5544
+ });
5545
+
5546
+ const server = requestIframeServer();
5547
+
5548
+ server.on('test', async (req, res) => {
5549
+ // Simulate window being closed after request is received
5550
+ const closedWindow = {
5551
+ closed: true,
5552
+ document: null
5553
+ } as any;
5554
+ // Replace targetWindow in response object
5555
+ (res as any).targetWindow = closedWindow;
5556
+
5557
+ const stream = new IframeWritableStream();
5558
+ try {
5559
+ await res.sendStream(stream);
5560
+ } catch (error: any) {
5561
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5562
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5563
+ }
5564
+ });
5565
+
5566
+ // Send request
5567
+ window.dispatchEvent(
5568
+ new MessageEvent('message', {
5569
+ data: {
5570
+ __requestIframe__: 1,
5571
+ timestamp: Date.now(),
5572
+ type: 'request',
5573
+ requestId: 'req123',
5574
+ path: 'test',
5575
+ body: { param: 'value' },
5576
+ role: MessageRole.CLIENT,
5577
+ targetId: server.id
5578
+ },
5579
+ origin,
5580
+ source: mockContentWindow as any
5581
+ })
5582
+ );
5583
+
5584
+ await new Promise(resolve => setTimeout(resolve, 50));
5585
+
5586
+ server.destroy();
5587
+ cleanupIframe(iframe);
5588
+ });
5589
+ });
5384
5590
  });
@@ -26,7 +26,10 @@ describe('Stream', () => {
26
26
  postMessage: mockPostMessage
27
27
  } as any;
28
28
  mockChannel = {
29
- send: (target: Window, message: any, origin: string) => target.postMessage(message, origin)
29
+ send: (target: Window, message: any, origin: string) => {
30
+ target.postMessage(message, origin);
31
+ return true;
32
+ }
30
33
  } as unknown as MessageChannel;
31
34
  });
32
35
 
@@ -87,6 +90,48 @@ describe('Stream', () => {
87
90
  expect(mockPostMessage).toHaveBeenCalledTimes(5);
88
91
  });
89
92
 
93
+ it('should stop streaming when target window is closed', async () => {
94
+ let streamDataCount = 0;
95
+ mockPostMessage.mockImplementation((msg: any) => {
96
+ if (msg?.type === 'stream_data') {
97
+ streamDataCount += 1;
98
+ // After first chunk, simulate target window closed
99
+ (mockTargetWindow as any).closed = true;
100
+ }
101
+ });
102
+
103
+ // Make mockChannel respect closed flag
104
+ (mockChannel as any).send = (target: any, message: any, origin: string) => {
105
+ if (target?.closed === true) return false;
106
+ target.postMessage(message, origin);
107
+ return true;
108
+ };
109
+
110
+ const stream = new IframeWritableStream({
111
+ iterator: async function* () {
112
+ yield 'chunk1';
113
+ yield 'chunk2';
114
+ yield 'chunk3';
115
+ }
116
+ });
117
+
118
+ // mockTargetWindow now supports closed/document checks in isWindowAvailable
119
+ (mockTargetWindow as any).closed = false;
120
+ (mockTargetWindow as any).document = {};
121
+
122
+ stream._bind({
123
+ requestId: 'req-123',
124
+ targetWindow: mockTargetWindow,
125
+ targetOrigin: 'https://example.com',
126
+ secretKey: 'test',
127
+ channel: mockChannel
128
+ });
129
+
130
+ await expect(stream.start()).rejects.toThrow('Stream was cancelled');
131
+ expect(stream.state).toBe('cancelled');
132
+ expect(streamDataCount).toBe(1);
133
+ });
134
+
90
135
  it('should start stream with next function', async () => {
91
136
  let callCount = 0;
92
137
  const stream = new IframeWritableStream({
@@ -221,7 +266,7 @@ describe('Stream', () => {
221
266
 
222
267
  it('should use channel if provided', async () => {
223
268
  const mockChannel = {
224
- send: jest.fn()
269
+ send: jest.fn(() => true)
225
270
  } as any;
226
271
 
227
272
  const stream = new IframeWritableStream();
@@ -7,7 +7,8 @@ import {
7
7
  createSetCookie,
8
8
  createClearCookie,
9
9
  matchCookiePath,
10
- CookieStore
10
+ CookieStore,
11
+ isWindowAvailable
11
12
  } from '../utils';
12
13
  import {
13
14
  validateProtocolVersion,
@@ -430,4 +431,43 @@ describe('utils', () => {
430
431
  });
431
432
  });
432
433
  });
434
+
435
+ describe('isWindowAvailable', () => {
436
+ it('should return true for valid window', () => {
437
+ expect(isWindowAvailable(window)).toBe(true);
438
+ });
439
+
440
+ it('should return false for null', () => {
441
+ expect(isWindowAvailable(null)).toBe(false);
442
+ });
443
+
444
+ it('should return false for undefined', () => {
445
+ expect(isWindowAvailable(undefined)).toBe(false);
446
+ });
447
+
448
+ it('should return false for closed window (window.open)', () => {
449
+ const mockWindow = {
450
+ closed: true,
451
+ document: {},
452
+ postMessage: jest.fn()
453
+ } as any;
454
+ expect(isWindowAvailable(mockWindow)).toBe(false);
455
+ });
456
+
457
+ it('should return true for open window (window.open)', () => {
458
+ const mockWindow = {
459
+ closed: false,
460
+ document: {},
461
+ postMessage: jest.fn()
462
+ } as any;
463
+ expect(isWindowAvailable(mockWindow)).toBe(true);
464
+ });
465
+
466
+ it('should return false when postMessage is missing', () => {
467
+ const mockWindow = {
468
+ closed: false
469
+ } as any;
470
+ expect(isWindowAvailable(mockWindow)).toBe(false);
471
+ });
472
+ });
433
473
  });
@@ -79,6 +79,8 @@ export declare const ErrorCode: {
79
79
  readonly STREAM_CANCELLED: "STREAM_CANCELLED";
80
80
  /** Stream not bound */
81
81
  readonly STREAM_NOT_BOUND: "STREAM_NOT_BOUND";
82
+ /** Target window closed */
83
+ readonly TARGET_WINDOW_CLOSED: "TARGET_WINDOW_CLOSED";
82
84
  };
83
85
  /**
84
86
  * Message type constants
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;IAC1B,+BAA+B;;IAE/B,wFAAwF;;CAEhF,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,eAAe,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAExF;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,oBAAoB;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,SAAS,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;CAClD;AAED;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;CAWb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAWjD,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;GAEG;AACH,eAAO,MAAM,SAAS;IACpB,+BAA+B;;IAE/B,oCAAoC;;IAEpC,4BAA4B;;IAE5B,oBAAoB;;IAEpB,uBAAuB;;IAEvB,kBAAkB;;IAElB,qCAAqC;;IAErC,uBAAuB;;IAEvB,mBAAmB;;IAEnB,uBAAuB;;IAEvB,uBAAuB;;CAEf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,WAAW;IACtB,sBAAsB;;IAEtB,mCAAmC;;IAEnC,8BAA8B;;IAE9B,uBAAuB;;IAEvB,oBAAoB;;IAEpB,wCAAwC;;IAExC,8CAA8C;;IAE9C,8CAA8C;;IAE9C,mBAAmB;;IAEnB,wBAAwB;;IAExB,iBAAiB;;IAEjB,mBAAmB;;IAEnB,oBAAoB;;CAEZ,CAAC;AAEX,eAAO,MAAM,WAAW;IACtB,kBAAkB;;IAElB,kBAAkB;;CAEV,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,WAAW,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAE5E;;GAEG;AACH,eAAO,MAAM,cAAc;IACzB;;;;;OAKG;;IAEH,0BAA0B;;IAE1B,kCAAkC;;CAE1B,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU;IACrB,sCAAsC;;IAEtC,mBAAmB;;IAEnB,+CAA+C;;IAE/C,oBAAoB;;IAEpB,0CAA0C;;CAElC,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,UAAU,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,WAAW,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAE5E;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,SAAS,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAEtE;;GAEG;AACH,eAAO,MAAM,UAAU;IACrB,yBAAyB;;IAEzB,kBAAkB;;CAEV,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,yBAAyB;IACpC,mBAAmB;;IAEnB,kBAAkB;;IAElB,oBAAoB;;IAEpB,qBAAqB;;CAEb,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,OAAO,yBAAyB,CAAC,MAAM,OAAO,yBAAyB,CAAC,CAAC;AAEtH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,UAAU,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,WAAW;IACtB,cAAc;;IAEd,gBAAgB;;IAEhB,YAAY;;IAEZ,YAAY;;IAEZ,gBAAgB;;CAER,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,WAAW,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAE5E;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9F,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"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"}
@@ -125,7 +125,9 @@ var ErrorCode = exports.ErrorCode = {
125
125
  /** Stream cancelled */
126
126
  STREAM_CANCELLED: 'STREAM_CANCELLED',
127
127
  /** Stream not bound */
128
- STREAM_NOT_BOUND: 'STREAM_NOT_BOUND'
128
+ STREAM_NOT_BOUND: 'STREAM_NOT_BOUND',
129
+ /** Target window closed */
130
+ TARGET_WINDOW_CLOSED: 'TARGET_WINDOW_CLOSED'
129
131
  };
130
132
 
131
133
  /**
@@ -33,6 +33,7 @@ declare const defaultMessages: {
33
33
  readonly ERROR: "Error";
34
34
  /** Client errors */
35
35
  readonly IFRAME_NOT_READY: "iframe.contentWindow is not available";
36
+ readonly TARGET_WINDOW_CLOSED: "Target window is closed or no longer available";
36
37
  /** ClientServer warnings */
37
38
  readonly CLIENT_SERVER_IGNORED_MESSAGE_WHEN_CLOSED: "Ignored message because client server is closed/destroyed (type: {0}, requestId: {1})";
38
39
  /** Stream related messages */
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/constants/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;GAEG;AACH,QAAA,MAAM,eAAe;IACnB,8BAA8B;;;;IAK9B,4BAA4B;;;;;IAM5B,qBAAqB;;;;IAKrB,8BAA8B;;;;;;IAO9B,oBAAoB;;IAGpB,4BAA4B;;IAI5B,8BAA8B;;;;;;;IAQ9B,8BAA8B;;;;;;;;;;;;;;;;;IAkB9B,8BAA8B;;;;;;;;;;;;;;;;;;CAkBtB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,eAAe,CAAC;AAOtD;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAIxD,CAAC;AAEH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAE/E;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAKpF;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAElE"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/constants/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;GAEG;AACH,QAAA,MAAM,eAAe;IACnB,8BAA8B;;;;IAK9B,4BAA4B;;;;;IAM5B,qBAAqB;;;;IAKrB,8BAA8B;;;;;;IAO9B,oBAAoB;;;IAIpB,4BAA4B;;IAI5B,8BAA8B;;;;;;;IAQ9B,8BAA8B;;;;;;;;;;;;;;;;;IAkB9B,8BAA8B;;;;;;;;;;;;;;;;;;CAkBtB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,eAAe,CAAC;AAOtD;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAIxD,CAAC;AAEH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAE/E;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAKpF;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAElE"}
@@ -53,6 +53,7 @@ var defaultMessages = {
53
53
  ERROR: 'Error',
54
54
  /** Client errors */
55
55
  IFRAME_NOT_READY: 'iframe.contentWindow is not available',
56
+ TARGET_WINDOW_CLOSED: 'Target window is closed or no longer available',
56
57
  /** ClientServer warnings */
57
58
  CLIENT_SERVER_IGNORED_MESSAGE_WHEN_CLOSED: 'Ignored message because client server is closed/destroyed (type: {0}, requestId: {1})',
58
59
  /** Stream related messages */
@@ -110,6 +110,11 @@ export declare class RequestIframeClientImpl implements RequestIframeClient, Str
110
110
  * Whether message handling is enabled
111
111
  */
112
112
  get isOpen(): boolean;
113
+ /**
114
+ * Check if target window is still available (not closed/removed)
115
+ * @returns true if target window is available, false otherwise
116
+ */
117
+ isAvailable(): boolean;
113
118
  /**
114
119
  * Enable message handling (register message handlers)
115
120
  */