/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; const { merge } = require('../util/object'); const { Class } = require('../core/heritage'); const { emit } = require('../event/core'); const { EventTarget } = require('../event/target'); const { getInnerId, getByInnerId } = require('../window/utils'); const { instanceOf, isObject } = require('../lang/type'); const system = require('../system/events'); const { when } = require('../system/unload'); const { WorkerSandbox } = require('./sandbox'); const { Ci } = require('chrome'); const { process, frames } = require('../remote/child'); const EVENTS = { 'chrome-page-shown': 'pageshow', 'content-page-shown': 'pageshow', 'chrome-page-hidden': 'pagehide', 'content-page-hidden': 'pagehide', 'inner-window-destroyed': 'detach', } // The parent Worker must have been created (or an async message sent to spawn // its creation) before creating the WorkerChild or messages from the content // script to the parent will get lost. const WorkerChild = Class({ implements: [EventTarget], initialize(options) { merge(this, options); keepAlive.set(this.id, this); this.windowId = getInnerId(this.window); if (this.contentScriptOptions) this.contentScriptOptions = JSON.parse(this.contentScriptOptions); this.port = EventTarget(); this.port.on('*', this.send.bind(this, 'event')); this.on('*', this.send.bind(this)); this.observe = this.observe.bind(this); for (let topic in EVENTS) system.on(topic, this.observe); this.receive = this.receive.bind(this); process.port.on('sdk/worker/message', this.receive); this.sandbox = WorkerSandbox(this, this.window); // If the document has an unexpected readyState, its worker-child instance is initialized // as frozen until one of the known readyState is reached. let initialDocumentReadyState = this.window.document.readyState; this.frozen = [ "loading", "interactive", "complete" ].includes(initialDocumentReadyState) ? false : true; if (this.frozen) { console.warn("SDK worker-child started as frozen on unexpected initial document.readyState", { initialDocumentReadyState, windowLocation: this.window.location.href, }); } this.frozenMessages = []; this.on('pageshow', () => { this.frozen = false; this.frozenMessages.forEach(args => this.sandbox.emit(...args)); this.frozenMessages = []; }); this.on('pagehide', () => { this.frozen = true; }); }, // messages receive(process, id, args) { if (id !== this.id) return; args = JSON.parse(args); if (this.frozen) this.frozenMessages.push(args); else this.sandbox.emit(...args); if (args[0] === 'detach') this.destroy(args[1]); }, send(...args) { process.port.emit('sdk/worker/event', this.id, JSON.stringify(args, exceptions)); }, // notifications observe({ type, subject }) { if (!this.sandbox) return; if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) { this.sandbox.emitSync(EVENTS[type]); emit(this, EVENTS[type]); } if (type === 'inner-window-destroyed' && subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) { this.destroy(); } }, get frame() { return frames.getFrameForWindow(this.window.top); }, // detach/destroy: unload and release the sandbox destroy(reason) { if (!this.sandbox) return; for (let topic in EVENTS) system.off(topic, this.observe); process.port.off('sdk/worker/message', this.receive); this.sandbox.destroy(reason); this.sandbox = null; keepAlive.delete(this.id); this.send('detach'); } }) exports.WorkerChild = WorkerChild; // Error instances JSON poorly function exceptions(key, value) { if (!isObject(value) || !instanceOf(value, Error)) return value; let _errorType = value.constructor.name; let { message, fileName, lineNumber, stack, name } = value; return { _errorType, message, fileName, lineNumber, stack, name }; } // workers for windows in this tab var keepAlive = new Map(); process.port.on('sdk/worker/create', (process, options, cpows) => { options.window = cpows.window; let worker = new WorkerChild(options); let frame = frames.getFrameForWindow(options.window.top); frame.port.emit('sdk/worker/connect', options.id, options.window.location.href); }); when(reason => { for (let worker of keepAlive.values()) worker.destroy(reason); });