Last active
August 18, 2019 19:02
-
-
Save webstrand/ae055442c6235241a41614e939e33d07 to your computer and use it in GitHub Desktop.
Subscribable value emitter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Bind1<T> = T extends (this: unknown, ...args: infer V) => infer W ? (...args: V) => W : T extends (this: infer U, ...args: infer V) => infer W ? (this: U, ...args: V) => W : never; | |
export type EmitterParameters<T extends Emitter<any>> = T extends Emitter<infer U> ? Parameters<U> : []; | |
export interface EmitterEmittable<F extends (...args: any[]) => any, E extends (...args: any[]) => any = F> { | |
/* | |
* Emit the {@link values}. | |
* | |
* @param defaultContext Default context to bind each function's `this`. | |
* @param values Values to emit. | |
*/ | |
emit(...values: Parameters<E>): void; | |
/** | |
* Emit the {@link values} and return an array containing the remitted | |
* values. | |
* | |
* @param defaultContext Default context to bind each function's `this`. | |
* @param values Values to emit. | |
* @returns An array containing the remitted values. | |
*/ | |
remit(...values: Parameters<E>): ReturnType<E>[]; | |
} | |
export interface EmitterSubscribable<F extends (...args: any[]) => any> { | |
/** | |
* Subscribe the function {@link fn} to the Emitter. Overwrites any previous | |
* subscription via {@link on} or {@link once} for the specific | |
* {@link context}. | |
* | |
* Note: Subscriptions added during emission will receive the currently | |
* emitting values, except when {@link retainOrder} is true. It is not | |
* recommended to use retainOrder when the emitter could ever possibly be | |
* emitting. | |
* | |
* @param fn Function called when the emitter emits or collects values. | |
* @param context Context to bind {@link fn}'s `this` when emitting. | |
* @param retainOrder When overwriting a previous subscription, ensure that | |
* the dispatch order is preserved. | |
* @typeparam T Function type. | |
* @returns True when a subscription was added, false when a subscription | |
* was overwritten. | |
*/ | |
on<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder?: boolean): boolean; | |
/** | |
* Subscribe the function {@link fn} to the Emitter. Overwrites any previous | |
* subscription via {@link on} or {@link once} for the specific | |
* {@link context}. Immediately before {@link fn} is called during emission, | |
* the `[{@link fn}, {@link context}]` is unsubscribed from the emitter. | |
* | |
* Note: Subscriptions added during emission will receive the currently | |
* emitting values, except when {@link retainOrder} is true. It is not | |
* recommended to use retainOrder when the emitter could ever possibly be | |
* emitting. | |
* | |
* @param fn Function called when the emitter emits or collects values. | |
* @param context Context to bind {@link fn}'s `this` when emitting. | |
* @param retainOrder When overwriting a previous subscription, ensure that | |
* the dispatch order is preserved. | |
* @typeparam T Function type. | |
* @returns True when a subscription was added, false when a subscription | |
* was overwritten. | |
*/ | |
once<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder?: boolean): boolean; | |
/** | |
* Unsubscribe the function {@link fn} from the Emitter. | |
* | |
* @param fn Function called when the emitter emits or collects values. | |
* @param context Context to bind {@link fn}'s `this` when emitting. | |
* @typeparam T Function type. | |
* @returns True when a subscription was removed, false otherwise. | |
*/ | |
off<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder?: boolean): boolean; | |
/** | |
* Unsubscribe all functions from the Emitter. | |
*/ | |
clear(): void; | |
} | |
export interface Emitter<F extends (...args: any[]) => any, E extends (...args: any[]) => any = F> extends EmitterSubscribable<F>, EmitterEmittable<F, E> {} | |
export interface BoundEmitter<F extends (...args: any[]) => any> extends EmitterSubscribable<F>, EmitterEmittable<F, Bind1<F>> {} | |
/** | |
* A type representing a subscription of some function and context to an | |
* {@link Emitter}. | |
* | |
* @typeparam F Call signature of the subscription. | |
*/ | |
type Subscription<F extends (...args: any[]) => any> = { | |
fn: F; | |
context: unknown; | |
once: boolean; | |
} | |
export interface EmitterBaseSubscribable<F extends (...args: any[]) => any> { | |
add(subscription: Subscription<F>): unknown; | |
lookup<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F>; | |
remove<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F>; | |
clear(): void; | |
} | |
type EmitterBaseSubscribableConstructor<F extends (...args: any[]) => any> = new (...args: any[]) => EmitterBaseSubscribable<F>; | |
export function mixinSubscribe<Base extends EmitterBaseSubscribableConstructor<F>, F extends (...args: any[]) => any>(base: Base) { | |
return class extends base implements EmitterSubscribable<F> { | |
binding!: Parameters<F>[0]; | |
on<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder: boolean = false): boolean { | |
const subscription = retainOrder ? this.lookup(fn, context) : this.remove(fn, context); | |
if(subscription !== null) { | |
subscription.once = false; | |
if(!retainOrder) { | |
this.add(subscription); | |
} | |
return false; | |
} | |
else { | |
this.add({ fn, context, once: false }); | |
return true; | |
} | |
} | |
chainOn<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder: boolean = false): Parameters<F>[0] { | |
this.on(fn, context, retainOrder); | |
return this.binding; | |
} | |
once<T extends F>(fn: T, context: ThisParameterType<T>, retainOrder: boolean = false): boolean { | |
const subscription = retainOrder ? this.lookup(fn, context) : this.remove(fn, context); | |
if(subscription !== null) { | |
subscription.once = true; | |
if(!retainOrder) { | |
this.add(subscription); | |
} | |
return false; | |
} | |
else { | |
this.add({ fn, context, once: true }); | |
return true; | |
} | |
} | |
off<T extends F>(fn: T, context: ThisParameterType<T>): boolean { | |
return this.remove(fn, context) !== null; | |
} | |
} | |
} | |
export interface EmitterBaseEmittable<F extends (...args: any[]) => any> { | |
readonly size?: number; | |
delete(subscription: Subscription<F>): boolean; | |
slice(): Iterable<Subscription<F>> | |
} | |
type EmitterBaseEmittableConstructor<F extends (...args: any[]) => any> = new (...args: any[]) => EmitterBaseEmittable<F>; | |
export function mixinEmit<Base extends EmitterBaseEmittableConstructor<F>, F extends (...args: any[]) => any, E extends (...args: any[]) => any = F>(base: Base) { | |
return class extends base implements EmitterEmittable<F, E> { | |
emit(...values: Parameters<E>): void; | |
emit(...values: unknown[]): void { | |
const selector = values.length; | |
for(const subscription of this.slice()) { | |
const fn = subscription.fn; | |
const context = subscription.context; | |
if(subscription.once) this.delete(subscription); | |
switch (selector) { | |
case 0: context !== null ? fn.call(context) : fn(); break; | |
case 1: context !== null ? fn.call(context, values[0]) : fn(values[0]); break; | |
case 2: context !== null ? fn.call(context, values[0], values[1]) : fn(values[0], values[1]); break; | |
case 3: context !== null ? fn.call(context, values[0], values[1], values[2]) : fn(values[0], values[1], values[2]); break; | |
case 4: context !== null ? fn.call(context, values[0], values[1], values[2], values[3]) : fn(values[0], values[1], values[2], values[3]); break; | |
case 5: context !== null ? fn.call(context, values[0], values[1], values[2], values[3], values[4]) : fn(values[0], values[1], values[2], values[3], values[4]); break; | |
case 6: context !== null ? fn.call(context, values[0], values[1], values[2], values[3], values[4], values[5]) : fn(values[0], values[1], values[2], values[3], values[4], values[5]); break; | |
default: context !== null ? fn.apply(context, values) : fn(...values); | |
} | |
} | |
} | |
remit(...values: Parameters<E>): ReturnType<E>[]; | |
remit(...values: unknown[]): any[] { | |
const collection: unknown[] = new Array(this.size); | |
let n = 0; | |
const selector = values.length; | |
for(const subscription of this.slice()) { | |
const fn = subscription.fn; | |
const context = subscription.context; | |
if(subscription.once) this.delete(subscription); | |
switch (selector) { | |
case 0: collection[n] = context !== null ? fn.call(context) : fn(); break; | |
case 1: collection[n] = context !== null ? fn.call(context, values[0]) : fn(values[0]); break; | |
case 2: collection[n] = context !== null ? fn.call(context, values[0], values[1]) : fn(values[0], values[1]); break; | |
case 3: collection[n] = context !== null ? fn.call(context, values[0], values[1], values[2]) : fn(values[0], values[1], values[2]); break; | |
case 4: collection[n] = context !== null ? fn.call(context, values[0], values[1], values[2], values[3]) : fn(values[0], values[1], values[2], values[3]); break; | |
case 5: collection[n] = context !== null ? fn.call(context, values[0], values[1], values[2], values[3], values[4]) : fn(values[0], values[1], values[2], values[3], values[4]); break; | |
case 6: collection[n] = context !== null ? fn.call(context, values[0], values[1], values[2], values[3], values[4], values[5]) : fn(values[0], values[1], values[2], values[3], values[4], values[5]); break; | |
default: collection[n] = context !== null ? fn.apply(context, values) : fn(...values); | |
} | |
n += 1; | |
} | |
return collection; | |
} | |
} | |
} | |
export interface EmitterBaseBoundEmittable<F extends (...args: any[]) => any> { | |
readonly size?: number; | |
delete(subscription: Subscription<F>): boolean; | |
slice(): Iterable<Subscription<F>> | |
} | |
type EmitterBaseBoundEmittableConstructor<F extends (...args: any[]) => any> = new (...args: any[]) => EmitterBaseBoundEmittable<F>; | |
export function mixinBoundEmit<Base extends EmitterBaseBoundEmittableConstructor<F>, F extends (...args: any[]) => any>(base: Base) { | |
return class extends base implements EmitterEmittable<F, Bind1<F>> { | |
binding!: Parameters<F>[0]; | |
emit(...values: Parameters<Bind1<F>>): void; | |
emit(...values: unknown[]): void { | |
const selector = values.length; | |
const binding = this.binding; | |
for(const subscription of this.slice()) { | |
const fn = subscription.fn; | |
const context = subscription.context; | |
if(subscription.once) this.delete(subscription); | |
switch (selector) { | |
case 0: context !== null ? fn.call(context, binding) : fn(binding); break; | |
case 1: context !== null ? fn.call(context, binding, values[0]) : fn(binding, values[0]); break; | |
case 2: context !== null ? fn.call(context, binding, values[0], values[1]) : fn(binding, values[0], values[1]); break; | |
case 3: context !== null ? fn.call(context, binding, values[0], values[1], values[2]) : fn(binding, values[0], values[1], values[2]); break; | |
case 4: context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3]) : fn(binding, values[0], values[1], values[2], values[3]); break; | |
case 5: context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3], values[4]) : fn(binding, values[0], values[1], values[2], values[3], values[4]); break; | |
case 6: context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3], values[4], values[5]) : fn(binding, values[0], values[1], values[2], values[3], values[4], values[5]); break; | |
default: context !== null ? fn.call(context, binding, ...values) : fn(...values); | |
} | |
} | |
} | |
remit(...values: Parameters<Bind1<F>>): ReturnType<Bind1<F>>[]; | |
remit(...values: unknown[]): any[] { | |
const collection: unknown[] = new Array(this.size); | |
let n = 0; | |
const selector = values.length; | |
const binding = this.binding; | |
for(const subscription of this.slice()) { | |
const fn = subscription.fn; | |
const context = subscription.context; | |
if(subscription.once) this.delete(subscription); | |
switch (selector) { | |
case 0: collection[n] = context !== null ? fn.call(context, binding) : fn(binding); break; | |
case 1: collection[n] = context !== null ? fn.call(context, binding, values[0]) : fn(binding, values[0]); break; | |
case 2: collection[n] = context !== null ? fn.call(context, binding, values[0], values[1]) : fn(binding, values[0], values[1]); break; | |
case 3: collection[n] = context !== null ? fn.call(context, binding, values[0], values[1], values[2]) : fn(binding, values[0], values[1], values[2]); break; | |
case 4: collection[n] = context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3]) : fn(binding, values[0], values[1], values[2], values[3]); break; | |
case 5: collection[n] = context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3], values[4]) : fn(binding, values[0], values[1], values[2], values[3], values[4]); break; | |
case 6: collection[n] = context !== null ? fn.call(context, binding, values[0], values[1], values[2], values[3], values[4], values[5]) : fn(binding, values[0], values[1], values[2], values[3], values[4], values[5]); break; | |
default: collection[n] = context !== null ? fn.call(context, binding, ...values) : fn(...values); | |
} | |
n += 1; | |
} | |
return collection; | |
} | |
} | |
} | |
export class EmitterBaseArray<F extends (...args: any[]) => any> extends Array<Subscription<F>> implements EmitterBaseSubscribable<F>, EmitterBaseEmittable<F> { | |
constructor() { super() } | |
get size() { return this.length } | |
add(subscription: Subscription<F>): void { | |
this.push(subscription); | |
} | |
lookup<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F> { | |
for(let i = 0, len = this.length; i !== len; i++) { | |
const subscription = this[i]; | |
if(subscription.fn === fn && subscription.context === context) return subscription; | |
} | |
return null; | |
} | |
delete(subscription: Subscription<F>): boolean { | |
for(let i = 0, len = this.length; i !== len; i++) { | |
if(this[i] === subscription) { | |
this.splice(i, 1); | |
return true; | |
} | |
} | |
return false; | |
} | |
remove<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F> { | |
for(let i = 0, len = this.length; i !== len; i++) { | |
const subscription = this[i]; | |
if(subscription.fn === fn && subscription.context === context) { | |
this.splice(i, 1); | |
return subscription; | |
} | |
} | |
return null; | |
} | |
clear(): void { | |
this.length = 0; | |
} | |
} | |
export class EmitterBaseSet<F extends (...args: any[]) => any> extends Set<Subscription<F>> implements EmitterBaseSubscribable<F>, EmitterBaseEmittable<F> { | |
index: Map<F, Map<unknown, Subscription<F>>> = new Map(); | |
lookup<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F> { | |
const bucket = this.index.get(fn); | |
if(bucket !== void 0) { | |
const subscription = bucket.get(context); | |
if(bucket.has(context)) return subscription!; | |
} | |
return null; | |
} | |
remove<T extends F>(fn: T, context: ThisParameterType<T>): null | Subscription<F> { | |
const bucket = this.index.get(fn); | |
if(bucket !== void 0) { | |
const subscription = bucket.get(context); | |
if(bucket.delete(context)) return subscription!; | |
} | |
return null; | |
} | |
add(subscription: Subscription<F>) { | |
const fn = subscription.fn; | |
const context = subscription.context; | |
const bucket = this.index.get(fn); | |
if(bucket === void 0) { | |
this.index.set(fn, new Map([[context, subscription]])); | |
} | |
else { | |
bucket.set(context, subscription); | |
} | |
return super.add(subscription); | |
} | |
slice() { | |
return Array.from(this); | |
} | |
} | |
export const Emitter = class EmitterImpl<F extends (...args: any[]) => any> extends mixinEmit(mixinSubscribe(EmitterBaseArray))<F> implements Emitter<F> { | |
static readonly void = Object.assign(new EmitterImpl<any>(), { | |
add() { }, | |
remove() { return null }, | |
clear() {}, | |
delete() { return false }, | |
}); | |
} | |
export const BoundEmitter = class BoundEmitterImpl<F extends (...args: any[]) => any> extends mixinSubscribe(mixinBoundEmit(EmitterBaseArray))<F> implements BoundEmitter<F> { | |
constructor(public binding: Parameters<F>[0]) { super() } | |
static readonly void = Object.assign(new BoundEmitterImpl<any>(null), { | |
add() { }, | |
remove() { return null }, | |
clear() {}, | |
delete() { return false }, | |
}); | |
} | |
class IndexedEmitter<F extends (...args: any[]) => any> extends mixinEmit(mixinSubscribe(EmitterBaseSet))<F> implements Emitter<F> { | |
static readonly void = Object.assign(new IndexedEmitter<any>(), { | |
add() { }, | |
remove() { return null }, | |
clear() {}, | |
delete() { return false }, | |
}); | |
} | |
class BoundIndexedEmitter<F extends (...args: any[]) => any> extends mixinSubscribe(mixinBoundEmit(EmitterBaseSet))<F> implements BoundEmitter<F> { | |
constructor(public binding: Parameters<F>[0]) { super() } | |
static readonly void = Object.assign(new BoundIndexedEmitter<any>(null), { | |
add() { }, | |
remove() { return null }, | |
clear() {}, | |
delete() { return false }, | |
}); | |
} | |
Object.freeze(Emitter.void); | |
Object.freeze(BoundEmitter.void); | |
Object.freeze(IndexedEmitter.void); | |
Object.freeze(BoundIndexedEmitter.void); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment