vlens
Version:
Data Centric Routing & Rendering Mini-Framework
218 lines (189 loc) • 7.23 kB
text/typescript
import * as refs from "./refs";
class ById {
constructor(public param: any) { }
}
// wraps an object in a special flag so that the cache uses the object's id instead of hashing its keys and values
export function byId(param: any): any {
if (typeof param === "object") {
return new ById(param)
} else {
return param
}
}
function makeSeed(): number {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
}
let _cache = new Map()
let _seed = makeSeed()
/**
The keys passed are hashed in such a way to avoid conflicts.
For example, if you pass the [97, 98] it will has to a different value than if you pass the string "ab"
This is because the type information is also used in the hash.
To help the cache be more robust in this way, don't pass results of calls to `refs.objectId`, instead, pass the object wrapped by `cache.byId`, and if you want to pass a function, pass it directly.
*/
export function get<P extends any[], R>(key: any, compute: (...p: P) => R, ...p: P): R {
let hash = hashAny(key, _seed);
let cached = _cache.get(hash)
if (cached !== undefined) {
return cached
} else {
let computed = compute.apply(null, p);
_cache.set(hash, computed);
return computed;
}
}
export function peek<T>(key: any): T | undefined {
let hash = hashAny(key, _seed);
return _cache.get(hash)
}
/**
Clears all references held by this cache to allow them to be garbage collected
*/
export function clearCache() {
// console.log("clearing cache")
_cache.clear()
_seed = makeSeed()
}
export function fn<P extends any[], R>(fn: (...p: P) => R, ...p: P): R {
return get([fn, ...p.map(byId)], fn, ...p)
}
export function fnPeek<P, T>(fn: (p: P) => T, p: P): T | undefined {
return peek([fn, byId(p)])
}
// small wrapper around the bind method to make it cachable
function _partial(target: Function, ...args: any[]): Function {
return target.bind(null, ...args)
}
export function partial<A0, A extends any[], R>(fn: (arg0: A0, ...args: A) => R, arg0: A0): (...args: A) => R;
export function partial<A0, A1, A extends any[], R>(fn: (arg0: A0, arg1: A1, ...args: A) => R, arg0: A0, arg1: A1): (...args: A) => R;
export function partial<A0, A1, A2, A extends any[], R>(fn: (arg0: A0, arg1: A1, arg2: A2, ...args: A) => R, arg0: A0, arg1: A1, arg2: A2): (...args: A) => R;
export function partial<A0, A1, A2, A3, A extends any[], R>(fn: (arg0: A0, arg1: A1, arg2: A2, arg3: A3, ...args: A) => R, arg0: A0, arg1: A1, arg2: A2, arg3: A3): (...args: A) => R;
export function partial(target: Function, ...args: any[]): Function {
return fn(_partial, target, ...args)
}
// simplifies the use of `cache.fn`, uses react hooks nomenclature
export function declareHook<P extends any[], R>(target: (...p: P) => R): (...p: P) => R {
function hook(...p: P): R {
return fn(target, ...p)
}
return hook;
}
// based on cyrb53
// can be used to hash an object or a list
// must always be composed of numbers, booleans, or strings.
// not supported: bigint, symbol
// functions are hashed by id (using refs.objectId)
// special ById class is used to hash objects by id
export function hashAny(input: any, seed = 0): number {
// We use these consts to identify data structure and make it robust against accidental similarity
// For example, if we have the array ['A', 'B'] and the object {'A': 'B'} we don't
// want to just digest the string 'A' then 'B' because then they will produce the same hash
// so we also digest information that describes the structure of the object
// ['A', 'B'] => ARRAY 2 ITEM 1 STRING 1 'A' ITEM 2 STRING 1 'B'
// {'A': 'B'} => OBJECT 1 KEY 1 'A' VALUE STRING 1 'B'
const NUM = 1;
const BOOL = 2;
const STRING = 3;
const ARRAY = 4;
const OBJECT = 5;
const NULL = 6;
const UNDEFINED = 7;
const KEY = 8;
const VALUE = 9;
const ITEM = 10;
const FUNC = 11;
const REF = 12;
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
function digest(n: number) {
h1 = Math.imul(h1 ^ n, 2654435761);
h2 = Math.imul(h2 ^ n, 1597334677);
}
function digestString(str: string) {
digest(str.length)
for (let i = 0; i < str.length; i++) {
let ch = str.charCodeAt(i);
digest(ch)
}
}
function digestObject(obj: any) {
let keys = Object.keys(obj);
// potential bottle neck but absolutely necessary for stability
keys.sort();
digest(OBJECT);
digest(keys.length);
for (let k of keys) {
digest(KEY);
digestString(k)
digest(VALUE);
digestAny(obj[k])
}
}
function digestArray(arr: any[]) {
digest(ARRAY);
digest(arr.length)
for (let i = 0; i < arr.length; i++) {
digest(ITEM);
digest(i);
digestAny(arr[i])
}
}
function digestAny(obj: any) {
switch (typeof obj) {
case 'number':
digest(NUM);
digest(obj);
break;
case 'boolean':
digest(BOOL);
digest(obj ? 1 : 0);
break;
case 'string':
digest(STRING);
digestString(obj);
break;
case 'object':
if (obj === null) {
digest(NULL);
} else if (Array.isArray(obj)) {
digestArray(obj);
} else if (obj instanceof ById) {
digest(REF);
digest(refs.objectId(obj.param))
} else {
digestObject(obj);
}
break;
case 'function':
digest(FUNC);
digest(refs.objectId(obj))
case 'undefined':
digest(UNDEFINED);
break;
default:
throw 'Unexpected type'
}
}
digestAny(input);
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
// cyrb53:
// source: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
export function cyrb53(str: string, seed = 0): number {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};