Skip to content

Instantly share code, notes, and snippets.

@zwade
Created September 5, 2020 16:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zwade/258aaad6c29f5046a32b455beee27b95 to your computer and use it in GitHub Desktop.
Save zwade/258aaad6c29f5046a32b455beee27b95 to your computer and use it in GitHub Desktop.
Crazy Proxy-based Transactional Object
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