Skip to content

Instantly share code, notes, and snippets.

@sjkillen
Last active March 8, 2018 02:05
Show Gist options
  • Save sjkillen/57e58fbc36509e6c1c4cdb47617053ee to your computer and use it in GitHub Desktop.
Save sjkillen/57e58fbc36509e6c1c4cdb47617053ee to your computer and use it in GitHub Desktop.
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);
}
}
}
}
}
/**
* 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