@platform/state
Version:
A small, simple, strongly typed, [rx/observable] state-machine.
113 lines (112 loc) • 3.62 kB
JavaScript
import { id } from '@platform/util.value';
import { enablePatches, isDraft, original, produceWithPatches, setAutoFreeze } from 'immer';
import { Subject } from 'rxjs';
import { share } from 'rxjs/operators';
import { is } from '../common';
import { Patch } from '../Patch';
import * as events from './StateObject.events';
import * as merge from './StateObject.merge';
if (typeof setAutoFreeze === 'function') {
setAutoFreeze(false);
}
if (typeof enablePatches === 'function') {
enablePatches();
}
export class StateObject {
constructor(args) {
this._dispose$ = new Subject();
this.dispose$ = this._dispose$.pipe(share());
this._event$ = new Subject();
this.event = events.create(this._event$, this._dispose$);
this.change = (fn, options = {}) => {
const cid = id.cuid();
const type = (options.action || '').trim();
const from = this.state;
const { to, op, patches } = next(from, fn);
if (Patch.isEmpty(patches)) {
return { op, cid, patches };
}
const changing = {
op,
cid,
from,
to,
patches,
cancelled: false,
cancel: () => (changing.cancelled = true),
action: type,
};
this.fire({ type: 'StateObject/changing', payload: changing });
const cancelled = changing.cancelled ? changing : undefined;
if (cancelled) {
this.fire({ type: 'StateObject/cancelled', payload: cancelled });
}
const changed = cancelled
? undefined
: { op, cid, from, to, patches, action: type };
if (changed) {
this._state = to;
this.fire({ type: 'StateObject/changed', payload: changed });
}
return {
op,
cid,
changed,
cancelled,
patches,
};
};
this.fire = (e) => this._event$.next(e);
this._state = Object.assign({}, args.initial);
this.original = this.state;
}
static create(initial) {
return new StateObject({ initial });
}
static readonly(obj) {
return obj;
}
static toObject(input) {
return isDraft(input) ? original(input) : input;
}
static isStateObject(input) {
return is.stateObject(input);
}
dispose() {
if (!this.isDisposed) {
this.fire({
type: 'StateObject/disposed',
payload: { original: this.original, final: this.state },
});
this._dispose$.next();
this._dispose$.complete();
}
}
get isDisposed() {
return this._dispose$.isStopped;
}
get state() {
return this._state;
}
get readonly() {
return this;
}
}
StateObject.merge = merge.create(StateObject.create);
const next = (from, fn) => {
if (typeof fn === 'function') {
const [to, forward, backward] = produceWithPatches(from, (draft) => {
fn(draft);
return undefined;
});
const patches = Patch.toPatchSet(forward, backward);
const op = 'update';
return { op, to, patches };
}
else {
const [to, forward, backward] = produceWithPatches(from, () => fn);
const patches = Patch.toPatchSet(forward, backward);
const op = 'replace';
return { op, to, patches };
}
};