@raydiant/playlist-window-manager 0.16.0 → 0.18.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.
@@ -15,4 +15,5 @@ export default class IFrameRenderer implements PresentationWindowRenderer {
15
15
  destroy(): void;
16
16
  private getWindow;
17
17
  private globalMessageHandler;
18
+ private deserializeMessage;
18
19
  }
@@ -35,34 +35,28 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  }
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
+ var messagePrefix = 'raydiant:';
38
39
  var IFrameRenderer = /** @class */ (function () {
39
40
  function IFrameRenderer(container) {
40
41
  var _this = this;
41
42
  this.container = container;
42
43
  this.windows = {};
43
44
  this.globalMessageHandler = function (event) {
44
- var _a;
45
- var windowId;
46
- var type;
47
- var payload;
48
- try {
49
- (_a = JSON.parse(event.data), windowId = _a.windowId, type = _a.type, payload = _a.payload);
50
- }
51
- catch (err) {
52
- console.warn("Invalid message received from iframe '" + JSON.stringify(event.data) + "', ignoring message");
53
- return;
54
- }
55
- if (!windowId) {
56
- // ignore messages that don't have windowId
57
- // such message are comming from 3rd party IFrames like google
58
- return;
59
- }
60
- var window = _this.getWindow(windowId);
61
- if (!window) {
62
- throw new Error("Failed add receive message callback to window, window with window id: '" + windowId + "' was not found");
63
- }
64
- if (window.receiveMessageCallback) {
65
- window.receiveMessageCallback(type, payload);
45
+ // Ignore messages that don't start with the message prefix.
46
+ if (typeof event.data === 'string' &&
47
+ event.data.startsWith(messagePrefix)) {
48
+ var _a = _this.deserializeMessage(event.data), windowId = _a.windowId, type = _a.type, payload = _a.payload;
49
+ if (!windowId || !type) {
50
+ console.warn("Invalid message received from child window '" + event.data + "'");
51
+ return;
52
+ }
53
+ var window_1 = _this.getWindow(windowId);
54
+ if (!window_1) {
55
+ throw new Error("Failed add receive message callback to window, window with window id: '" + windowId + "' was not found");
56
+ }
57
+ if (window_1.receiveMessageCallback) {
58
+ window_1.receiveMessageCallback(type, payload);
59
+ }
66
60
  }
67
61
  };
68
62
  // Post message is a single channel for all window so we need to
@@ -178,7 +172,7 @@ var IFrameRenderer = /** @class */ (function () {
178
172
  if (!window.frame.contentWindow) {
179
173
  throw new Error("Failed to send message '" + message + "', contentWindow missing for '" + windowId + "'");
180
174
  }
181
- window.frame.contentWindow.postMessage(message, '*');
175
+ window.frame.contentWindow.postMessage("" + messagePrefix + message, '*');
182
176
  };
183
177
  IFrameRenderer.prototype.muteWindow = function (windowId) {
184
178
  var window = this.getWindow(windowId);
@@ -202,6 +196,21 @@ var IFrameRenderer = /** @class */ (function () {
202
196
  IFrameRenderer.prototype.getWindow = function (windowId) {
203
197
  return this.windows[windowId];
204
198
  };
199
+ IFrameRenderer.prototype.deserializeMessage = function (message) {
200
+ var _a;
201
+ var windowId = '';
202
+ var type = '';
203
+ var payload;
204
+ var json = message.substring(messagePrefix.length);
205
+ try {
206
+ // Messages are sent as json in the format of { type: string, payload: unknown }.
207
+ (_a = JSON.parse(json), windowId = _a.windowId, type = _a.type, payload = _a.payload);
208
+ }
209
+ catch (err) {
210
+ // Not a valid message, return empty type and payload.
211
+ }
212
+ return { windowId: windowId, type: type, payload: payload };
213
+ };
205
214
  return IFrameRenderer;
206
215
  }());
207
216
  exports.default = IFrameRenderer;
@@ -58,6 +58,7 @@ export default class PresentationWindow {
58
58
  private error;
59
59
  private workerState;
60
60
  private workerSubscriptions;
61
+ private runtimeSubscriptions;
61
62
  constructor(options: PresentationWindowOptions);
62
63
  load(): Promise<void>;
63
64
  show(isFocused?: boolean): void;
@@ -102,6 +103,8 @@ export default class PresentationWindow {
102
103
  private disableInteruptLock;
103
104
  private handleEmbeddedMessage;
104
105
  private handleLegacyMessage;
106
+ private handleReady;
107
+ private handleComplete;
105
108
  private handleError;
106
109
  private logDebug;
107
110
  private logInfo;
@@ -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/2.0.0/index.html';
66
+ var defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.0/index.html';
67
67
  var isValidNumber = function (value) {
68
68
  return typeof value === 'number' && !isNaN(value);
69
69
  };
@@ -91,35 +91,85 @@ var PresentationWindow = /** @class */ (function () {
91
91
  this.error = null;
92
92
  this.workerState = {};
93
93
  this.workerSubscriptions = new Set();
94
+ this.runtimeSubscriptions = new Set();
94
95
  this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
95
96
  var windowId, workerState;
96
97
  return __generator(this, function (_a) {
97
98
  windowId = this.options.windowId;
98
- if (type === 'worker-subscribe') {
99
- if (typeof payload === 'string') {
100
- this.logInfo("Received 'worker-subscribe' from window '" + windowId + "'");
101
- // The payload is the worker name.
102
- this.workerSubscriptions.add(payload);
103
- workerState = this.workerState[payload];
104
- if (workerState !== undefined) {
105
- this.sendMessage('worker-state', {
106
- name: payload,
107
- state: workerState
108
- });
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);
109
113
  }
114
+ break;
110
115
  }
111
- else {
112
- this.logWarning("Invalid payload for worker-subscribe message: " + payload);
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;
113
130
  }
114
- }
115
- else if (type === 'worker-unsubscribe') {
116
- if (typeof payload === 'string') {
117
- this.logInfo("Received 'worker-unsubscribe' from window '" + windowId + "'");
118
- // The payload is the worker name.
119
- this.workerSubscriptions.delete(payload);
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;
120
141
  }
121
- else {
122
- this.logWarning("Invalid payload for worker-unsubscribe message: " + payload);
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);
123
173
  }
124
174
  }
125
175
  return [2 /*return*/];
@@ -135,15 +185,11 @@ var PresentationWindow = /** @class */ (function () {
135
185
  }
136
186
  if (type === 'presentation_ready') {
137
187
  this.logInfo("Received 'presentation_ready' from window '" + windowId + "'");
138
- this.didReceiveReady = true;
139
- this.checkIsReadyCallback();
188
+ this.handleReady();
140
189
  }
141
190
  if (type === 'presentation_complete') {
142
191
  this.logInfo("Received 'presentation_complete' from window '" + windowId + "'");
143
- this.didReceiveComplete = true;
144
- if (this.completeCallback) {
145
- this.completeCallback();
146
- }
192
+ this.handleComplete();
147
193
  }
148
194
  if (type === 'enable_interrupt_lock') {
149
195
  this.logInfo("Received 'enable_interrupt_lock' from window '" + windowId + "'");
@@ -318,13 +364,17 @@ var PresentationWindow = /** @class */ (function () {
318
364
  this.sendMessage('play');
319
365
  // Play any child window managers.
320
366
  Object.values(this.windowManagers).forEach(function (wm) {
321
- // 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
322
368
  // the child window is paused.
323
369
  if (!wm.isPlaying() || wm.isPaused()) {
324
370
  wm.play();
325
371
  }
326
372
  });
327
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
+ }
328
378
  this.lastPlayedAt = new Date();
329
379
  };
330
380
  PresentationWindow.prototype.pause = function () {
@@ -351,6 +401,9 @@ var PresentationWindow = /** @class */ (function () {
351
401
  this.disableInterruptLockCallback = callback;
352
402
  };
353
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.
354
407
  if (this.isEmbeddedApp()) {
355
408
  return this.didEmbeddedAppLoad;
356
409
  }
@@ -774,6 +827,16 @@ var PresentationWindow = /** @class */ (function () {
774
827
  this.disableInterruptLockCallback();
775
828
  }
776
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
+ };
777
840
  PresentationWindow.prototype.logDebug = function (message) {
778
841
  this.options.logger.debug(message, this.getLogContext());
779
842
  };
package/build/preload.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  var electron_1 = require("electron");
4
4
  var internals = {
5
5
  sendMessage: function (windowId, type, payload) {
6
- electron_1.ipcRenderer.send.apply(electron_1.ipcRenderer, [windowId, type].concat(payload));
6
+ electron_1.ipcRenderer.send(windowId, type, payload);
7
7
  }
8
8
  };
9
9
  // Set __RAYDIANT_INTERNALS on window so it is accessible in to the running application.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raydiant/playlist-window-manager",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Window manager for playlist playback.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -9,6 +9,8 @@ interface IFrameRendererWindow {
9
9
  receiveMessageCallback?: (type: string, payload?: any) => void;
10
10
  }
11
11
 
12
+ const messagePrefix = 'raydiant:';
13
+
12
14
  export default class IFrameRenderer implements PresentationWindowRenderer {
13
15
  private windows: {
14
16
  [windowId: string]: IFrameRendererWindow;
@@ -153,7 +155,7 @@ export default class IFrameRenderer implements PresentationWindowRenderer {
153
155
  );
154
156
  }
155
157
 
156
- window.frame.contentWindow.postMessage(message, '*');
158
+ window.frame.contentWindow.postMessage(`${messagePrefix}${message}`, '*');
157
159
  }
158
160
 
159
161
  public muteWindow(windowId: string) {
@@ -187,36 +189,46 @@ export default class IFrameRenderer implements PresentationWindowRenderer {
187
189
  }
188
190
 
189
191
  private globalMessageHandler = (event: MessageEvent) => {
190
- let windowId: string;
191
- let type: string;
192
- let payload: any;
193
-
194
- try {
195
- ({ windowId, type, payload } = JSON.parse(event.data));
196
- } catch (err) {
197
- console.warn(
198
- `Invalid message received from iframe '${JSON.stringify(
199
- event.data
200
- )}', ignoring message`
201
- );
202
- return;
192
+ // Ignore messages that don't start with the message prefix.
193
+ if (
194
+ typeof event.data === 'string' &&
195
+ event.data.startsWith(messagePrefix)
196
+ ) {
197
+ const { windowId, type, payload } = this.deserializeMessage(event.data);
198
+
199
+ if (!windowId || !type) {
200
+ console.warn(
201
+ `Invalid message received from child window '${event.data}'`
202
+ );
203
+ return;
204
+ }
205
+
206
+ const window = this.getWindow(windowId);
207
+ if (!window) {
208
+ throw new Error(
209
+ `Failed add receive message callback to window, window with window id: '${windowId}' was not found`
210
+ );
211
+ }
212
+
213
+ if (window.receiveMessageCallback) {
214
+ window.receiveMessageCallback(type, payload);
215
+ }
203
216
  }
217
+ };
204
218
 
205
- if (!windowId) {
206
- // ignore messages that don't have windowId
207
- // such message are comming from 3rd party IFrames like google
208
- return;
209
- }
219
+ private deserializeMessage(message: string) {
220
+ let windowId = '';
221
+ let type = '';
222
+ let payload: unknown;
210
223
 
211
- const window = this.getWindow(windowId);
212
- if (!window) {
213
- throw new Error(
214
- `Failed add receive message callback to window, window with window id: '${windowId}' was not found`
215
- );
224
+ const json = message.substring(messagePrefix.length);
225
+ try {
226
+ // Messages are sent as json in the format of { type: string, payload: unknown }.
227
+ ({ windowId, type, payload } = JSON.parse(json));
228
+ } catch (err) {
229
+ // Not a valid message, return empty type and payload.
216
230
  }
217
231
 
218
- if (window.receiveMessageCallback) {
219
- window.receiveMessageCallback(type, payload);
220
- }
221
- };
232
+ return { windowId, type, payload };
233
+ }
222
234
  }
@@ -51,7 +51,7 @@ export interface PresentationWindowOptions {
51
51
  }
52
52
 
53
53
  // TODO: Automatically inject latest version when building in CI.
54
- const defaultAppRendererUrl = 'https://apps.raydiant.com/2.0.0/index.html';
54
+ const defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.0/index.html';
55
55
 
56
56
  const isValidNumber = (value: any) =>
57
57
  typeof value === 'number' && !isNaN(value);
@@ -86,6 +86,7 @@ export default class PresentationWindow {
86
86
 
87
87
  private workerState: WorkerState = {};
88
88
  private workerSubscriptions = new Set<string>();
89
+ private runtimeSubscriptions = new Set<string>();
89
90
 
90
91
  constructor(private options: PresentationWindowOptions) {
91
92
  this.builderState = options.builderState;
@@ -218,12 +219,15 @@ export default class PresentationWindow {
218
219
 
219
220
  // Play any child window managers.
220
221
  Object.values(this.windowManagers).forEach(wm => {
221
- // 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
222
223
  // the child window is paused.
223
224
  if (!wm.isPlaying() || wm.isPaused()) {
224
225
  wm.play();
225
226
  }
226
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');
227
231
  }
228
232
 
229
233
  this.lastPlayedAt = new Date();
@@ -260,6 +264,9 @@ export default class PresentationWindow {
260
264
  }
261
265
 
262
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.
263
270
  if (this.isEmbeddedApp()) {
264
271
  return this.didEmbeddedAppLoad;
265
272
  }
@@ -731,37 +738,103 @@ export default class PresentationWindow {
731
738
  private handleEmbeddedMessage = async (type: string, payload?: any) => {
732
739
  const { windowId } = this.options;
733
740
 
734
- if (type === 'worker-subscribe') {
735
- if (typeof payload === 'string') {
736
- this.logInfo(`Received 'worker-subscribe' from window '${windowId}'`);
737
-
738
- // The payload is the worker name.
739
- this.workerSubscriptions.add(payload);
740
-
741
- // Send the current state to the worker if it exists as soon
742
- // as the app subscribes.
743
- const workerState = this.workerState[payload];
744
- if (workerState !== undefined) {
745
- this.sendMessage('worker-state', {
746
- name: payload,
747
- state: workerState
748
- });
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);
749
757
  }
750
- } else {
751
- this.logWarning(
752
- `Invalid payload for worker-subscribe message: ${payload}`
753
- );
758
+ break;
754
759
  }
755
- } else if (type === 'worker-unsubscribe') {
756
- if (typeof payload === 'string') {
757
- this.logInfo(`Received 'worker-unsubscribe' from window '${windowId}'`);
758
760
 
759
- // The payload is the worker name.
760
- this.workerSubscriptions.delete(payload);
761
- } else {
762
- this.logWarning(
763
- `Invalid payload for worker-unsubscribe message: ${payload}`
764
- );
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}`);
765
838
  }
766
839
  }
767
840
  };
@@ -776,18 +849,14 @@ export default class PresentationWindow {
776
849
 
777
850
  if (type === 'presentation_ready') {
778
851
  this.logInfo(`Received 'presentation_ready' from window '${windowId}'`);
779
- this.didReceiveReady = true;
780
- this.checkIsReadyCallback();
852
+ this.handleReady();
781
853
  }
782
854
 
783
855
  if (type === 'presentation_complete') {
784
856
  this.logInfo(
785
857
  `Received 'presentation_complete' from window '${windowId}'`
786
858
  );
787
- this.didReceiveComplete = true;
788
- if (this.completeCallback) {
789
- this.completeCallback();
790
- }
859
+ this.handleComplete();
791
860
  }
792
861
 
793
862
  if (type === 'enable_interrupt_lock') {
@@ -871,6 +940,18 @@ export default class PresentationWindow {
871
940
  }
872
941
  };
873
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
+
874
955
  private handleError = (error: any) => {
875
956
  this.error = error;
876
957
 
package/src/preload.ts CHANGED
@@ -7,7 +7,7 @@ interface RaydiantInternals {
7
7
 
8
8
  const internals: RaydiantInternals = {
9
9
  sendMessage: (windowId: string, type: string, payload?: any) => {
10
- ipcRenderer.send(windowId, type, ...payload);
10
+ ipcRenderer.send(windowId, type, payload);
11
11
  }
12
12
  };
13
13