michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental", michael@0: "engines": { michael@0: "Firefox": "> 28" michael@0: } michael@0: }; michael@0: michael@0: const { Class } = require("../../core/heritage"); michael@0: const { EventTarget } = require("../../event/target"); michael@0: const { emit, off, setListeners } = require("../../event/core"); michael@0: const { Reactor, foldp, send, merges } = require("../../event/utils"); michael@0: const { Disposable } = require("../../core/disposable"); michael@0: const { OutputPort } = require("../../output/system"); michael@0: const { InputPort } = require("../../input/system"); michael@0: const { identify } = require("../id"); michael@0: const { pairs, object, map, each } = require("../../util/sequence"); michael@0: const { patch, diff } = require("diffpatcher/index"); michael@0: const { isLocalURL } = require("../../url"); michael@0: const { compose } = require("../../lang/functional"); michael@0: const { contract } = require("../../util/contract"); michael@0: const { id: addonID, data: { url: resolve }} = require("../../self"); michael@0: const { Frames } = require("../../input/frame"); michael@0: michael@0: michael@0: const output = new OutputPort({ id: "frame-change" }); michael@0: const mailbox = new OutputPort({ id: "frame-mailbox" }); michael@0: const input = Frames; michael@0: michael@0: michael@0: const makeID = url => michael@0: ("frame-" + addonID + "-" + url). michael@0: split("/").join("-"). michael@0: split(".").join("-"). michael@0: replace(/[^A-Za-z0-9_\-]/g, ""); michael@0: michael@0: const validate = contract({ michael@0: name: { michael@0: is: ["string", "undefined"], michael@0: ok: x => /^[a-z][a-z0-9-_]+$/i.test(x), michael@0: msg: "The `option.name` must be a valid alphanumeric string (hyphens and " + michael@0: "underscores are allowed) starting with letter." michael@0: }, michael@0: url: { michael@0: map: x => x.toString(), michael@0: is: ["string"], michael@0: ok: x => isLocalURL(x), michael@0: msg: "The `options.url` must be a valid local URI." michael@0: } michael@0: }); michael@0: michael@0: const Source = function({id, ownerID}) { michael@0: this.id = id; michael@0: this.ownerID = ownerID; michael@0: }; michael@0: Source.postMessage = ({id, ownerID}, data, origin) => { michael@0: send(mailbox, object([id, { michael@0: inbox: { michael@0: target: {id: id, ownerID: ownerID}, michael@0: timeStamp: Date.now(), michael@0: data: data, michael@0: origin: origin michael@0: } michael@0: }])); michael@0: }; michael@0: Source.prototype.postMessage = function(data, origin) { michael@0: Source.postMessage(this, data, origin); michael@0: }; michael@0: michael@0: const Message = function({type, data, source, origin, timeStamp}) { michael@0: this.type = type; michael@0: this.data = data; michael@0: this.origin = origin; michael@0: this.timeStamp = timeStamp; michael@0: this.source = new Source(source); michael@0: }; michael@0: michael@0: michael@0: const frames = new Map(); michael@0: const sources = new Map(); michael@0: michael@0: const Frame = Class({ michael@0: extends: EventTarget, michael@0: implements: [Disposable, Source], michael@0: initialize: function(params={}) { michael@0: const options = validate(params); michael@0: const id = makeID(options.name || options.url); michael@0: michael@0: if (frames.has(id)) michael@0: throw Error("Frame with this id already exists: " + id); michael@0: michael@0: const initial = { id: id, url: resolve(options.url) }; michael@0: this.id = id; michael@0: michael@0: setListeners(this, params); michael@0: michael@0: frames.set(this.id, this); michael@0: michael@0: send(output, object([id, initial])); michael@0: }, michael@0: get url() { michael@0: const state = reactor.value[this.id]; michael@0: return state && state.url; michael@0: }, michael@0: destroy: function() { michael@0: send(output, object([this.id, null])); michael@0: frames.delete(this.id); michael@0: off(this); michael@0: }, michael@0: // `JSON.stringify` serializes objects based of the return michael@0: // value of this method. For convinienc we provide this method michael@0: // to serialize actual state data. michael@0: toJSON: function() { michael@0: return { id: this.id, url: this.url }; michael@0: } michael@0: }); michael@0: identify.define(Frame, frame => frame.id); michael@0: michael@0: exports.Frame = Frame; michael@0: michael@0: const reactor = new Reactor({ michael@0: onStep: (present, past) => { michael@0: const delta = diff(past, present); michael@0: michael@0: each(([id, update]) => { michael@0: const frame = frames.get(id); michael@0: if (update) { michael@0: if (!past[id]) michael@0: emit(frame, "register"); michael@0: michael@0: if (update.outbox) michael@0: emit(frame, "message", new Message(present[id].outbox)); michael@0: michael@0: each(([ownerID, state]) => { michael@0: const readyState = state ? state.readyState : "detach"; michael@0: const type = readyState === "loading" ? "attach" : michael@0: readyState === "interactive" ? "ready" : michael@0: readyState === "complete" ? "load" : michael@0: readyState; michael@0: michael@0: // TODO: Cache `Source` instances somewhere to preserve michael@0: // identity. michael@0: emit(frame, type, {type: type, michael@0: source: new Source({id: id, ownerID: ownerID})}); michael@0: }, pairs(update.owners)); michael@0: } michael@0: }, pairs(delta)); michael@0: } michael@0: }); michael@0: reactor.run(input);