Crazy Proxy-based Transactional Object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Base<T> = | |
T extends (infer V)[] ? { push: (v: V) => void } : | |
{}; | |
export type TransactionObject<T> = | |
T extends string | number | boolean | Function | undefined | null ? T : | |
T extends {} ? { [K in keyof T]: TransactionObject<T[K]> } & Base<T> : never; | |
type TransactionOptions = { | |
commit: () => void; | |
abort: () => void; | |
} | |
type AcidBase<T> = { | |
tx: (fn: (obj: TransactionObject<T>, options: TransactionOptions) => void) => void; | |
} | |
export type AcidObject<T> = | |
T extends string | number | boolean | Function | undefined | null ? T : | |
T extends {} ? ({ [K in keyof T]: AcidObject<T[K]> } & Base<T> & AcidBase<T>) : never; | |
export type Operations<T> = | |
| { $kind: "set", value: T } | |
| (T extends (infer T1)[] ? { $kind: "push", value: T1, location?: number } : never) | |
export type DeepPartial<T> = | |
T extends string | number | boolean | Function | undefined | null ? never | undefined : | |
T extends {} ? ({ [K in keyof T]: DeepPartial<T[K]> | Operations<T> }) : never; | |
export function partialApply<T>(obj: T, part: DeepPartial<T>) { | |
const recurse = <S>(obj: S, part: DeepPartial<S>): S => { | |
if (typeof part === "object" && "$kind" in part) { | |
let op: Operations<S> = part as unknown as Operations<S>; | |
switch (op["$kind"]) { | |
case "set": { | |
return op.value; | |
} | |
case "push": { | |
let arr = obj as unknown as (unknown)[]; | |
if (op.location === undefined) { | |
return [ | |
...arr, | |
op.value, | |
] as unknown as S; | |
} else { | |
return [ | |
...arr.slice(0, op.location), | |
op.value, | |
...arr.slice(op.location) | |
] as unknown as S; | |
} | |
} | |
} | |
} | |
let overlay: Partial<S> = Object.create(null); | |
let shallowPartial = part as Partial<S>; | |
for (let key of (Object.keys(shallowPartial) as (keyof S)[])) { | |
overlay[key] = recurse<S[keyof S]>(obj[key], shallowPartial[key] as any) as S[keyof S]; | |
} | |
return { | |
...obj, | |
...overlay, | |
}; | |
} | |
return recurse(obj, part); | |
} | |
export class TransactionManager<T extends {}> { | |
private onChange: (partial: DeepPartial<T>, newValue: T) => void; | |
private reference: { ref: T }; | |
private assignments: { ref: DeepPartial<T> } = { ref: {} as DeepPartial<T> }; | |
private inProgress: boolean = false; | |
constructor(originalObject: T , onChange: (partial: DeepPartial<T>, newValue: T) => void) { | |
this.reference = { ref: originalObject }; | |
this.onChange = onChange; | |
} | |
public assign(path: (string | number)[], value: Operations<any>) { | |
let activeObj: any = this.assignments; | |
let index: string | number = "ref"; | |
for (let component of path) { | |
activeObj[index] = activeObj[index] ?? {}; | |
activeObj = activeObj[index]; | |
index = component; | |
} | |
activeObj[index] = value; | |
} | |
public start() { | |
this.inProgress = true; | |
} | |
public getOptions(): TransactionOptions { | |
return { | |
commit: () => { | |
this.inProgress = false; | |
this.reference.ref = partialApply(this.reference.ref, this.assignments.ref); | |
this.onChange(this.assignments.ref, this.reference.ref); | |
this.assignments = { ref: {} } as { ref: DeepPartial<T> }; | |
}, | |
abort: () => { | |
this.assignments = { ref: {} } as { ref: DeepPartial<T> }; | |
this.inProgress = false | |
} | |
}; | |
} | |
public atPath(path: (string | number)[]): any { | |
let base: any = this.reference.ref; | |
return path.reduce((b, key) => b[key], base); | |
} | |
get changes(): DeepPartial<T> { | |
if (!this.inProgress) { | |
return this.assignments.ref; | |
} else { | |
return {} as DeepPartial<T>; | |
} | |
} | |
} | |
export const Acid = <T extends {}>(obj: T, onChange: (partial: DeepPartial<T>, newValue: T) => void): AcidObject<T> => { | |
const manager = new TransactionManager<T>(obj, onChange); | |
return AcidObject<T>([], manager); | |
} | |
export const AcidObject = <T extends {}>(path: (string | number)[], manager: TransactionManager<any>): AcidObject<T> => { | |
const transaction = (fn: (obj: TransactionObject<T>, options: TransactionOptions) => void) => { | |
manager.start() | |
const transactionObject = TransactionObject(manager.atPath(path), path, manager); | |
fn(transactionObject, manager.getOptions()); | |
} | |
return new Proxy({} as unknown as T, { | |
get(target, field: keyof T) { | |
switch (field) { | |
case "tx": return transaction; | |
} | |
let value = manager.atPath(path)[field]; | |
if (value instanceof Function) { | |
return value; | |
} | |
if (value instanceof Object) { | |
// TODO(zwade): Cache this construction? | |
return AcidObject(path.concat(field as string | number), manager); | |
} | |
return value; | |
}, | |
set(target, field, value) { | |
transaction((txObj, opts) => { | |
(txObj as any)[field] = value; | |
opts.commit(); | |
}) | |
return true; | |
}, | |
ownKeys(target) { | |
return Object.getOwnPropertyNames(manager.atPath(path)); | |
}, | |
getOwnPropertyDescriptor(target, field) { | |
console.log(field); | |
return Object.getOwnPropertyDescriptor(manager.atPath(path), field); | |
}, | |
}) as AcidObject<T>; | |
}; | |
export const TransactionObject = <T extends {}>(obj: T, path: (string | number)[], manager: TransactionManager<any>): TransactionObject<T> => { | |
let overlays: { [K in keyof T]?: T[K] } = {}; | |
return new Proxy(obj, { | |
get(target, field: keyof T) { | |
let value = overlays[field] ?? target[field]; | |
if (value instanceof Function) { | |
return value; | |
} | |
if (value instanceof Object) { | |
// TODO(zwade): Cache this construction? | |
return TransactionObject(value, path.concat(field as string | number), manager); | |
} | |
return value; | |
}, | |
set(target, field: keyof T, value) { | |
manager.assign(path.concat(field as string | number), { $kind: "set", value }) | |
overlays[field] = value; | |
return true; | |
} | |
}) as TransactionObject<T>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment