diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
commit | ac46df8daea09899ce30dc8fd70986e258c746bf (patch) | |
tree | 2750d3125fc253fd5b0671e4bd268eff1fd97296 /toolkit/jetpack/sdk/core | |
parent | 8cecf8d5208f3945b35f879bba3015bb1a11bec6 (diff) | |
download | UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.gz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.lz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.xz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.zip |
Move Add-on SDK source to toolkit/jetpack
Diffstat (limited to 'toolkit/jetpack/sdk/core')
-rw-r--r-- | toolkit/jetpack/sdk/core/disposable.js | 186 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/core/heritage.js | 184 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/core/namespace.js | 43 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/core/observer.js | 89 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/core/promise.js | 118 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/core/reference.js | 29 |
6 files changed, 649 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/core/disposable.js b/toolkit/jetpack/sdk/core/disposable.js new file mode 100644 index 000000000..19f7eaa9f --- /dev/null +++ b/toolkit/jetpack/sdk/core/disposable.js @@ -0,0 +1,186 @@ +/* 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" +}; + +const { Class } = require("./heritage"); +const { Observer, subscribe, unsubscribe, observe } = require("./observer"); +const { isWeak } = require("./reference"); +const SDKWeakSet = require("../lang/weak-set"); + +const method = require("../../method/core"); + +const unloadSubject = require('@loader/unload'); +const addonUnloadTopic = "sdk:loader:destroy"; + +const uninstall = method("disposable/uninstall"); +exports.uninstall = uninstall; + +const shutdown = method("disposable/shutdown"); +exports.shutdown = shutdown; + +const disable = method("disposable/disable"); +exports.disable = disable; + +const upgrade = method("disposable/upgrade"); +exports.upgrade = upgrade; + +const downgrade = method("disposable/downgrade"); +exports.downgrade = downgrade; + +const unload = method("disposable/unload"); +exports.unload = unload; + +const dispose = method("disposable/dispose"); +exports.dispose = dispose; +dispose.define(Object, object => object.dispose()); + +const setup = method("disposable/setup"); +exports.setup = setup; +setup.define(Object, (object, ...args) => object.setup(...args)); + +// DisposablesUnloadObserver is the class which subscribe the +// Observer Service to be notified when the add-on loader is +// unloading to be able to dispose all the existent disposables. +const DisposablesUnloadObserver = Class({ + implements: [Observer], + initialize: function(...args) { + // Set of the non-weak disposables registered to be disposed. + this.disposables = new Set(); + // Target of the weak disposables registered to be disposed + // (and tracked on this target using the SDK weak-set module). + this.weakDisposables = {}; + }, + subscribe(disposable) { + if (isWeak(disposable)) { + SDKWeakSet.add(this.weakDisposables, disposable); + } else { + this.disposables.add(disposable); + } + }, + unsubscribe(disposable) { + if (isWeak(disposable)) { + SDKWeakSet.remove(this.weakDisposables, disposable); + } else { + this.disposables.delete(disposable); + } + }, + tryUnloadDisposable(disposable) { + try { + if (disposable) { + unload(disposable); + } + } catch(e) { + console.error("Error unloading a", + isWeak(disposable) ? "weak disposable" : "disposable", + disposable, e); + } + }, + unloadAll() { + // Remove all the subscribed disposables. + for (let disposable of this.disposables) { + this.tryUnloadDisposable(disposable); + } + + this.disposables.clear(); + + // Remove all the subscribed weak disposables. + for (let disposable of SDKWeakSet.iterator(this.weakDisposables)) { + this.tryUnloadDisposable(disposable); + } + + SDKWeakSet.clear(this.weakDisposables); + } +}); +const disposablesUnloadObserver = new DisposablesUnloadObserver(); + +// The DisposablesUnloadObserver instance is the only object which subscribes +// the Observer Service directly, it observes add-on unload notifications in +// order to trigger `unload` on all its subscribed disposables. +observe.define(DisposablesUnloadObserver, (obj, subject, topic, data) => { + const isUnloadTopic = topic === addonUnloadTopic; + const isUnloadSubject = subject.wrappedJSObject === unloadSubject; + if (isUnloadTopic && isUnloadSubject) { + unsubscribe(disposablesUnloadObserver, addonUnloadTopic); + disposablesUnloadObserver.unloadAll(); + } +}); + +subscribe(disposablesUnloadObserver, addonUnloadTopic, false); + +// Set's up disposable instance. +const setupDisposable = disposable => { + disposablesUnloadObserver.subscribe(disposable); +}; +exports.setupDisposable = setupDisposable; + +// Tears down disposable instance. +const disposeDisposable = disposable => { + disposablesUnloadObserver.unsubscribe(disposable); +}; +exports.disposeDisposable = disposeDisposable; + +// Base type that takes care of disposing it's instances on add-on unload. +// Also makes sure to remove unload listener if it's already being disposed. +const Disposable = Class({ + initialize: function(...args) { + // First setup instance before initializing it's disposal. If instance + // fails to initialize then there is no instance to be disposed at the + // unload. + setup(this, ...args); + setupDisposable(this); + }, + destroy: function(reason) { + // Destroying disposable removes unload handler so that attempt to dispose + // won't be made at unload & delegates to dispose. + disposeDisposable(this); + unload(this, reason); + }, + setup: function() { + // Implement your initialize logic here. + }, + dispose: function() { + // Implement your cleanup logic here. + } +}); +exports.Disposable = Disposable; + +const unloaders = { + destroy: dispose, + uninstall: uninstall, + shutdown: shutdown, + disable: disable, + upgrade: upgrade, + downgrade: downgrade +}; + +const unloaded = new WeakMap(); +unload.define(Disposable, (disposable, reason) => { + if (!unloaded.get(disposable)) { + unloaded.set(disposable, true); + // Pick an unload handler associated with an unload + // reason (falling back to destroy if not found) and + // delegate unloading to it. + const unload = unloaders[reason] || unloaders.destroy; + unload(disposable); + } +}); + +// If add-on is disabled manually, it's being upgraded, downgraded +// or uninstalled `dispose` is invoked to undo any changes that +// has being done by it in this session. +disable.define(Disposable, dispose); +downgrade.define(Disposable, dispose); +upgrade.define(Disposable, dispose); +uninstall.define(Disposable, dispose); + +// If application is shut down no dispose is invoked as undo-ing +// changes made by instance is likely to just waste of resources & +// increase shutdown time. Although specefic components may choose +// to implement shutdown handler that does something better. +shutdown.define(Disposable, disposable => {}); diff --git a/toolkit/jetpack/sdk/core/heritage.js b/toolkit/jetpack/sdk/core/heritage.js new file mode 100644 index 000000000..fc87ba1f5 --- /dev/null +++ b/toolkit/jetpack/sdk/core/heritage.js @@ -0,0 +1,184 @@ +/* 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": "unstable" +}; + +var getPrototypeOf = Object.getPrototypeOf; +var getNames = x => [...Object.getOwnPropertyNames(x), + ...Object.getOwnPropertySymbols(x)]; +var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +var create = Object.create; +var freeze = Object.freeze; +var unbind = Function.call.bind(Function.bind, Function.call); + +// This shortcut makes sure that we do perform desired operations, even if +// associated methods have being overridden on the used object. +var owns = unbind(Object.prototype.hasOwnProperty); +var apply = unbind(Function.prototype.apply); +var slice = Array.slice || unbind(Array.prototype.slice); +var reduce = Array.reduce || unbind(Array.prototype.reduce); +var map = Array.map || unbind(Array.prototype.map); +var concat = Array.concat || unbind(Array.prototype.concat); + +// Utility function to get own properties descriptor map. +function getOwnPropertyDescriptors(object) { + return reduce(getNames(object), function(descriptor, name) { + descriptor[name] = getOwnPropertyDescriptor(object, name); + return descriptor; + }, {}); +} + +function isDataProperty(property) { + var value = property.value; + var type = typeof(property.value); + return "value" in property && + (type !== "object" || value === null) && + type !== "function"; +} + +function getDataProperties(object) { + var properties = getOwnPropertyDescriptors(object); + return getNames(properties).reduce(function(result, name) { + var property = properties[name]; + if (isDataProperty(property)) { + result[name] = { + value: property.value, + writable: true, + configurable: true, + enumerable: false + }; + } + return result; + }, {}) +} + +/** + * Takes `source` object as an argument and returns identical object + * with the difference that all own properties will be non-enumerable + */ +function obscure(source) { + var descriptor = reduce(getNames(source), function(descriptor, name) { + var property = getOwnPropertyDescriptor(source, name); + property.enumerable = false; + descriptor[name] = property; + return descriptor; + }, {}); + return create(getPrototypeOf(source), descriptor); +} +exports.obscure = obscure; + +/** + * Takes arbitrary number of source objects and returns fresh one, that + * inherits from the same prototype as a first argument and implements all + * own properties of all argument objects. If two or more argument objects + * have own properties with the same name, the property is overridden, with + * precedence from right to left, implying, that properties of the object on + * the left are overridden by a same named property of the object on the right. + */ +var mix = function(source) { + var descriptor = reduce(slice(arguments), function(descriptor, source) { + return reduce(getNames(source), function(descriptor, name) { + descriptor[name] = getOwnPropertyDescriptor(source, name); + return descriptor; + }, descriptor); + }, {}); + + return create(getPrototypeOf(source), descriptor); +}; +exports.mix = mix; + +/** + * Returns a frozen object with that inherits from the given `prototype` and + * implements all own properties of the given `properties` object. + */ +function extend(prototype, properties) { + return create(prototype, getOwnPropertyDescriptors(properties)); +} +exports.extend = extend; + +/** + * Returns a constructor function with a proper `prototype` setup. Returned + * constructor's `prototype` inherits from a given `options.extends` or + * `Class.prototype` if omitted and implements all the properties of the + * given `option`. If `options.implemens` array is passed, it's elements + * will be mixed into prototype as well. Also, `options.extends` can be + * a function or a prototype. If function than it's prototype is used as + * an ancestor of the prototype, if it's an object that it's used directly. + * Also `options.implements` may contain functions or objects, in case of + * functions their prototypes are used for mixing. + */ +var Class = new function() { + function prototypeOf(input) { + return typeof(input) === 'function' ? input.prototype : input; + } + var none = freeze([]); + + return function Class(options) { + // Create descriptor with normalized `options.extends` and + // `options.implements`. + var descriptor = { + // Normalize extends property of `options.extends` to a prototype object + // in case it's constructor. If property is missing that fallback to + // `Type.prototype`. + extends: owns(options, 'extends') ? + prototypeOf(options.extends) : Class.prototype, + // Normalize `options.implements` to make sure that it's array of + // prototype objects instead of constructor functions. + implements: owns(options, 'implements') ? + freeze(map(options.implements, prototypeOf)) : none + }; + + // Create array of property descriptors who's properties will be defined + // on the resulting prototype. Note: Using reflection `concat` instead of + // method as it may be overridden. + var descriptors = concat(descriptor.implements, options, descriptor, { + constructor: constructor + }); + + // Note: we use reflection `apply` in the constructor instead of method + // call since later may be overridden. + function constructor() { + var instance = create(prototype, attributes); + if (initialize) apply(initialize, instance, arguments); + return instance; + } + // Create `prototype` that inherits from given ancestor passed as + // `options.extends`, falling back to `Type.prototype`, implementing all + // properties of given `options.implements` and `options` itself. + var prototype = extend(descriptor.extends, mix.apply(mix, descriptors)); + var initialize = prototype.initialize; + + // Combine ancestor attributes with prototype's attributes so that + // ancestors attributes also become initializeable. + var attributes = mix(descriptor.extends.constructor.attributes || {}, + getDataProperties(prototype)); + + constructor.attributes = attributes; + Object.defineProperty(constructor, 'prototype', { + configurable: false, + writable: false, + value: prototype + }); + return constructor; + }; +} +Class.prototype = extend(null, obscure({ + constructor: function constructor() { + this.initialize.apply(this, arguments); + return this; + }, + initialize: function initialize() { + // Do your initialization logic here + }, + // Copy useful properties from `Object.prototype`. + toString: Object.prototype.toString, + toLocaleString: Object.prototype.toLocaleString, + toSource: Object.prototype.toSource, + valueOf: Object.prototype.valueOf, + isPrototypeOf: Object.prototype.isPrototypeOf +})); +exports.Class = freeze(Class); diff --git a/toolkit/jetpack/sdk/core/namespace.js b/toolkit/jetpack/sdk/core/namespace.js new file mode 100644 index 000000000..3ceb73b72 --- /dev/null +++ b/toolkit/jetpack/sdk/core/namespace.js @@ -0,0 +1,43 @@ +/* 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": "unstable" +}; + +const create = Object.create; +const prototypeOf = Object.getPrototypeOf; + +/** + * Returns a new namespace, function that may can be used to access an + * namespaced object of the argument argument. Namespaced object are associated + * with owner objects via weak references. Namespaced objects inherit from the + * owners ancestor namespaced object. If owner's ancestor is `null` then + * namespaced object inherits from given `prototype`. Namespaces can be used + * to define internal APIs that can be shared via enclosing `namespace` + * function. + * @examples + * const internals = ns(); + * internals(object).secret = secret; + */ +function ns() { + const map = new WeakMap(); + return function namespace(target) { + if (!target) // If `target` is not an object return `target` itself. + return target; + // If target has no namespaced object yet, create one that inherits from + // the target prototype's namespaced object. + if (!map.has(target)) + map.set(target, create(namespace(prototypeOf(target) || null))); + + return map.get(target); + }; +}; + +// `Namespace` is a e4x function in the scope, so we export the function also as +// `ns` as alias to avoid clashing. +exports.ns = ns; +exports.Namespace = ns; diff --git a/toolkit/jetpack/sdk/core/observer.js b/toolkit/jetpack/sdk/core/observer.js new file mode 100644 index 000000000..7e11bf8f9 --- /dev/null +++ b/toolkit/jetpack/sdk/core/observer.js @@ -0,0 +1,89 @@ +/* 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" +}; + + +const { Cc, Ci, Cr, Cu } = require("chrome"); +const { Class } = require("./heritage"); +const { isWeak } = require("./reference"); +const method = require("../../method/core"); + +const observerService = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); + +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + +// This is a method that will be invoked when notification observer +// subscribed to occurs. +const observe = method("observer/observe"); +exports.observe = observe; + +// Method to subscribe to the observer notification. +const subscribe = method("observe/subscribe"); +exports.subscribe = subscribe; + + +// Method to unsubscribe from the observer notifications. +const unsubscribe = method("observer/unsubscribe"); +exports.unsubscribe = unsubscribe; + + +// This is wrapper class that takes a `delegate` and produces +// instance of `nsIObserver` which will delegate to a given +// object when observer notification occurs. +const ObserverDelegee = Class({ + initialize: function(delegate) { + this.delegate = delegate; + }, + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + + return this; + }, + observe: function(subject, topic, data) { + observe(this.delegate, subject, topic, data); + } +}); + + +// Class that can be either mixed in or inherited from in +// order to subscribe / unsubscribe for observer notifications. +const Observer = Class({}); +exports.Observer = Observer; + +// Weak maps that associates instance of `ObserverDelegee` with +// an actual observer. It ensures that `ObserverDelegee` instance +// won't be GC-ed until given `observer` is. +const subscribers = new WeakMap(); + +// Implementation of `subscribe` for `Observer` type just registers +// observer for an observer service. If `isWeak(observer)` is `true` +// observer service won't hold strong reference to a given `observer`. +subscribe.define(Observer, (observer, topic) => { + if (!subscribers.has(observer)) { + const delegee = new ObserverDelegee(observer); + subscribers.set(observer, delegee); + addObserver(delegee, topic, isWeak(observer)); + } +}); + +// Unsubscribes `observer` from observer notifications for the +// given `topic`. +unsubscribe.define(Observer, (observer, topic) => { + const delegee = subscribers.get(observer); + if (delegee) { + subscribers.delete(observer); + removeObserver(delegee, topic); + } +}); diff --git a/toolkit/jetpack/sdk/core/promise.js b/toolkit/jetpack/sdk/core/promise.js new file mode 100644 index 000000000..f4bd7b0f5 --- /dev/null +++ b/toolkit/jetpack/sdk/core/promise.js @@ -0,0 +1,118 @@ +/* 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'; + +/* + * Uses `Promise.jsm` as a core implementation, with additional sugar + * from previous implementation, with inspiration from `Q` and `when` + * + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm + * https://github.com/cujojs/when + * https://github.com/kriskowal/q + */ +const PROMISE_URI = 'resource://gre/modules/Promise.jsm'; + +getEnvironment.call(this, function ({ require, exports, module, Cu }) { + +const Promise = Cu.import(PROMISE_URI, {}).Promise; +const { Debugging, defer, resolve, all, reject, race } = Promise; + +module.metadata = { + 'stability': 'unstable' +}; + +var promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call; + var concat = Array.prototype.concat; + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute (args) { + return call.apply(call, args); + } + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function (values) { + return resolve(unknown) + .then(value => values.concat([value])); + }); + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised(...args) { + // create array of [ f, this, args... ] + return [f, this, ...args]. + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute); + }; + }; +})(); + +exports.promised = promised; +exports.all = all; +exports.defer = defer; +exports.resolve = resolve; +exports.reject = reject; +exports.race = race; +exports.Promise = Promise; +exports.Debugging = Debugging; +}); + +function getEnvironment (callback) { + let Cu, _exports, _module, _require; + + // CommonJS / SDK + if (typeof(require) === 'function') { + Cu = require('chrome').Cu; + _exports = exports; + _module = module; + _require = require; + } + // JSM + else if (String(this).indexOf('BackstagePass') >= 0) { + Cu = this['Components'].utils; + _exports = this.Promise = {}; + _module = { uri: __URI__, id: 'promise/core' }; + _require = uri => { + let imports = {}; + Cu.import(uri, imports); + return imports; + }; + this.EXPORTED_SYMBOLS = ['Promise']; + // mozIJSSubScriptLoader.loadSubscript + } else if (~String(this).indexOf('Sandbox')) { + Cu = this['Components'].utils; + _exports = this; + _module = { id: 'promise/core' }; + _require = uri => {}; + } + + callback({ + Cu: Cu, + exports: _exports, + module: _module, + require: _require + }); +} + diff --git a/toolkit/jetpack/sdk/core/reference.js b/toolkit/jetpack/sdk/core/reference.js new file mode 100644 index 000000000..04549cd0f --- /dev/null +++ b/toolkit/jetpack/sdk/core/reference.js @@ -0,0 +1,29 @@ +/* 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" +}; + +const method = require("../../method/core"); +const { Class } = require("./heritage"); + +// Object that inherit or mix WeakRefence inn will register +// weak observes for system notifications. +const WeakReference = Class({}); +exports.WeakReference = WeakReference; + + +// If `isWeak(object)` is `true` observer installed +// for such `object` will be weak, meaning that it will +// be GC-ed if nothing else but observer is observing it. +// By default everything except `WeakReference` will return +// `false`. +const isWeak = method("reference/weak?"); +exports.isWeak = isWeak; + +isWeak.define(Object, _ => false); +isWeak.define(WeakReference, _ => true); |