Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active August 18, 2019 19:02
Show Gist options
  • Save webstrand/ae055442c6235241a41614e939e33d07 to your computer and use it in GitHub Desktop.
Save webstrand/ae055442c6235241a41614e939e33d07 to your computer and use it in GitHub Desktop.
Subscribable value emitter
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