@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
JavaScript
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
};