diff --git a/client/lib/json-patch-lib.ts b/client/lib/json-patch-lib.ts index a961aa0..fdef794 100644 --- a/client/lib/json-patch-lib.ts +++ b/client/lib/json-patch-lib.ts @@ -1,4 +1,4 @@ -import { PatchOp } from './types' +import { DBCache, PatchOp } from './types' export interface Validator { (operation: Operation, index: number, doc: T, existingPathFragment: string): void @@ -15,6 +15,7 @@ export interface AddOperation extends BaseOperation { export interface RemoveOperation extends BaseOperation { op: PatchOp.REMOVE + value?: undefined } export interface ReplaceOperation extends BaseOperation { @@ -22,67 +23,60 @@ export interface ReplaceOperation extends BaseOperation { value: T } -export type Doc = { [key: string]: any } - export type Operation = AddOperation | RemoveOperation | ReplaceOperation -export function getValueByPointer (doc: any, pointer: string): any { +export function getValueByPointer (doc: Record, pointer: string): any { if (pointer === '/') return doc - const pathArr = pointer.split('/') - pathArr.shift() + try { - return pathArr.reduce((acc, next) => acc[next], doc) + return pointer.split('/').slice(1).reduce((acc, next) => acc[next], doc) } catch (e) { return undefined } } -export function applyOperation (doc: Doc, op: Operation): Operation | null { - let undo: Operation | null = null - const pathArr = op.path.split('/') - pathArr.shift() - pathArr.reduce((node, key, i) => { - if (!isObject) { - throw Error('patch cannot be applied. Path contains non object') - } +export function applyOperation (doc: DBCache>, { path, op, value }: Operation): Operation | null { + const current = getValueByPointer(doc.data, path) + const remove = { op: PatchOp.REMOVE, path} as const + const add = { op: PatchOp.ADD, path, value: current} as const + const replace = { op: PatchOp.REPLACE, path, value: current } as const - if (i < pathArr.length - 1) { - // iterate node - return node[key] - } + doc.data = recursiveApply(doc.data, path.split('/').slice(1), value) - // if last key - const curVal = node[key] - if (op.op === 'add' || op.op === 'replace') { - node[key] = op.value - if (curVal) { - undo = { - op: PatchOp.REPLACE, - path: op.path, - value: curVal, - } - } else { - undo = { - op: PatchOp.REMOVE, - path: op.path, - } - } - } else { - delete node[key] - if (curVal) { - undo = { - op: PatchOp.ADD, - path: op.path, - value: curVal, - } - } - } - }, doc) - - return undo + switch (op) { + case PatchOp.REMOVE: + return current === undefined + ? null + : add + case PatchOp.REPLACE: + case PatchOp.ADD: + return current === undefined + ? remove + : replace + } } -function isObject (val: any): val is Doc { +function recursiveApply> (data: T, path: readonly string[], value?: any): T { + if (!path.length) return value + + if (!isObject(data)) { + throw Error('Patch cannot be applied. Path contains non object') + } + + const updated = recursiveApply(data[path[0]], path.slice(1), value) + const result = { + ...data, + [path[0]]: updated, + } + + if (updated === undefined) { + delete result[path[0]] + } + + return result +} + +function isObject (val: any): val is Record { return typeof val === 'object' && !Array.isArray(val) && val !== null } diff --git a/client/lib/patch-db.ts b/client/lib/patch-db.ts index 6643efd..9da3c4c 100644 --- a/client/lib/patch-db.ts +++ b/client/lib/patch-db.ts @@ -36,8 +36,6 @@ export class PatchDB { clean () { this.sourcesSub.unsubscribe() - if (this.updatesSub) { - this.updatesSub.unsubscribe() - } + this.updatesSub?.unsubscribe() } } diff --git a/client/lib/store.ts b/client/lib/store.ts index d8cabd0..f48a5ba 100644 --- a/client/lib/store.ts +++ b/client/lib/store.ts @@ -70,20 +70,12 @@ export class Store { this.processStashed(revision.id) } - private handleDump (dump: Dump): void { - Object.keys(this.cache.data).forEach(key => { - if (dump.value[key] === undefined) { - delete this.cache.data[key] - } - }) - - Object.entries(dump.value).forEach(([key, val]) => { - (this.cache.data as any)[key] = val - }) - this.stash.deleteRange(this.cache.sequence, dump.id, false) + private handleDump ({ value, id }: Dump): void { + this.cache.data = { ...value } + this.stash.deleteRange(this.cache.sequence, id, false) this.updateWatchedNodes('') - this.updateSequence(dump.id) - this.processStashed(dump.id + 1) + this.updateSequence(id) + this.processStashed(id + 1) } private processStashed (id: number): void { @@ -95,9 +87,7 @@ export class Store { let stashEntry = this.stash.get(this.stash.maxKey() as number) while (stashEntry && stashEntry.revision.id > id) { - stashEntry.undo.forEach(u => { - applyOperation(this.cache.data, u) - }) + stashEntry.undo.forEach(u => applyOperation(this.cache, u)) stashEntry = this.stash.nextLowerPair(stashEntry.revision.id)?.[1] } } @@ -106,18 +96,16 @@ export class Store { let revision = this.stash.get(id)?.revision while (revision) { let undo: Operation[] = [] - let success = false + try { revision.patch.forEach(op => { - const u = applyOperation(this.cache.data, op) + const u = applyOperation(this.cache, op) if (u) undo.push(u) }) success = true } catch (e) { - undo.forEach(u => { - applyOperation(this.cache.data, u) - }) + undo.forEach(u => applyOperation(this.cache, u)) undo = [] } @@ -163,6 +151,6 @@ export class Store { } private isRevision (update: Update): update is Revision { - return !!(update as Revision).patch + return 'patch' in update } -} \ No newline at end of file +} diff --git a/client/lib/types.ts b/client/lib/types.ts index 278aaa9..221dd86 100644 --- a/client/lib/types.ts +++ b/client/lib/types.ts @@ -23,7 +23,7 @@ export interface Bootstrapper { update (cache: DBCache): Promise } -export interface DBCache{ +export interface DBCache>{ sequence: number, data: T }