@raydiant/playlist-window-manager 0.24.1 → 0.25.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/PresentationWindow.d.ts +7 -0
- package/build/PresentationWindow.js +179 -7
- package/build/PresentationWindow.test.js +66 -0
- package/build/types.d.ts +5 -0
- package/package.json +1 -1
- 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
|
@@ -68,6 +68,8 @@ export default class PresentationWindow {
|
|
|
68
68
|
private paymentSubscriptions;
|
|
69
69
|
private createObjectURLResolvers;
|
|
70
70
|
private createObjectURLTimeouts;
|
|
71
|
+
private interruptLocks;
|
|
72
|
+
private previouslyLocked;
|
|
71
73
|
constructor(options: PresentationWindowOptions);
|
|
72
74
|
load(): Promise<void>;
|
|
73
75
|
show(isFocused?: boolean): void;
|
|
@@ -147,4 +149,9 @@ export default class PresentationWindow {
|
|
|
147
149
|
private isValidOpenChildWindowPayload;
|
|
148
150
|
private shouldMute;
|
|
149
151
|
private createObjectURLForEmbeddedApp;
|
|
152
|
+
private createInterruptLock;
|
|
153
|
+
private renewInterruptLock;
|
|
154
|
+
private releaseInterruptLock;
|
|
155
|
+
private createLockTimeout;
|
|
156
|
+
private runInterruptCallback;
|
|
150
157
|
}
|
|
@@ -100,6 +100,22 @@ var isValidChargeRequest = function (value) {
|
|
|
100
100
|
typeof value.requestId === 'string' &&
|
|
101
101
|
typeof value.amount === 'number';
|
|
102
102
|
};
|
|
103
|
+
var isValidAcquireLockRequest = function (value) {
|
|
104
|
+
return value && typeof value === 'object' && typeof value.requestId === 'string';
|
|
105
|
+
};
|
|
106
|
+
var isValidRenewLockRequest = function (value) {
|
|
107
|
+
return value &&
|
|
108
|
+
typeof value === 'object' &&
|
|
109
|
+
typeof value.requestId === 'string' &&
|
|
110
|
+
typeof value.lockId === 'string';
|
|
111
|
+
};
|
|
112
|
+
var isValidReleaseLockRequest = function (value) {
|
|
113
|
+
return value &&
|
|
114
|
+
typeof value === 'object' &&
|
|
115
|
+
typeof value.requestId === 'string' &&
|
|
116
|
+
typeof value.lockId === 'string';
|
|
117
|
+
};
|
|
118
|
+
var lockDuration = 1000 * 60; // 1 minute
|
|
103
119
|
var PresentationWindow = /** @class */ (function () {
|
|
104
120
|
function PresentationWindow(options) {
|
|
105
121
|
var _this = this;
|
|
@@ -131,8 +147,10 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
131
147
|
this.paymentSubscriptions = new Set();
|
|
132
148
|
this.createObjectURLResolvers = {};
|
|
133
149
|
this.createObjectURLTimeouts = {};
|
|
150
|
+
this.interruptLocks = {};
|
|
151
|
+
this.previouslyLocked = false;
|
|
134
152
|
this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
|
|
135
|
-
var windowId, workerState;
|
|
153
|
+
var windowId, workerState, lock, lock, lock;
|
|
136
154
|
return __generator(this, function (_a) {
|
|
137
155
|
windowId = this.options.windowId;
|
|
138
156
|
switch (type) {
|
|
@@ -296,6 +314,48 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
296
314
|
}
|
|
297
315
|
break;
|
|
298
316
|
}
|
|
317
|
+
case 'runtime-acquire-interrupt-lock-request': {
|
|
318
|
+
if (isValidAcquireLockRequest(payload)) {
|
|
319
|
+
this.logInfo("Received 'runtime-acquire-interrupt-lock-request'");
|
|
320
|
+
lock = this.createInterruptLock();
|
|
321
|
+
this.sendMessageObject('runtime-acquire-interrupt-lock-response', {
|
|
322
|
+
requestId: payload.requestId,
|
|
323
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.logWarning("Invalid payload for runtime-acquire-interrupt-lock-request message: ".concat(payload));
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case 'runtime-renew-interrupt-lock-request': {
|
|
332
|
+
if (isValidRenewLockRequest(payload)) {
|
|
333
|
+
this.logInfo("Received 'runtime-renew-interrupt-lock-request'");
|
|
334
|
+
lock = this.renewInterruptLock(payload.lockId);
|
|
335
|
+
this.sendMessageObject('runtime-renew-interrupt-lock-response', {
|
|
336
|
+
requestId: payload.requestId,
|
|
337
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.logWarning("Invalid payload for runtime-renew-interrupt-lock-request message: ".concat(payload));
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case 'runtime-release-interrupt-lock-request': {
|
|
346
|
+
if (isValidReleaseLockRequest(payload)) {
|
|
347
|
+
this.logInfo("Received 'runtime-release-interrupt-lock-request'");
|
|
348
|
+
lock = this.releaseInterruptLock(payload.lockId);
|
|
349
|
+
this.sendMessageObject('runtime-release-interrupt-lock-response', {
|
|
350
|
+
requestId: payload.requestId,
|
|
351
|
+
lock: { id: lock.id, expiresAt: lock.expiresAt },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
this.logWarning("Invalid payload for runtime-release-interrupt-lock-request message: ".concat(payload));
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
299
359
|
default: {
|
|
300
360
|
this.logWarning("Received unknown embedded message type: ".concat(type));
|
|
301
361
|
}
|
|
@@ -557,8 +617,29 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
557
617
|
return !!this.error;
|
|
558
618
|
};
|
|
559
619
|
PresentationWindow.prototype.isLocked = function () {
|
|
560
|
-
|
|
561
|
-
|
|
620
|
+
// Legacy check for a lock on the current window or child windows. These are set by enableInterruptLock
|
|
621
|
+
// and disableInterruptLock, which are only used by RaydiantKit apps (ie. Virtual Rooms).
|
|
622
|
+
if (this.isWindowLocked) {
|
|
623
|
+
this.logDebug("Window '".concat(this.options.windowId, "' is locked due to enableInterruptLock"));
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
if (Object.values(this.isChildWindowLocked).some(Boolean)) {
|
|
627
|
+
this.logDebug("Window '".concat(this.options.windowId, "' is locked due to enableInterruptLock on child windows"));
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
// Check interrupt locks on current window or child windows, used by embedded apps (apps created through
|
|
631
|
+
// the Developer Portal).
|
|
632
|
+
var now = +new Date();
|
|
633
|
+
var isExpired = function (lock) { return lock.expiresAt < now; };
|
|
634
|
+
if (!Object.values(this.interruptLocks).every(isExpired)) {
|
|
635
|
+
this.logDebug("Window '".concat(this.options.windowId, "' is locked due to interrupt lock"));
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
if (Object.values(this.windowManagers).some(function (wm) { return wm.isLocked(); })) {
|
|
639
|
+
this.logDebug("Window '".concat(this.options.windowId, "' is locked due to interrupt lock on child windows"));
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
562
643
|
};
|
|
563
644
|
PresentationWindow.prototype.setPresentation = function (presentation, playlistWindowManagerState, ruleEngineState) {
|
|
564
645
|
this.presentation = presentation;
|
|
@@ -952,8 +1033,24 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
952
1033
|
themeOverride: themeOverride,
|
|
953
1034
|
forceMute: forceMute,
|
|
954
1035
|
isPreview: isPreview,
|
|
955
|
-
onEnableInterruptLock: function () {
|
|
956
|
-
|
|
1036
|
+
onEnableInterruptLock: function () {
|
|
1037
|
+
// enableInterruptLock is only used by legacy RaydiantKit apps (ie. Virtual Rooms), while
|
|
1038
|
+
// runInterruptCallback is used by Developer Portal apps.
|
|
1039
|
+
if (_this.isEmbeddedApp()) {
|
|
1040
|
+
_this.runInterruptCallback();
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
_this.enableInteruptLock(windowName);
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
onDisableInterruptLock: function () {
|
|
1047
|
+
if (_this.isEmbeddedApp()) {
|
|
1048
|
+
_this.runInterruptCallback();
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
_this.disableInteruptLock(windowName);
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
957
1054
|
getTerminal: this.options.getTerminal,
|
|
958
1055
|
});
|
|
959
1056
|
// Hook up child window manager events to bubble up to the parent.
|
|
@@ -1015,6 +1112,8 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
1015
1112
|
this.logWarning("Failed to close child window manager for '".concat(windowName, "'."));
|
|
1016
1113
|
}
|
|
1017
1114
|
};
|
|
1115
|
+
// Legacy enable interrupt lock for RaydiantKit apps (ie. Virtual Rooms). This has been
|
|
1116
|
+
// replaced by createInterruptLock for embedded apps.
|
|
1018
1117
|
PresentationWindow.prototype.enableInteruptLock = function (windowName) {
|
|
1019
1118
|
if (windowName) {
|
|
1020
1119
|
this.isChildWindowLocked[windowName] = true;
|
|
@@ -1022,11 +1121,12 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
1022
1121
|
else {
|
|
1023
1122
|
this.isWindowLocked = true;
|
|
1024
1123
|
}
|
|
1025
|
-
// If the presentation is ready and locked, trigger the enable interrupt callback.
|
|
1026
1124
|
if (this.isReady() && this.isLocked() && this.enableInterruptLockCallback) {
|
|
1027
1125
|
this.enableInterruptLockCallback();
|
|
1028
1126
|
}
|
|
1029
1127
|
};
|
|
1128
|
+
// Legacy disable interrupt lock for RaydiantKit apps (ie. Virtual Rooms). This has been
|
|
1129
|
+
// replaced by releaseInterruptLock for embedded apps.
|
|
1030
1130
|
PresentationWindow.prototype.disableInteruptLock = function (windowName) {
|
|
1031
1131
|
if (windowName) {
|
|
1032
1132
|
this.isChildWindowLocked[windowName] = false;
|
|
@@ -1034,7 +1134,6 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
1034
1134
|
else {
|
|
1035
1135
|
this.isWindowLocked = false;
|
|
1036
1136
|
}
|
|
1037
|
-
// If the presentation is ready and unlocked, trigger the disable interrupt callback.
|
|
1038
1137
|
if (this.isReady() &&
|
|
1039
1138
|
!this.isLocked() &&
|
|
1040
1139
|
this.disableInterruptLockCallback) {
|
|
@@ -1200,6 +1299,79 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
1200
1299
|
return '';
|
|
1201
1300
|
});
|
|
1202
1301
|
};
|
|
1302
|
+
PresentationWindow.prototype.createInterruptLock = function () {
|
|
1303
|
+
var lockId = (0, uuid_1.v4)();
|
|
1304
|
+
var lock = {
|
|
1305
|
+
id: lockId,
|
|
1306
|
+
expiresAt: +new Date() + lockDuration,
|
|
1307
|
+
timeout: this.createLockTimeout(lockId),
|
|
1308
|
+
};
|
|
1309
|
+
this.interruptLocks[lock.id] = lock;
|
|
1310
|
+
this.runInterruptCallback();
|
|
1311
|
+
return lock;
|
|
1312
|
+
};
|
|
1313
|
+
PresentationWindow.prototype.renewInterruptLock = function (id) {
|
|
1314
|
+
var lock = this.interruptLocks[id];
|
|
1315
|
+
if (lock) {
|
|
1316
|
+
// Reset expiration time.
|
|
1317
|
+
lock.expiresAt = +new Date() + lockDuration;
|
|
1318
|
+
// Reset timeout.
|
|
1319
|
+
if (lock.timeout) {
|
|
1320
|
+
clearTimeout(lock.timeout);
|
|
1321
|
+
}
|
|
1322
|
+
lock.timeout = this.createLockTimeout(id);
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
// If the lock doesn't exist it was already released, create a new lock with the same id.
|
|
1326
|
+
lock = {
|
|
1327
|
+
id: id,
|
|
1328
|
+
expiresAt: +new Date() + lockDuration,
|
|
1329
|
+
timeout: this.createLockTimeout(id),
|
|
1330
|
+
};
|
|
1331
|
+
this.interruptLocks[lock.id] = lock;
|
|
1332
|
+
}
|
|
1333
|
+
this.runInterruptCallback();
|
|
1334
|
+
return lock;
|
|
1335
|
+
};
|
|
1336
|
+
PresentationWindow.prototype.releaseInterruptLock = function (id) {
|
|
1337
|
+
var lock = this.interruptLocks[id];
|
|
1338
|
+
if (lock) {
|
|
1339
|
+
if (lock.timeout) {
|
|
1340
|
+
clearTimeout(lock.timeout);
|
|
1341
|
+
}
|
|
1342
|
+
delete this.interruptLocks[id];
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
// If the lock doesn't exist it was already released, return a fake lock object to
|
|
1346
|
+
// send a response back to the running app.
|
|
1347
|
+
lock = { id: id, expiresAt: 0, timeout: null };
|
|
1348
|
+
}
|
|
1349
|
+
this.runInterruptCallback();
|
|
1350
|
+
return lock;
|
|
1351
|
+
};
|
|
1352
|
+
PresentationWindow.prototype.createLockTimeout = function (id) {
|
|
1353
|
+
var _this = this;
|
|
1354
|
+
return setTimeout(function () {
|
|
1355
|
+
_this.logInfo("Interrupt lock expired, releasing: ".concat(id));
|
|
1356
|
+
_this.releaseInterruptLock(id);
|
|
1357
|
+
}, lockDuration);
|
|
1358
|
+
};
|
|
1359
|
+
PresentationWindow.prototype.runInterruptCallback = function () {
|
|
1360
|
+
var currentlyLocked = this.isLocked();
|
|
1361
|
+
// If locked and the lock state has changed, run the interrupt lock callback.
|
|
1362
|
+
// Or if not locked and the lock state has changed, run the disable interrupt lock callback.
|
|
1363
|
+
if (currentlyLocked &&
|
|
1364
|
+
this.previouslyLocked !== currentlyLocked &&
|
|
1365
|
+
this.enableInterruptLockCallback) {
|
|
1366
|
+
this.enableInterruptLockCallback();
|
|
1367
|
+
}
|
|
1368
|
+
else if (!currentlyLocked &&
|
|
1369
|
+
this.previouslyLocked !== currentlyLocked &&
|
|
1370
|
+
this.disableInterruptLockCallback) {
|
|
1371
|
+
this.disableInterruptLockCallback();
|
|
1372
|
+
}
|
|
1373
|
+
this.previouslyLocked = currentlyLocked;
|
|
1374
|
+
};
|
|
1203
1375
|
return PresentationWindow;
|
|
1204
1376
|
}());
|
|
1205
1377
|
exports.default = PresentationWindow;
|
|
@@ -337,4 +337,70 @@ describe('payments', function () {
|
|
|
337
337
|
});
|
|
338
338
|
}); });
|
|
339
339
|
});
|
|
340
|
+
describe('interrupt locks', function () {
|
|
341
|
+
test('acquire, renew, release lock', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
342
|
+
var testRenderer, onEnableInterruptLock, onDisableInterruptLock, window, sendMessageObjectCall, lock, renewLockSendMessageObjectCall, renewedLock, releaseLockSendMessageObjectCall, releasedLock;
|
|
343
|
+
return __generator(this, function (_a) {
|
|
344
|
+
switch (_a.label) {
|
|
345
|
+
case 0:
|
|
346
|
+
testRenderer = (0, testUtils_1.createRenderer)();
|
|
347
|
+
onEnableInterruptLock = jest.fn();
|
|
348
|
+
onDisableInterruptLock = jest.fn();
|
|
349
|
+
window = (0, testUtils_1.createPresentationWindow)({
|
|
350
|
+
windowId: 'test-interrupt-lock-window-id',
|
|
351
|
+
presentation: (0, testUtils_1.createPresentation)(),
|
|
352
|
+
applicationVersion: (0, testUtils_1.createApplicationVersion)({
|
|
353
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
354
|
+
}),
|
|
355
|
+
renderer: testRenderer,
|
|
356
|
+
});
|
|
357
|
+
window.onEnableInterruptLock(onEnableInterruptLock);
|
|
358
|
+
window.onDisableInterruptLock(onDisableInterruptLock);
|
|
359
|
+
return [4 /*yield*/, window.load()];
|
|
360
|
+
case 1:
|
|
361
|
+
_a.sent();
|
|
362
|
+
// Acquire lock.
|
|
363
|
+
testRenderer.runReceiveMessageCallback('runtime-acquire-interrupt-lock-request', { requestId: 'acquire-request' });
|
|
364
|
+
sendMessageObjectCall = testRenderer.sendMessageObject.mock.calls[0];
|
|
365
|
+
expect(sendMessageObjectCall[0]).toBe('test-interrupt-lock-window-id');
|
|
366
|
+
expect(sendMessageObjectCall[1]).toBe('runtime-acquire-interrupt-lock-response');
|
|
367
|
+
expect(sendMessageObjectCall[2].requestId).toBe('acquire-request');
|
|
368
|
+
lock = sendMessageObjectCall[2].lock;
|
|
369
|
+
expect(typeof lock.id).toBe('string');
|
|
370
|
+
expect(lock.id).toBeTruthy();
|
|
371
|
+
expect(typeof lock.expiresAt).toBe('number');
|
|
372
|
+
expect(lock.expiresAt).toBeTruthy();
|
|
373
|
+
expect(onEnableInterruptLock).toHaveBeenCalled();
|
|
374
|
+
// Wait 1ms to make sure we have a different expiresAt value.
|
|
375
|
+
return [4 /*yield*/, delay(1)];
|
|
376
|
+
case 2:
|
|
377
|
+
// Wait 1ms to make sure we have a different expiresAt value.
|
|
378
|
+
_a.sent();
|
|
379
|
+
// Renew lock.
|
|
380
|
+
testRenderer.runReceiveMessageCallback('runtime-renew-interrupt-lock-request', { requestId: 'renew-request', lockId: lock.id });
|
|
381
|
+
renewLockSendMessageObjectCall = testRenderer.sendMessageObject.mock.calls[1];
|
|
382
|
+
expect(renewLockSendMessageObjectCall[0]).toBe('test-interrupt-lock-window-id');
|
|
383
|
+
expect(renewLockSendMessageObjectCall[1]).toBe('runtime-renew-interrupt-lock-response');
|
|
384
|
+
expect(renewLockSendMessageObjectCall[2].requestId).toBe('renew-request');
|
|
385
|
+
renewedLock = renewLockSendMessageObjectCall[2].lock;
|
|
386
|
+
expect(renewedLock.id).toBe(lock.id);
|
|
387
|
+
expect(renewedLock.expiresAt).toBeGreaterThan(lock.expiresAt);
|
|
388
|
+
// Ensure onEnableInterruptLock is not called again.
|
|
389
|
+
expect(onEnableInterruptLock).toHaveBeenCalledTimes(1);
|
|
390
|
+
// Release lock.
|
|
391
|
+
testRenderer.runReceiveMessageCallback('runtime-release-interrupt-lock-request', { requestId: 'release-request', lockId: lock.id });
|
|
392
|
+
releaseLockSendMessageObjectCall = testRenderer.sendMessageObject.mock.calls[2];
|
|
393
|
+
expect(releaseLockSendMessageObjectCall[0]).toBe('test-interrupt-lock-window-id');
|
|
394
|
+
expect(releaseLockSendMessageObjectCall[1]).toBe('runtime-release-interrupt-lock-response');
|
|
395
|
+
expect(releaseLockSendMessageObjectCall[2].requestId).toBe('release-request');
|
|
396
|
+
releasedLock = releaseLockSendMessageObjectCall[2].lock;
|
|
397
|
+
expect(releasedLock.id).toBe(lock.id);
|
|
398
|
+
expect(releasedLock.expiresAt).toBe(renewedLock.expiresAt);
|
|
399
|
+
// Ensure onDisableInterruptLock is called.
|
|
400
|
+
expect(onDisableInterruptLock).toHaveBeenCalled();
|
|
401
|
+
return [2 /*return*/];
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}); });
|
|
405
|
+
});
|
|
340
406
|
// TODO: Backfill tests for all legacy and embedded apps.
|
package/build/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -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