Files
meteor/packages/callback-hook/hook.js
Dean Brettle 587e02a692 Fix issue #4862. Don't wrap login callbacks with bindEnvironment.
Changes to packages/callback-hook:
- Add bindEnvironment option to Hook constructor (defaults to true).
- Add internal helper function dontBindEnvironment() which does all the
  error handling stuff like Meteor.bindEnvironment() but doesn't change
  the environment.
- Use dontBindEnvironment() instead of Meteor.bindEnvironment() when
  bindEnvironment option is false.
- Add tests.

Changes to packages/accounts-base:
- Create hooks with { bindEnvironment: false }
- Add test for whether Meteor.userId() is available in login callbacks.
2015-08-03 10:52:11 -07:00

143 lines
4.8 KiB
JavaScript

// XXX This pattern is under development. Do not add more callsites
// using this package for now. See:
// https://meteor.hackpad.com/Design-proposal-Hooks-YxvgEW06q6f
//
// Encapsulates the pattern of registering callbacks on a hook.
//
// The `each` method of the hook calls its iterator function argument
// with each registered callback. This allows the hook to
// conditionally decide not to call the callback (if, for example, the
// observed object has been closed or terminated).
//
// By default, callbacks are bound with `Meteor.bindEnvironment`, so they will be
// called with the Meteor environment of the calling code that
// registered the callback. Override by passing { bindEnvironment: false }
// to the constructor.
//
// Registering a callback returns an object with a single `stop`
// method which unregisters the callback.
//
// The code is careful to allow a callback to be safely unregistered
// while the callbacks are being iterated over.
//
// If the hook is configured with the `exceptionHandler` option, the
// handler will be called if a called callback throws an exception.
// By default (if the exception handler doesn't itself throw an
// exception, or if the iterator function doesn't return a falsy value
// to terminate the calling of callbacks), the remaining callbacks
// will still be called.
//
// Alternatively, the `debugPrintExceptions` option can be specified
// as string describing the callback. On an exception the string and
// the exception will be printed to the console log with
// `Meteor._debug`, and the exception otherwise ignored.
//
// If an exception handler isn't specified, exceptions thrown in the
// callback will propagate up to the iterator function, and will
// terminate calling the remaining callbacks if not caught.
Hook = function (options) {
var self = this;
options = options || {};
self.nextCallbackId = 0;
self.callbacks = {};
// Whether to wrap callbacks with Meteor.bindEnvironment
self.bindEnvironment = true;
if (options.bindEnvironment === false)
self.bindEnvironment = false;
if (options.exceptionHandler)
self.exceptionHandler = options.exceptionHandler;
else if (options.debugPrintExceptions) {
if (! _.isString(options.debugPrintExceptions))
throw new Error("Hook option debugPrintExceptions should be a string");
self.exceptionHandler = options.debugPrintExceptions;
}
};
_.extend(Hook.prototype, {
register: function (callback) {
var self = this;
var exceptionHandler = self.exceptionHandler || function (exception) {
// Note: this relies on the undocumented fact that if bindEnvironment's
// onException throws, and you are invoking the callback either in the
// browser or from within a Fiber in Node, the exception is propagated.
throw exception;
};
if (self.bindEnvironment) {
callback = Meteor.bindEnvironment(callback, exceptionHandler);
} else {
callback = dontBindEnvironment(callback, exceptionHandler);
}
var id = self.nextCallbackId++;
self.callbacks[id] = callback;
return {
stop: function () {
delete self.callbacks[id];
}
};
},
// For each registered callback, call the passed iterator function
// with the callback.
//
// The iterator function can choose whether or not to call the
// callback. (For example, it might not call the callback if the
// observed object has been closed or terminated).
//
// The iteration is stopped if the iterator function returns a falsy
// value or throws an exception.
//
each: function (iterator) {
var self = this;
// Invoking bindEnvironment'd callbacks outside of a Fiber in Node doesn't
// run them to completion (and exceptions thrown from onException are not
// propagated), so we need to be in a Fiber.
Meteor._nodeCodeMustBeInFiber();
var ids = _.keys(self.callbacks);
for (var i = 0; i < ids.length; ++i) {
var id = ids[i];
// check to see if the callback was removed during iteration
if (_.has(self.callbacks, id)) {
var callback = self.callbacks[id];
if (! iterator(callback))
break;
}
}
}
});
// Copied from Meteor.bindEnvironment and removed all the env stuff.
var dontBindEnvironment = function (func, onException, _this) {
if (!onException || typeof(onException) === 'string') {
var description = onException || "callback of async function";
onException = function (error) {
Meteor._debug(
"Exception in " + description + ":",
error && error.stack || error
);
};
}
return function (/* arguments */) {
var args = _.toArray(arguments);
var runAndHandleExceptions = function () {
try {
var ret = func.apply(_this, args);
} catch (e) {
onException(e);
}
return ret;
};
return runAndHandleExceptions();
};
};