@raydiant/playlist-window-manager 0.0.0-electron-upgrade-1

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.
Files changed (55) hide show
  1. package/.eslintrc +6 -0
  2. package/README.md +16 -0
  3. package/babel.config.js +13 -0
  4. package/build/BrowserViewRenderer.d.ts +19 -0
  5. package/build/BrowserViewRenderer.js +213 -0
  6. package/build/BrowserWindowRenderer.d.ts +47 -0
  7. package/build/BrowserWindowRenderer.js +524 -0
  8. package/build/IFrameRenderer.d.ts +20 -0
  9. package/build/IFrameRenderer.js +226 -0
  10. package/build/PlaylistWindowManager.d.ts +111 -0
  11. package/build/PlaylistWindowManager.js +1056 -0
  12. package/build/PlaylistWindowManagerState.d.ts +28 -0
  13. package/build/PlaylistWindowManagerState.js +60 -0
  14. package/build/PresentationWindow.d.ts +149 -0
  15. package/build/PresentationWindow.js +1235 -0
  16. package/build/PresentationWindow.test.d.ts +1 -0
  17. package/build/PresentationWindow.test.js +340 -0
  18. package/build/WindowLoadError.d.ts +3 -0
  19. package/build/WindowLoadError.js +29 -0
  20. package/build/WindowTimeoutError.d.ts +3 -0
  21. package/build/WindowTimeoutError.js +29 -0
  22. package/build/WindowUnresponsiveError.d.ts +3 -0
  23. package/build/WindowUnresponsiveError.js +29 -0
  24. package/build/embeddedApps.d.ts +2 -0
  25. package/build/embeddedApps.js +59 -0
  26. package/build/embeddedApps.test.d.ts +1 -0
  27. package/build/embeddedApps.test.js +123 -0
  28. package/build/index.d.ts +10 -0
  29. package/build/index.js +30 -0
  30. package/build/preload.d.ts +1 -0
  31. package/build/preload.js +20 -0
  32. package/build/testUtils.d.ts +43 -0
  33. package/build/testUtils.js +209 -0
  34. package/build/types.d.ts +162 -0
  35. package/build/types.js +2 -0
  36. package/jest.config.js +6 -0
  37. package/package.json +30 -0
  38. package/src/BrowserViewRenderer.ts +180 -0
  39. package/src/BrowserWindowRenderer.ts +595 -0
  40. package/src/IFrameRenderer.ts +251 -0
  41. package/src/PlaylistWindowManager.ts +1094 -0
  42. package/src/PlaylistWindowManagerState.ts +70 -0
  43. package/src/PresentationWindow.test.ts +333 -0
  44. package/src/PresentationWindow.ts +1434 -0
  45. package/src/WindowLoadError.ts +8 -0
  46. package/src/WindowTimeoutError.ts +8 -0
  47. package/src/WindowUnresponsiveError.ts +8 -0
  48. package/src/embeddedApps.test.ts +231 -0
  49. package/src/embeddedApps.ts +74 -0
  50. package/src/index.ts +13 -0
  51. package/src/preload.ts +27 -0
  52. package/src/testUtils.ts +160 -0
  53. package/src/types.ts +199 -0
  54. package/tasks/npm-publish.sh +13 -0
  55. package/tsconfig.json +7 -0
