UNPKG

@thi.ng/atom

Version:

Mutable wrappers for nested immutable values with optional undo/redo history and transaction support

139 lines (138 loc) 3.46 kB
import { assert } from "@thi.ng/errors/assert"; import { illegalState } from "@thi.ng/errors/illegal-state"; import { setInUnsafe } from "@thi.ng/paths/set-in"; import { updateInUnsafe } from "@thi.ng/paths/update-in"; import { nextID } from "./idgen.js"; const defTransacted = (parent) => new Transacted(parent); const beginTransaction = (parent) => new Transacted(parent).begin(); const updateAsTransaction = (parent, fn) => { new Transacted(parent).updateAsTransaction(fn); return parent; }; class Transacted { parent; current; id; isActive; _watches; constructor(parent) { this.parent = parent; this.current = void 0; this.isActive = false; this.id = `tx-${nextID()}`; } get value() { return this.deref(); } set value(val) { this.reset(val); } get isTransaction() { return this.isActive; } deref() { return this.isActive ? this.current : this.parent.deref(); } equiv(o) { return this === o; } reset(val) { this.ensureTx(); this.current = val; return val; } resetIn(path, val) { this.ensureTx(); return this.current = setInUnsafe(this.current, path, val); } resetInUnsafe(path, val) { return this.resetIn(path, val); } swap(fn, ...args) { this.ensureTx(); return this.current = fn.apply(null, [this.current, ...args]); } swapIn(path, fn, ...args) { this.ensureTx(); return this.current = updateInUnsafe(this.current, path, fn, ...args); } swapInUnsafe(path, fn, ...args) { return this.swapIn(path, fn, ...args); } begin() { assert(!this.isActive, "transaction already started"); this.current = this.parent.deref(); this.isActive = true; this.parent.addWatch( this.id + "--guard--", () => illegalState( `${this.id} parent state changed during active transaction` ) ); return this; } commit() { const val = this.current; this.cancel(); return this.parent.reset(val); } cancel() { this.ensureTx(); this.parent.removeWatch(this.id + "--guard--"); this.current = void 0; this.isActive = false; } /** * Starts a new transaction and calls given `fn` with this instance to * update the state (presumably in multiple stages) as a single transaction. * If the update function returns true, the transaction will be committed, * else cancelled. * * @remarks * **IMPORTANT:** Within body of the update function **only** work with the * transaction wrapper given as argument! **DO NOT** update the original * state atom! * * If an error occurs during the update, the transaction will be canceled * and the error re-thrown. * * @param fn */ updateAsTransaction(fn) { try { this.begin(); fn(this) ? this.commit() : this.cancel(); } catch (e) { this.cancel(); throw e; } } addWatch(id, watch) { return this.parent.addWatch( this.id + id, (_, prev, curr) => watch(id, prev, curr) ); } removeWatch(id) { return this.parent.removeWatch(this.id + id); } notifyWatches(old, curr) { this.parent.notifyWatches(old, curr); } release() { delete this.parent; delete this.current; delete this.isActive; delete this._watches; return true; } ensureTx() { assert(this.isActive, "no active transaction"); } } export { Transacted, beginTransaction, defTransacted, updateAsTransaction };