mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
370 lines
13 KiB
JavaScript
370 lines
13 KiB
JavaScript
|
/**
|
||
|
* dispose.js provides tools to components that needs to dispose of resources, such as
|
||
|
* destroy DOM, and unsubscribe from events. The motivation with examples is presented here:
|
||
|
*
|
||
|
* https://phab.getgrist.com/w/disposal/
|
||
|
*/
|
||
|
|
||
|
|
||
|
var ko = require('knockout');
|
||
|
var util = require('util');
|
||
|
var _ = require("underscore");
|
||
|
|
||
|
// Use the browser globals in a way that allows replacing them with mocks in tests.
|
||
|
var G = require('./browserGlobals').get('DocumentFragment', 'Node');
|
||
|
|
||
|
/**
|
||
|
* Disposable is a base class for components that need cleanup (e.g. maintain DOM, listen to
|
||
|
* events, subscribe to anything). It provides a .dispose() method that should be called to
|
||
|
* destroy the component, and .autoDispose() method that the component should use to take
|
||
|
* responsibility for other pieces that require cleanup.
|
||
|
*
|
||
|
* To define a disposable prototype:
|
||
|
* function Foo() { ... }
|
||
|
* dispose.makeDisposable(Foo);
|
||
|
*
|
||
|
* To define a disposable ES6 class:
|
||
|
* class Foo extends dispose.Disposable { create() {...} }
|
||
|
*
|
||
|
* NB: Foo should not have its construction logic in a constructor but in a `create` method
|
||
|
* instead. If Foo defines a constructor (for taking advantage of type checking) the constructor
|
||
|
* should only call super `super(arg1, arg2 ...)`. Any way calling `new Foo(...args)` safely
|
||
|
* construct the component.
|
||
|
*
|
||
|
* In Foo's constructor or methods, take ownership of other objects:
|
||
|
* this.bar = this.autoDispose(Bar.create(...));
|
||
|
* The argument will get disposed when `this` is disposed. If it's a DOM node, it will get removed
|
||
|
* using ko.removeNode(). If it has a `dispose` method, it will be called.
|
||
|
*
|
||
|
* For more customized disposal:
|
||
|
* this.baz = this.autoDisposeWith('destroy', new Baz());
|
||
|
* this.elem = this.autoDisposeWith(ko.cleanNode, document.createElement(...));
|
||
|
* When `this` is disposed, will call this.baz.destroy(), and ko.cleanNode(this.elem).
|
||
|
*
|
||
|
* To call another method on disposal (e.g. to add custom disposal logic):
|
||
|
* this.autoDisposeCallback(this.myUnsubscribeAllMethod);
|
||
|
* The method will be called with `this` as context, and no arguments.
|
||
|
*
|
||
|
* To create Foo:
|
||
|
* var foo = Foo.create(args...);
|
||
|
* `Foo.create` ensures that if the constructor throws an exception, any calls to .autoDispose()
|
||
|
* that happened before that are honored.
|
||
|
*
|
||
|
* To dispose of Foo:
|
||
|
* foo.dispose();
|
||
|
* Owned objects will be disposed in reverse order from which `autoDispose` were called. Note that
|
||
|
* `foo` is no longer usable afterwards, and all its properties are wiped.
|
||
|
* If Foo has a `stopListening` method (e.g. inherits from Backbone.Events), `dispose` will call
|
||
|
* it automatically, as if it were added with `this.autoDisposeCallback(this.stopListening)`.
|
||
|
*
|
||
|
* To release an owned object:
|
||
|
* this.disposeRelease(this.bar);
|
||
|
*
|
||
|
* To dispose of an owned object early:
|
||
|
* this.disposeDiscard(this.bar);
|
||
|
*
|
||
|
* To determine if a reference refers to object that has already been disposed:
|
||
|
* foo.isDisposed()
|
||
|
*/
|
||
|
class Disposable {
|
||
|
/**
|
||
|
* A safe constructor which calls dispose() in case the creation throws an exception.
|
||
|
*/
|
||
|
constructor(...args) {
|
||
|
safelyConstruct(this.create, this, args);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Static method to allow rewriting old classes into ES6 without modifying their
|
||
|
* instantiation to use `new Foo()` (i.e. you can continue to use `Foo.create()`).
|
||
|
*/
|
||
|
static create(...args) {
|
||
|
return new this(...args);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Object.assign(Disposable.prototype, {
|
||
|
/**
|
||
|
* Take ownership of `obj`, and dispose it when `this.dispose` is called.
|
||
|
* @param {Object} obj: Object to take ownership of. It can be a DOM node or an object with a
|
||
|
* `dispose` method.
|
||
|
* @returns {Object} obj
|
||
|
*/
|
||
|
autoDispose: function(obj) {
|
||
|
return this.autoDisposeWith(defaultDisposer, obj);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* As for autoDispose, but we receive a promise of an object. We wait for it to
|
||
|
* resolve and then take ownership of it. We return a promise that resolves to
|
||
|
* the object, or to null if the owner is disposed in the meantime.
|
||
|
*/
|
||
|
autoDisposePromise: function(objPromise) {
|
||
|
return objPromise.then(obj => {
|
||
|
if (this.isDisposed()) {
|
||
|
defaultDisposer(obj);
|
||
|
return null;
|
||
|
}
|
||
|
this.autoDispose(obj);
|
||
|
return obj;
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Take ownership of `obj`, and dispose it when `this.dispose` is called by calling the
|
||
|
* specified function.
|
||
|
* @param {Function|String} disposer: If a function, disposer(obj) will be called to dispose the
|
||
|
* object, with `this` as the context. If a string, then obj[disposer]() will be called. E.g.
|
||
|
* this.autoDisposeWith('destroy', a); // will call a.destroy()
|
||
|
* this.autoDisposeWith(ko.cleanNode, b); // will call ko.cleanNode(b)
|
||
|
* @param {Object} obj: Object to take ownership of, on which `disposer` will be called.
|
||
|
* @returns {Object} obj
|
||
|
*/
|
||
|
autoDisposeWith: function(disposer, obj) {
|
||
|
var list = this._disposalList || (this._disposalList = []);
|
||
|
list.push({ obj: obj,
|
||
|
disposer: typeof disposer === 'string' ? methodDisposer(disposer) : disposer });
|
||
|
return obj;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Adds the given callback to be called when `this.dispose` is called.
|
||
|
* @param {Function} callback: Called on disposal with `this` as the context and no arguments.
|
||
|
* @returns nothing
|
||
|
*/
|
||
|
autoDisposeCallback: function(callback) {
|
||
|
this.autoDisposeWith(callFuncHelper, callback);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Remove `obj` from the list of owned objects; it will not be disposed on `this.dispose`.
|
||
|
* @param {Object} obj: Object to release.
|
||
|
* @returns {Object} obj
|
||
|
*/
|
||
|
disposeRelease: function(obj) {
|
||
|
removeObjectToDispose(this._disposalList, obj);
|
||
|
return obj;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Dispose of an owned object `obj` now, and remove it from the list of owned objects.
|
||
|
* @param {Object} obj: Object to release.
|
||
|
* @returns nothing
|
||
|
*/
|
||
|
disposeDiscard: function(obj) {
|
||
|
var entry = removeObjectToDispose(this._disposalList, obj);
|
||
|
if (entry) {
|
||
|
entry.disposer.call(this, obj);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns whether this object has already been disposed.
|
||
|
*/
|
||
|
isDisposed: function() {
|
||
|
return this._disposalList === WIPED_VALUE;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Clean up `this` by disposing of all owned objects, and calling `stopListening()` if defined.
|
||
|
*/
|
||
|
dispose: function() {
|
||
|
if (this.isDisposed()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var disposalList = this._disposalList;
|
||
|
this._disposalList = WIPED_VALUE; // This makes isDisposed() true.
|
||
|
if (disposalList) {
|
||
|
// Go backwards through the disposal list, and dispose of everything.
|
||
|
for (var i = disposalList.length - 1; i >= 0; i--) {
|
||
|
var entry = disposalList[i];
|
||
|
disposeHelper(this, entry.disposer, entry.obj);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Call stopListening if it exists. This is a convenience when using Backbone.Events. It's
|
||
|
// equivalent to calling this.autoDisposeCallback(this.stopListening) in constructor.
|
||
|
if (typeof this.stopListening === 'function') {
|
||
|
// Wrap in disposeHelper so that errors get caught.
|
||
|
disposeHelper(this, callFuncHelper, this.stopListening);
|
||
|
}
|
||
|
|
||
|
// Finish by wiping out the object, since nothing should use it after dispose().
|
||
|
// See https://phab.getgrist.com/w/disposal/ for more motivation.
|
||
|
wipeOutObject(this);
|
||
|
}
|
||
|
});
|
||
|
exports.Disposable = Disposable;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* The recommended way to make an object disposable. It simply adds the methods of `Disposable` to
|
||
|
* its prototype, and also adds a `Class.create()` function, for a safer way to construct objects
|
||
|
* (see `safeCreate` for explanation). For instance,
|
||
|
* function Foo(args...) {...}
|
||
|
* dispose.makeDisposable(Foo);
|
||
|
* Now you can create Foo objects with:
|
||
|
* var foo = Foo.create(args...);
|
||
|
* And dispose of them with:
|
||
|
* foo.dispose();
|
||
|
*/
|
||
|
function makeDisposable(Constructor) {
|
||
|
Object.assign(Constructor.prototype, Disposable.prototype);
|
||
|
Constructor.create = safeConstructor;
|
||
|
}
|
||
|
exports.makeDisposable = makeDisposable;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Helper to create and construct an object safely: `safeCreate(Foo, ...)` is similar to `new
|
||
|
* Foo(...)`. The difference is that in case of an exception in the constructor, the dispose()
|
||
|
* method will be called on the partially constructed object.
|
||
|
* If you call makeDisposable(Foo), then Foo.create(...) is equivalent and more convenient.
|
||
|
* @returns {Object} the newly constructed object.
|
||
|
*/
|
||
|
function safeCreate(Constructor, varArgs) {
|
||
|
return safeConstructor.apply(Constructor, Array.prototype.slice.call(arguments, 1));
|
||
|
}
|
||
|
exports.safeCreate = safeCreate;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Helper used by makeDisposable() for the `create` property of a disposable class. E.g. when
|
||
|
* assigned to Foo.create, the call `Foo.create(args)` becomes similar to `new Foo(args)`, but
|
||
|
* calls dispose() in case the constructor throws an exception.
|
||
|
*/
|
||
|
var safeConstructor = function(varArgs) {
|
||
|
var Constructor = this;
|
||
|
var obj = Object.create(Constructor.prototype);
|
||
|
return safelyConstruct(Constructor, obj, arguments);
|
||
|
};
|
||
|
|
||
|
var safelyConstruct = function(Constructor, obj, args) {
|
||
|
try {
|
||
|
Constructor.apply(obj, args);
|
||
|
return obj;
|
||
|
} catch (e) {
|
||
|
// Be a bit more helpful and concise in reporting errors: print error as an object (that
|
||
|
// includes its stacktrace in FF and Chrome), and avoid printing it multiple times as it
|
||
|
// bubbles up through the stack of safeConstructor calls.
|
||
|
if (!e.printed) {
|
||
|
let name = obj.constructor.name || Constructor.name;
|
||
|
console.error("Error constructing %s:", name, e);
|
||
|
// assigning printed to a string throws: TypeError: Cannot create property 'printed' on [...]
|
||
|
if (_.isObject(e)) {
|
||
|
e.printed = true;
|
||
|
}
|
||
|
}
|
||
|
obj.dispose();
|
||
|
throw e;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// It doesn't matter what the value is, but some values cause more helpful errors than others.
|
||
|
// E.g. if x = "disposed", then x.foo() throws "undefined is not a function", while when x = null,
|
||
|
// x.foo() throws "Cannot read property 'foo' of null", which seems more helpful.
|
||
|
var WIPED_VALUE = null;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Wipe out the given object by setting each property to a dummy value. This is helpful for
|
||
|
* objects that are disposed and should be ready to be garbage-collected. The goals are:
|
||
|
* - If anything still refers to the object and uses it, we'll get an early error, rather than
|
||
|
* silently keep going, potentially doing useless work (or worse) and wasting resources.
|
||
|
* - If anything still refers to the object but doesn't use it, the fields of the object can
|
||
|
* still be garbage-collected.
|
||
|
* - If there are circular references between the object and its properties, they get broken,
|
||
|
* making the job easier for the garbage collector.
|
||
|
*/
|
||
|
function wipeOutObject(obj) {
|
||
|
for (var k in obj) {
|
||
|
if (obj.hasOwnProperty(k)) {
|
||
|
obj[k] = WIPED_VALUE;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Internal helper used by disposeDiscard() and disposeRelease(). It finds, removes, and returns
|
||
|
* an entry from the given disposalList.
|
||
|
*/
|
||
|
function removeObjectToDispose(disposalList, obj) {
|
||
|
if (disposalList) {
|
||
|
for (var i = 0; i < disposalList.length; i++) {
|
||
|
if (disposalList[i].obj === obj) {
|
||
|
var entry = disposalList[i];
|
||
|
disposalList.splice(i, 1);
|
||
|
return entry;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Internal helper to allow adding cleanup callbacks to the disposalList. It acts as the
|
||
|
* "disposer" for callback, by simply calling them with the same context that it is called with.
|
||
|
*/
|
||
|
var callFuncHelper = function(callback) {
|
||
|
callback.call(this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Internal helper to dispose objects that need a differently-named method to be called on them.
|
||
|
* It's used by `autoDisposeWith` when the disposer is a string method name.
|
||
|
*/
|
||
|
function methodDisposer(methodName) {
|
||
|
return function(obj) {
|
||
|
obj[methodName]();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Internal helper to call a disposer on an object. It swallows errors (but reports them) to make
|
||
|
* sure that when we dispose of an object, an error in disposing of one owned part doesn't stop
|
||
|
* the disposal of the other parts.
|
||
|
*/
|
||
|
function disposeHelper(owner, disposer, obj) {
|
||
|
try {
|
||
|
disposer.call(owner, obj);
|
||
|
} catch (e) {
|
||
|
console.error("While disposing %s, error disposing %s: %s",
|
||
|
describe(owner), describe(obj), e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper for reporting errors during disposal. Try to report the type of the object.
|
||
|
*/
|
||
|
function describe(obj) {
|
||
|
return (obj && obj.constructor && obj.constructor.name ? obj.constructor.name :
|
||
|
util.inspect(obj, {depth: 1}));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Internal helper that implements the default disposal for an object. It just supports removing
|
||
|
* DOM nodes with ko.removeNode, and calling dispose() on any part that has a `dispose` method.
|
||
|
*/
|
||
|
function defaultDisposer(obj) {
|
||
|
if (obj instanceof G.Node) {
|
||
|
// This does both knockout- and jquery-related cleaning, and removes the node from the DOM.
|
||
|
ko.removeNode(obj);
|
||
|
} else if (typeof obj.dispose === 'function') {
|
||
|
obj.dispose();
|
||
|
} else {
|
||
|
throw new Error("Object has no 'dispose' method");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes all children of the given node, and all knockout bindings. You can use it as
|
||
|
* this.autoDisposeWith(dispose.emptyNode, node);
|
||
|
*/
|
||
|
function emptyNode(node) {
|
||
|
ko.virtualElements.emptyNode(node);
|
||
|
ko.cleanNode(node);
|
||
|
}
|
||
|
|
||
|
exports.emptyNode = emptyNode;
|