@@ -0,0 +1,595 @@
1
+ import * as path from 'path';
2
+ import {
3
+ BrowserWindow,
4
+ BrowserView,
5
+ ipcMain,
6
+ IpcMainEvent,
7
+ Event,
8
+ BrowserWindowConstructorOptions,
9
+ BrowserViewConstructorOptions,
10
+ RenderProcessGoneDetails,
11
+ } from 'electron';
12
+ import {
13
+ PresentationWindowRenderer,
14
+ CreateWindowOptions,
15
+ WindowBounds,
16
+ Logger,
17
+ } from './types';
18
+ import WindowUnresponsiveError from './WindowUnresponsiveError';
19
+
20
+ interface ProxySettings {
21
+ proxyRules: string;
22
+ proxyBypassRules: string;
23
+ }
24
+
25
+ interface BrowserWindowRendererWindow {
26
+ parentWindowId?: string;
27
+ view: BrowserWindow | BrowserView;
28
+ bounds: WindowBounds;
29
+ receiveMessageCallback: (
30
+ event: IpcMainEvent,
31
+ type: string,
32
+ payload?: any
33
+ ) => void;
34
+ unresponsiveCallback: () => void;
35
+ renderProcessGoneCallback: (
36
+ event: Event,
37
+ details: RenderProcessGoneDetails
38
+ ) => void;
39
+ }
40
+
41
+ interface BrowserWindowRendererOptions {
42
+ bounds: WindowBounds;
43
+ frame: boolean;
44
+ hasShadow: boolean;
45
+ renderChildWindowsAsBrowserViews: boolean;
46
+ useWindowPool: boolean;
47
+ logger: Logger;
48
+ proxySettings?: ProxySettings;
49
+ windowCrashHandler?: () => void;
50
+ }
51
+
52
+ export default class BrowserWindowRenderer
53
+ implements PresentationWindowRenderer
54
+ {
55
+ private windows: {
56
+ [windowId: string]: BrowserWindowRendererWindow;
57
+ } = {};
58
+ private windowCrashHandler: (reason: string) => void = (_: string) => {};
59
+
60
+ private browserWindowPool = new WindowPool<BrowserWindow>();
61
+ private browserViewPool = new WindowPool<BrowserView>();
62
+
63
+ // Create a hidden window to preload BrowserViews.
64
+ private offscreenWindow = new BrowserWindow({
65
+ show: false,
66
+ frame: this.opts.frame,
67
+ hasShadow: this.opts.hasShadow,
68
+ webPreferences: {
69
+ preload: path.join(__dirname, 'preload.js'),
70
+ contextIsolation: false,
71
+ },
72
+ });
73
+
74
+ private proxySettings: ProxySettings | null = null;
75
+
76
+ private logger: Logger;
77
+
78
+ constructor(private opts: BrowserWindowRendererOptions) {
79
+ this.offscreenWindow.setBounds(opts.bounds);
80
+ if (opts.proxySettings) {
81
+ this.proxySettings = opts.proxySettings;
82
+ }
83
+
84
+ this.logger = opts.logger;
85
+
86
+ if (opts.windowCrashHandler) {
87
+ this.windowCrashHandler = opts.windowCrashHandler;
88
+ }
89
+ }
90
+
91
+ public async createWindow({
92
+ windowId,
93
+ parentWindowId,
94
+ bounds = this.opts.bounds,
95
+ backgroundColor,
96
+ receiveMessageCallback,
97
+ errorCallback,
98
+ }: CreateWindowOptions) {
99
+ const window: BrowserWindowRendererWindow = {
100
+ parentWindowId,
101
+ // If we are creating a child window and renderChildWindowsAsBrowserViews is true,
102
+ // render all child windows as browser views.
103
+ view:
104
+ parentWindowId && this.opts.renderChildWindowsAsBrowserViews
105
+ ? this.createOrGetPooledBrowserView(windowId, {
106
+ webPreferences: {
107
+ preload: path.join(__dirname, 'preload.js'),
108
+ contextIsolation: false,
109
+ },
110
+ })
111
+ : this.createOrGetPooledBrowserWindow(windowId, {
112
+ show: false,
113
+ frame: this.opts.frame,
114
+ // NOTE: useWindowPool: true does not work with renderChildWindowsAsBrowserViews: false
115
+ // because the parent can't be changed after creating the BrowserWindow.
116
+ parent: parentWindowId
117
+ ? this.getParentBrowserView(parentWindowId)
118
+ : undefined,
119
+ transparent: backgroundColor === 'transparent',
120
+ hasShadow: this.opts.hasShadow,
121
+ webPreferences: {
122
+ preload: path.join(__dirname, 'preload.js'),
123
+ contextIsolation: false,
124
+ },
125
+ }),
126
+ // If we are creating a child window and renderChildWindowsAsBrowserViews is false that means
127
+ // we are rendering child windows with BrowserWindow. BrowserWindows are positioned absolutely so
128
+ // to place them in the correct spot relative to their parent we need to add the main window's x / y.
129
+ // BrowserViews are relatively positioned so we don't need to do that.
130
+ bounds:
131
+ parentWindowId && !this.opts.renderChildWindowsAsBrowserViews
132
+ ? {
133
+ ...bounds,
134
+ x: bounds.x + this.opts.bounds.x,
135
+ y: bounds.y + this.opts.bounds.y,
136
+ }
137
+ : bounds,
138
+
139
+ receiveMessageCallback: (
140
+ _: IpcMainEvent,
141
+ type: string,
142
+ payload?: any
143
+ ) => {
144
+ if (receiveMessageCallback) {
145
+ receiveMessageCallback(type, payload);
146
+ }
147
+ },
148
+
149
+ unresponsiveCallback: () => {
150
+ this.logger.error(`Window received 'unresponsive' event`, { windowId });
151
+ this.windowCrashHandler('unresponsive');
152
+ errorCallback(
153
+ new WindowUnresponsiveError(`Window '${windowId}' is unresponsive`)
154
+ );
155
+ },
156
+
157
+ renderProcessGoneCallback: (
158
+ _: Event,
159
+ details: RenderProcessGoneDetails
160
+ ) => {
161
+ this.logger.error(
162
+ `Window received 'render-process-gone' event. Reason: ${details.reason}`,
163
+ { windowId }
164
+ );
165
+
166
+ // TODO: Handle different reasons for render-process-gone?
167
+ // https://www.electronjs.org/docs/api/web-contents#event-render-process-gone
168
+
169
+ this.windowCrashHandler('render-process-gone');
170
+
171
+ errorCallback(
172
+ new WindowUnresponsiveError(
173
+ `Window '${windowId}' render process gone`
174
+ )
175
+ );
176
+ },
177
+ };
178
+
179
+ // Attached crash handlers to skip presentation on error.
180
+ window.view.webContents.on(
181
+ 'render-process-gone',
182
+ window.renderProcessGoneCallback
183
+ );
184
+
185
+ window.view.webContents.on('unresponsive', window.unresponsiveCallback);
186
+
187
+ if (window.view instanceof BrowserView) {
188
+ this.moveWindowOffscreen(window);
189
+ }
190
+
191
+ this.windows[windowId] = window;
192
+ // Listen for messages from window.
193
+ ipcMain.on(windowId, window.receiveMessageCallback);
194
+
195
+ if (backgroundColor) {
196
+ // To make a transparent window you need to set it's value to #00000000
197
+ // Electron issue: https://github.com/electron/electron/issues/20357
198
+ window.view.setBackgroundColor(
199
+ backgroundColor === 'transparent' ? '#00000000' : backgroundColor
200
+ );
201
+ }
202
+
203
+ window.view.setBounds(window.bounds);
204
+
205
+ if (this.proxySettings) {
206
+ await window.view.webContents.session.setProxy(this.proxySettings);
207
+ }
208
+
209
+ if (this.opts.useWindowPool) {
210
+ this.logPoolStats();
211
+ }
212
+ }
213
+
214
+ public async loadWindow(windowId: string, url: string) {
215
+ const window = this.getWindow(windowId);
216
+ if (!window) {
217
+ throw new Error(`Failed to load window, missing window id '${windowId}'`);
218
+ }
219
+
220
+ await window.view.webContents.loadURL(url);
221
+ // window.view.webContents.openDevTools();
222
+ }
223
+
224
+ public updateWindowBounds(windowId: string, bounds: WindowBounds) {
225
+ const window = this.getWindow(windowId);
226
+ if (!window) {
227
+ throw new Error(
228
+ `Failed to update window bounds, missing window id '${windowId}'`
229
+ );
230
+ }
231
+ window.bounds = bounds;
232
+
233
+ if (window.view instanceof BrowserWindow && window.parentWindowId) {
234
+ // Child BrowserWindows are absolutely positioned so we need to add the main windows
235
+ // x/y to when updating the child window's bounds.
236
+ window.view.setBounds({
237
+ ...window.bounds,
238
+ x: window.bounds.x + this.opts.bounds.x,
239
+ y: window.bounds.y + this.opts.bounds.y,
240
+ });
241
+ } else {
242
+ window.view.setBounds(window.bounds);
243
+ }
244
+ }
245
+
246
+ public showWindow(windowId: string, isFocused: boolean) {
247
+ const window = this.getWindow(windowId);
248
+ if (!window) {
249
+ throw new Error(`Failed to show window, missing window id '${windowId}'`);
250
+ }
251
+
252
+ if (window.view instanceof BrowserWindow) {
253
+ if (isFocused) {
254
+ window.view.show();
255
+ } else {
256
+ window.view.showInactive();
257
+ }
258
+ } else if (window.view instanceof BrowserView && window.parentWindowId) {
259
+ this.addWindowToParent(window.parentWindowId, window);
260
+ if (isFocused) {
261
+ // The setTimeout is required to set focus after the BrowserView has
262
+ // been added to the parent BrowserWindow.
263
+ setTimeout(() => window.view.webContents.focus(), 0);
264
+ }
265
+ }
266
+ }
267
+
268
+ public hideWindow(windowId: string) {
269
+ const window = this.getWindow(windowId);
270
+ if (!window) {
271
+ throw new Error(`Failed to hide window, missing window id '${windowId}'`);
272
+ }
273
+
274
+ if (window.view instanceof BrowserWindow) {
275
+ window.view.hide();
276
+ } else if (window.view instanceof BrowserView) {
277
+ this.moveWindowOffscreen(window);
278
+ }
279
+ }
280
+
281
+ public async removeWindow(windowId: string) {
282
+ const window = this.getWindow(windowId);
283
+ if (!window) {
284
+ throw new Error(
285
+ `Failed to remove window, missing window id '${windowId}'`
286
+ );
287
+ }
288
+
289
+ // Remove crash handlers
290
+ window.view.webContents.removeListener(
291
+ 'render-process-gone',
292
+ window.renderProcessGoneCallback
293
+ );
294
+
295
+ window.view.webContents.removeListener(
296
+ 'unresponsive',
297
+ window.unresponsiveCallback
298
+ );
299
+
300
+ // Remove message handler
301
+ ipcMain.removeListener(windowId, window.receiveMessageCallback);
302
+
303
+ if (window.view instanceof BrowserView) {
304
+ if (window.parentWindowId) {
305
+ this.removeWindowFromParent(window.parentWindowId, window);
306
+ }
307
+ await this.destroyOrUnloadPooledBrowserView(windowId, window.view);
308
+ } else if (window.view instanceof BrowserWindow) {
309
+ await this.destroyOrUnloadPooledBrowserWindow(windowId, window.view);
310
+ }
311
+
312
+ delete this.windows[windowId];
313
+
314
+ if (this.opts.useWindowPool) {
315
+ this.logPoolStats();
316
+ }
317
+ }
318
+
319
+ public sendMessageString(windowId: string, message: string) {
320
+ const window = this.getWindow(windowId);
321
+ if (!window) {
322
+ throw new Error(
323
+ `Failed to send message to window, missing window id '${windowId}'`
324
+ );
325
+ }
326
+
327
+ window.view.webContents.send('electron-main', message);
328
+ }
329
+
330
+ public sendMessageObject(windowId: string, type: string, payload?: any) {
331
+ const window = this.getWindow(windowId);
332
+ if (!window) {
333
+ throw new Error(
334
+ `Failed to send message to window, missing window id '${windowId}'`
335
+ );
336
+ }
337
+
338
+ window.view.webContents.send('electron-main', { type, payload });
339
+ }
340
+
341
+ public muteWindow(windowId: string) {
342
+ const window = this.getWindow(windowId);
343
+ if (!window) {
344
+ throw new Error(`Failed to mute window, missing window id '${windowId}'`);
345
+ }
346
+
347
+ window.view.webContents.setAudioMuted(true);
348
+ }
349
+
350
+ public unMuteWindow(windowId: string) {
351
+ const window = this.getWindow(windowId);
352
+ if (!window) {
353
+ throw new Error(
354
+ `Failed to unmute window, missing window id '${windowId}'`
355
+ );
356
+ }
357
+
358
+ window.view.webContents.setAudioMuted(false);
359
+ }
360
+
361
+ private getWindow(windowId: string): BrowserWindowRendererWindow | null {
362
+ return this.windows[windowId];
363
+ }
364
+
365
+ private moveWindowOffscreen(window: BrowserWindowRendererWindow) {
366
+ if (!(window.view instanceof BrowserView)) {
367
+ throw new Error(
368
+ `Failed move window offscreen, window is not a BrowserView`
369
+ );
370
+ }
371
+
372
+ if (window.parentWindowId) {
373
+ this.removeWindowFromParent(window.parentWindowId, window);
374
+ }
375
+ this.offscreenWindow.addBrowserView(window.view);
376
+ }
377
+
378
+ private addWindowToParent(
379
+ parentWindowId: string,
380
+ window: BrowserWindowRendererWindow
381
+ ) {
382
+ const parentWindow = this.getWindow(parentWindowId);
383
+ if (!parentWindow) {
384
+ throw new Error(
385
+ `Failed to add window to parent, missing parent window id '${parentWindowId}'`
386
+ );
387
+ }
388
+
389
+ if (parentWindow.view instanceof BrowserWindow) {
390
+ if (window.view instanceof BrowserView) {
391
+ this.offscreenWindow.removeBrowserView(window.view);
392
+ parentWindow.view.addBrowserView(window.view);
393
+ } else {
394
+ throw new Error(
395
+ `Failed add window to parent window id '${parentWindowId}', child window is not a BrowserView`
396
+ );
397
+ }
398
+ } else {
399
+ throw new Error(
400
+ `Failed add window to parent window id '${parentWindowId}', parent window is not a BrowserWindow`
401
+ );
402
+ }
403
+ }
404
+
405
+ private removeWindowFromParent(
406
+ parentWindowId: string,
407
+ window: BrowserWindowRendererWindow
408
+ ) {
409
+ const parentWindow = this.getWindow(parentWindowId);
410
+ if (!parentWindow) {
411
+ throw new Error(
412
+ `Failed to remove window from parent, missing parent window id '${parentWindowId}'`
413
+ );
414
+ }
415
+
416
+ if (parentWindow.view instanceof BrowserWindow) {
417
+ if (window.view instanceof BrowserView) {
418
+ parentWindow.view.removeBrowserView(window.view);
419
+ } else {
420
+ throw new Error(
421
+ `Failed remove window from parent window id '${parentWindowId}', child window is not a BrowserView`
422
+ );
423
+ }
424
+ } else {
425
+ throw new Error(
426
+ `Failed remove window from parent window id '${parentWindowId}', parent window is not a BrowserWindow`
427
+ );
428
+ }
429
+ }
430
+
431
+ private getParentBrowserView(parentWindowId: string): BrowserWindow {
432
+ const parentWindow = this.getWindow(parentWindowId);
433
+ if (!parentWindow) {
434
+ throw new Error(
435
+ `Failed to get parent BrowserView, missing parent window id '${parentWindowId}'`
436
+ );
437
+ }
438
+
439
+ if (!(parentWindow.view instanceof BrowserWindow)) {
440
+ throw new Error(
441
+ `Failed to get parent BrowserView, window is not a BrowserView`
442
+ );
443
+ }
444
+
445
+ return parentWindow.view;
446
+ }
447
+
448
+ private createOrGetPooledBrowserWindow(
449
+ windowId: string,
450
+ options: BrowserWindowConstructorOptions
451
+ ): BrowserWindow {
452
+ let browserWindow: BrowserWindow | null = null;
453
+
454
+ if (this.opts.useWindowPool) {
455
+ browserWindow = this.browserWindowPool.get();
456
+ if (browserWindow) {
457
+ this.logger.info(`Found free browser window from pool`, { windowId });
458
+ } else {
459
+ this.logger.info(`No free browser windows, adding new window to pool`, {
460
+ windowId,
461
+ });
462
+ }
463
+ }
464
+
465
+ if (!browserWindow) {
466
+ browserWindow = new BrowserWindow(options);
467
+ if (this.opts.useWindowPool) {
468
+ this.browserWindowPool.add(browserWindow);
469
+ }
470
+ }
471
+
472
+ return browserWindow;
473
+ }
474
+
475
+ private async destroyOrUnloadPooledBrowserWindow(
476
+ windowId: string,
477
+ browserWindow: BrowserWindow
478
+ ) {
479
+ if (this.opts.useWindowPool) {
480
+ this.hideWindow(windowId);
481
+ await browserWindow.webContents.loadURL('about:blank');
482
+ this.browserWindowPool.release(browserWindow);
483
+ } else {
484
+ if (browserWindow.isDestroyed()) {
485
+ this.logger.warn(
486
+ `Attempted to destroy BrowserWindow but it's already destroyed`,
487
+ { windowId }
488
+ );
489
+ } else {
490
+ // https://github.com/electron/electron/issues/26929#issuecomment-763837953
491
+ setImmediate(() => {
492
+ browserWindow.destroy();
493
+ });
494
+ }
495
+ }
496
+ }
497
+
498
+ private createOrGetPooledBrowserView(
499
+ windowId: string,
500
+ options: BrowserViewConstructorOptions
501
+ ): BrowserView {
502
+ let browserView: BrowserView | null = null;
503
+
504
+ if (this.opts.useWindowPool) {
505
+ browserView = this.browserViewPool.get();
506
+ if (browserView) {
507
+ this.logger.info(`Found free browser view from pool`, { windowId });
508
+ } else {
509
+ this.logger.info(`No free browser views, adding new view to pool`, {
510
+ windowId,
511
+ });
512
+ }
513
+ }
514
+
515
+ if (!browserView) {
516
+ browserView = new BrowserView(options);
517
+ if (this.opts.useWindowPool) {
518
+ this.browserViewPool.add(browserView);
519
+ }
520
+ }
521
+
522
+ return browserView;
523
+ }
524
+
525
+ private async destroyOrUnloadPooledBrowserView(
526
+ windowId: string,
527
+ browserView: BrowserView
528
+ ) {
529
+ if (this.opts.useWindowPool) {
530
+ this.hideWindow(windowId);
531
+ await browserView.webContents.loadURL('about:blank');
532
+ this.browserViewPool.release(browserView);
533
+ } else {
534
+ if (browserView.webContents.isDestroyed()) {
535
+ this.logger.warn(
536
+ `Attempted to destroy BrowserView but it's already destroyed`,
537
+ { windowId }
538
+ );
539
+ } else {
540
+ // Call destroy on webContents as workaround for BrowserView.destroy() being deprecated.
541
+ // https://github.com/electron/electron/pull/23578
542
+ // https://github.com/electron/electron/issues/26929
543
+ setImmediate(() => {
544
+ (browserView.webContents as any).destroy();
545
+ });
546
+ }
547
+ }
548
+ }
549
+
550
+ private logPoolStats() {
551
+ const bwSize = this.browserWindowPool.size();
552
+ const bvSize = this.browserViewPool.size();
553
+ this.logger.info(
554
+ `BrowserWindow pool total=${bwSize.total} inuse=${bwSize.inuse} free=${bwSize.free}`
555
+ );
556
+ this.logger.info(
557
+ `BrowserView pool total=${bvSize.total} inuse=${bvSize.inuse} free=${bvSize.free}`
558
+ );
559
+ }
560
+ }
561
+
562
+ class WindowPool<T extends BrowserWindow | BrowserView> {
563
+ private inuse: Set<T> = new Set();
564
+ private free: Set<T> = new Set();
565
+
566
+ add(window: T) {
567
+ this.inuse.add(window);
568
+ }
569
+
570
+ get(): T | null {
571
+ const firstFreeWindow = this.free.values().next().value;
572
+ if (!firstFreeWindow) return null;
573
+
574
+ // Mark window as inuse, call poo.release(window) to mark as free to use.
575
+ this.free.delete(firstFreeWindow);
576
+ this.inuse.add(firstFreeWindow);
577
+
578
+ return firstFreeWindow;
579
+ }
580
+
581
+ release(window: T) {
582
+ this.inuse.delete(window);
583
+ this.free.add(window);
584
+ }
585
+
586
+ size() {
587
+ const inuse = this.inuse.size;
588
+ const free = this.free.size;
589
+ return {
590
+ total: inuse + free,
591
+ inuse,
592
+ free,
593
+ };
594
+ }
595
+ }