wana
Version:
Easy observable state for React
83 lines (72 loc) • 2.1 kB
text/typescript
import queueMicrotask from '@alloc/queue-microtask'
import { batchedUpdates } from 'react-batched-updates'
type Effect = () => void
/** The FIFO queue of async `Auto` updates */
let runQueue: Effect[] = []
/** The queue of `withAuto` updates. Sorted by deepest child last */
const renderQueue: RenderConfig[] = []
type RenderConfig = { depth: number; effect: Effect }
export interface Batch {
/**
* Run the given effect in the next microtask, immediately before
* rendering any components with stale observations.
*
* To avoid infinite loops, the "run" step only runs the first 100,000
* effects in any given microtask. Any remaining effects are scheduled
* for the next microtask.
*/
run(effect: Effect): void
/**
* The "render" step is sorted by `withAuto` depth.
* After the "run" step, all `Auto` objects are updated, and `withAuto`
* components can render now.
*/
render(depth: number, effect: Effect): void
}
export const batch: Batch = {
run(effect) {
runQueue.push(effect)
flushAsync()
},
render(depth, effect) {
let i = renderQueue.findIndex(other => other.depth > depth)
if (i < 0) i = renderQueue.length
renderQueue.splice(i, 0, { depth, effect })
flushAsync()
},
}
let isQueued = false
function flushAsync() {
if (!isQueued) {
isQueued = true
queueMicrotask(() => {
isQueued = false
if (flushSync()) {
flushAsync()
}
})
}
}
/**
* Flush the queue of delayed reactions.
*
* Returns `true` if the queue wasn't flushed entirely.
*
* Useful when testing `wana`-integrated components/hooks.
*/
export function flushSync() {
batchedUpdates(() => {
// Run any pending reactions.
let runs = 0
for (const effect of runQueue) {
if (++runs > 1e5) break
effect()
}
// Postpone remaining runs until next flush.
runQueue = runQueue.slice(runs)
// Stale components always rerender after reactions.
renderQueue.forEach(({ effect }) => effect())
renderQueue.length = 0
})
return !!(runQueue.length || renderQueue.length)
}