UNPKG

temporeest

Version:
145 lines (126 loc) 3.87 kB
import { Context, Changeset, MutableHeteroModelMap, Transaction, IModel, UpdateChangeset, } from '@aphro/context-runtime-ts'; import { SID_of } from '@strut/sid'; import { Task } from './NotifyQueue.js'; import Persistor from './Persistor.js'; export type CombinedChangesets = Map<SID_of<IModel>, Changeset<IModel>>; export class ChangesetExecutor { constructor( private ctx: Context, private changesets: readonly Changeset<IModel>[], // private options: CommitOptions = {}, ) {} // Ideally we return the transaction list... // to replicate to logs. execute(): Transaction { // Merge multiple updates to the same object into a single changeset const combined = this._combineChangesets(); this.removeNoops(combined); const [transaction, notifications] = this.apply(combined); // TODO: Should we do this tick or next tick? setTimeout(() => { for (const n of notifications) { n(); } }, 0); const ret = { ...transaction, persistHandle: new Persistor(this.ctx).persist(transaction), }; this.ctx.commitLog.push(ret); return ret; } private removeNoops(changesets: CombinedChangesets) { for (const [id, changeset] of changesets) { if (changeset.type === 'update') { if (changeset.model._isNoop(changeset.updates)) { changesets.delete(id); } } } } private apply(changesets: CombinedChangesets): [Omit<Transaction, 'persistHandle'>, Set<Task>] { const nodes = new MutableHeteroModelMap(); const notifications: Set<Task> = new Set(); for (const [id, cs] of changesets) { const [model, notifBatch] = this.processChanges(cs); nodes.set(id, model); for (const notif of notifBatch) { notifications.add(notif); } } return [ { changes: changesets, nodes, // options: this.options, }, notifications, ]; } private processChanges(changeset: Changeset<IModel>): [IModel | null, Set<() => void>] { switch (changeset.type) { case 'create': { const ret = changeset.spec.createFrom(this.ctx, changeset.updates as any, false); const [_, notifs] = ret._merge(changeset.updates); return [ret, notifs]; } case 'update': { const [_, notifs] = changeset.model._merge(changeset.updates); return [changeset.model, notifs]; } case 'delete': { this.ctx.cache.remove( changeset.id, changeset.model.spec.storage.db, changeset.model.spec.storage.tablish, ); const node = changeset.model; // TODO: delete notifications? node.destroy(); return [null, new Set()]; } } } _combineChangesets(): CombinedChangesets { const merged: CombinedChangesets = new Map(); for (const changeset of this.changesets) { const existing = merged.get(changeset.id); if (!existing) { merged.set(changeset.id, changeset); continue; } if (existing.type === 'delete') { // No need to merge. Deleted is deleted. continue; } if (changeset.type === 'delete') { // Replace the existing one with the delete. merged.set(changeset.id, changeset); continue; } if (changeset.type === 'create') { throw new Error('Creating the same node twice'); } if (existing.type === 'create') { throw new Error('Updating a nod ebefore it is created'); } merged.set(changeset.id, { type: 'update', updates: { ...existing.updates, ...changeset.updates, }, spec: changeset.model.spec, model: changeset.model, id: changeset.id, } as UpdateChangeset<any, any>); } return merged; } }