@foblex/mutator
Version:
Angular Signals state engine with deep patch + undo/redo and notifier.
177 lines (170 loc) • 5.12 kB
JavaScript
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