Last active
March 8, 2018 02:05
-
-
Save sjkillen/57e58fbc36509e6c1c4cdb47617053ee to your computer and use it in GitHub Desktop.
This file contains hidden or 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
interface EventRegister<T> { | |
event: keyof T, | |
// any used here because ts is picky, doesn't make a difference | |
// may be able to replace with T[keyof T] in future versions | |
callback: {(data: any): void}, | |
once: boolean | |
} | |
/** | |
* Basic callback based publish / subscribe baseclass | |
* Based off node.js 's EventEmitter | |
* Typing mechanism based off lib.d.ts for WebSocket events | |
* @param <T> an interface of keys/values "event" => EventType | |
* See Projections.ts for subclass implementation | |
*/ | |
export class EventEmitter<T> { | |
private callbacks: EventRegister<T>[] = []; | |
/** | |
* Register a listener for an event | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
on<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
this.callbacks.push({ | |
event, callback, once: false | |
}); | |
} | |
/** | |
* Same as .on(), but gets removed after it runs once | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
once<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
this.callbacks.push({ | |
event, callback, once: true | |
}); | |
} | |
/** | |
* Stop listening for an event | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
off<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
let i = 0; | |
for (const other of this.callbacks) { | |
if (event === other.event && callback === other.callback) { | |
this.callbacks.splice(i, 1); | |
} | |
i++; | |
} | |
} | |
/** | |
* Fire an event and execute all listener callbacks | |
* @param event name | |
* @param eventData to send to callbacks | |
*/ | |
protected emit<B extends keyof T>(event: B, eventData: T[B]) { | |
for (const register of this.callbacks) { | |
if (register.event === event) { | |
register.callback(eventData); | |
if (register.once) { | |
this.off(register.event, register.callback); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or 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
/** | |
* Some useful classes to be used throughout the project | |
*/ | |
/** | |
* SVG namespace | |
*/ | |
export const SVGNS = "http://www.w3.org/2000/svg"; | |
/** | |
* Make sure a DOM query returns an element | |
*/ | |
export function assertElement(test: Element | null ): Element { | |
if (!test) throw new Error("Invalid Template"); | |
return test; | |
} | |
/** | |
* Iterate an iterable with its indices | |
* yielded indices will start at offset | |
*/ | |
export function *enumerate<T>(iter: Iterable<T> | Array<T>, offset = 0): IterableIterator<[number, T]> { | |
if (Array.isArray(iter)) { | |
yield *offsetArrayEnumerate(iter, offset); | |
return; | |
} | |
let i = -1; | |
for (const item of iter) { | |
i++; | |
if (i >= offset){ | |
yield [i, item]; | |
} | |
} | |
} | |
/** | |
* Iterate an array with indices but start at a given index | |
* yielded indices will start at offset | |
* @param array to iterate | |
* @param offset to start at | |
*/ | |
function *offsetArrayEnumerate<T>(array: Array<T>, offset = 0): IterableIterator<[number, T]> { | |
for (let i = offset; i < array.length; i++) { | |
yield [i, array[i]]; | |
} | |
} | |
/** | |
* Iterator that supports peeking without removing | |
*/ | |
export interface PeekableIterator<T> extends IterableIterator<T> { | |
peek(): IteratorResult<T> | |
} | |
export namespace PeekableIterator { | |
/** | |
* Upgrades an iterable to be peekable | |
* If given an iterator, iterator is not clones | |
* @param o iterable | |
*/ | |
export function from<T>(o: Iterable<T>): PeekableIterator<T> { | |
const iter = o[Symbol.iterator](); | |
return <any>{ | |
store: null, | |
__proto__: iter, | |
peek(): IteratorResult<T> { | |
if (!this.store) { | |
this.store = this.next(); | |
} | |
return this.store; | |
}, | |
next(): IteratorResult<T> { | |
if (this.store) { | |
const o = this.store; | |
this.store = null; | |
return o; | |
} else { | |
return this.next(); | |
} | |
} | |
}; | |
} | |
} | |
/** | |
* Install a service worker if they are supported, otherwise do nothing | |
* @param path to worker | |
* @param scope of worker | |
* From: https://github.com/mdn/sw-test/ | |
*/ | |
export function installServiceWorker(path: string, scope: string) { | |
if ('serviceWorker' in navigator) { | |
navigator.serviceWorker.register(path, { scope }) | |
.then(reg => { | |
if(reg.installing) { | |
console.log('Service worker installing'); | |
} else if(reg.waiting) { | |
console.log('Service worker installed'); | |
} else if(reg.active) { | |
console.log('Service worker active'); | |
} | |
}).catch(error => { | |
console.error('Registration failed with: ', error); | |
}); | |
} | |
} | |
/** | |
* Interface to make sure event listeners get cleaned up | |
* Not needed for classes that live throughout the entire | |
* duration of the application | |
*/ | |
export interface Destructable { | |
destroy(): void; | |
} | |
export namespace Destructable { | |
export function isDestructable(test: any | Destructable): test is Destructable { | |
return typeof test.destroy === "function" | |
} | |
} | |
/** | |
* Specify dimensions for 2D things | |
*/ | |
export interface Dimensions { | |
width: number; | |
height: number; | |
} | |
/** | |
* Make an ajax request and return text | |
* @param url to fetch | |
* @param callback when request is complete. | |
*/ | |
export function getText(url: string): Promise<string> { | |
return new Promise((resolve, reject) => { | |
const request = new XMLHttpRequest(); | |
request.onreadystatechange = function() { | |
if (request.readyState == 4 && request.status == 200) { | |
resolve(request.responseText); | |
} else if (request.status >= 400) { | |
reject(new Error("Request Failed")); | |
} | |
} | |
request.open("GET", url, true); | |
request.send(); | |
}) | |
} | |
/** | |
* HTTP Get binary data. Works on node and browser | |
* Detect node enviroment from: https://stackoverflow.com/questions/17575790 | |
* @param url to fetch from | |
* @returns the binary data | |
*/ | |
export const getBinary = (function(){ | |
const isNode = (new Function(`try { return this !== window } catch(e) { return true }`))(); | |
if (isNode) { | |
/** | |
* This library is already cross platform, but too big for frontend | |
* require("xhr-request") is ignored in webpack.config.js | |
*/ | |
const request = require("xhr-request") as any; | |
return function(url: string): Promise<ArrayBuffer> { | |
return new Promise((resolve, reject) => { | |
// Force http with ambiguous urls | |
url = url.replace(/^(https:|http:){0}\/\//, "http://"); | |
request(url, { | |
method: 'GET', | |
responseType: 'arraybuffer', | |
}, | |
(err: Error, data: ArrayBuffer) => { | |
if (err) reject(err); | |
else resolve(data); | |
} | |
); | |
}) | |
} | |
} | |
return function(url: string): Promise<ArrayBuffer> { | |
return new Promise((resolve, reject) => { | |
const request = new XMLHttpRequest(); | |
request.responseType = "arraybuffer"; | |
request.onload = () => { | |
const buffer = request.response; | |
if (buffer) { | |
resolve(buffer); | |
} else { | |
reject(new Error("no data returned")); | |
} | |
}; | |
request.open("GET", `http:${url}`, true); | |
request.send(); | |
}) | |
} | |
}()) | |
/** | |
* Promise that can be canceled and replaced with another promise | |
* transparently to all waiters | |
* @todo could be improved with real cancelable promises | |
* once they are standardized | |
*/ | |
type promiseExecutor<T> = (resolve: (v?: T | PromiseLike<T>) => void, reject: (v?: T | PromiseLike<T>) => void ) => void; | |
export class ReplaceablePromise<T> extends Promise<T> { | |
private resolve: (v?: T | PromiseLike<T>) => void; | |
private reject: (v?: T | PromiseLike<T>) => void; | |
private version: Symbol; | |
/** | |
* Constructor same as Promise.constructor | |
* @param provide | |
*/ | |
constructor(provide: promiseExecutor<T>) { | |
let res: (v?: T | PromiseLike<T>) => void, | |
rej: (v?: T | PromiseLike<T>) => void; | |
super((resolve, reject) => { | |
// `this` does not exist yet inside function | |
// because super calls function synchronously | |
res = resolve; | |
rej = reject; | |
}); | |
// This immediately invoked function trickery is to fool | |
// typescript into thinking that it is called async | |
// because it assumes the above is called async | |
// (Try removing the IIF if you are skeptical) | |
// See: https://github.com/Microsoft/TypeScript/issues/11498 | |
// Or my dupe :/ | |
// https://github.com/Microsoft/TypeScript/issues/16186 | |
(function(this: ReplaceablePromise<T>) { | |
this.resolve = res; | |
this.reject = rej; | |
this.setup(provide) | |
}).call(this); | |
} | |
/** | |
* Set up callbacks for when promises are resolved | |
* @param then promise constructor callback or .then function of a new promise | |
*/ | |
private setup(then: promiseExecutor<T>) { | |
this.version = Symbol(); | |
const version = this.version; | |
then( | |
v => { | |
if (version == this.version) { | |
this.resolve(v); | |
} | |
}, | |
v => { | |
if (version == this.version) { | |
this.reject(v); | |
} | |
} | |
); | |
} | |
/** | |
* Replace this promise with another one | |
* any .then's will be resolved after withNew resolves | |
* @param withNew promise to use | |
*/ | |
replace(withNew: Promise<T>) { | |
this.setup(withNew.then.bind(withNew)); | |
} | |
/** | |
* Promise that never resolves (but can be replaced with something that does resolve) | |
*/ | |
static never = new ReplaceablePromise(() => {}); | |
} | |
interface EventRegister<T> { | |
event: keyof T, | |
// any used here because ts is picky, doesn't make a difference | |
// may be able to replace with T[keyof T] in future versions | |
callback: {(data: any): void}, | |
once: boolean | |
} | |
/** | |
* Basic callback based publish / subscribe baseclass | |
* Based off node.js 's EventEmitter | |
* Typing mechanism based off lib.d.ts for WebSocket events | |
* @param <T> an interface of keys/values "event" => EventType | |
* See Projections.ts for subclass implementation | |
*/ | |
export class EventEmitter<T> { | |
private callbacks: EventRegister<T>[] = []; | |
/** | |
* Register a listener for an event | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
on<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
this.callbacks.push({ | |
event, callback, once: false | |
}); | |
} | |
/** | |
* Same as .on(), but gets removed after it runs once | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
once<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
this.callbacks.push({ | |
event, callback, once: true | |
}); | |
} | |
/** | |
* Similar to on, but generates promises | |
* @todo make it stoppable | |
*/ | |
*when<B extends keyof T>(event: B): IterableIterator<Promise<T[B]>> { | |
const waiters: {(v: T[B]): void}[] = []; | |
const backlog: T[B][] = []; | |
this.on(event, data => { | |
const waiter = waiters.shift(); | |
if (waiter) { | |
waiter(data); | |
} else { | |
backlog.push(data); | |
} | |
}); | |
for (;;) { | |
yield new Promise<T[B]>(resolve => { | |
if (backlog.length) { | |
resolve(backlog.shift()); | |
} else { | |
waiters.push(resolve); | |
} | |
}); | |
} | |
} | |
/** | |
* Stop listening for an event | |
* @param event name | |
* @param callback to execute when event is emitted | |
*/ | |
off<B extends keyof T>(event: B, callback: {(data: T[B]): void}) { | |
let i = 0; | |
for (const other of this.callbacks) { | |
if (event === other.event && callback === other.callback) { | |
this.callbacks.splice(i, 1); | |
} | |
i++; | |
} | |
} | |
/** | |
* Fire an event and execute all listener callbacks | |
* @param event name | |
* @param eventData to send to callbacks | |
*/ | |
protected emit<B extends keyof T>(event: B, eventData: T[B]) { | |
for (const register of this.callbacks) { | |
if (register.event === event) { | |
register.callback(eventData); | |
if (register.once) { | |
this.off(register.event, register.callback); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Zip two iterators together | |
* Stop after the shortest iterator | |
*/ | |
export function *zip<A, B>(a: Iterable<A>, b: Iterable<B>): IterableIterator<[A, B]> { | |
const bIter = b[Symbol.iterator](); | |
for (const aItem of a) { | |
const { value: bItem, done } = bIter.next(); | |
if (done) break; | |
yield [aItem, bItem]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment