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.
- package/README.CN.md +7 -0
- package/README.md +7 -0
- package/library/__tests__/channel.test.ts +16 -4
- package/library/__tests__/debug.test.ts +22 -0
- package/library/__tests__/dispatcher.test.ts +8 -4
- package/library/__tests__/requestIframe.test.ts +207 -1
- package/library/__tests__/stream.test.ts +47 -2
- package/library/__tests__/utils.test.ts +41 -1
- package/library/constants/index.d.ts +2 -0
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +3 -1
- package/library/constants/messages.d.ts +1 -0
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +1 -0
- package/library/core/client.d.ts +5 -0
- package/library/core/client.d.ts.map +1 -1
- package/library/core/client.js +54 -18
- package/library/core/response.d.ts.map +1 -1
- package/library/core/response.js +51 -32
- package/library/core/server.d.ts.map +1 -1
- package/library/core/server.js +10 -0
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +7 -0
- package/library/message/channel.d.ts +2 -2
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +5 -1
- package/library/message/dispatcher.d.ts +2 -2
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +2 -2
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +81 -39
- package/library/types/index.d.ts +3 -1
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/debug.d.ts.map +1 -1
- package/library/utils/debug.js +6 -2
- package/library/utils/error.d.ts +21 -0
- package/library/utils/error.d.ts.map +1 -0
- package/library/utils/error.js +34 -0
- package/library/utils/index.d.ts +7 -0
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +42 -1
- 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) =>
|
|
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;;
|
|
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
|
|
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 */
|
package/library/core/client.d.ts
CHANGED
|
@@ -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
|
*/
|