@raydiant/playlist-window-manager 0.24.1 → 0.25.1-canary.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.
- package/build/BrowserViewRenderer.d.ts +2 -3
- package/build/BrowserViewRenderer.js +23 -48
- package/build/BrowserWindowRenderer.d.ts +2 -3
- package/build/BrowserWindowRenderer.js +42 -69
- package/build/IFrameRenderer.d.ts +2 -4
- package/build/IFrameRenderer.js +44 -63
- package/build/PlaylistWindowManager.d.ts +1 -10
- package/build/PlaylistWindowManager.js +18 -87
- package/build/PlaylistWindowManagerState.js +3 -3
- package/build/PresentationWindow.d.ts +5 -32
- package/build/PresentationWindow.js +93 -506
- package/build/WindowLoadError.js +1 -3
- package/build/WindowTimeoutError.js +1 -3
- package/build/WindowUnresponsiveError.js +1 -3
- package/build/embeddedApps.d.ts +2 -2
- package/build/embeddedApps.js +15 -36
- package/build/embeddedApps.test.js +36 -89
- package/build/index.js +6 -19
- package/build/preload.js +14 -14
- package/build/types.d.ts +2 -44
- package/package.json +1 -1
- package/src/BrowserViewRenderer.ts +6 -5
- package/src/BrowserWindowRenderer.ts +16 -14
- package/src/PresentationWindow.test.ts +95 -0
- package/src/PresentationWindow.ts +224 -8
- package/src/types.ts +6 -0
- package/tasks/npm-publish.sh +1 -1
- package/build/PresentationWindow.test.d.ts +0 -1
- package/build/PresentationWindow.test.js +0 -340
- package/build/testUtils.d.ts +0 -43
- package/build/testUtils.js +0 -209
|
@@ -330,4 +330,99 @@ describe('payments', () => {
|
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
+
describe('interrupt locks', () => {
|
|
334
|
+
test('acquire, renew, release lock', async () => {
|
|
335
|
+
const testRenderer = createRenderer();
|
|
336
|
+
|
|
337
|
+
const onEnableInterruptLock = jest.fn();
|
|
338
|
+
const onDisableInterruptLock = jest.fn();
|
|
339
|
+
|
|
340
|
+
const window = createPresentationWindow({
|
|
341
|
+
windowId: 'test-interrupt-lock-window-id',
|
|
342
|
+
presentation: createPresentation(),
|
|
343
|
+
applicationVersion: createApplicationVersion({
|
|
344
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
345
|
+
}),
|
|
346
|
+
renderer: testRenderer,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
window.onEnableInterruptLock(onEnableInterruptLock);
|
|
350
|
+
window.onDisableInterruptLock(onDisableInterruptLock);
|
|
351
|
+
|
|
352
|
+
await window.load();
|
|
353
|
+
|
|
354
|
+
// Acquire lock.
|
|
355
|
+
testRenderer.runReceiveMessageCallback(
|
|
356
|
+
'runtime-acquire-interrupt-lock-request',
|
|
357
|
+
{ requestId: 'acquire-request' }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Ensure lock response is sent.
|
|
361
|
+
const sendMessageObjectCall = testRenderer.sendMessageObject.mock.calls[0];
|
|
362
|
+
expect(sendMessageObjectCall[0]).toBe('test-interrupt-lock-window-id');
|
|
363
|
+
expect(sendMessageObjectCall[1]).toBe(
|
|
364
|
+
'runtime-acquire-interrupt-lock-response'
|
|
365
|
+
);
|
|
366
|
+
expect(sendMessageObjectCall[2].requestId).toBe('acquire-request');
|
|
367
|
+
const lock = sendMessageObjectCall[2].lock;
|
|
368
|
+
expect(typeof lock.id).toBe('string');
|
|
369
|
+
expect(lock.id).toBeTruthy();
|
|
370
|
+
expect(typeof lock.expiresAt).toBe('number');
|
|
371
|
+
expect(lock.expiresAt).toBeTruthy();
|
|
372
|
+
|
|
373
|
+
expect(onEnableInterruptLock).toHaveBeenCalled();
|
|
374
|
+
|
|
375
|
+
// Wait 1ms to make sure we have a different expiresAt value.
|
|
376
|
+
await delay(1);
|
|
377
|
+
|
|
378
|
+
// Renew lock.
|
|
379
|
+
testRenderer.runReceiveMessageCallback(
|
|
380
|
+
'runtime-renew-interrupt-lock-request',
|
|
381
|
+
{ requestId: 'renew-request', lockId: lock.id }
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Ensure renew lock response is sent.
|
|
385
|
+
const renewLockSendMessageObjectCall =
|
|
386
|
+
testRenderer.sendMessageObject.mock.calls[1];
|
|
387
|
+
expect(renewLockSendMessageObjectCall[0]).toBe(
|
|
388
|
+
'test-interrupt-lock-window-id'
|
|
389
|
+
);
|
|
390
|
+
expect(renewLockSendMessageObjectCall[1]).toBe(
|
|
391
|
+
'runtime-renew-interrupt-lock-response'
|
|
392
|
+
);
|
|
393
|
+
expect(renewLockSendMessageObjectCall[2].requestId).toBe('renew-request');
|
|
394
|
+
const renewedLock = renewLockSendMessageObjectCall[2].lock;
|
|
395
|
+
expect(renewedLock.id).toBe(lock.id);
|
|
396
|
+
expect(renewedLock.expiresAt).toBeGreaterThan(lock.expiresAt);
|
|
397
|
+
|
|
398
|
+
// Ensure onEnableInterruptLock is not called again.
|
|
399
|
+
expect(onEnableInterruptLock).toHaveBeenCalledTimes(1);
|
|
400
|
+
|
|
401
|
+
// Release lock.
|
|
402
|
+
testRenderer.runReceiveMessageCallback(
|
|
403
|
+
'runtime-release-interrupt-lock-request',
|
|
404
|
+
{ requestId: 'release-request', lockId: lock.id }
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// Ensure release lock response is sent.
|
|
408
|
+
const releaseLockSendMessageObjectCall =
|
|
409
|
+
testRenderer.sendMessageObject.mock.calls[2];
|
|
410
|
+
expect(releaseLockSendMessageObjectCall[0]).toBe(
|
|
411
|
+
'test-interrupt-lock-window-id'
|
|
412
|
+
);
|
|
413
|
+
expect(releaseLockSendMessageObjectCall[1]).toBe(
|
|
414
|
+
'runtime-release-interrupt-lock-response'
|
|
415
|
+
);
|
|
416
|
+
expect(releaseLockSendMessageObjectCall[2].requestId).toBe(
|
|
417
|
+
'release-request'
|
|
418
|
+
);
|
|
419
|
+
const releasedLock = releaseLockSendMessageObjectCall[2].lock;
|
|
420
|
+
expect(releasedLock.id).toBe(lock.id);
|
|
421
|
+
expect(releasedLock.expiresAt).toBe(renewedLock.expiresAt);
|
|
422
|
+
|
|
423
|
+
// Ensure onDisableInterruptLock is called.
|
|
424
|
+
expect(onDisableInterruptLock).toHaveBeenCalled();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
333
428
|
// TODO: Backfill tests for all legacy and embedded apps.
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ChargeRequest,
|
|
25
25
|
ChargeResponse,
|
|
26
26
|
Terminal,
|
|
27
|
+
InterruptLock,
|
|
27
28
|
} from './types';
|
|
28
29
|
|
|
29
30
|
export interface PresentationWindowOptions {
|
|
@@ -81,6 +82,29 @@ const isValidChargeRequest = (value: any): value is ChargeRequest =>
|
|
|
81
82
|
typeof value.requestId === 'string' &&
|
|
82
83
|
typeof value.amount === 'number';
|
|
83
84
|
|
|
85
|
+
const isValidAcquireLockRequest = (
|
|
86
|
+
value: any
|
|
87
|
+
): value is { requestId: string } =>
|
|
88
|
+
value && typeof value === 'object' && typeof value.requestId === 'string';
|
|
89
|
+
|
|
90
|
+
const isValidRenewLockRequest = (
|
|
91
|
+
value: any
|
|
92
|
+
): value is { requestId: string; lockId: string } =>
|
|
93
|
+
value &&
|
|
94
|
+
typeof value === 'object' &&
|
|
95
|
+
typeof value.requestId === 'string' &&
|
|
96
|
+
typeof value.lockId === 'string';
|
|
97
|
+
|
|
98
|
+
const isValidReleaseLockRequest = (
|
|
99
|
+
value: any
|
|
100
|
+
): value is { requestId: string; lockId: string } =>
|
|
101
|
+
value &&
|
|
102
|
+
typeof value === 'object' &&
|
|
103
|
+
typeof value.requestId === 'string' &&
|
|
104
|
+
typeof value.lockId === 'string';
|
|
105
|
+
|
|
106
|
+
const lockDuration = 1000 * 60; // 1 minute
|
|
107
|
+
|
|
84
108
|
export default class PresentationWindow {
|
|
85
109
|
private didReceiveReady: boolean = false;
|
|
86
110
|
private readyCallback: (() => void) | null = null;
|
|
@@ -124,6 +148,9 @@ export default class PresentationWindow {
|
|
|
124
148
|
ReturnType<typeof setTimeout>
|
|
125
149
|
> = {};
|
|
126
150
|
|
|
151
|
+
private interruptLocks: Record<string, InterruptLock> = {};
|
|
152
|
+
private previouslyLocked = false;
|
|
153
|
+
|
|
127
154
|
constructor(private options: PresentationWindowOptions) {
|
|
128
155
|
this.builderState = options.builderState;
|
|
129
156
|
this.presentation = options.presentation;
|
|
@@ -335,10 +362,39 @@ export default class PresentationWindow {
|
|
|
335
362
|
}
|
|
336
363
|
|
|
337
364
|
public isLocked() {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
365
|
+
// Legacy check for a lock on the current window or child windows. These are set by enableInterruptLock
|
|
366
|
+
// and disableInterruptLock, which are only used by RaydiantKit apps (ie. Virtual Rooms).
|
|
367
|
+
if (this.isWindowLocked) {
|
|
368
|
+
this.logDebug(
|
|
369
|
+
`Window '${this.options.windowId}' is locked due to enableInterruptLock`
|
|
370
|
+
);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
if (Object.values(this.isChildWindowLocked).some(Boolean)) {
|
|
374
|
+
this.logDebug(
|
|
375
|
+
`Window '${this.options.windowId}' is locked due to enableInterruptLock on child windows`
|
|
376
|
+
);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check interrupt locks on current window or child windows, used by embedded apps (apps created through
|
|
381
|
+
// the Developer Portal).
|
|
382
|
+
const now = +new Date();
|
|
383
|
+
const isExpired = (lock: InterruptLock) => lock.expiresAt < now;
|
|
384
|
+
if (!Object.values(this.interruptLocks).every(isExpired)) {
|
|
385
|
+
this.logDebug(
|
|
386
|
+
`Window '${this.options.windowId}' is locked due to interrupt lock`
|
|
387
|
+
);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (Object.values(this.windowManagers).some((wm) => wm.isLocked())) {
|
|
391
|
+
this.logDebug(
|
|
392
|
+
`Window '${this.options.windowId}' is locked due to interrupt lock on child windows`
|
|
393
|
+
);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return false;
|
|
342
398
|
}
|
|
343
399
|
|
|
344
400
|
public setPresentation(
|
|
@@ -783,8 +839,22 @@ export default class PresentationWindow {
|
|
|
783
839
|
themeOverride,
|
|
784
840
|
forceMute,
|
|
785
841
|
isPreview,
|
|
786
|
-
onEnableInterruptLock: () =>
|
|
787
|
-
|
|
842
|
+
onEnableInterruptLock: () => {
|
|
843
|
+
// enableInterruptLock is only used by legacy RaydiantKit apps (ie. Virtual Rooms), while
|
|
844
|
+
// runInterruptCallback is used by Developer Portal apps.
|
|
845
|
+
if (this.isEmbeddedApp()) {
|
|
846
|
+
this.runInterruptCallback();
|
|
847
|
+
} else {
|
|
848
|
+
this.enableInteruptLock(windowName);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
onDisableInterruptLock: () => {
|
|
852
|
+
if (this.isEmbeddedApp()) {
|
|
853
|
+
this.runInterruptCallback();
|
|
854
|
+
} else {
|
|
855
|
+
this.disableInteruptLock(windowName);
|
|
856
|
+
}
|
|
857
|
+
},
|
|
788
858
|
getTerminal: this.options.getTerminal,
|
|
789
859
|
});
|
|
790
860
|
|
|
@@ -847,6 +917,8 @@ export default class PresentationWindow {
|
|
|
847
917
|
}
|
|
848
918
|
}
|
|
849
919
|
|
|
920
|
+
// Legacy enable interrupt lock for RaydiantKit apps (ie. Virtual Rooms). This has been
|
|
921
|
+
// replaced by createInterruptLock for embedded apps.
|
|
850
922
|
private enableInteruptLock(windowName?: string) {
|
|
851
923
|
if (windowName) {
|
|
852
924
|
this.isChildWindowLocked[windowName] = true;
|
|
@@ -854,12 +926,13 @@ export default class PresentationWindow {
|
|
|
854
926
|
this.isWindowLocked = true;
|
|
855
927
|
}
|
|
856
928
|
|
|
857
|
-
// If the presentation is ready and locked, trigger the enable interrupt callback.
|
|
858
929
|
if (this.isReady() && this.isLocked() && this.enableInterruptLockCallback) {
|
|
859
930
|
this.enableInterruptLockCallback();
|
|
860
931
|
}
|
|
861
932
|
}
|
|
862
933
|
|
|
934
|
+
// Legacy disable interrupt lock for RaydiantKit apps (ie. Virtual Rooms). This has been
|
|
935
|
+
// replaced by releaseInterruptLock for embedded apps.
|
|
863
936
|
private disableInteruptLock(windowName?: string) {
|
|
864
937
|
if (windowName) {
|
|
865
938
|
this.isChildWindowLocked[windowName] = false;
|
|
@@ -867,7 +940,6 @@ export default class PresentationWindow {
|
|
|
867
940
|
this.isWindowLocked = false;
|
|
868
941
|
}
|
|
869
942
|
|
|
870
|
-
// If the presentation is ready and unlocked, trigger the disable interrupt callback.
|
|
871
943
|
if (
|
|
872
944
|
this.isReady() &&
|
|
873
945
|
!this.isLocked() &&
|
|
@@ -1105,6 +1177,61 @@ export default class PresentationWindow {
|
|
|
1105
1177
|
break;
|
|
1106
1178
|
}
|
|
1107
1179
|
|
|
1180
|
+
case 'runtime-acquire-interrupt-lock-request': {
|
|
1181
|
+
if (isValidAcquireLockRequest(payload)) {
|
|
1182
|
+
this.logInfo(`Received 'runtime-acquire-interrupt-lock-request'`);
|
|
1183
|
+
|
|
1184
|
+
const lock = this.createInterruptLock();
|
|
1185
|
+
|
|
1186
|
+
this.sendMessageObject('runtime-acquire-interrupt-lock-response', {
|
|
1187
|
+
requestId: payload.requestId,
|
|
1188
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
1189
|
+
});
|
|
1190
|
+
} else {
|
|
1191
|
+
this.logWarning(
|
|
1192
|
+
`Invalid payload for runtime-acquire-interrupt-lock-request message: ${payload}`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
case 'runtime-renew-interrupt-lock-request': {
|
|
1200
|
+
if (isValidRenewLockRequest(payload)) {
|
|
1201
|
+
this.logInfo(`Received 'runtime-renew-interrupt-lock-request'`);
|
|
1202
|
+
|
|
1203
|
+
const lock = this.renewInterruptLock(payload.lockId);
|
|
1204
|
+
|
|
1205
|
+
this.sendMessageObject('runtime-renew-interrupt-lock-response', {
|
|
1206
|
+
requestId: payload.requestId,
|
|
1207
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
this.logWarning(
|
|
1211
|
+
`Invalid payload for runtime-renew-interrupt-lock-request message: ${payload}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
case 'runtime-release-interrupt-lock-request': {
|
|
1218
|
+
if (isValidReleaseLockRequest(payload)) {
|
|
1219
|
+
this.logInfo(`Received 'runtime-release-interrupt-lock-request'`);
|
|
1220
|
+
|
|
1221
|
+
const lock = this.releaseInterruptLock(payload.lockId);
|
|
1222
|
+
|
|
1223
|
+
this.sendMessageObject('runtime-release-interrupt-lock-response', {
|
|
1224
|
+
requestId: payload.requestId,
|
|
1225
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
1226
|
+
});
|
|
1227
|
+
} else {
|
|
1228
|
+
this.logWarning(
|
|
1229
|
+
`Invalid payload for runtime-release-interrupt-lock-request message: ${payload}`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1108
1235
|
default: {
|
|
1109
1236
|
this.logWarning(`Received unknown embedded message type: ${type}`);
|
|
1110
1237
|
}
|
|
@@ -1407,6 +1534,95 @@ export default class PresentationWindow {
|
|
|
1407
1534
|
return '';
|
|
1408
1535
|
});
|
|
1409
1536
|
}
|
|
1537
|
+
|
|
1538
|
+
private createInterruptLock() {
|
|
1539
|
+
const lockId = uuidv4();
|
|
1540
|
+
const lock = {
|
|
1541
|
+
id: lockId,
|
|
1542
|
+
expiresAt: +new Date() + lockDuration,
|
|
1543
|
+
timeout: this.createLockTimeout(lockId),
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
this.interruptLocks[lock.id] = lock;
|
|
1547
|
+
|
|
1548
|
+
this.runInterruptCallback();
|
|
1549
|
+
|
|
1550
|
+
return lock;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private renewInterruptLock(id: string) {
|
|
1554
|
+
let lock = this.interruptLocks[id];
|
|
1555
|
+
if (lock) {
|
|
1556
|
+
// Reset expiration time.
|
|
1557
|
+
lock.expiresAt = +new Date() + lockDuration;
|
|
1558
|
+
|
|
1559
|
+
// Reset timeout.
|
|
1560
|
+
if (lock.timeout) {
|
|
1561
|
+
clearTimeout(lock.timeout);
|
|
1562
|
+
}
|
|
1563
|
+
lock.timeout = this.createLockTimeout(id);
|
|
1564
|
+
} else {
|
|
1565
|
+
// If the lock doesn't exist it was already released, create a new lock with the same id.
|
|
1566
|
+
lock = {
|
|
1567
|
+
id,
|
|
1568
|
+
expiresAt: +new Date() + lockDuration,
|
|
1569
|
+
timeout: this.createLockTimeout(id),
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
this.interruptLocks[lock.id] = lock;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
this.runInterruptCallback();
|
|
1576
|
+
|
|
1577
|
+
return lock;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
private releaseInterruptLock(id: string) {
|
|
1581
|
+
let lock = this.interruptLocks[id];
|
|
1582
|
+
if (lock) {
|
|
1583
|
+
if (lock.timeout) {
|
|
1584
|
+
clearTimeout(lock.timeout);
|
|
1585
|
+
}
|
|
1586
|
+
delete this.interruptLocks[id];
|
|
1587
|
+
} else {
|
|
1588
|
+
// If the lock doesn't exist it was already released, return a fake lock object to
|
|
1589
|
+
// send a response back to the running app.
|
|
1590
|
+
lock = { id, expiresAt: 0, timeout: null };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
this.runInterruptCallback();
|
|
1594
|
+
|
|
1595
|
+
return lock;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private createLockTimeout(id: string) {
|
|
1599
|
+
return setTimeout(() => {
|
|
1600
|
+
this.logInfo(`Interrupt lock expired, releasing: ${id}`);
|
|
1601
|
+
this.releaseInterruptLock(id);
|
|
1602
|
+
}, lockDuration);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private runInterruptCallback() {
|
|
1606
|
+
const currentlyLocked = this.isLocked();
|
|
1607
|
+
|
|
1608
|
+
// If locked and the lock state has changed, run the interrupt lock callback.
|
|
1609
|
+
// Or if not locked and the lock state has changed, run the disable interrupt lock callback.
|
|
1610
|
+
if (
|
|
1611
|
+
currentlyLocked &&
|
|
1612
|
+
this.previouslyLocked !== currentlyLocked &&
|
|
1613
|
+
this.enableInterruptLockCallback
|
|
1614
|
+
) {
|
|
1615
|
+
this.enableInterruptLockCallback();
|
|
1616
|
+
} else if (
|
|
1617
|
+
!currentlyLocked &&
|
|
1618
|
+
this.previouslyLocked !== currentlyLocked &&
|
|
1619
|
+
this.disableInterruptLockCallback
|
|
1620
|
+
) {
|
|
1621
|
+
this.disableInterruptLockCallback();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
this.previouslyLocked = currentlyLocked;
|
|
1625
|
+
}
|
|
1410
1626
|
}
|
|
1411
1627
|
|
|
1412
1628
|
const isValidCreateObjectURLResultPayload = (
|
package/src/types.ts
CHANGED
package/tasks/npm-publish.sh
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|