epic-state
Version:
Reactive state management for frontend libraries.
102 lines (86 loc) • 2.84 kB
text/typescript
import { log } from './helper'
import { callPlugins } from './plugin'
import type { CallPluginOptions } from './types'
declare global {
var stateDisableBatching: boolean
}
const batching: { updates: CallPluginOptions[]; scheduler?: number } = {
updates: [],
scheduler: undefined,
}
export const scheduleUpdate = (update: CallPluginOptions) => {
batching.updates.unshift(update) // Most recent update will be processed first, to allow filtering already applied changes.
if (batching.scheduler === undefined) {
batching.scheduler = schedule(process)
}
}
// Process all batched updates.
const noDeadline = {
didTimeout: false,
timeRemaining() {
return 99999
},
}
export const batch = () => process(noDeadline)
function schedule(callback: IdleRequestCallback) {
// NOTE if window isn't present and batching isn't explicitly enabled there will be no batching.
if ((typeof window === 'undefined' && globalThis.stateDisableBatching !== false) || globalThis.stateDisableBatching === true) {
callback(noDeadline)
return
}
if (window.requestIdleCallback) {
return window.requestIdleCallback(callback)
}
// requestIdleCallback polyfill (not supported in Safari)
// https://github.com/pladaria/requestidlecallback-polyfill
// See react scheduler for better implementation.
window.requestIdleCallback =
window.requestIdleCallback ||
function idleCallbackPolyfill(innerCallback: IdleRequestCallback, _options?: IdleRequestOptions) {
const start = Date.now()
setTimeout(() => {
innerCallback({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start))
},
})
}, 1)
return 0
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function cancelIdleCallbackPolyfill(id) {
clearTimeout(id)
}
return schedule(callback)
}
function process(deadline: IdleDeadline) {
if (batching.updates.length === 0) {
log('Trying to batch empty updates')
return
}
let shouldYield = false
let maxTries = 500
while (batching.updates.length > 0 && !shouldYield && maxTries > 0) {
maxTries -= 1
const update = batching.updates.shift()
if (update) {
callPlugins(update)
// Filter out already applied updates.
batching.updates = batching.updates.filter(
(potentialUpdate) => potentialUpdate.property !== update.property || potentialUpdate.parent !== update.parent,
)
}
// Yield current rendering cycle if out of time.
shouldYield = deadline.timeRemaining() < 1
}
if (maxTries === 0) {
log('Ran out of tries at process.', 'warning')
}
// Continuing to process in next iteration.
if (batching.updates.length > 0) {
schedule(process)
}
batching.scheduler = undefined
}