// tslint:disable max-classes-per-file import * as jiff from 'jiff'; import * as moment from 'moment'; import { SubscribableOrPromise } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { Subscriber } from 'rxjs/Subscriber'; import { Subscription } from 'rxjs/Subscription'; import { ObjectUnsubscribedError } from 'rxjs/util/ObjectUnsubscribedError'; import { clone } from 'lodash'; import { isObject } from 'lodash'; import { isString } from 'lodash'; import { map } from 'lodash'; import { Deferred as IDeferred, JSONPatch, LocalForage } from './types'; let hiddenKeyRegex = /^\$+/; let TIMESTAMP_RE = /^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d)$/; // tslint:disable-line max-line-length export function convertJsonDates(jsonData: any) { let outObj; if (Array.isArray(jsonData)) { outObj = []; } else { outObj = {}; } for (let key of Object.keys(jsonData)) { let val = jsonData[key]; let res = val; if (isString(val)) { // The value is a string - does it match any of our things we want to convert if (TIMESTAMP_RE.test(val)) { // We're probably a timestamp let dt = moment(val); if (dt.isValid()) { res = dt.toDate(); } } } else if (Array.isArray(val) || isObject(val)) { res = convertJsonDates(val); } outObj[key] = res; } return outObj; } export function toJSON(obj: any) { return JSON.parse(JSON.stringify(obj)); } export function diff(obj1: any, obj2: any) { obj1 = toJSON(obj1); obj2 = toJSON(obj2); return jiff.diff(obj1, obj2); } export function applyPatch(res: any, patch: JSONPatch) { // Go to JSON - don't do anything with dates let obj = JSON.parse(JSON.stringify(res, toJsonReplacer)); obj = jiff.patch(patch, obj); // Go back to dates etc obj = convertJsonDates(obj); removeResValues(res); setResValues(res, obj); } export function mergeObjects(mine: any, old: any, yours: any) { // Make copies - we might modify the objects if ids dont exist mine = clone(mine); old = clone(old); // First sort out _id's. We could have a new id if one doesn't exist on mine and old if (!mine._id && !old._id && yours._id) { mine._id = yours._id; old._id = yours._id; } // We also need to convert into and out of dates mine = toJSON(mine); old = toJSON(old); yours = toJSON(yours); let yourpatch = jiff.diff(old, yours); let mypatch = jiff.diff(old, mine); let mypaths = map(mypatch, 'path'); let patch = yourpatch.filter((patchval: any) => { return mypaths.indexOf(patchval.path) === -1; }); let patched = jiff.patch(patch, mine); return convertJsonDates(patched); } export function forEachVal(res: any, cb: Function) { for (let key in res) { if (!hiddenKeyRegex.test(key)) { cb(res, key); } } } export function toJsonReplacer(key: any, value: any) { let val = value; if (isString(key) && key.charAt(0) === '$') { val = undefined; } return val; } export function fromJsonReviver(_: any, value: any) { let val = value; if (isString(value) && TIMESTAMP_RE.test(value)) { let dt = moment(value); if (dt.isValid()) { val = dt.toDate(); } } return val; } export function toObject(res: any) { // The toJsonReplacer gets rid of attributes beginning with $ and the fromJsonReviver // converts date strings back into dates let str = JSON.stringify(res, toJsonReplacer); try { return JSON.parse(str, fromJsonReviver); } catch (err) { // Older versions of IE8 throw 'Out of stack space' errors if we use a reviver function // due to the bug described here: http://support.microsoft.com/kb/976662. Everyone hates // IE if (err.message === 'Out of stack space') { return convertJsonDates(JSON.parse(str)); } throw(err); } } export function removeResValues(res: any) { forEachVal(res, (_: any, key: any) => { delete res[key]; }); } export function setResValues(res: any, vals: any) { for (let key of Object.keys(vals)) { res[key] = vals[key]; } } export function persistentStorageKey(url: string) { return 'or2ws:' + url; } export function advancedStorage(localForage: LocalForage) { return localForage.driver() !== 'localStorageWrapper'; } // lifted from here -> http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 export function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); // tslint:disable-line return v.toString(16); }); } export class Deferred implements IDeferred { public promise: Promise; public resolve: (val?: T) => void; public reject: (err: Error) => void; constructor() { /* A method to resolve the associated Promise with the value passed. * If the promise is already settled it does nothing. * * @param {anything} value : This value is used to resolve the promise * If the value is a Promise then the associated promise assumes the state * of Promise passed as value. */ this.resolve = null; /* A method to reject the assocaited Promise with the value passed. * If the promise is already settled it does nothing. * * @param {anything} reason: The reason for the rejection of the Promise. * Generally its an Error object. If however a Promise is passed, then the Promise * itself will be the reason for rejection no matter the state of the Promise. */ this.reject = null; /* A newly created Pomise object. * Initially in pending state. */ this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); Object.freeze(this); } } export function isSubscribableOrPromise(value: any): value is SubscribableOrPromise { return value && (typeof value.subscribe === 'function' || typeof value.then === 'function'); } // Like BehaviorSubject but waits for a promise to resolve export class ResolvedSubject extends Subject { private _value: T; private _resolved: boolean; constructor(promise: Promise) { super(); promise.then(val => { this._resolved = true; this.next(val); }); } public get value() { return this.getValue(); } public getValue() { if (this.hasError) { throw this.thrownError; } else if (this.closed) { throw new ObjectUnsubscribedError(); } else { return this._value; } } public next(value: T) { if (this._resolved) { this._value = value; super.next(value); } } protected _subscribe(subscriber: Subscriber): Subscription { const subscription = super._subscribe(subscriber); if (this._resolved && subscription && !subscription.closed) { subscriber.next(this._value); } return subscription; } } export function convertToResolvedSubject(subject: Subject, promise: Promise) { let resolvedSub = new ResolvedSubject(promise); subject.subscribe(resolvedSub); return resolvedSub; }