diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/frame/model.js')
-rw-r--r-- | toolkit/jetpack/sdk/ui/frame/model.js | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/frame/model.js b/toolkit/jetpack/sdk/ui/frame/model.js new file mode 100644 index 000000000..627310874 --- /dev/null +++ b/toolkit/jetpack/sdk/ui/frame/model.js @@ -0,0 +1,154 @@ +/* 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); |