UNPKG

espo

Version:

Library for stateful and reactive programming: observables, ownership and lifetimes, automatic deinit, procedural reactivity. Lightweight alternative to MobX.

515 lines (416 loc) 11 kB
/* Primary API */ export function isDe(val) {return isComplex(val) && hasMeth(val, `deinit`)} export function isObs(val) {return isDe(val) && isTrig(val) && hasMeth(val, `sub`) && hasMeth(val, `unsub`)} export function isTrig(val) {return isComplex(val) && hasMeth(val, `trig`)} export function isSub(val) {return isFun(val) || isTrig(val)} export function isSubber(val) {return isFun(val) || (isComplex(val) && hasMeth(val, `subTo`))} export function isRunTrig(val) {return isComplex(val) && hasMeth(val, `run`) && isTrig(val)} export function ph(ref) {return ref ? ref[keyPh] : undefined} export function self(ref) {return ref ? ref[keySelf] || ref : undefined} export function de(ref) {return new Proxy(ref, deinitPh)} export function obs(ref) {return pro(ref, new (getPh(ref) || ObsPh)())} export function comp(ref, fun) {return pro(ref, new (getPh(ref) || CompPh)(fun))} export function lazyComp(ref, fun) {return pro(ref, new (getPh(ref) || LazyCompPh)(fun))} export class Deinit {constructor() {return de(this)}} export class Obs {constructor() {return obs(this)}} export class Comp {constructor(fun) {return comp(this, fun)}} export class LazyComp {constructor(fun) {return lazyComp(this, fun)}} export function deinit(val) {if (isDe(val)) val.deinit()} /* Secondary API (lower level, semi-undocumented) */ export const ctx = {subber: undefined} export const keyPh = Symbol.for(`ph`) export const keySelf = Symbol.for(`self`) export class Rec extends Set { constructor() { super() this.new = new Set() this.act = false } onRun() {} run(...args) { if (this.act) throw Error(`unexpected overlapping rec.run`) const {subber} = ctx ctx.subber = this this.act = true this.new.clear() sch.pause() try { return this.onRun(...args) } finally { ctx.subber = subber this.forEach(recDelOld, this) try {sch.resume()} finally {this.act = false} } } trig() {} subTo(obs) { req(obs, isObs) if (this.new.has(obs)) return this.new.add(obs) this.add(obs) obs.sub(this) } deinit() { this.forEach(recDel, this) } get [Symbol.toStringTag]() {return this.constructor.name} } export class Moebius extends Rec { constructor(ref) { super() this.ref = req(ref, isRunTrig) } onRun(...args) { return this.ref.run(...args) } trig() { if (!this.act) this.ref.trig() } } export class Loop extends Rec { constructor(ref) { super() this.ref = req(ref, isSub) } onRun() { subTrig(this.ref) } trig() { if (!this.act) this.run() } } export class DeinitPh { has(tar, key) { return key in tar || key === keyPh || key === keySelf || key === `deinit` } get(tar, key) { if (key === keyPh) return this if (key === keySelf) return tar if (key === `deinit`) return dePhDeinit return tar[key] } set(tar, key, val) { set(tar, key, val) return true } deleteProperty(tar, key) { del(tar, key) return true } } export const deinitPh = new DeinitPh() // WTB better name. Undocumented. export class ObsBase extends Set { onInit() {} onDeinit() {} sub(val) { const {size} = this this.add(req(val, isSub)) if (!size) this.onInit() } unsub(val) { const {size} = this this.delete(val) if (size && !this.size) this.onDeinit() } trig() { if (sch.paused) { sch.add(this) return } this.forEach(subTrig) } deinit() { this.forEach(this.unsub, this) } get [Symbol.toStringTag]() {return this.constructor.name} } export class ObsPh extends ObsBase { constructor() { super() this.pro = undefined } has() { return DeinitPh.prototype.has.apply(this, arguments) } get(tar, key) { if (key === keyPh) return this if (key === keySelf) return tar if (key === `deinit`) return phDeinit if (!hidden(tar, key)) ctxSub(this) return tar[key] } set(tar, key, val) { if (set(tar, key, val)) this.trig() return true } deleteProperty(tar, key) { if (del(tar, key)) this.trig() return true } onInit() { const {pro} = this if (hasMeth(pro, `onInit`)) pro.onInit() } onDeinit() { const {pro} = this if (hasMeth(pro, `onDeinit`)) pro.onDeinit() } } export class LazyCompPh extends ObsPh { constructor(fun) { super() this.fun = req(fun, isFun) this.out = true // means "outdated" this.cre = new CompRec(this) } get(tar, key) { if (key === keyPh) return this if (key === keySelf) return tar if (key === `deinit`) return phDeinit if (!hidden(tar, key)) { ctxSub(this) if (this.out) { this.out = false this.cre.run() } } return tar[key] } // Invoked by `CompRec`. run() {return this.fun.call(this.pro, this.pro)} onTrig() {this.out = true} onInit() {this.cre.init()} onDeinit() {this.cre.deinit()} } export class CompPh extends LazyCompPh { onTrig() {this.cre.run()} } export class CompRec extends Moebius { subTo(obs) { req(obs, isObs) this.new.add(obs) if (this.ref.size) { this.add(obs) obs.sub(this) } } init() { this.new.forEach(compRecSub, this) } trig() { if (!this.act) this.ref.onTrig() } } export class Sched extends Set { constructor() { super() this.p = 0 } get paused() {return this.p > 0} pause() {this.p++} resume() { if (!this.p) return this.p-- if (!this.p) this.forEach(schFlush, this) } get [Symbol.toStringTag]() {return this.constructor.name} } export const sch = new Sched() export function ctxSub(obs) { const {subber} = ctx if (isFun(subber)) subber(obs) else if (isSubber(subber)) subber.subTo(obs) } export function mut(tar, src) { req(tar, isStruct) if (!src) return tar req(src, isStruct) sch.pause() try { for (const key of keys(src)) tar[key] = src[key] return tar } finally {sch.resume()} } export function priv(ref, key, val) { Object.defineProperty(ref, req(key, isKey), { value: val, writable: true, configurable: true, enumerable: false, }) } export function privs(ref, vals) { req(vals, isStruct) for (const key of keys(vals)) priv(ref, key, vals[key]) } export function pub(ref, key, val) { Object.defineProperty(ref, req(key, isKey), { value: val, writable: true, configurable: true, enumerable: true, }) } export function pubs(ref, vals) { req(vals, isStruct) for (const key of keys(vals)) pub(ref, key, vals[key]) } export function bind(ref, ...funs) { funs.forEach(bindTo, req(ref, isComplex)) } function bindTo(fun) { req(fun, isFun) if (!fun.name) throw Error(`can't bind anon function ${fun}`) priv(this, fun.name, fun.bind(this)) } export function bindAll(ref) { bindFrom(ref, Object.getPrototypeOf(ref)) } function bindFrom(ref, proto) { if (!proto || proto === root) return const src = descs(proto) for (const key of keys(src)) bindAt(src[key], key, ref) bindFrom(ref, Object.getPrototypeOf(proto)) } function bindAt(desc, key, ref) { if (key === `constructor` || hasOwn(ref, key)) return const {value} = desc if (isFun(value)) priv(ref, key, value.bind(ref)) } export function lazyGet(cls) { req(cls, isCls) const proto = cls.prototype const src = descs(proto) for (const key of keys(src)) lazyAt(src[key], key, proto) return cls } function lazyAt({get, set, enumerable, configurable}, key, proto) { if (!get || set || !configurable) return Object.defineProperty(proto, key, { get: function lazyGet() { const val = get.call(this) pub(this, key, val) return val }, set: function lazySet(val) {pub(this, key, val)}, enumerable, configurable: true, }) } export function paused(fun, ...args) { sch.pause() try {return fun.apply(this, args)} finally {sch.resume()} } export function inert(fun, ...args) { const {subber} = ctx ctx.subber = undefined try {return fun.apply(this, args)} finally {ctx.subber = subber} } /* Internal utils */ function getPh(ref) {return ref.constructor && ref.constructor.ph} function pro(ref, ph) { const pro = new Proxy(ref, ph) ph.pro = pro return pro } function set(ref, key, next) { const de = ownEnum(ref, key) const prev = ref[key] ref[key] = next if (Object.is(prev, next)) return false if (de) deinit(prev) return true } function del(ref, key) { if (!own(ref, key)) return false const de = ownEnum(ref, key) const val = ref[key] delete ref[key] if (de) deinit(val) return true } function dePhDeinit() { deinitAll(this) deinit(self(this)) } function phDeinit() { ph(this).deinit() const ref = self(this) deinitAll(ref) deinit(ref) } export function deinitAll(ref) { req(ref, isComplex) for (const key of keys(ref)) deinit(ref[key]) } function subTrig(val) { if (isFun(val)) val() else val.trig() } function recDelOld(obs) { if (!this.new.has(obs)) recDel.call(this, obs) } function recDel(obs) { this.delete(obs) obs.unsub(this) } function compRecSub(obs) { this.add(obs) obs.sub(this) } function schFlush(obs) { this.delete(obs) obs.trig() } export function hasHidden(val, key) { req(key, isKey) return isComplex(val) && hidden(val, key) } function hidden(val, key) { return !ownEnum(val, key) && key in val } export function hasOwn(val, key) { req(key, isKey) return isComplex(val) && own(val, key) } function own(val, key) { return root.hasOwnProperty.call(val, key) } export function hasOwnEnum(val, key) { req(key, isKey) return isComplex(val) && ownEnum(val, key) } export function ownEnum(val, key) { return root.propertyIsEnumerable.call(val, key) } const root = Object.prototype const descs = Object.getOwnPropertyDescriptors function keys(val) {return Object.keys(req(val, isStruct))} export function isFun(val) {return typeof val === `function`} export function isComplex(val) {return isObj(val) || isFun(val)} export function isObj(val) {return val !== null && typeof val === `object`} export function isStruct(val) {return isObj(val) && !Array.isArray(val)} export function isKey(val) {return isStr(val) || isSym(val) } export function isStr(val) {return typeof val === `string`} export function isSym(val) {return typeof val === `symbol`} export function isCls(val) {return isFun(val) && typeof val.prototype === `object`} export function isComp(val) {return isObj(val) || isFun(val)} export function hasMeth(val, key) {return isComp(val) && key in val && isFun(val[key])} export function req(val, test) { if (!test(val)) throw Error(`expected ${show(val)} to satisfy test ${show(test)}`) return val } export function reqInst(val, cls) { if (!(val instanceof cls)) { throw TypeError(`expected ${show(val)} to be an instance of ${show(cls)}`) } return val } // Placeholder, might improve. function show(val) {return String(val)}