Initial Commit
This commit is contained in:
3
src/assertions/assertUnreachable.ts
Normal file
3
src/assertions/assertUnreachable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function assertUnreachable(never: never): never {
|
||||
throw new Error(`Should not have reached [${never}]`);
|
||||
}
|
||||
3
src/assertions/index.ts
Normal file
3
src/assertions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import assertUnreachable from './assertUnreachable';
|
||||
|
||||
export {assertUnreachable};
|
||||
23
src/disposables/Disposable.ts
Normal file
23
src/disposables/Disposable.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type IDisposable from './IDisposable';
|
||||
|
||||
export default class Disposable implements IDisposable {
|
||||
private _isDisposed = false;
|
||||
private _disposable: () => void;
|
||||
|
||||
constructor(disposable: () => void) {
|
||||
this._disposable = disposable;
|
||||
}
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposable();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
17
src/disposables/DisposableList.ts
Normal file
17
src/disposables/DisposableList.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type IDisposable from './IDisposable';
|
||||
|
||||
export default class DisposableList implements IDisposable {
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
|
||||
public add(disposable: IDisposable): void {
|
||||
this._disposables.push(disposable);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
while (this._disposables.length) {
|
||||
const disposable = this._disposables.shift() as IDisposable;
|
||||
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/disposables/IDisposable.ts
Normal file
3
src/disposables/IDisposable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
6
src/disposables/index.ts
Normal file
6
src/disposables/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type IDisposable from './IDisposable';
|
||||
import Disposable from './Disposable';
|
||||
import DisposableList from './DisposableList';
|
||||
|
||||
export type {IDisposable};
|
||||
export {Disposable, DisposableList};
|
||||
23
src/events/EventEmitter.ts
Normal file
23
src/events/EventEmitter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type IDisposable from '../disposables/IDisposable';
|
||||
import type IEvent from './IEvent';
|
||||
import {Disposable} from '../disposables';
|
||||
|
||||
export default class EventEmitter<T> implements IDisposable {
|
||||
private readonly _listeners: Array<(event: IEvent<T>) => void> = [];
|
||||
|
||||
public subscribe(listener: (event: IEvent<T>) => void): Disposable {
|
||||
const listenerIndex = this._listeners.push(listener);
|
||||
|
||||
return new Disposable(() => this._listeners.splice(listenerIndex - 1, 1));
|
||||
}
|
||||
|
||||
public emit<Event extends IEvent<T>>(event: Event): void {
|
||||
this._listeners.forEach(listener => listener(event));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
while (this._listeners.length) {
|
||||
this._listeners.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/events/EventPublisher.ts
Normal file
39
src/events/EventPublisher.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type IEvent from './IEvent';
|
||||
import {Disposable, DisposableList} from '../disposables';
|
||||
import EventEmitter from './EventEmitter';
|
||||
|
||||
export default class EventPublisher {
|
||||
private readonly _eventEmitters: Record<string, EventEmitter<any>> = {};
|
||||
private readonly _disposables = new DisposableList();
|
||||
|
||||
constructor() {
|
||||
this._eventEmitters['no-subscribers'] = new EventEmitter();
|
||||
}
|
||||
|
||||
public addNoSubscriberHandler(handler: (event: IEvent<any>) => Promise<void> | void) {
|
||||
return this._eventEmitters['no-subscribers'].subscribe(handler);
|
||||
}
|
||||
|
||||
public subscribe<T>(event: string | number, handler: (event: IEvent<T>) => Promise<void> | void): Disposable {
|
||||
const emitter = this._eventEmitters[event] ?? this.createEmitter<T>(event);
|
||||
const subscription = emitter.subscribe(handler);
|
||||
|
||||
this._disposables.add(subscription);
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public publish<T>(eventName: string | number, event: IEvent<T>): void {
|
||||
(this._eventEmitters[eventName] || this._eventEmitters['no-subscribers']).emit<IEvent<T>>(event);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
Object.values(this._eventEmitters).forEach(emitter => emitter.dispose());
|
||||
}
|
||||
|
||||
private createEmitter<T>(event: string | number): EventEmitter<IEvent<T>> {
|
||||
const eventEmitter = new EventEmitter<IEvent<T>>();
|
||||
|
||||
return (this._eventEmitters[event] = eventEmitter);
|
||||
}
|
||||
}
|
||||
4
src/events/IEvent.ts
Normal file
4
src/events/IEvent.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default interface IEvent<Payload> {
|
||||
type: string;
|
||||
payload?: Payload;
|
||||
}
|
||||
6
src/events/index.ts
Normal file
6
src/events/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type IEvent from './IEvent';
|
||||
import EventEmitter from './EventEmitter';
|
||||
import EventPublisher from './EventPublisher';
|
||||
|
||||
export type {IEvent};
|
||||
export {EventEmitter, EventPublisher};
|
||||
3
src/functions/index.ts
Normal file
3
src/functions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import {createRangeIterator} from './rangeIterator';
|
||||
|
||||
export {createRangeIterator};
|
||||
16
src/functions/rangeIterator.ts
Normal file
16
src/functions/rangeIterator.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function createRangeIterator(start: number, end: number, step = 1) {
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next() {
|
||||
if (start < end) {
|
||||
start = start + step;
|
||||
|
||||
return {value: start, done: false};
|
||||
}
|
||||
|
||||
return {value: end, done: true};
|
||||
}
|
||||
};
|
||||
}
|
||||
12
src/index.ts
Normal file
12
src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type IDisposable from './disposables/IDisposable';
|
||||
import type IEvent from './events/IEvent';
|
||||
|
||||
export type {IDisposable, IEvent};
|
||||
|
||||
export {assertUnreachable} from './assertions';
|
||||
export {Disposable, DisposableList} from './disposables';
|
||||
export {EventEmitter, EventPublisher} from './events';
|
||||
export {Averager, Random} from './maths';
|
||||
export {Subject, ReadOnlySubject} from './observables';
|
||||
export {Strings} from './strings';
|
||||
export {createRangeIterator} from './functions';
|
||||
33
src/maths/Averager.ts
Normal file
33
src/maths/Averager.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {Subject, ReadOnlySubject} from '../observables';
|
||||
|
||||
export default class Averager {
|
||||
private readonly _sampleSize: number;
|
||||
private readonly _samples: number[] = [];
|
||||
private readonly _average = new Subject<number>(0);
|
||||
private readonly _readOnlyAverage = new ReadOnlySubject<number>(this._average);
|
||||
private _onPush: (sample: number) => void = this.onPushOnly.bind(this);
|
||||
|
||||
constructor(sampleSize: number) {
|
||||
this._sampleSize = sampleSize;
|
||||
}
|
||||
|
||||
get average(): ReadOnlySubject<number> {
|
||||
return this._readOnlyAverage;
|
||||
}
|
||||
|
||||
public push(value: number): void {
|
||||
this._onPush(value);
|
||||
}
|
||||
|
||||
private onPushOnly(sample: number) {
|
||||
if (this._samples.push(sample) === this._sampleSize) {
|
||||
this._onPush = this.pushAndCalculateAverage.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
private pushAndCalculateAverage(sample: number) {
|
||||
this._samples.push(sample);
|
||||
this._samples.shift();
|
||||
this._average.value = this._samples.reduce((sum, sampleValue) => sum + sampleValue, 0) / this._sampleSize;
|
||||
}
|
||||
}
|
||||
23
src/maths/Random.ts
Normal file
23
src/maths/Random.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export default class Random {
|
||||
private static readonly lowercaseLetters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
private static readonly uppercaseLatters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private static readonly letters = this.lowercaseLetters + this.uppercaseLatters;
|
||||
|
||||
public static number(): number {
|
||||
return Math.random();
|
||||
}
|
||||
|
||||
public static numberInRange(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
public static string(length: number): string {
|
||||
let result = '';
|
||||
|
||||
for (let idx = 0; idx < length; idx++) {
|
||||
result += this.letters[this.numberInRange(0, this.letters.length - 1)];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
4
src/maths/index.ts
Normal file
4
src/maths/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Averager from './Averager';
|
||||
import Random from './Random';
|
||||
|
||||
export {Averager, Random};
|
||||
18
src/observables/ReadOnlySubject.ts
Normal file
18
src/observables/ReadOnlySubject.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Disposable from '../disposables/Disposable';
|
||||
import Subject from './Subject';
|
||||
|
||||
export default class ReadOnlySubject<T> {
|
||||
private readonly _subject: Subject<T>;
|
||||
|
||||
constructor(subject: Subject<T>) {
|
||||
this._subject = subject;
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._subject.value;
|
||||
}
|
||||
|
||||
public subscribe(listener: (value: T) => void): Disposable {
|
||||
return this._subject.subscribe(listener);
|
||||
}
|
||||
}
|
||||
39
src/observables/Subject.ts
Normal file
39
src/observables/Subject.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import Disposable from '../disposables/Disposable';
|
||||
|
||||
export default class Subject<T> {
|
||||
private readonly _listeners: Array<(value: T) => void> = [];
|
||||
private _value: T;
|
||||
|
||||
constructor(value: T) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
set value(value: T) {
|
||||
if (this._value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._value = value;
|
||||
this.notifyListeners(value);
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public subscribe(listener: (value: T) => void): Disposable {
|
||||
const listenerIndex = this._listeners.push(listener);
|
||||
|
||||
listener(this.value);
|
||||
|
||||
return new Disposable(() => {
|
||||
this._listeners.splice(listenerIndex, 1);
|
||||
});
|
||||
}
|
||||
|
||||
private notifyListeners(value: T): void {
|
||||
for (const listener of this._listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/observables/index.ts
Normal file
4
src/observables/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Subject from './Subject';
|
||||
import ReadOnlySubject from './ReadOnlySubject';
|
||||
|
||||
export {Subject, ReadOnlySubject};
|
||||
15
src/strings/index.ts
Normal file
15
src/strings/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export class Strings {
|
||||
static random(length = 8) {
|
||||
if (length > 11) {
|
||||
throw new Error(`[Strings] max length [11]`);
|
||||
}
|
||||
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.substring(2, length + 2);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
throw new Error('Strings is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user