UNPKG

updraft

Version:

Javascript ORM-like storage in SQLite (WebSQL or other), synced to the cloud

360 lines (320 loc) 9.16 kB
// written to React"s immutability helpers spec // see https://facebook.github.io/react/docs/update.html ///<reference path="../typings/index.d.ts"/> ///<reference path="./assign.ts"/> ///<reference path="./verify.ts"/> namespace Updraft { export namespace Delta { export interface setter<T> { $set: T; } export interface increment { $inc: number; } export interface push<T> { $push: Array<T>; } export interface unshift<T> { $unshift: Array<T>; } export interface splice<T> { // array.splice(start, deleteCount[, item1[, item2[, ...]]]) $splice: Array<Array<number | T>>; } export interface merge<T> { $merge: T; } export interface add<T> { $add: Array<T>; } export interface deleter<T> { $delete: Array<T>; } export type primitive<T> = setter<T>; export type none = void; export type bool = primitive<boolean>; export type num = primitive<number> | increment; export type str = primitive<string>; export type date = setter<Date>; export type obj = primitive<Object> | merge<Object> | deleter<string>; export type enm<T> = primitive<T>; export type array<T> = setter<Array<T>> | push<T> | unshift<T> | splice<T> | merge<T>; export type strArray = array<string>; export type numArray = array<number>; export type objArray = array<Object>; export type set<T> = setter<Set<T>> | add<T> | deleter<T>; export type strSet = set<string>; } export function shallowCopy<T>(x: T): T { /* istanbul ignore else: not sure about this one */ if (Array.isArray(x)) { return (<any>x).concat(); } else if (x instanceof Set) { return <any>new Set<T>(<any>x); } else if (typeof x === "object") { return assign(new (<any>x).constructor(), x); } else { /* istanbul ignore next: correct AFAIK but unreachable */ return x; } } export function shallowEqual<T>(a: T, b: T): boolean { if (Array.isArray(a) && Array.isArray(b)) { let aa: any[] = <any>a; let bb: any[] = <any>b; if (aa.length == bb.length) { for (let i = 0; i < aa.length; i++) { if (aa[i] != bb[i]) { return false; } } return true; } return false; } else if (a instanceof Set && b instanceof Set) { let aa: Set<any> = <any>a; let bb: Set<any> = <any>b; if (aa.size == bb.size) { let equal = true; aa.forEach((elt) => { if (equal && !bb.has(elt)) { equal = false; } }); return equal; } return false; } else if (a instanceof Date && b instanceof Date) { return (<Date><any>a).getTime() == (<Date><any>b).getTime(); } else if (a && typeof a == "object" && b && typeof b == "object") { let akeys = Object.keys(a); let bkeys = Object.keys(b); if (akeys.length == bkeys.length) { for (let key of akeys) { if (!(key in b) || a[key] != b[key]) { return false; } } return true; } return false; } return a == b; } export let hasOwnProperty = {}.hasOwnProperty; export function keyOf(obj: Object) { return Object.keys(obj)[0]; } let command = { set: keyOf({$set: null}), increment: keyOf({$inc: null}), push: keyOf({$push: null}), unshift: keyOf({$unshift: null}), splice: keyOf({$splice: null}), merge: keyOf({$merge: null}), add: keyOf({$add: null}), deleter: keyOf({$delete: null}), }; function verifyArrayCase(value: any, spec: any, c: string) { verify( Array.isArray(value), "update(): expected target of %s to be an array; got %s.", c, value ); let specValue = spec[c]; verify( Array.isArray(specValue), "update(): expected spec of %s to be an array; got %s. " + "Did you forget to wrap your parameter in an array?", c, specValue ); } function verifySetCase(value: any, spec: any, c: string) { verify( value instanceof Set, "update(): expected target of %s to be a set; got %s.", c, value ); let specValue = spec[c]; verify( Array.isArray(specValue), "update(): expected spec of %s to be an array; got %s. " + "Did you forget to wrap your parameter in an array?", c, specValue ); } export function update<Element, Delta>(value: Element, spec: Delta): Element { verify( typeof spec === "object", "update(): You provided a key path to update() that did not contain one " + "of %s. Did you forget to include {%s: ...}?", Object.keys(command).join(", "), command.set ); // verify( // Object.keys(spec).reduce( function(previousValue: boolean, currentValue: string): boolean { // return previousValue && (keyOf(spec[currentValue]) in command); // }, true), // "update(): argument has an unknown key; supported keys are (%s). delta: %s", // Object.keys(command).join(", "), // spec // ); if (hasOwnProperty.call(spec, command.set)) { verify( Object.keys(spec).length === 1, "Cannot have more than one key in an object with %s", command.set ); return shallowEqual(value, spec[command.set]) ? value : spec[command.set]; } if (hasOwnProperty.call(spec, command.increment)) { verify( typeof(value) === "number" && typeof(spec[command.increment]) === "number", "Source (%s) and argument (%s) to %s must be numbers", value, spec[command.increment], command.increment ); return value + spec[command.increment]; } let changed = false; if (hasOwnProperty.call(spec, command.merge)) { let mergeObj = spec[command.merge]; let nextValue = <any>shallowCopy(value); verify( mergeObj && typeof mergeObj === "object", "update(): %s expects a spec of type 'object'; got %s", command.merge, mergeObj ); verify( nextValue && typeof nextValue === "object", "update(): %s expects a target of type 'object'; got %s", command.merge, nextValue ); assign(nextValue, spec[command.merge]); return shallowEqual(value, nextValue) ? value : nextValue; } if (hasOwnProperty.call(spec, command.deleter) && (typeof value === "object") && !(value instanceof Set)) { let keys = <any[]>spec[command.deleter]; verify( keys && Array.isArray(keys), "update(): %s expects a spec of type 'array'; got %s", command.deleter, keys ); let nextValue = <any>shallowCopy(value); changed = false; keys.forEach((key: string) => { if (key in value) { delete nextValue[key]; changed = true; } }); return changed ? <any>nextValue : value; } if (hasOwnProperty.call(spec, command.push)) { let nextValue: any[] = <any>shallowCopy(value) || []; verifyArrayCase(nextValue, spec, command.push); if (spec[command.push].length) { nextValue.push.apply(nextValue, spec[command.push]); return <any>nextValue; } else { return value; } } if (hasOwnProperty.call(spec, command.unshift)) { verifyArrayCase(value, spec, command.unshift); if (spec[command.unshift].length) { let nextValue: any[] = <any>shallowCopy(value); nextValue.unshift.apply(nextValue, spec[command.unshift]); return <any>nextValue; } else { return value; } } if (hasOwnProperty.call(spec, command.splice)) { let nextValue: any = <any>shallowCopy(value); verify( Array.isArray(value), "Expected %s target to be an array; got %s", command.splice, value ); verify( Array.isArray(spec[command.splice]), "update(): expected spec of %s to be an array of arrays; got %s. " + "Did you forget to wrap your parameters in an array?", command.splice, spec[command.splice] ); spec[command.splice].forEach(function(args: any) { verify( Array.isArray(args), "update(): expected spec of %s to be an array of arrays; got %s. " + "Did you forget to wrap your parameters in an array?", command.splice, spec[command.splice] ); (<any>nextValue).splice.apply(nextValue, args); }); return shallowEqual(nextValue, value) ? value : nextValue; } if (hasOwnProperty.call(spec, command.add)) { let nextValue: Set<any> = <any>shallowCopy(value) || new Set<any>(); verifySetCase(nextValue, spec, command.add); spec[command.add].forEach(function(item: any) { if (!nextValue.has(item)) { nextValue.add(item); changed = true; } }); return changed ? <any>nextValue : value; } if (hasOwnProperty.call(spec, command.deleter) && (value instanceof Set)) { let nextValue: Set<any> = <any>shallowCopy(value); verifySetCase(value, spec, command.deleter); spec[command.deleter].forEach(function(item: any) { if (nextValue.delete(item)) { changed = true; } }); return changed ? <any>nextValue : value; } let nextValue: any; for (let k in spec) { if (typeof value === "object" && !(command.hasOwnProperty(k))) { let oldValue = value[k]; let newValue = update(oldValue, spec[k]); if (oldValue !== newValue) { if (!nextValue) { nextValue = <any>shallowCopy(value); } nextValue[k] = newValue; changed = true; } } } return changed ? nextValue : value; } }