@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.
@@ -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
- return (this.isWindowLocked ||
561
- Object.values(this.isChildWindowLocked).some(Boolean));
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 () { return _this.enableInteruptLock(windowName); },
956
- onDisableInterruptLock: function () { return _this.disableInteruptLock(windowName); },
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
@@ -166,3 +166,8 @@ export interface ChargeResponse {
166
166
  export interface Terminal {
167
167
  status: string;
168
168
  }
169
+ export interface InterruptLock {
170
+ id: string;
171
+ expiresAt: number;
172
+ timeout: ReturnType<typeof setTimeout> | null;
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raydiant/playlist-window-manager",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Window manager for playlist playback.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -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
- return (
339
- this.isWindowLocked ||
340
- Object.values(this.isChildWindowLocked).some(Boolean)
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: () => this.enableInteruptLock(windowName),
787
- onDisableInterruptLock: () => this.disableInteruptLock(windowName),
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
@@ -203,3 +203,9 @@ export interface ChargeResponse {
203
203
  export interface Terminal {
204
204
  status: string;
205
205
  }
206
+
207
+ export interface InterruptLock {
208
+ id: string;
209
+ expiresAt: number;
210
+ timeout: ReturnType<typeof setTimeout> | null;
211
+ }
@@ -10,4 +10,4 @@ mkdir build
10
10
  yarn build
11
11
 
12
12
  # Publish
13
- yarn publish --access public
13
+ yarn publish --non-interactive --access public