model.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "experimental",
  7. "engines": {
  8. "Firefox": "> 28"
  9. }
  10. };
  11. const { Class } = require("../../core/heritage");
  12. const { EventTarget } = require("../../event/target");
  13. const { emit, off, setListeners } = require("../../event/core");
  14. const { Reactor, foldp, send, merges } = require("../../event/utils");
  15. const { Disposable } = require("../../core/disposable");
  16. const { OutputPort } = require("../../output/system");
  17. const { InputPort } = require("../../input/system");
  18. const { identify } = require("../id");
  19. const { pairs, object, map, each } = require("../../util/sequence");
  20. const { patch, diff } = require("diffpatcher/index");
  21. const { isLocalURL } = require("../../url");
  22. const { compose } = require("../../lang/functional");
  23. const { contract } = require("../../util/contract");
  24. const { id: addonID, data: { url: resolve }} = require("../../self");
  25. const { Frames } = require("../../input/frame");
  26. const output = new OutputPort({ id: "frame-change" });
  27. const mailbox = new OutputPort({ id: "frame-mailbox" });
  28. const input = Frames;
  29. const makeID = url =>
  30. ("frame-" + addonID + "-" + url).
  31. split("/").join("-").
  32. split(".").join("-").
  33. replace(/[^A-Za-z0-9_\-]/g, "");
  34. const validate = contract({
  35. name: {
  36. is: ["string", "undefined"],
  37. ok: x => /^[a-z][a-z0-9-_]+$/i.test(x),
  38. msg: "The `option.name` must be a valid alphanumeric string (hyphens and " +
  39. "underscores are allowed) starting with letter."
  40. },
  41. url: {
  42. map: x => x.toString(),
  43. is: ["string"],
  44. ok: x => isLocalURL(x),
  45. msg: "The `options.url` must be a valid local URI."
  46. }
  47. });
  48. const Source = function({id, ownerID}) {
  49. this.id = id;
  50. this.ownerID = ownerID;
  51. };
  52. Source.postMessage = ({id, ownerID}, data, origin) => {
  53. send(mailbox, object([id, {
  54. inbox: {
  55. target: {id: id, ownerID: ownerID},
  56. timeStamp: Date.now(),
  57. data: data,
  58. origin: origin
  59. }
  60. }]));
  61. };
  62. Source.prototype.postMessage = function(data, origin) {
  63. Source.postMessage(this, data, origin);
  64. };
  65. const Message = function({type, data, source, origin, timeStamp}) {
  66. this.type = type;
  67. this.data = data;
  68. this.origin = origin;
  69. this.timeStamp = timeStamp;
  70. this.source = new Source(source);
  71. };
  72. const frames = new Map();
  73. const sources = new Map();
  74. const Frame = Class({
  75. extends: EventTarget,
  76. implements: [Disposable, Source],
  77. initialize: function(params={}) {
  78. const options = validate(params);
  79. const id = makeID(options.name || options.url);
  80. if (frames.has(id))
  81. throw Error("Frame with this id already exists: " + id);
  82. const initial = { id: id, url: resolve(options.url) };
  83. this.id = id;
  84. setListeners(this, params);
  85. frames.set(this.id, this);
  86. send(output, object([id, initial]));
  87. },
  88. get url() {
  89. const state = reactor.value[this.id];
  90. return state && state.url;
  91. },
  92. destroy: function() {
  93. send(output, object([this.id, null]));
  94. frames.delete(this.id);
  95. off(this);
  96. },
  97. // `JSON.stringify` serializes objects based of the return
  98. // value of this method. For convinienc we provide this method
  99. // to serialize actual state data.
  100. toJSON: function() {
  101. return { id: this.id, url: this.url };
  102. }
  103. });
  104. identify.define(Frame, frame => frame.id);
  105. exports.Frame = Frame;
  106. const reactor = new Reactor({
  107. onStep: (present, past) => {
  108. const delta = diff(past, present);
  109. each(([id, update]) => {
  110. const frame = frames.get(id);
  111. if (update) {
  112. if (!past[id])
  113. emit(frame, "register");
  114. if (update.outbox)
  115. emit(frame, "message", new Message(present[id].outbox));
  116. each(([ownerID, state]) => {
  117. const readyState = state ? state.readyState : "detach";
  118. const type = readyState === "loading" ? "attach" :
  119. readyState === "interactive" ? "ready" :
  120. readyState === "complete" ? "load" :
  121. readyState;
  122. // TODO: Cache `Source` instances somewhere to preserve
  123. // identity.
  124. emit(frame, type, {type: type,
  125. source: new Source({id: id, ownerID: ownerID})});
  126. }, pairs(update.owners));
  127. }
  128. }, pairs(delta));
  129. }
  130. });
  131. reactor.run(input);