promise-worker-bi 3.0.2 → 4.0.3

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.
@@ -0,0 +1,434 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ let messageIDs = 0;
6
+ const MSGTYPE_QUERY = 0;
7
+ const MSGTYPE_RESPONSE = 1;
8
+ const MSGTYPE_HOST_ID = 2;
9
+ const MSGTYPE_HOST_CLOSE = 3;
10
+ const MSGTYPE_WORKER_ERROR = 4;
11
+ const MSGTYPES = [MSGTYPE_QUERY, MSGTYPE_RESPONSE, MSGTYPE_HOST_ID, MSGTYPE_HOST_CLOSE, MSGTYPE_WORKER_ERROR]; // Inlined from https://github.com/then/is-promise
12
+
13
+ const isPromise = obj => !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function";
14
+
15
+ const toFakeError = error => {
16
+ const fakeError = {
17
+ name: error.name,
18
+ message: error.message
19
+ };
20
+
21
+ if (typeof error.stack === "string") {
22
+ fakeError.stack = error.stack;
23
+ } // These are non-standard properties, I think only in some versions of Firefox
24
+ // @ts-ignore
25
+
26
+
27
+ if (typeof error.fileName === "string") {
28
+ // @ts-ignore
29
+ fakeError.fileName = error.fileName;
30
+ } // @ts-ignore
31
+
32
+
33
+ if (typeof error.columnNumber === "number") {
34
+ // @ts-ignore
35
+ fakeError.columnNumber = error.columnNumber;
36
+ } // @ts-ignore
37
+
38
+
39
+ if (typeof error.lineNumber === "number") {
40
+ // @ts-ignore
41
+ fakeError.lineNumber = error.lineNumber;
42
+ }
43
+
44
+ return fakeError;
45
+ }; // Object rather than FakeError for convenience
46
+
47
+
48
+ const fromFakeError = fakeError => {
49
+ const error = new Error();
50
+ return Object.assign(error, fakeError);
51
+ };
52
+
53
+ const logError = err => {
54
+ // Logging in the console makes debugging in the worker easier
55
+
56
+ /* eslint-disable no-console */
57
+ console.error("Error in Worker:");
58
+ console.error(err); // Safari needs it on new line
59
+
60
+ /* eslint-enable no-console */
61
+ };
62
+
63
+ class PWBBase {
64
+ constructor() {
65
+ // console.log('constructor', worker);
66
+ this._callbacks = new Map();
67
+
68
+ this._queryCallback = () => {}; // @ts-ignore
69
+
70
+
71
+ this._onMessage = this._onMessage.bind(this);
72
+ }
73
+
74
+ register(cb) {
75
+ // console.log('register', cb);
76
+ this._queryCallback = cb;
77
+ } // eslint-disable-next-line
78
+
79
+
80
+ _postMessage(obj, targetHostID) {
81
+ throw new Error("Not implemented");
82
+ }
83
+
84
+ _postResponse(messageID, error, result, hostID) {
85
+ // console.log('_postResponse', messageID, error, result);
86
+ if (error) {
87
+ logError(error);
88
+
89
+ this._postMessage([MSGTYPE_RESPONSE, messageID, toFakeError(error)], hostID);
90
+ } else {
91
+ this._postMessage([MSGTYPE_RESPONSE, messageID, null, result], hostID);
92
+ }
93
+ }
94
+
95
+ _handleQuery(messageID, query, hostID) {
96
+ // console.log('_handleQuery', messageID, query);
97
+ try {
98
+ const result = this._queryCallback(query, hostID);
99
+
100
+ if (!isPromise(result)) {
101
+ this._postResponse(messageID, null, result, hostID);
102
+ } else {
103
+ result.then(finalResult => {
104
+ this._postResponse(messageID, null, finalResult, hostID);
105
+ }, finalError => {
106
+ this._postResponse(messageID, finalError, hostID);
107
+ });
108
+ }
109
+ } catch (err) {
110
+ this._postResponse(messageID, err);
111
+ }
112
+ } // Either return messageID and type if further processing is needed, or undefined otherwise
113
+
114
+
115
+ _onMessageCommon(e) {
116
+ // eslint-disable-line no-undef
117
+ // console.log('_onMessage', e.data);
118
+ const message = e.data;
119
+
120
+ if (!Array.isArray(message) || message.length < 3 || message.length > 4) {
121
+ return; // Ignore - this message is not for us
122
+ }
123
+
124
+ if (typeof message[0] !== "number" || MSGTYPES.indexOf(message[0]) < 0) {
125
+ throw new Error("Invalid messageID");
126
+ }
127
+
128
+ const type = message[0];
129
+
130
+ if (typeof message[1] !== "number") {
131
+ throw new Error("Invalid messageID");
132
+ }
133
+
134
+ const messageID = message[1];
135
+
136
+ if (type === MSGTYPE_QUERY) {
137
+ const query = message[2];
138
+
139
+ if (typeof message[3] !== "number" && message[3] !== undefined) {
140
+ throw new Error("Invalid hostID");
141
+ }
142
+
143
+ const hostID = message[3];
144
+
145
+ this._handleQuery(messageID, query, hostID);
146
+
147
+ return;
148
+ }
149
+
150
+ if (type === MSGTYPE_RESPONSE) {
151
+ if (message[2] !== null && typeof message[2] !== "object") {
152
+ throw new Error("Invalid error");
153
+ }
154
+
155
+ const error = message[2] === null ? null : fromFakeError(message[2]);
156
+ const result = message[3];
157
+
158
+ const callback = this._callbacks.get(messageID);
159
+
160
+ if (callback === undefined) {
161
+ // Ignore - user might have created multiple PromiseWorkers.
162
+ // This message is not for us.
163
+ return;
164
+ }
165
+
166
+ this._callbacks.delete(messageID);
167
+
168
+ callback(error, result);
169
+ return;
170
+ }
171
+
172
+ return {
173
+ message,
174
+ type
175
+ };
176
+ }
177
+
178
+ }
179
+
180
+ class PWBHost extends PWBBase {
181
+ // Only defined on host
182
+ constructor(worker) {
183
+ super(); // The following if statement used to check `worker instanceof Worker` but I have recieved
184
+ // reports that in some weird cases, Safari will inappropriately return false for that, even
185
+ // in obvious cases like:
186
+ //
187
+ // blob = new Blob(["self.onmessage = function() {};"], { type: "text/javascript" });
188
+ // worker = new Worker(window.URL.createObjectURL(blob));
189
+ // console.log(worker instanceof Worker);
190
+ //
191
+ // So instead, let's do this test for worker.port which only exists on shared workers.
192
+ // @ts-ignore
193
+
194
+ if (worker.port === undefined) {
195
+ this._workerType = "Worker"; // @ts-ignore
196
+
197
+ worker.addEventListener("message", this._onMessage);
198
+ } else {
199
+ this._workerType = "SharedWorker"; // @ts-ignore - it doesn't know if _worker is Worker or SharedWorker, but I do
200
+
201
+ worker.port.addEventListener("message", this._onMessage); // @ts-ignore - it doesn't know if _worker is Worker or SharedWorker, but I do
202
+
203
+ worker.port.start(); // Handle tab close. This isn't perfect, but there is no perfect method
204
+ // http://stackoverflow.com/q/13662089/786644 and this should work like
205
+ // 99% of the time. It is a memory leak if it fails, but for most use
206
+ // cases, it shouldn't be noticeable.
207
+
208
+ window.addEventListener("beforeunload", () => {
209
+ // Prevent firing if we don't know hostID yet
210
+ if (this._hostID !== undefined) {
211
+ this._postMessage([MSGTYPE_HOST_CLOSE, -1, this._hostID]);
212
+ }
213
+ });
214
+ }
215
+
216
+ this._worker = worker;
217
+ this._hostIDQueue = [];
218
+ }
219
+
220
+ registerError(cb) {
221
+ // console.log('registerError', cb);
222
+ this._errorCallback = cb; // Some browsers (Firefox) call onerror on every host, while others
223
+ // (Chrome) do nothing. Let's disable that everywhere, for consistency.
224
+
225
+ this._worker.addEventListener("error", e => {
226
+ e.preventDefault();
227
+ e.stopPropagation();
228
+ });
229
+ }
230
+
231
+ _postMessage(obj) {
232
+ // console.log('_postMessage', obj);
233
+ if (this._workerType === "Worker") {
234
+ // @ts-ignore - it doesn't know if _worker is Worker or SharedWorker, but I do
235
+ this._worker.postMessage(obj);
236
+ } else if (this._workerType === "SharedWorker") {
237
+ // @ts-ignore - it doesn't know if _worker is Worker or SharedWorker, but I do
238
+ this._worker.port.postMessage(obj);
239
+ } else {
240
+ throw new Error("WTF");
241
+ }
242
+ }
243
+
244
+ postMessage(userMessage) {
245
+ // console.log('postMessage', userMessage, targetHostID);
246
+ const actuallyPostMessage = (resolve, reject) => {
247
+ const messageID = messageIDs;
248
+ messageIDs += 1;
249
+ const messageToSend = [MSGTYPE_QUERY, messageID, userMessage, this._hostID];
250
+
251
+ this._callbacks.set(messageID, (error, result) => {
252
+ if (error) {
253
+ reject(error);
254
+ } else {
255
+ resolve(result);
256
+ }
257
+ });
258
+
259
+ this._postMessage(messageToSend);
260
+ };
261
+
262
+ return new Promise((resolve, reject) => {
263
+ // Don't send a message until hostID is known, otherwise it's a race
264
+ // condition and sometimes hostID will be undefined.
265
+ if (this._hostIDQueue !== undefined) {
266
+ this._hostIDQueue.push(() => {
267
+ actuallyPostMessage(resolve, reject);
268
+ });
269
+ } else {
270
+ actuallyPostMessage(resolve, reject);
271
+ }
272
+ });
273
+ }
274
+
275
+ _onMessage(e) {
276
+ const common = this._onMessageCommon(e);
277
+
278
+ if (!common) {
279
+ return;
280
+ }
281
+
282
+ const message = common.message,
283
+ type = common.type;
284
+
285
+ if (type === MSGTYPE_HOST_ID) {
286
+ if (message[2] !== undefined && typeof message[2] !== "number") {
287
+ throw new Error("Invalid hostID");
288
+ }
289
+
290
+ const hostID = message[2];
291
+ this._hostID = hostID;
292
+
293
+ if (this._hostIDQueue !== undefined) {
294
+ this._hostIDQueue.forEach(func => {
295
+ // Not entirely sure why setTimeout is needed, might be just for unit tests
296
+ setTimeout(() => {
297
+ func();
298
+ }, 0);
299
+ });
300
+
301
+ this._hostIDQueue = undefined; // Never needed again after initial setup
302
+ }
303
+ } else if (type === MSGTYPE_WORKER_ERROR) {
304
+ if (message[2] !== undefined && message[2] !== null && typeof message[2] === "object") {
305
+ const error = fromFakeError(message[2]);
306
+
307
+ if (this._errorCallback !== undefined) {
308
+ this._errorCallback(error);
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ }
315
+
316
+ class PWBWorker extends PWBBase {
317
+ constructor() {
318
+ super(); // Only actually used for SharedWorker
319
+
320
+ this._hosts = new Map();
321
+ this._maxHostID = -1;
322
+
323
+ if ( // @ts-ignore
324
+ typeof SharedWorkerGlobalScope !== "undefined" && // @ts-ignore
325
+ self instanceof SharedWorkerGlobalScope) {
326
+ this._workerType = "SharedWorker";
327
+ self.addEventListener("connect", e => {
328
+ // @ts-ignore
329
+ const port = e.ports[0];
330
+ port.addEventListener("message", e2 => this._onMessage(e2)); // eslint-disable-line no-undef
331
+
332
+ port.start();
333
+ this._maxHostID += 1;
334
+ const hostID = this._maxHostID;
335
+
336
+ this._hosts.set(hostID, {
337
+ port
338
+ }); // Send back hostID to this host, otherwise it has no way to know it
339
+
340
+
341
+ this._postMessage([MSGTYPE_HOST_ID, -1, hostID], hostID);
342
+ });
343
+ self.addEventListener("error", e => {
344
+ logError(e.error); // Just send to first host, so as to not duplicate error tracking
345
+
346
+ const hostID = this._hosts.keys().next().value;
347
+
348
+ if (hostID !== undefined) {
349
+ this._postMessage([MSGTYPE_WORKER_ERROR, -1, toFakeError(e.error)], hostID);
350
+ }
351
+ });
352
+ } else {
353
+ this._workerType = "Worker";
354
+ self.addEventListener("message", this._onMessage); // Since this is not a Shared Worker, hostID is always 0 so it's not strictly required to
355
+ // send this back, but it makes the API a bit more consistent if there is the same
356
+ // initialization handshake in both cases.
357
+
358
+ this._postMessage([MSGTYPE_HOST_ID, -1, 0], 0);
359
+
360
+ self.addEventListener("error", e => {
361
+ logError(e.error);
362
+
363
+ this._postMessage([MSGTYPE_WORKER_ERROR, -1, toFakeError(e.error)]);
364
+ });
365
+ }
366
+ }
367
+
368
+ _postMessage(obj, targetHostID) {
369
+ // console.log('_postMessage', obj, targetHostID);
370
+ if (this._workerType === "SharedWorker") {
371
+ // If targetHostID has been deleted, this will do nothing, which is fine I think
372
+ this._hosts.forEach(({
373
+ port
374
+ }, hostID) => {
375
+ if (targetHostID === undefined || targetHostID === hostID) {
376
+ port.postMessage(obj);
377
+ }
378
+ });
379
+ } else if (this._workerType === "Worker") {
380
+ // @ts-ignore
381
+ self.postMessage(obj);
382
+ } else {
383
+ throw new Error("WTF");
384
+ }
385
+ }
386
+
387
+ postMessage(userMessage, targetHostID) {
388
+ // console.log('postMessage', userMessage, targetHostID);
389
+ const actuallyPostMessage = (resolve, reject) => {
390
+ const messageID = messageIDs;
391
+ messageIDs += 1;
392
+ const messageToSend = [MSGTYPE_QUERY, messageID, userMessage];
393
+
394
+ this._callbacks.set(messageID, (error, result) => {
395
+ if (error) {
396
+ reject(error);
397
+ } else {
398
+ resolve(result);
399
+ }
400
+ });
401
+
402
+ this._postMessage(messageToSend, targetHostID);
403
+ };
404
+
405
+ return new Promise((resolve, reject) => {
406
+ actuallyPostMessage(resolve, reject);
407
+ });
408
+ }
409
+
410
+ _onMessage(e) {
411
+ const common = this._onMessageCommon(e);
412
+
413
+ if (!common) {
414
+ return;
415
+ }
416
+
417
+ const message = common.message,
418
+ type = common.type;
419
+
420
+ if (type === MSGTYPE_HOST_CLOSE) {
421
+ if (typeof message[2] !== "number") {
422
+ throw new Error("Invalid hostID");
423
+ }
424
+
425
+ const hostID = message[2];
426
+
427
+ this._hosts.delete(hostID);
428
+ }
429
+ }
430
+
431
+ }
432
+
433
+ exports.PWBHost = PWBHost;
434
+ exports.PWBWorker = PWBWorker;