@raydiant/playlist-window-manager 0.15.2 → 0.17.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/PlaylistWindowManager.d.ts +3 -1
- package/build/PlaylistWindowManager.js +15 -1
- package/build/PresentationWindow.d.ts +12 -3
- package/build/PresentationWindow.js +146 -25
- package/build/preload.js +14 -14
- package/build/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/PlaylistWindowManager.ts +21 -2
- package/src/PresentationWindow.ts +183 -23
- package/src/preload.ts +21 -15
- package/src/types.ts +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as RE from '@raydiant/playlist-rule-engine';
|
|
2
2
|
import EventEmitter from 'eventemitter3';
|
|
3
|
-
import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger } from './types';
|
|
3
|
+
import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger, WorkerState } from './types';
|
|
4
4
|
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
5
5
|
export interface PlaylistWindowManagerOptions {
|
|
6
6
|
ruleEngine: RE.RuleEngine;
|
|
@@ -40,6 +40,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
40
40
|
private themeOverride?;
|
|
41
41
|
private forceMute;
|
|
42
42
|
private isFocused;
|
|
43
|
+
private workerState;
|
|
43
44
|
constructor(options: PlaylistWindowManagerOptions);
|
|
44
45
|
private currentPresentationId;
|
|
45
46
|
private nextPresentationId;
|
|
@@ -73,6 +74,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
73
74
|
hide(): Promise<void>;
|
|
74
75
|
isReady(): boolean;
|
|
75
76
|
isLocked(): boolean;
|
|
77
|
+
runWorkerSubscriptions(state: WorkerState): void;
|
|
76
78
|
private createWindow;
|
|
77
79
|
private loadCurrentWindow;
|
|
78
80
|
private shouldPreload;
|
|
@@ -93,6 +93,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
93
93
|
_this.lastCompleteAt = null;
|
|
94
94
|
_this.forceMute = false;
|
|
95
95
|
_this.isFocused = false;
|
|
96
|
+
_this.workerState = {};
|
|
96
97
|
_this.handleEnableInterruptLock = function () {
|
|
97
98
|
var onEnableInterruptLock = _this.options.onEnableInterruptLock;
|
|
98
99
|
if (onEnableInterruptLock) {
|
|
@@ -619,6 +620,18 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
619
620
|
}
|
|
620
621
|
return this.currentWindow.isLocked();
|
|
621
622
|
};
|
|
623
|
+
PlaylistWindowManager.prototype.runWorkerSubscriptions = function (state) {
|
|
624
|
+
// Persist worker state so new windows can be initialized with it.
|
|
625
|
+
this.workerState = state;
|
|
626
|
+
// Run worker subcriptions on the current window.
|
|
627
|
+
if (this.currentWindow) {
|
|
628
|
+
this.currentWindow.runWorkerSubscriptions(state);
|
|
629
|
+
}
|
|
630
|
+
// Run worker subscriptions on the next window.
|
|
631
|
+
if (this.nextWindow) {
|
|
632
|
+
this.nextWindow.runWorkerSubscriptions(state);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
622
635
|
PlaylistWindowManager.prototype.createWindow = function (pr) {
|
|
623
636
|
var _a = this.options, readyTimeoutDurationSeconds = _a.readyTimeoutDurationSeconds, appRendererUrl = _a.appRendererUrl, backgroundColor = _a.backgroundColor, device = _a.device, logger = _a.logger, windowName = _a.windowName, parentWindowId = _a.parentWindowId, isPreview = _a.isPreview;
|
|
624
637
|
if (!this.ruleEngine) {
|
|
@@ -660,7 +673,8 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
660
673
|
playlistWindowManagerState: this.state,
|
|
661
674
|
ruleEngineState: this.ruleEngine.state,
|
|
662
675
|
globalRule: this.ruleEngine.globalRule,
|
|
663
|
-
isPreview: isPreview
|
|
676
|
+
isPreview: isPreview,
|
|
677
|
+
workerState: this.workerState
|
|
664
678
|
});
|
|
665
679
|
};
|
|
666
680
|
PlaylistWindowManager.prototype.loadCurrentWindow = function () {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as RE from '@raydiant/playlist-rule-engine';
|
|
2
2
|
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
3
3
|
import PlaylistWindowManager from './PlaylistWindowManager';
|
|
4
|
-
import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState } from './types';
|
|
4
|
+
import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState, WorkerState } from './types';
|
|
5
5
|
export interface PresentationWindowOptions {
|
|
6
6
|
windowManager?: PlaylistWindowManager;
|
|
7
7
|
windowId: string;
|
|
@@ -27,6 +27,7 @@ export interface PresentationWindowOptions {
|
|
|
27
27
|
backgroundColor?: string;
|
|
28
28
|
isPreview?: boolean;
|
|
29
29
|
forceMute?: boolean;
|
|
30
|
+
workerState: WorkerState;
|
|
30
31
|
onPresentationProperties?: (properties: any[], string: {
|
|
31
32
|
[key: string]: string;
|
|
32
33
|
}) => void;
|
|
@@ -55,6 +56,9 @@ export default class PresentationWindow {
|
|
|
55
56
|
private theme;
|
|
56
57
|
private forceMute;
|
|
57
58
|
private error;
|
|
59
|
+
private workerState;
|
|
60
|
+
private workerSubscriptions;
|
|
61
|
+
private runtimeSubscriptions;
|
|
58
62
|
constructor(options: PresentationWindowOptions);
|
|
59
63
|
load(): Promise<void>;
|
|
60
64
|
show(isFocused?: boolean): void;
|
|
@@ -84,8 +88,9 @@ export default class PresentationWindow {
|
|
|
84
88
|
private isEmbeddedApp;
|
|
85
89
|
private allChildWindowManagersReady;
|
|
86
90
|
private checkIsReadyCallback;
|
|
91
|
+
private appendWindowIdToHash;
|
|
87
92
|
private loadEmbeddedApp;
|
|
88
|
-
private
|
|
93
|
+
private loadLegacyApp;
|
|
89
94
|
private createAndLoadWindow;
|
|
90
95
|
private loadWindow;
|
|
91
96
|
private sendMessage;
|
|
@@ -96,7 +101,10 @@ export default class PresentationWindow {
|
|
|
96
101
|
private closeChildWindow;
|
|
97
102
|
private enableInteruptLock;
|
|
98
103
|
private disableInteruptLock;
|
|
99
|
-
private
|
|
104
|
+
private handleEmbeddedMessage;
|
|
105
|
+
private handleLegacyMessage;
|
|
106
|
+
private handleReady;
|
|
107
|
+
private handleComplete;
|
|
100
108
|
private handleError;
|
|
101
109
|
private logDebug;
|
|
102
110
|
private logInfo;
|
|
@@ -117,6 +125,7 @@ export default class PresentationWindow {
|
|
|
117
125
|
playlistName: string | undefined;
|
|
118
126
|
rule: RE.RuleOperatorType | RE.RuleGroupType | RE.RuleScheduleType | null | undefined;
|
|
119
127
|
};
|
|
128
|
+
runWorkerSubscriptions(state: WorkerState): void;
|
|
120
129
|
private createRuleEngine;
|
|
121
130
|
private isValidOpenChildWindowPayload;
|
|
122
131
|
private shouldMute;
|
|
@@ -63,7 +63,7 @@ var PlaylistWindowManager_1 = __importDefault(require("./PlaylistWindowManager")
|
|
|
63
63
|
var WindowTimeoutError_1 = __importDefault(require("./WindowTimeoutError"));
|
|
64
64
|
var WindowLoadError_1 = __importDefault(require("./WindowLoadError"));
|
|
65
65
|
// TODO: Automatically inject latest version when building in CI.
|
|
66
|
-
var defaultAppRendererUrl = 'https://apps.raydiant.com/
|
|
66
|
+
var defaultAppRendererUrl = 'https://apps.raydiant.com/2.0.0/index.html';
|
|
67
67
|
var isValidNumber = function (value) {
|
|
68
68
|
return typeof value === 'number' && !isNaN(value);
|
|
69
69
|
};
|
|
@@ -89,7 +89,93 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
89
89
|
this.disableInterruptLockCallback = null;
|
|
90
90
|
this.forceMute = false;
|
|
91
91
|
this.error = null;
|
|
92
|
-
this.
|
|
92
|
+
this.workerState = {};
|
|
93
|
+
this.workerSubscriptions = new Set();
|
|
94
|
+
this.runtimeSubscriptions = new Set();
|
|
95
|
+
this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
|
|
96
|
+
var windowId, workerState;
|
|
97
|
+
return __generator(this, function (_a) {
|
|
98
|
+
windowId = this.options.windowId;
|
|
99
|
+
switch (type) {
|
|
100
|
+
case 'ready': {
|
|
101
|
+
this.logInfo("Received 'ready' from window '" + windowId + "'");
|
|
102
|
+
this.handleReady();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'complete': {
|
|
106
|
+
if (payload === undefined) {
|
|
107
|
+
this.logInfo("Received 'complete' from window '" + windowId + "'");
|
|
108
|
+
this.handleComplete();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.logInfo("Received 'complete' with error from window '" + windowId + ", error=" + payload + "'");
|
|
112
|
+
this.handleError(payload);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'runtime-subscribe': {
|
|
117
|
+
if (typeof payload === 'string') {
|
|
118
|
+
this.logInfo("Received 'runtime-subscribe' from window '" + windowId + "'");
|
|
119
|
+
// The payload is the event name (ex. 'play').
|
|
120
|
+
this.runtimeSubscriptions.add(payload);
|
|
121
|
+
// Immediately send play if the app if already playing.
|
|
122
|
+
if (this.isPlaying()) {
|
|
123
|
+
this.sendMessage('play');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.logWarning("Invalid payload for runtime-subscribe message: " + payload);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'runtime-unsubscribe': {
|
|
132
|
+
if (typeof payload === 'string') {
|
|
133
|
+
this.logInfo("Received 'runtime-unsubscribe' from window '" + windowId + "'");
|
|
134
|
+
// The payload is the event name (ex. 'play').
|
|
135
|
+
this.runtimeSubscriptions.delete(payload);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.logWarning("Invalid payload for runtime-subscribe message: " + payload);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'worker-subscribe': {
|
|
143
|
+
if (typeof payload === 'string') {
|
|
144
|
+
this.logInfo("Received 'worker-subscribe' from window '" + windowId + "'");
|
|
145
|
+
// The payload is the worker name.
|
|
146
|
+
this.workerSubscriptions.add(payload);
|
|
147
|
+
workerState = this.workerState[payload];
|
|
148
|
+
if (workerState !== undefined) {
|
|
149
|
+
this.sendMessage('worker-state', {
|
|
150
|
+
name: payload,
|
|
151
|
+
state: workerState
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.logWarning("Invalid payload for worker-subscribe message: " + payload);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'worker-unsubscribe': {
|
|
161
|
+
if (typeof payload === 'string') {
|
|
162
|
+
this.logInfo("Received 'worker-unsubscribe' from window '" + windowId + "'");
|
|
163
|
+
// The payload is the worker name.
|
|
164
|
+
this.workerSubscriptions.delete(payload);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.logWarning("Invalid payload for worker-unsubscribe message: " + payload);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
default: {
|
|
172
|
+
this.logWarning("Received unknown embedded message type: " + type);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [2 /*return*/];
|
|
176
|
+
});
|
|
177
|
+
}); };
|
|
178
|
+
this.handleLegacyMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
|
|
93
179
|
var _a, windowId, onPresentationProperties;
|
|
94
180
|
return __generator(this, function (_b) {
|
|
95
181
|
_a = this.options, windowId = _a.windowId, onPresentationProperties = _a.onPresentationProperties;
|
|
@@ -99,15 +185,11 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
99
185
|
}
|
|
100
186
|
if (type === 'presentation_ready') {
|
|
101
187
|
this.logInfo("Received 'presentation_ready' from window '" + windowId + "'");
|
|
102
|
-
this.
|
|
103
|
-
this.checkIsReadyCallback();
|
|
188
|
+
this.handleReady();
|
|
104
189
|
}
|
|
105
190
|
if (type === 'presentation_complete') {
|
|
106
191
|
this.logInfo("Received 'presentation_complete' from window '" + windowId + "'");
|
|
107
|
-
this.
|
|
108
|
-
if (this.completeCallback) {
|
|
109
|
-
this.completeCallback();
|
|
110
|
-
}
|
|
192
|
+
this.handleComplete();
|
|
111
193
|
}
|
|
112
194
|
if (type === 'enable_interrupt_lock') {
|
|
113
195
|
this.logInfo("Received 'enable_interrupt_lock' from window '" + windowId + "'");
|
|
@@ -168,12 +250,13 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
168
250
|
this.ruleEngineState = options.ruleEngineState;
|
|
169
251
|
this.theme = options.theme;
|
|
170
252
|
this.forceMute = options.forceMute || false;
|
|
253
|
+
this.workerState = options.workerState;
|
|
171
254
|
}
|
|
172
255
|
PresentationWindow.prototype.load = function () {
|
|
173
256
|
if (this.isEmbeddedApp()) {
|
|
174
257
|
return this.loadEmbeddedApp();
|
|
175
258
|
}
|
|
176
|
-
return this.
|
|
259
|
+
return this.loadLegacyApp();
|
|
177
260
|
};
|
|
178
261
|
PresentationWindow.prototype.show = function (isFocused) {
|
|
179
262
|
var _this = this;
|
|
@@ -225,6 +308,8 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
225
308
|
this.errorCallback = null;
|
|
226
309
|
this.enableInterruptLockCallback = null;
|
|
227
310
|
this.disableInterruptLockCallback = null;
|
|
311
|
+
// Clean up subscriptions
|
|
312
|
+
this.workerSubscriptions.clear();
|
|
228
313
|
// Clear timers
|
|
229
314
|
if (this.readyTimeout) {
|
|
230
315
|
clearTimeout(this.readyTimeout);
|
|
@@ -279,13 +364,17 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
279
364
|
this.sendMessage('play');
|
|
280
365
|
// Play any child window managers.
|
|
281
366
|
Object.values(this.windowManagers).forEach(function (wm) {
|
|
282
|
-
// We only want trigger play on child windows the first play of the main window or if
|
|
367
|
+
// We only want to trigger play on child windows the first play of the main window or if
|
|
283
368
|
// the child window is paused.
|
|
284
369
|
if (!wm.isPlaying() || wm.isPaused()) {
|
|
285
370
|
wm.play();
|
|
286
371
|
}
|
|
287
372
|
});
|
|
288
373
|
}
|
|
374
|
+
else if (this.runtimeSubscriptions.has('play')) {
|
|
375
|
+
// If the embedded app has subscribed to the play event, send it a play event.
|
|
376
|
+
this.sendMessage('play');
|
|
377
|
+
}
|
|
289
378
|
this.lastPlayedAt = new Date();
|
|
290
379
|
};
|
|
291
380
|
PresentationWindow.prototype.pause = function () {
|
|
@@ -312,6 +401,9 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
312
401
|
this.disableInterruptLockCallback = callback;
|
|
313
402
|
};
|
|
314
403
|
PresentationWindow.prototype.isReady = function () {
|
|
404
|
+
// TODO: To support the 'ready' event in embedded apps, they will need to tell the
|
|
405
|
+
// runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
|
|
406
|
+
// for ready" option in the Developer Portal.
|
|
315
407
|
if (this.isEmbeddedApp()) {
|
|
316
408
|
return this.didEmbeddedAppLoad;
|
|
317
409
|
}
|
|
@@ -379,7 +471,7 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
379
471
|
PresentationWindow.prototype.isEmbeddedApp = function () {
|
|
380
472
|
var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview;
|
|
381
473
|
// If the application has a source url, always use it for rendering the preview.
|
|
382
|
-
// This fixes an issue with Hoopla which is an embedded app but uses a
|
|
474
|
+
// This fixes an issue with Hoopla which is an embedded app but uses a legacy app
|
|
383
475
|
// for it's preview.
|
|
384
476
|
if (isPreview && applicationVersion.sourceUrl) {
|
|
385
477
|
return false;
|
|
@@ -408,6 +500,13 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
408
500
|
}
|
|
409
501
|
}
|
|
410
502
|
};
|
|
503
|
+
PresentationWindow.prototype.appendWindowIdToHash = function (urlStr, windowId) {
|
|
504
|
+
var url = new URL(urlStr);
|
|
505
|
+
var hashParams = new URLSearchParams(url.hash.slice(1));
|
|
506
|
+
hashParams.set('__raydiantWindowId', windowId);
|
|
507
|
+
url.hash = hashParams.toString();
|
|
508
|
+
return url.toString();
|
|
509
|
+
};
|
|
411
510
|
PresentationWindow.prototype.loadEmbeddedApp = function () {
|
|
412
511
|
return __awaiter(this, void 0, void 0, function () {
|
|
413
512
|
var _a, windowId, applicationVersion, url;
|
|
@@ -420,16 +519,11 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
420
519
|
}
|
|
421
520
|
url = embeddedApps_1.default(this.presentation, applicationVersion);
|
|
422
521
|
this.logInfo("Create window '" + windowId + "' for embedded app url '" + url + "'");
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return [4 /*yield*/, this.createAndLoadWindow(url, '#ffffff')];
|
|
522
|
+
return [4 /*yield*/, this.createAndLoadWindow(this.appendWindowIdToHash(url, windowId),
|
|
523
|
+
// Embedded apps should not have a backgroundColor because some sites expect
|
|
524
|
+
// the browser default of white will be applied.
|
|
525
|
+
'#ffffff', this.handleEmbeddedMessage)];
|
|
428
526
|
case 1:
|
|
429
|
-
// Embedded apps should not have a backgroundColor bexcause some sites expect
|
|
430
|
-
// the browser default of white will be applied. We also don't attach the message
|
|
431
|
-
// listeners for embedded apps. We could eventually allow embedded apps to send
|
|
432
|
-
// lifecycle events but we should consider namespacing our message events.
|
|
433
527
|
_b.sent();
|
|
434
528
|
this.didEmbeddedAppLoad = true;
|
|
435
529
|
this.checkIsReadyCallback();
|
|
@@ -438,17 +532,16 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
438
532
|
});
|
|
439
533
|
});
|
|
440
534
|
};
|
|
441
|
-
PresentationWindow.prototype.
|
|
535
|
+
PresentationWindow.prototype.loadLegacyApp = function () {
|
|
442
536
|
return __awaiter(this, void 0, void 0, function () {
|
|
443
|
-
var _a, windowId, _b, appRendererUrl, backgroundColor
|
|
537
|
+
var _a, windowId, _b, appRendererUrl, backgroundColor;
|
|
444
538
|
return __generator(this, function (_c) {
|
|
445
539
|
switch (_c.label) {
|
|
446
540
|
case 0:
|
|
447
541
|
_a = this.options, windowId = _a.windowId, _b = _a.appRendererUrl, appRendererUrl = _b === void 0 ? defaultAppRendererUrl : _b, backgroundColor = _a.backgroundColor;
|
|
448
|
-
url = appRendererUrl + "#windowId=" + windowId;
|
|
449
542
|
this.logInfo("Create window '" + windowId + "'");
|
|
450
|
-
return [4 /*yield*/, this.createAndLoadWindow(
|
|
451
|
-
this.
|
|
543
|
+
return [4 /*yield*/, this.createAndLoadWindow(this.appendWindowIdToHash(appRendererUrl, windowId), backgroundColor || '#000000', // Default to black bg for Raydiant apps
|
|
544
|
+
this.handleLegacyMessage)];
|
|
452
545
|
case 1:
|
|
453
546
|
_c.sent();
|
|
454
547
|
return [4 /*yield*/, this.waitForPesentationReady()];
|
|
@@ -734,6 +827,16 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
734
827
|
this.disableInterruptLockCallback();
|
|
735
828
|
}
|
|
736
829
|
};
|
|
830
|
+
PresentationWindow.prototype.handleReady = function () {
|
|
831
|
+
this.didReceiveReady = true;
|
|
832
|
+
this.checkIsReadyCallback();
|
|
833
|
+
};
|
|
834
|
+
PresentationWindow.prototype.handleComplete = function () {
|
|
835
|
+
this.didReceiveComplete = true;
|
|
836
|
+
if (this.completeCallback) {
|
|
837
|
+
this.completeCallback();
|
|
838
|
+
}
|
|
839
|
+
};
|
|
737
840
|
PresentationWindow.prototype.logDebug = function (message) {
|
|
738
841
|
this.options.logger.debug(message, this.getLogContext());
|
|
739
842
|
};
|
|
@@ -766,6 +869,24 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
766
869
|
PresentationWindow.prototype.metadata = function () {
|
|
767
870
|
return this.getLogContext();
|
|
768
871
|
};
|
|
872
|
+
PresentationWindow.prototype.runWorkerSubscriptions = function (state) {
|
|
873
|
+
// For each worker subscription, compare the previous state to the new state.
|
|
874
|
+
// If the state has changed, send the new state to the subscription.
|
|
875
|
+
for (var _i = 0, _a = Array.from(this.workerSubscriptions); _i < _a.length; _i++) {
|
|
876
|
+
var workerName = _a[_i];
|
|
877
|
+
var workerState = state[workerName];
|
|
878
|
+
var previousWorkerState = this.workerState[workerName];
|
|
879
|
+
if (workerState !== undefined &&
|
|
880
|
+
!fast_deep_equal_1.default(workerState, previousWorkerState)) {
|
|
881
|
+
this.sendMessage('worker-state', {
|
|
882
|
+
name: workerName,
|
|
883
|
+
state: workerState
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Persist the new state so we can compare if the previous values have changed..
|
|
888
|
+
this.workerState = state;
|
|
889
|
+
};
|
|
769
890
|
PresentationWindow.prototype.createRuleEngine = function (playlistId) {
|
|
770
891
|
var globalRule = this.options.globalRule;
|
|
771
892
|
return new RE.RuleEngine(playlistId, this.ruleEngineState, globalRule
|
package/build/preload.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
var electron_1 = require("electron");
|
|
4
|
-
var
|
|
5
|
-
|
|
3
|
+
var electron_1 = require("electron");
|
|
4
|
+
var internals = {
|
|
5
|
+
sendMessage: function (windowId, type, payload) {
|
|
6
|
+
electron_1.ipcRenderer.send(windowId, type, payload);
|
|
7
|
+
}
|
|
6
8
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Set __RAYDIANT_INTERNALS on window so it is accessible in to the running application.
|
|
10
|
+
window.__RAYDIANT_INTERNALS = internals;
|
|
11
|
+
// Forward messages from the main process to the app (render process) via receiveMessage.
|
|
12
|
+
// __RAYDIANT_INTERNALS.receiveMessage is required to be set by the app with the Raydiant SDK.
|
|
13
|
+
electron_1.ipcRenderer.on('electron-main', function (_, message) {
|
|
14
|
+
if (typeof internals.receiveMessage === 'function') {
|
|
15
|
+
internals.receiveMessage(message);
|
|
11
16
|
}
|
|
12
17
|
else {
|
|
13
|
-
|
|
18
|
+
console.error('Failed to handle message from main process, receiveMessage is not defined');
|
|
14
19
|
}
|
|
15
|
-
};
|
|
16
|
-
// Add sendMessage and messageHandler to window so that app renderer knows we're
|
|
17
|
-
// running in an Electron environment.
|
|
18
|
-
window.sendMessage = sendMessage;
|
|
19
|
-
window.messageHandler = messageHandler;
|
|
20
|
-
electron_1.ipcRenderer.on('electron-main', messageHandler);
|
|
20
|
+
});
|
package/build/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
Device,
|
|
7
7
|
WindowBounds,
|
|
8
8
|
Theme,
|
|
9
|
-
Logger
|
|
9
|
+
Logger,
|
|
10
|
+
WorkerState
|
|
10
11
|
} from './types';
|
|
11
12
|
import PresentationWindow from './PresentationWindow';
|
|
12
13
|
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
@@ -99,6 +100,8 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
99
100
|
private forceMute: boolean = false;
|
|
100
101
|
private isFocused: boolean = false;
|
|
101
102
|
|
|
103
|
+
private workerState: WorkerState = {};
|
|
104
|
+
|
|
102
105
|
constructor(private options: PlaylistWindowManagerOptions) {
|
|
103
106
|
super();
|
|
104
107
|
|
|
@@ -421,6 +424,21 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
421
424
|
return this.currentWindow.isLocked();
|
|
422
425
|
}
|
|
423
426
|
|
|
427
|
+
public runWorkerSubscriptions(state: WorkerState) {
|
|
428
|
+
// Persist worker state so new windows can be initialized with it.
|
|
429
|
+
this.workerState = state;
|
|
430
|
+
|
|
431
|
+
// Run worker subcriptions on the current window.
|
|
432
|
+
if (this.currentWindow) {
|
|
433
|
+
this.currentWindow.runWorkerSubscriptions(state);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Run worker subscriptions on the next window.
|
|
437
|
+
if (this.nextWindow) {
|
|
438
|
+
this.nextWindow.runWorkerSubscriptions(state);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
private createWindow(pr: RE.NextPresentationID): PresentationWindow {
|
|
425
443
|
const {
|
|
426
444
|
readyTimeoutDurationSeconds,
|
|
@@ -483,7 +501,8 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
483
501
|
playlistWindowManagerState: this.state,
|
|
484
502
|
ruleEngineState: this.ruleEngine.state,
|
|
485
503
|
globalRule: this.ruleEngine.globalRule,
|
|
486
|
-
isPreview
|
|
504
|
+
isPreview,
|
|
505
|
+
workerState: this.workerState
|
|
487
506
|
});
|
|
488
507
|
}
|
|
489
508
|
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
WindowBounds,
|
|
15
15
|
Logger,
|
|
16
16
|
OpenChildWindowOptions,
|
|
17
|
-
BuilderState
|
|
17
|
+
BuilderState,
|
|
18
|
+
WorkerState
|
|
18
19
|
} from './types';
|
|
19
20
|
|
|
20
21
|
export interface PresentationWindowOptions {
|
|
@@ -42,6 +43,7 @@ export interface PresentationWindowOptions {
|
|
|
42
43
|
backgroundColor?: string;
|
|
43
44
|
isPreview?: boolean;
|
|
44
45
|
forceMute?: boolean;
|
|
46
|
+
workerState: WorkerState;
|
|
45
47
|
onPresentationProperties?: (
|
|
46
48
|
properties: any[],
|
|
47
49
|
string: { [key: string]: string }
|
|
@@ -49,7 +51,7 @@ export interface PresentationWindowOptions {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// TODO: Automatically inject latest version when building in CI.
|
|
52
|
-
const defaultAppRendererUrl = 'https://apps.raydiant.com/
|
|
54
|
+
const defaultAppRendererUrl = 'https://apps.raydiant.com/2.0.0/index.html';
|
|
53
55
|
|
|
54
56
|
const isValidNumber = (value: any) =>
|
|
55
57
|
typeof value === 'number' && !isNaN(value);
|
|
@@ -82,6 +84,10 @@ export default class PresentationWindow {
|
|
|
82
84
|
private forceMute: boolean = false;
|
|
83
85
|
private error: Error | null = null;
|
|
84
86
|
|
|
87
|
+
private workerState: WorkerState = {};
|
|
88
|
+
private workerSubscriptions = new Set<string>();
|
|
89
|
+
private runtimeSubscriptions = new Set<string>();
|
|
90
|
+
|
|
85
91
|
constructor(private options: PresentationWindowOptions) {
|
|
86
92
|
this.builderState = options.builderState;
|
|
87
93
|
this.presentation = options.presentation;
|
|
@@ -89,13 +95,14 @@ export default class PresentationWindow {
|
|
|
89
95
|
this.ruleEngineState = options.ruleEngineState;
|
|
90
96
|
this.theme = options.theme;
|
|
91
97
|
this.forceMute = options.forceMute || false;
|
|
98
|
+
this.workerState = options.workerState;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
public load(): Promise<void> {
|
|
95
102
|
if (this.isEmbeddedApp()) {
|
|
96
103
|
return this.loadEmbeddedApp();
|
|
97
104
|
}
|
|
98
|
-
return this.
|
|
105
|
+
return this.loadLegacyApp();
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
public show(isFocused: boolean = true): void {
|
|
@@ -154,6 +161,9 @@ export default class PresentationWindow {
|
|
|
154
161
|
this.enableInterruptLockCallback = null;
|
|
155
162
|
this.disableInterruptLockCallback = null;
|
|
156
163
|
|
|
164
|
+
// Clean up subscriptions
|
|
165
|
+
this.workerSubscriptions.clear();
|
|
166
|
+
|
|
157
167
|
// Clear timers
|
|
158
168
|
if (this.readyTimeout) {
|
|
159
169
|
clearTimeout(this.readyTimeout);
|
|
@@ -209,12 +219,15 @@ export default class PresentationWindow {
|
|
|
209
219
|
|
|
210
220
|
// Play any child window managers.
|
|
211
221
|
Object.values(this.windowManagers).forEach(wm => {
|
|
212
|
-
// We only want trigger play on child windows the first play of the main window or if
|
|
222
|
+
// We only want to trigger play on child windows the first play of the main window or if
|
|
213
223
|
// the child window is paused.
|
|
214
224
|
if (!wm.isPlaying() || wm.isPaused()) {
|
|
215
225
|
wm.play();
|
|
216
226
|
}
|
|
217
227
|
});
|
|
228
|
+
} else if (this.runtimeSubscriptions.has('play')) {
|
|
229
|
+
// If the embedded app has subscribed to the play event, send it a play event.
|
|
230
|
+
this.sendMessage('play');
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
this.lastPlayedAt = new Date();
|
|
@@ -251,6 +264,9 @@ export default class PresentationWindow {
|
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
public isReady() {
|
|
267
|
+
// TODO: To support the 'ready' event in embedded apps, they will need to tell the
|
|
268
|
+
// runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
|
|
269
|
+
// for ready" option in the Developer Portal.
|
|
254
270
|
if (this.isEmbeddedApp()) {
|
|
255
271
|
return this.didEmbeddedAppLoad;
|
|
256
272
|
}
|
|
@@ -329,7 +345,7 @@ export default class PresentationWindow {
|
|
|
329
345
|
const { applicationVersion, isPreview } = this.options;
|
|
330
346
|
|
|
331
347
|
// If the application has a source url, always use it for rendering the preview.
|
|
332
|
-
// This fixes an issue with Hoopla which is an embedded app but uses a
|
|
348
|
+
// This fixes an issue with Hoopla which is an embedded app but uses a legacy app
|
|
333
349
|
// for it's preview.
|
|
334
350
|
if (isPreview && applicationVersion.sourceUrl) {
|
|
335
351
|
return false;
|
|
@@ -365,6 +381,15 @@ export default class PresentationWindow {
|
|
|
365
381
|
}
|
|
366
382
|
}
|
|
367
383
|
|
|
384
|
+
private appendWindowIdToHash(urlStr: string, windowId: string) {
|
|
385
|
+
const url = new URL(urlStr);
|
|
386
|
+
const hashParams = new URLSearchParams(url.hash.slice(1));
|
|
387
|
+
hashParams.set('__raydiantWindowId', windowId);
|
|
388
|
+
url.hash = hashParams.toString();
|
|
389
|
+
|
|
390
|
+
return url.toString();
|
|
391
|
+
}
|
|
392
|
+
|
|
368
393
|
private async loadEmbeddedApp(): Promise<void> {
|
|
369
394
|
const { windowId, applicationVersion } = this.options;
|
|
370
395
|
|
|
@@ -375,34 +400,35 @@ export default class PresentationWindow {
|
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
const url = createEmbeddedApp(this.presentation, applicationVersion);
|
|
378
|
-
|
|
379
403
|
this.logInfo(`Create window '${windowId}' for embedded app url '${url}'`);
|
|
380
404
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
405
|
+
await this.createAndLoadWindow(
|
|
406
|
+
this.appendWindowIdToHash(url, windowId),
|
|
407
|
+
// Embedded apps should not have a backgroundColor because some sites expect
|
|
408
|
+
// the browser default of white will be applied.
|
|
409
|
+
'#ffffff',
|
|
410
|
+
this.handleEmbeddedMessage
|
|
411
|
+
);
|
|
386
412
|
this.didEmbeddedAppLoad = true;
|
|
387
413
|
|
|
388
414
|
this.checkIsReadyCallback();
|
|
389
415
|
}
|
|
390
416
|
|
|
391
|
-
private async
|
|
417
|
+
private async loadLegacyApp(): Promise<void> {
|
|
392
418
|
const {
|
|
393
419
|
windowId,
|
|
394
420
|
appRendererUrl = defaultAppRendererUrl,
|
|
395
421
|
backgroundColor
|
|
396
422
|
} = this.options;
|
|
397
423
|
|
|
398
|
-
let url = `${appRendererUrl}#windowId=${windowId}`;
|
|
399
|
-
|
|
400
424
|
this.logInfo(`Create window '${windowId}'`);
|
|
425
|
+
|
|
401
426
|
await this.createAndLoadWindow(
|
|
402
|
-
|
|
427
|
+
this.appendWindowIdToHash(appRendererUrl, windowId),
|
|
403
428
|
backgroundColor || '#000000', // Default to black bg for Raydiant apps
|
|
404
|
-
this.
|
|
429
|
+
this.handleLegacyMessage
|
|
405
430
|
);
|
|
431
|
+
|
|
406
432
|
await this.waitForPesentationReady();
|
|
407
433
|
}
|
|
408
434
|
|
|
@@ -709,7 +735,111 @@ export default class PresentationWindow {
|
|
|
709
735
|
}
|
|
710
736
|
}
|
|
711
737
|
|
|
712
|
-
private
|
|
738
|
+
private handleEmbeddedMessage = async (type: string, payload?: any) => {
|
|
739
|
+
const { windowId } = this.options;
|
|
740
|
+
|
|
741
|
+
switch (type) {
|
|
742
|
+
case 'ready': {
|
|
743
|
+
this.logInfo(`Received 'ready' from window '${windowId}'`);
|
|
744
|
+
this.handleReady();
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
case 'complete': {
|
|
749
|
+
if (payload === undefined) {
|
|
750
|
+
this.logInfo(`Received 'complete' from window '${windowId}'`);
|
|
751
|
+
this.handleComplete();
|
|
752
|
+
} else {
|
|
753
|
+
this.logInfo(
|
|
754
|
+
`Received 'complete' with error from window '${windowId}, error=${payload}'`
|
|
755
|
+
);
|
|
756
|
+
this.handleError(payload);
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
case 'runtime-subscribe': {
|
|
762
|
+
if (typeof payload === 'string') {
|
|
763
|
+
this.logInfo(
|
|
764
|
+
`Received 'runtime-subscribe' from window '${windowId}'`
|
|
765
|
+
);
|
|
766
|
+
// The payload is the event name (ex. 'play').
|
|
767
|
+
this.runtimeSubscriptions.add(payload);
|
|
768
|
+
|
|
769
|
+
// Immediately send play if the app if already playing.
|
|
770
|
+
if (this.isPlaying()) {
|
|
771
|
+
this.sendMessage('play');
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
this.logWarning(
|
|
775
|
+
`Invalid payload for runtime-subscribe message: ${payload}`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
case 'runtime-unsubscribe': {
|
|
782
|
+
if (typeof payload === 'string') {
|
|
783
|
+
this.logInfo(
|
|
784
|
+
`Received 'runtime-unsubscribe' from window '${windowId}'`
|
|
785
|
+
);
|
|
786
|
+
// The payload is the event name (ex. 'play').
|
|
787
|
+
this.runtimeSubscriptions.delete(payload);
|
|
788
|
+
} else {
|
|
789
|
+
this.logWarning(
|
|
790
|
+
`Invalid payload for runtime-subscribe message: ${payload}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
case 'worker-subscribe': {
|
|
797
|
+
if (typeof payload === 'string') {
|
|
798
|
+
this.logInfo(`Received 'worker-subscribe' from window '${windowId}'`);
|
|
799
|
+
|
|
800
|
+
// The payload is the worker name.
|
|
801
|
+
this.workerSubscriptions.add(payload);
|
|
802
|
+
|
|
803
|
+
// Send the current state to the worker if it exists as soon
|
|
804
|
+
// as the app subscribes.
|
|
805
|
+
const workerState = this.workerState[payload];
|
|
806
|
+
if (workerState !== undefined) {
|
|
807
|
+
this.sendMessage('worker-state', {
|
|
808
|
+
name: payload,
|
|
809
|
+
state: workerState
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
this.logWarning(
|
|
814
|
+
`Invalid payload for worker-subscribe message: ${payload}`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
case 'worker-unsubscribe': {
|
|
821
|
+
if (typeof payload === 'string') {
|
|
822
|
+
this.logInfo(
|
|
823
|
+
`Received 'worker-unsubscribe' from window '${windowId}'`
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
// The payload is the worker name.
|
|
827
|
+
this.workerSubscriptions.delete(payload);
|
|
828
|
+
} else {
|
|
829
|
+
this.logWarning(
|
|
830
|
+
`Invalid payload for worker-unsubscribe message: ${payload}`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
default: {
|
|
837
|
+
this.logWarning(`Received unknown embedded message type: ${type}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
private handleLegacyMessage = async (type: string, payload?: any) => {
|
|
713
843
|
const { windowId, onPresentationProperties } = this.options;
|
|
714
844
|
|
|
715
845
|
if (type === 'loaded') {
|
|
@@ -719,18 +849,14 @@ export default class PresentationWindow {
|
|
|
719
849
|
|
|
720
850
|
if (type === 'presentation_ready') {
|
|
721
851
|
this.logInfo(`Received 'presentation_ready' from window '${windowId}'`);
|
|
722
|
-
this.
|
|
723
|
-
this.checkIsReadyCallback();
|
|
852
|
+
this.handleReady();
|
|
724
853
|
}
|
|
725
854
|
|
|
726
855
|
if (type === 'presentation_complete') {
|
|
727
856
|
this.logInfo(
|
|
728
857
|
`Received 'presentation_complete' from window '${windowId}'`
|
|
729
858
|
);
|
|
730
|
-
this.
|
|
731
|
-
if (this.completeCallback) {
|
|
732
|
-
this.completeCallback();
|
|
733
|
-
}
|
|
859
|
+
this.handleComplete();
|
|
734
860
|
}
|
|
735
861
|
|
|
736
862
|
if (type === 'enable_interrupt_lock') {
|
|
@@ -814,6 +940,18 @@ export default class PresentationWindow {
|
|
|
814
940
|
}
|
|
815
941
|
};
|
|
816
942
|
|
|
943
|
+
private handleReady() {
|
|
944
|
+
this.didReceiveReady = true;
|
|
945
|
+
this.checkIsReadyCallback();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private handleComplete() {
|
|
949
|
+
this.didReceiveComplete = true;
|
|
950
|
+
if (this.completeCallback) {
|
|
951
|
+
this.completeCallback();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
817
955
|
private handleError = (error: any) => {
|
|
818
956
|
this.error = error;
|
|
819
957
|
|
|
@@ -874,6 +1012,28 @@ export default class PresentationWindow {
|
|
|
874
1012
|
return this.getLogContext();
|
|
875
1013
|
}
|
|
876
1014
|
|
|
1015
|
+
public runWorkerSubscriptions(state: WorkerState) {
|
|
1016
|
+
// For each worker subscription, compare the previous state to the new state.
|
|
1017
|
+
// If the state has changed, send the new state to the subscription.
|
|
1018
|
+
for (const workerName of Array.from(this.workerSubscriptions)) {
|
|
1019
|
+
const workerState = state[workerName];
|
|
1020
|
+
const previousWorkerState = this.workerState[workerName];
|
|
1021
|
+
|
|
1022
|
+
if (
|
|
1023
|
+
workerState !== undefined &&
|
|
1024
|
+
!deepEqual(workerState, previousWorkerState)
|
|
1025
|
+
) {
|
|
1026
|
+
this.sendMessage('worker-state', {
|
|
1027
|
+
name: workerName,
|
|
1028
|
+
state: workerState
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Persist the new state so we can compare if the previous values have changed..
|
|
1034
|
+
this.workerState = state;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
877
1037
|
private createRuleEngine(playlistId: string) {
|
|
878
1038
|
const { globalRule } = this.options;
|
|
879
1039
|
return new RE.RuleEngine(
|
package/src/preload.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
|
1
|
+
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
interface RaydiantInternals {
|
|
4
|
+
sendMessage: (windowId: string, type: string, payload?: any) => void;
|
|
5
|
+
receiveMessage?: (message: string) => void;
|
|
6
|
+
}
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
receiveMessage(message);
|
|
11
|
-
} else {
|
|
12
|
-
throw new Error('receiveMessage is not defined');
|
|
8
|
+
const internals: RaydiantInternals = {
|
|
9
|
+
sendMessage: (windowId: string, type: string, payload?: any) => {
|
|
10
|
+
ipcRenderer.send(windowId, type, payload);
|
|
13
11
|
}
|
|
14
12
|
};
|
|
15
13
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
(window as any).sendMessage = sendMessage;
|
|
19
|
-
(window as any).messageHandler = messageHandler;
|
|
14
|
+
// Set __RAYDIANT_INTERNALS on window so it is accessible in to the running application.
|
|
15
|
+
(window as any).__RAYDIANT_INTERNALS = internals;
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
// Forward messages from the main process to the app (render process) via receiveMessage.
|
|
18
|
+
// __RAYDIANT_INTERNALS.receiveMessage is required to be set by the app with the Raydiant SDK.
|
|
19
|
+
ipcRenderer.on('electron-main', (_: IpcRendererEvent, message: string) => {
|
|
20
|
+
if (typeof internals.receiveMessage === 'function') {
|
|
21
|
+
internals.receiveMessage(message);
|
|
22
|
+
} else {
|
|
23
|
+
console.error(
|
|
24
|
+
'Failed to handle message from main process, receiveMessage is not defined'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
});
|