diff options
Diffstat (limited to 'components/jetpack/sdk/content/worker-child.js')
-rw-r--r-- | components/jetpack/sdk/content/worker-child.js | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/components/jetpack/sdk/content/worker-child.js b/components/jetpack/sdk/content/worker-child.js new file mode 100644 index 000000000..dbf65a933 --- /dev/null +++ b/components/jetpack/sdk/content/worker-child.js @@ -0,0 +1,158 @@ +/* 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); +}); |