UNPKG

refui

Version:

The JavaScript framework that refuels your UI projects, across web, native, and embedded

660 lines (571 loc) 12.9 kB
import { nop, removeFromArr } from './utils.js' let sigID = 0 let ticking = false let currentEffect = null let currentDisposers = null let currentResolve = null let currentTick = null let signalQueue = new Set() let effectQueue = new Set() let runQueue = new Set() // Scheduler part function scheduleSignal(signalEffects) { return signalQueue.add(signalEffects) } function scheduleEffect(effects) { return effectQueue.add(effects) } function flushRunQueue() { for (let i of runQueue) i() runQueue.clear() } function sortQueue(a, b) { return a._id - b._id } function flushQueue(queue, sorted) { while (queue.size) { const queueArr = Array.from(queue) queue.clear() if (sorted && queueArr.length > 1) { queueArr.sort(sortQueue) const tempArr = [...(new Set([].concat(...queueArr).reverse()))].reverse() runQueue = new Set(tempArr) } else if (queueArr.length > 10000) { let flattenedArr = [] for (let i = 0; i < queueArr.length; i += 10000) { flattenedArr = flattenedArr.concat(...queueArr.slice(i, i + 10000)) } runQueue = new Set(flattenedArr) } else { runQueue = new Set([].concat(...queueArr)) } flushRunQueue() } } function tick() { if (!ticking) { ticking = true currentResolve() } return currentTick } function nextTick(cb) { return tick().then(cb) } function flushQueues() { if (signalQueue.size || effectQueue.size) { flushQueue(signalQueue, true) signalQueue = new Set(signalQueue) flushQueue(effectQueue) effectQueue = new Set(effectQueue) return Promise.resolve().then(flushQueues) } } function tickHandler(resolve) { currentResolve = resolve } function resetTick() { ticking = false currentTick = new Promise(tickHandler).then(flushQueues) currentTick.finally(resetTick) } // Signal part function pure(cb) { cb._pure = true return cb } function isPure(cb) { return !!cb._pure } function _dispose_raw() { for (let i of this) i(true) this.length = 0 } function _dispose_with_callback(dispose_raw, batch) { this(batch) dispose_raw(batch) } function _dispose_with_upstream(prevDisposers, batch) { if (!batch) { removeFromArr(prevDisposers, this) } this(batch) } function createDisposer(disposers, prevDisposers, cleanup) { let _cleanup = _dispose_raw.bind(disposers) if (cleanup) { _cleanup = _dispose_with_callback.bind(cleanup, _cleanup) } if (prevDisposers) { _cleanup = _dispose_with_upstream.bind(_cleanup, prevDisposers) prevDisposers.push(_cleanup) } return _cleanup } function collectDisposers(disposers, fn, cleanup) { const prevDisposers = currentDisposers const _dispose = createDisposer(disposers, prevDisposers, cleanup) currentDisposers = disposers try { fn() } finally { currentDisposers = prevDisposers } return _dispose } function _onDispose(cb) { const disposers = currentDisposers function cleanup(batch) { if (!batch) { removeFromArr(disposers, cleanup) } cb(batch) } disposers.push(cleanup) return cleanup } function onDispose(cb) { if (currentDisposers) { return _onDispose(cb) } return nop } function useEffect(effect) { onDispose(effect()) } function _frozen(capturedDisposers, capturedEffects, ...args) { const prevDisposers = currentDisposers const prevEffect = currentEffect currentDisposers = capturedDisposers currentEffect = capturedEffects try { return this(...args) } finally { currentDisposers = prevDisposers currentEffect = prevEffect } } function freeze(fn) { return _frozen.bind(fn, currentDisposers, currentEffect) } function untrack(fn) { const prevDisposers = currentDisposers const prevEffect = currentEffect currentDisposers = null currentEffect = null try { return fn() } finally { currentDisposers = prevDisposers currentEffect = prevEffect } } const Signal = class { constructor(value, compute) { if (process.env.NODE_ENV === 'development' && new.target !== Signal) { throw new Error('Signal must not be extended!') } // eslint-disable-next-line no-plusplus const id = sigID++ const userEffects = [] const signalEffects = [] const disposeCtx = currentDisposers userEffects._id = id signalEffects._id = id const internal = { id, value, compute, disposeCtx, userEffects, signalEffects } Object.defineProperty(this, '_', { value: internal, writable: false, enumerable: false, configurable: false }) if (compute) { watch(pure(this.set.bind(this, value))) } else if (isSignal(value)) { value.connect(pure(this.set.bind(this, value))) } } get value() { return this.get() } set value(val) { this.set(val) } get connected() { const { userEffects, signalEffects } = this._ return !!(userEffects.length || signalEffects.length) } get() { this.connect(currentEffect) return this._.value } set(val) { const { compute, value } = this._ val = compute ? peek(compute(read(val))) : read(val) if (value !== val) { this._.value = val this.trigger() } } peek() { return this._.value } poke(val) { this._.value = val } trigger() { const { userEffects, signalEffects } = this._ scheduleSignal(signalEffects) scheduleEffect(userEffects) tick() } connect(effect) { if (!effect) { return } const { userEffects, signalEffects, disposeCtx } = this._ const effects = isPure(effect) ? signalEffects : userEffects if (!effects.includes(effect)) { effects.push(effect) if (currentDisposers && currentDisposers !== disposeCtx) { _onDispose(function() { removeFromArr(effects, effect) if (runQueue.size) { runQueue.delete(effect) } }) } } if (currentEffect !== effect) { effect() } } and(val) { return signal(this, function(i) { return read(val) && i }) } or(val) { return signal(this, function(i) { return read(val) || i }) } eq(val) { return signal(this, function(i) { return read(val) === i }) } neq(val) { return signal(this, function(i) { return read(val) !== i }) } gt(val) { return signal(this, function(i) { return i > read(val) }) } lt(val) { return signal(this, function(i) { return i < read(val) }) } toJSON() { return this.get() } *[Symbol.iterator]() { yield* this.get() } [Symbol.toPrimitive](hint) { const val = this.get() switch (hint) { case 'string': return String(val) case 'number': return Number(val) default: if (Object(val) !== val) { return val } return !!val } } } function isSignal(val) { return val && val.constructor === Signal } function watch(effect) { const prevEffect = currentEffect currentEffect = effect const _dispose = collectDisposers([], effect) currentEffect = prevEffect return _dispose } function peek(val) { while (isSignal(val)) { val = val.peek() } return val } function poke(val, newVal) { if (isSignal(val)) { return val.poke(newVal) } return newVal } function read(val) { if (isSignal(val)) { val = peek(val.get()) } return val } function readAll(vals, handler) { return handler(...vals.map(read)) } function _write(val, newVal) { if (typeof newVal === 'function') { newVal = newVal(peek(val)) } val.value = newVal return peek(val) } function write(val, newVal) { if (isSignal(val)) { return _write(val, newVal) } if (typeof newVal === 'function') { return newVal(val) } return newVal } function listen(vals, cb) { for (let val of vals) { if (isSignal(val)) { val.connect(cb) } } } function signal(value, compute) { return new Signal(value, compute) } function computed(fn) { return signal(null, fn) } function merge(vals, handler) { return computed(readAll.bind(null, vals, handler)) } function tpl(strs, ...exprs) { const raw = { raw: strs } return signal(null, function() { return String.raw(raw, ...exprs) }) } function connect(sigs, effect) { const prevEffect = currentEffect currentEffect = effect for (let sig of sigs) { sig.connect(effect) } effect() currentEffect = prevEffect } function bind(handler, val) { if (isSignal(val)) { val.connect(function() { handler(peek(val)) }) } else if (typeof val === 'function') { watch(function() { handler(val()) }) } else { handler(val) } } function derive(sig, key, compute) { if (isSignal(sig)) { const derivedSig = signal(null, compute) let disposer = null const _dispose = function() { if (disposer) { disposer() disposer = null } } sig.connect(pure(function() { _dispose() const newVal = peek(sig) if (!newVal) { return } untrack(function() { disposer = watch(function() { derivedSig.value = read(newVal[key]) }) }) })) onDispose(_dispose) return derivedSig } else { return signal(sig[key], compute) } } function extract(sig, ...extractions) { if (!extractions.length) { extractions = Object.keys(peek(sig)) } return extractions.reduce(function(mapped, i) { mapped[i] = signal(sig, function(val) { return val && peek(val[i]) }) return mapped }, {}) } function derivedExtract(sig, ...extractions) { if (!extractions.length) { extractions = Object.keys(peek(sig)) } return extractions.reduce(function(mapped, i) { mapped[i] = derive(sig, i) return mapped }, {}) } function makeReactive(obj) { return Object.defineProperties({}, Object.entries(obj).reduce(function(descriptors, [key, value]) { if (isSignal(value)) { descriptors[key] = { get: value.get.bind(value), set: value.set.bind(value), enumerable: true, configurable: false } } else { descriptors[key] = { value, enumerable: true } } return descriptors }, {})) } function onCondition(sig, compute) { let currentVal = null let conditionMap = new Map() let conditionValMap = new Map() sig.connect( pure(function() { const newVal = peek(sig) if (currentVal !== newVal) { const prevMatchSet = conditionMap.get(currentVal) const newMatchSet = conditionMap.get(newVal) currentVal = newVal if (prevMatchSet) { for (let i of prevMatchSet) i.value = false } if (newMatchSet) { for (let i of newMatchSet) i.value = true } } }) ) if (currentDisposers) { _onDispose(function() { conditionMap = new Map() conditionValMap = new Map() }) } function match(condition) { let currentCondition = peek(condition) let matchSet = conditionMap.get(currentCondition) if (isSignal(condition)) { let matchSig = conditionValMap.get(condition) if (!matchSig) { matchSig = signal(currentCondition === currentVal, compute) conditionValMap.set(condition, matchSig) condition.connect(function() { currentCondition = peek(condition) if (matchSet) { removeFromArr(matchSet, matchSig) } matchSet = conditionMap.get(currentCondition) if (!matchSet) { matchSet = [] conditionMap.set(currentCondition, matchSet) } matchSet.push(matchSig) matchSig.value = currentCondition === currentVal }) if (currentDisposers) { _onDispose(function() { conditionValMap.delete(condition) if (matchSet.length === 1) conditionMap.delete(currentCondition) else removeFromArr(matchSet, matchSig) }) } } return matchSig } else { if (!matchSet) { matchSet = [] conditionMap.set(currentCondition, matchSet) } let matchSig = conditionValMap.get(currentCondition) if (!matchSig) { matchSig = signal(currentCondition === currentVal, compute) conditionValMap.set(currentCondition, matchSig) matchSet.push(matchSig) if (currentDisposers) { _onDispose(function() { conditionValMap.delete(currentCondition) if (matchSet.length === 1) { conditionMap.delete(currentCondition) } else { removeFromArr(matchSet, matchSig) } }) } } return matchSig } } return match } resetTick() export { Signal, signal, isSignal, computed, connect, bind, derive, extract, derivedExtract, makeReactive, tpl, watch, peek, poke, read, readAll, merge, write, listen, scheduleEffect as schedule, tick, nextTick, collectDisposers, onCondition, onDispose, useEffect, untrack, freeze }