UNPKG

@foblex/mutator

Version:

Angular Signals state engine with deep patch + undo/redo and notifier.

177 lines (170 loc) 5.12 kB
import { InjectionToken, inject, signal } from '@angular/core'; function deepDelete(target, patch) { const out = Array.isArray(target) ? [...target] : { ...target }; for (const k in patch) { const pv = patch[k]; if (pv && typeof pv === 'object' && !Array.isArray(pv)) { if (out[k]) out[k] = deepDelete(out[k], pv); } else { delete out[k]; } } return out; } function deepMerge(target, patch) { if (typeof target !== 'object' || target === null) return patch; if (typeof patch !== 'object' || patch === null) return target; const out = Array.isArray(target) ? [...target] : { ...target }; for (const key in patch) { if (!Object.prototype.hasOwnProperty.call(patch, key)) continue; const patchValue = patch[key]; const targetValue = target[key]; if (isPlainObject(patchValue) && isPlainObject(targetValue)) { out[key] = deepMerge(targetValue, patchValue); } else { out[key] = patchValue; } } return out; } function isPlainObject(value) { return (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === '[object Object]'); } const MUTATOR_CONFIG = new InjectionToken('F_MUTATOR_CONFIG'); function provideMutator(config = {}) { return [ { provide: MUTATOR_CONFIG, useValue: config, } ]; } function deepClone(value) { if (typeof structuredClone === 'function') { return structuredClone(value); } return JSON.parse(JSON.stringify(value)); } class Mutator { _base = {}; _undoStack = []; _redoStack = []; _config = inject(MUTATOR_CONFIG, { optional: true }); _limit = this._config?.limit || 50; _change = signal({ version: 0, notifier: null }, ...(ngDevMode ? [{ debugName: "_change" }] : [])); _canUndo = signal(false, ...(ngDevMode ? [{ debugName: "_canUndo" }] : [])); _canRedo = signal(false, ...(ngDevMode ? [{ debugName: "_canRedo" }] : [])); get changes() { return this._change.asReadonly(); } get canUndo() { return this._canUndo.asReadonly(); } get canRedo() { return this._canRedo.asReadonly(); } get base() { return deepClone(this._base); } initialize(value) { this._base = value; this._undoStack.length = 0; this._redoStack.length = 0; this._updateSignals(); this._change.set({ version: 0, notifier: null }); } create(patch, notifier) { this._addChanges({ value: patch, action: 'create' }, notifier || null); } update(patch, notifier) { this._addChanges({ value: patch, action: 'update' }, notifier || null); } delete(patch, notifier) { this._addChanges({ value: patch, action: 'delete' }, notifier || null); } _addChanges(changes, notifier) { this._undoStack.push(deepClone(changes)); if (this._undoStack.length > this._limit) { const oldest = this._undoStack.shift(); this._base = this._applyChange(this._base, oldest); } this._redoStack.length = 0; this._updateSignals(); this._setChangeObject(notifier); } _setChangeObject(notifier) { this._change.set({ version: this._change().version + 1, notifier }); } getSnapshot() { let result = deepClone(this._base); for (const change of this._undoStack) { result = this._applyChange(result, change); } return result; } _applyChange(result, change) { switch (change.action) { case 'create': case 'update': return deepMerge(result, change.value); case 'delete': return deepDelete(result, change.value); default: throw new Error(`Unknown action: ${change.action}`); } } undo() { const last = this._undoStack.pop(); if (last) { this._redoStack.push(last); this._updateSignals(); this._setChangeObject(null); } } redo() { const restored = this._redoStack.pop(); if (restored) { this._undoStack.push(restored); this._updateSignals(); this._setChangeObject(null); } } patchBase(patch) { this._base = deepMerge(this._base, deepClone(patch)); } deleteFromBase(patch) { this._base = deepDelete(this._base, deepClone(patch)); } _updateSignals() { this._canUndo.set(this._undoStack.length > 0); this._canRedo.set(this._redoStack.length > 0); } } /** * Generated bundle index. Do not edit. */ export { MUTATOR_CONFIG, Mutator, deepDelete, deepMerge, provideMutator }; //# sourceMappingURL=foblex-mutator.mjs.map