Created
November 18, 2023 02:00
-
-
Save malerba118/ba1bf1f8e901ceb64865a387efc84542 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import { observable, makeObservable, runInAction, action } from "mobx"; | |
import { HistoryManager } from "./history"; | |
import { v4 as uuid } from "uuid"; | |
type Constructor<T> = { | |
new (...params: any[]): T; | |
[x: string | number | symbol]: any; | |
}; | |
type BaseData = { | |
id: string; | |
deleted_at?: number | null; | |
[x: string | number | symbol]: any; | |
}; | |
export function Model< | |
T extends { id: string; deleted_at?: number | null } = BaseData | |
>() { | |
abstract class _Model { | |
id: string; | |
deletedAt: number | null; | |
constructor(data: T) { | |
this.id = data.id; | |
this.deletedAt = data.deleted_at ?? null; | |
makeObservable(this, { | |
deletedAt: observable.ref, | |
delete: action, | |
}); | |
} | |
static _store: Store | null; | |
static _collection: Collection | null; | |
static collectionName: string; | |
static create<Instance extends _Model>( | |
this: Constructor<Instance>, | |
data: T, | |
context?: any | |
) { | |
if (this._collection.instances[data.id]) { | |
// load any new data into existing instance | |
this._collection.instances[data.id].loadJSON(data); | |
} else { | |
// create new instance if none exists | |
runInAction(() => { | |
this._collection.instances[data.id] = new this(data, context); | |
}); | |
} | |
return this._collection.instances[data.id] as Instance; | |
} | |
static getById<Instance extends _Model>( | |
this: Constructor<Instance>, | |
id: string | |
) { | |
let instance = this._collection.instances[id] as Instance; | |
if (instance?.isDeleted) { | |
return null as any as Instance; | |
} | |
for (const child of this._children) { | |
instance = child.getById(id); | |
if (instance) break; | |
} | |
return instance as Instance; | |
} | |
static getAll<Instance extends _Model>(this: Constructor<Instance>) { | |
let all = Object.values(this._collection.instances).filter( | |
(inst: any) => !inst.isDeleted | |
); | |
this._children.forEach((child: any) => { | |
all = all.concat(child.getAll()); | |
}); | |
return all as Instance[]; | |
} | |
static get _children(): any[] { | |
return this._store!.models.filter( | |
(model) => Object.getPrototypeOf(model) === this | |
); | |
} | |
delete() { | |
this.deletedAt = Date.now(); | |
} | |
clone(overrides: Partial<T> = {}) { | |
// @ts-ignore | |
return this.constructor.create({ | |
...JSON.parse(JSON.stringify(this.toJSON())), | |
id: uuid(), | |
...overrides, | |
}); | |
} | |
get isDeleted() { | |
return this.deletedAt != null; | |
} | |
toJSON(): T { | |
return { | |
id: this.id, | |
deleted_at: this.deletedAt, | |
} as T; | |
} | |
loadJSON(data: T) { | |
this.id = data.id; | |
this.deletedAt = data.deleted_at ?? null; | |
} | |
} | |
return _Model; | |
} | |
type BaseModel = ReturnType<typeof Model>; | |
interface CollectionParams { | |
model: BaseModel; | |
} | |
export class Collection { | |
model: BaseModel; | |
instances: Record<string, any>; | |
constructor(params: CollectionParams) { | |
this.model = params.model; | |
this.instances = {}; | |
makeObservable(this, { instances: observable.shallow }); | |
} | |
} | |
type StoreData = { | |
schema_version: number; | |
[collections: string]: any; | |
}; | |
interface StoreParams { | |
models: BaseModel[]; | |
schemaVersion: number; | |
} | |
export class Store { | |
models: BaseModel[]; | |
schemaVersion: number; | |
_history = new HistoryManager(); | |
constructor(params: StoreParams) { | |
this.models = params.models; | |
this.models.forEach((model) => { | |
model._store = this; | |
model._collection = new Collection({ model }); | |
}); | |
this.schemaVersion = params.schemaVersion; | |
makeObservable(this, { | |
loadJSON: action, | |
}); | |
this._history.onChange((ev) => { | |
if (ev.action === "undo" || ev.action === "redo") { | |
this.loadJSON(ev.item); | |
} | |
}); | |
} | |
history = { | |
_instance: this as Store, | |
commit({ replace = false }: { replace?: boolean } = {}) { | |
if (replace) { | |
return this._instance._history.replace( | |
JSON.parse(JSON.stringify(this._instance.toJSON())) | |
); | |
} else { | |
return this._instance._history.push( | |
JSON.parse(JSON.stringify(this._instance.toJSON())) | |
); | |
} | |
}, | |
undo() { | |
this._instance._history.undo(); | |
}, | |
redo() { | |
this._instance._history.redo(); | |
}, | |
get activeItem() { | |
return this._instance._history.activeItem; | |
}, | |
}; | |
toJSON() { | |
const data: StoreData = { | |
schema_version: this.schemaVersion, | |
}; | |
this.models.forEach((model) => { | |
if (model.hasOwnProperty("collectionName")) { | |
data[model.collectionName] = model._collection!.instances; | |
} | |
}); | |
return data; | |
} | |
loadJSON(data: StoreData) { | |
if (data.schema_version !== this.schemaVersion) { | |
throw new Error("Schema verison mismatch"); | |
} | |
this.models.forEach((model: any) => { | |
if (model.hasOwnProperty("collectionName")) { | |
if (data[model.collectionName]) { | |
// upsert new values | |
Object.values(data[model.collectionName]).forEach((val) => { | |
model.create(val); | |
}); | |
// delete values that don't exist | |
Object.keys(model._collection.instances).forEach((id) => { | |
if (!data[model.collectionName][id]) { | |
delete model._collection.instances[id]; | |
} | |
}); | |
} | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment