/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
goog.module('goog.delegate.DelegateRegistry');
const {ENABLE_ASSERTS, assert} = goog.require('goog.asserts');
const {binarySelect} = goog.require('goog.array');
const {freeze} = goog.require('goog.debug');
/**
* @record
* @template T
*/
class Registration {
constructor() {
/**
* The registered delegate instance. Exactly one of `instance` or
* `ctor` must be provided.
* @type {T|undefined}
*/
this.instance;
/**
* The registered delegate constructor. Exactly one of `instance` or
* `ctor` must be provided.
* @type {function(new: T, ...?)|undefined}
*/
this.ctor;
/**
* An optional numeric priority (higher = first).
* @type {number|undefined}
*/
this.priority;
}
}
/**
* Base class for delegate registries. Does not specify a policy for handling
* multiple delegates.
* @template T
*/
class DelegateRegistryBase {
constructor() {
/** @private @const {!Array<!Registration<T>>} */
this.registered_ = [];
/** @private {boolean} */
this.allowLateRegistration_ = false;
/** @private {boolean} */
this.cacheInstantiation_ = false;
/** @private {boolean} */
this.delegatesConstructed_ = false;
}
/**
* Configures this registry to allow late registration. Normally it is an
* error to register a delegate after calling `delegate()` or `delegates()`.
* If late registration is allowed, then this is no longer an error. This
* check only ever happens in debug mode. Returns this.
* @return {THIS}
* @this {THIS}
* @template THIS
*/
allowLateRegistration() {
if (ENABLE_ASSERTS) {
/** @type {!DelegateRegistryBase} */ (this).allowLateRegistration_ = true;
}
return /** @type {?} */ (this);
}
/**
* Configures this registry to automatically cache instantiated instances,
* rather than calling the constructor every time `delegates()` is called.
* Returns this.
* @return {THIS}
* @this {THIS}
* @template THIS
*/
cacheInstantiation() {
/** @type {!DelegateRegistryBase} */ (this).cacheInstantiation_ = true;
return /** @type {?} */ (this);
}
/**
* Returns the first (highest priority) registered delegate, or undefined
* if none was registered.
* @param {(function(function(new: T, ...?)): T)=} instantiate A function to
* instantiate constructors registered with `registerClass`. By default,
* this just calls the constructor with no arguments.
* @return {T|undefined}
*/
delegate(instantiate = undefined) {
if (ENABLE_ASSERTS) {
this.delegatesConstructed_ = true;
}
return this.registered_.length ?
this.instantiate_(this.registered_[0], instantiate) :
undefined;
}
/**
* Returns an array of all registered delegates, creating a fresh instance
* of any registered classes. The `instantiate` argument can be passed to
* override how constructors are called. The array will be frozen in debug
* mode.
* @param {(function(function(new: T, ...?)): T)=} instantiate A function to
* instantiate constructors registered with `registerClass`. By default,
* this just calls the constructor with no arguments.
* @return {!Array<T>}
*/
delegates(instantiate = undefined) {
if (ENABLE_ASSERTS) {
this.delegatesConstructed_ = true;
}
return freeze(this.registered_.map(r => this.instantiate_(r, instantiate)));
}
/**
* @param {!Registration<T>} registration
* @param {(function(function(new: T, ...?)): T)=} instantiate
* @return {T}
* @private
*/
instantiate_(registration, instantiate = (ctor) => new ctor()) {
if (!registration.ctor) return registration.instance;
const instance = instantiate(registration.ctor);
if (this.cacheInstantiation_) {
delete registration.ctor;
registration.instance = instance;
}
return instance;
}
/**
* Checks whether a new registration may be added.
* @private
*/
checkRegistration_() {
assert(
this.allowLateRegistration_ || !this.delegatesConstructed_,
'Cannot register new delegates after instantiation.');
}
}
/**
* Delegates provide a system for hygienic modification of a delegating class's
* behavior. The basic idea is that, rather than monkey-patching prototype
* methods, a class can instead provide extension points by calling out to
* delegates. Later code can then register delegates, and when the delegating
* class is instantiated, any registered delegates will be instantiated and
* returned.
*
* The usage has four parts:
* - A *delegate interface* is defined to provide specific overridable hooks.
* This can be a simple function `@typedef`, or an entire `@interface` or
* `@record`.
* - A *delegate registry* for this interface is instantiated, often as a
* static field on the interface.
* - One or more *delegates* are defined that implement this interface.
* Delegates are registered with the registry. Different registry classes
* support different policies for registering more than one delegate.
* - After delegates are registered, the delegating class asks the registry for
* the *list of delegates*, which are then instantiated if necessary.
*
* In some circumstances (particularly if a delegate method will be called from
* multiple places) it may make sense to provide an additional wrapper between
* the delegate list and the delegating (sometimes called "modded") class, to
* ensure that the delegates are used correctly.
*
* ## Example usage
*
* For example, consider a class `Foo` that wants to provide a few extension
* points for the behaviors `zorch` and `snarf`. We can set up the delegation
* as follows:
*
* <code class="highlight highlight-source-js"><pre>
* const DelegateRegistry = goog.require('goog.delegate.DelegateRegistry');
* const delegates = goog.require('goog.delegate.delegates');
* class Foo {
* constructor() {
* /** @private @const {!Array<!Foo.Delegate>} */
* this.delegates_ = Foo.registry.delegates();
* }
* frobnicate(x, y, z) {
* const w = delegates.callFirst(this.delegates_, d => d.zorch(x, y));
* return this.delegates_.map(d => d.snarf(z, w));
* }
* }
* /** @interface */
* Foo.Delegate = class {
* zorch(a, b) {}
* snarf(a, b) {}
* }
* /** @const {!DelegateRegistry<!Foo.Delegate>} */
* Foo.registry = new DelegateRegistry();
* </pre></code>
*
* A file inserted later in the bundle can define a delegate and register itself
* with the registry:
*
* <code class="highlight highlight-source-js"><pre>
* /** @implements {Foo.Delegate} */
* class WibblyFooDelegate {
* zorch(a, b) { return a + b; }
* snarf(a, b) { return a - b; }
* }
* Foo.registry.registerClass(WibbyFooDelegate);
* </pre></code>
*
* In many cases, the delegates need to be initialized with an instance of the
* modded class. To support this, a function may be passed to the `delegates()`
* method to override how the constructor is called.
*
*
* ## Multiple Delegates
*
* Two different registry classes are defined, each with a different policy for
* how to handle multiple delegates. The simpler one, `DelegateRegistry`,
* allows multiple delegates to be registered and returns them in the order they
* were registered. If only one delegate is expected,
* `DelegateRegistry.prototype.expectAtMostOneDelegate()` performs assertions
* (in debug mode) that at most one delegate is added, though in production
* mode it will still register them all - The use of `delegate()` or
* `goog.delegate.delegates.callFirst()` is recommended in this case to ensure
* reasonable behavior.
*
* The more sophisticated one, `DelegateRegistry.Prioritized`, requires passing
* a unique priority to each delegate registration (collisions are asserted in
* debug mode, but will fall back to registration order in production).
*
*
* ## Wrapped Delegator
*
* In some cases it makes sense to wrap the delegate list in a dedicated
* delegator object, rather than having the modded class use it directly:
*
* <code class="highlight highlight-source-js"><pre>
* /** @record */
* class MyDelegateInterface {
* /** @param {number} arg */
* foo(arg) {}
* /** @return {number|undefined} */
* bar() {}
* /** @return {string} */
* baz() {}
* }
* class MyDelegator {
* /** @param {!Array<!MyDelegateInterface>} delegates */
* constructor(delegates) { this.delegates_ = delegates; }
* /** @param {number} */
* foo(arg) { this.delegates_.forEach(d => d.foo(arg)); }
* /** @return {number} */
* bar() {
* const result =
* delegates.callUntilNotNullOrUndefined(this.delegates_, d => d.bar());
* return result != null ? result : 42;
* }
* /** @return {!Array<string>} */
* baz() { return this.delegates_.map(d => d.baz()); }
* }
* </pre></code>
*
* In this example, the modded class will call into the delegates via the
* wrapper class, ensuring that the correct calling convention is always used.
*
* @extends {DelegateRegistryBase<T>}
* @template T
*/
class DelegateRegistry extends DelegateRegistryBase {
constructor() {
super();
/** @private {boolean} */
this.expectAtMostOneDelegate_ = false;
}
/**
* Configures this registry to accept at most one delegate.
* This only affects debug mode.
* @return {!DelegateRegistry<T>}
*/
expectAtMostOneDelegate() {
if (ENABLE_ASSERTS) {
this.expectAtMostOneDelegate_ = true;
}
return this;
}
/**
* @param {function(new: T, ...?)} ctor
*/
registerClass(ctor) {
this.checkRegistration_();
this.registered_.push({ctor});
}
/**
* @param {T} instance
*/
registerInstance(instance) {
this.checkRegistration_();
this.registered_.push({instance});
}
/** @override @private */
checkRegistration_() {
super.checkRegistration_();
if (ENABLE_ASSERTS && this.expectAtMostOneDelegate_ &&
this.registered_.length) {
assert(
false, 'delegate already registered: %s',
this.registered_[0].ctor || this.registered_[0].instance);
}
}
}
/**
* A delegate registry that allows multiple delegates, each of which must have a
* numeric priority specified when it is registered. Iteration will start with
* the highest number and proceed to the lowest number. If two delegates are
* added with the same priority, an error will be given in debug mode.
* @see DelegateRegistry
*
* @extends {DelegateRegistryBase<T>}
* @template T
*/
DelegateRegistry.Prioritized = class extends DelegateRegistryBase {
/**
* @param {function(new: T, ...?)} ctor
* @param {number} priority
*/
registerClass(ctor, priority) {
this.add_({ctor, priority});
}
/**
* @param {T} instance
* @param {number} priority
*/
registerInstance(instance, priority) {
this.add_({instance, priority});
}
/**
* @param {!Registration<T>} registration
* @private
*/
add_(registration) {
this.checkRegistration_();
const priority = registration.priority;
// Note: index will always be negative since the evaluator never returns 0.
// This ensures that ties will be broken to the right. Sort highest-first.
const index =
~binarySelect(this.registered_, (r) => r.priority < priority ? -1 : 1);
const previous = index > 0 ? this.registered_[index - 1] : null;
if (ENABLE_ASSERTS && previous && previous.priority <= priority) {
assert(
false, 'two delegates registered with same priority (%s): %s and %s',
priority, previous.ctor || previous.instance,
registration.ctor || registration.instance);
}
this.registered_.splice(index, 0, registration);
}
};
exports = DelegateRegistry;