Skip to content

Instantly share code, notes, and snippets.

@malerba118
Created November 18, 2023 02:00
Show Gist options
  • Save malerba118/ba1bf1f8e901ceb64865a387efc84542 to your computer and use it in GitHub Desktop.
Save malerba118/ba1bf1f8e901ceb64865a387efc84542 to your computer and use it in GitHub Desktop.
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