56 lines
1.6 KiB
JavaScript
56 lines
1.6 KiB
JavaScript
// Debounced save scheduler: it owns a timer, so it is a factory with a
|
|
// destroy() that the caller must run when it unmounts.
|
|
//
|
|
// The pending call and the timer live in one `state` object mutated only by
|
|
// the named functions below — never by a callback closing over a loose
|
|
// variable.
|
|
|
|
function cancelTimer(state) {
|
|
if (state.timerId === null) return
|
|
clearTimeout(state.timerId)
|
|
state.timerId = null
|
|
}
|
|
|
|
function takePendingRun(state) {
|
|
const pendingRun = state.pendingRun
|
|
state.pendingRun = null
|
|
return pendingRun
|
|
}
|
|
|
|
// Returns a promise that never rejects: a failing save is already reported by
|
|
// the store's result object, and a thrown callback must not crash the UI.
|
|
async function runPending(pendingRun) {
|
|
try {
|
|
await pendingRun()
|
|
} catch (error) {
|
|
console.error('Scheduled save failed', error)
|
|
}
|
|
}
|
|
|
|
export function createSaveScheduler({ delayMs = 1200 } = {}) {
|
|
const state = { timerId: null, pendingRun: null }
|
|
|
|
// Always returns a promise, so callers can await it without testing
|
|
// whether something was pending.
|
|
function flush() {
|
|
cancelTimer(state)
|
|
const pendingRun = takePendingRun(state)
|
|
if (pendingRun === null) return Promise.resolve()
|
|
return runPending(pendingRun)
|
|
}
|
|
|
|
// Replaces the call waiting in line: only the last one ever runs.
|
|
function schedule(run) {
|
|
cancelTimer(state)
|
|
state.pendingRun = run
|
|
state.timerId = setTimeout(flush, delayMs)
|
|
}
|
|
|
|
// Cancels: the pending call is dropped on purpose, destroy() does not save.
|
|
function destroy() {
|
|
cancelTimer(state)
|
|
takePendingRun(state)
|
|
}
|
|
|
|
return { schedule, flush, destroy }
|
|
}
|