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