Skip to content

Commit

Permalink
chore: refactors queueMicroTask code and adds utils (#6)
Browse files Browse the repository at this point in the history
* chore: refactors queueMicroTask code and adds utils

* chore: changes necessary comments and error handling

---------

Co-authored-by: Harsh Kumar Choudhary <harsh.choudhary@HARSH-CHOUDHARY-MAC.local>
  • Loading branch information
harshkc and Harsh Kumar Choudhary authored Feb 29, 2024
1 parent d835297 commit de26785
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 128 deletions.
49 changes: 19 additions & 30 deletions src/idleCbWithPolyfill.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
function now() {
return Date.now()
}
import { now } from './utils/now'
import { isBrowser } from './utils/env'

const supportsRequestIdleCallback_ = typeof requestIdleCallback === 'function'
const supportsRequestIdleCallback_ = isBrowser && typeof requestIdleCallback === 'function'

/**
* A minimal shim of the native IdleDeadline class.
Expand All @@ -24,44 +23,34 @@ class IdleDeadline {
}

/**
* A minimal shim for the requestIdleCallback function. This accepts a
* callback function and runs it at the next idle period, passing in an
* object with a `timeRemaining()` method.
* Provides a cross-browser compatible shim for `requestIdleCallback` and
* `cancelIdleCallback` if native support is not available. Note that the
* shim's `timeRemaining` calculation is an approximation.
*/
function requestIdleCallbackShim(callback: (deadline: IdleDeadline) => void) {
const deadline = new IdleDeadline(now())
return setTimeout(() => callback(deadline), 0)
}

/**
* A minimal shim for the cancelIdleCallback function. This accepts a
* handle identifying the idle callback to cancel.
*/
function cancelIdleCallbackShim(handle: number | null) {
if (handle)
clearTimeout(handle)
}

/**
* The native `requestIdleCallback()` function or `cancelIdleCallbackShim()`
*.if the browser doesn't support it.
/*
The bind is used to ensure that the context of
the requestIdleCallback and cancelIdleCallback methods is always the window object,
regardless of how or where these functions are called,
This is necessary because these functions are native browser APIs and
are expected to be called with window as their context.
*/
const _rIC = supportsRequestIdleCallback_ ? requestIdleCallback : requestIdleCallbackShim

export const rIC = typeof window !== 'undefined' ? _rIC.bind(window) : _rIC
const rIC = supportsRequestIdleCallback_
? requestIdleCallback.bind(window)
: requestIdleCallbackShim

/**
* The bind method is used in this context to ensure that the this context of
* the requestIdleCallback and cancelIdleCallback functions is always the window object,
* regardless of how or where these functions are called.
* This is necessary because these functions are native browser APIs and
* are expected to be called with window as their context.
*/

/**
* The native `cancelIdleCallback()` function or `cancelIdleCallbackShim()`
* if the browser doesn't support it.
*/
const _cIC = supportsRequestIdleCallback_ ? cancelIdleCallback : cancelIdleCallbackShim
const cIC = supportsRequestIdleCallback_
? cancelIdleCallback.bind(window)
: cancelIdleCallbackShim

export const cIC = typeof window !== 'undefined' ? _cIC.bind(window) : _cIC
export { rIC, cIC }
88 changes: 34 additions & 54 deletions src/idleQueue.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import { cIC, rIC } from './idleCbWithPolyfill'
import { createQueueMicrotask } from './queueMicrotask'
import { createQueueMicrotask } from './utils/queueMicrotask'
import { isBrowser, isSafari } from './utils/env'
import { now } from './utils/now'

declare global {
interface Window {
safari: any
}
}
type DocumentVisibilityState = 'hidden' | 'visible' | 'prerender' | 'unloaded'

const DEFAULT_MIN_TASK_TIME = 0
function now() {
return Date.now()
interface State {
time: number
visibilityState: 'hidden' | 'visible' | 'prerender' | 'unloaded'
}
const isBrowser = typeof window !== 'undefined' && window.document && window.document.createElement

const isSafari_ = !!(
isBrowser
&& 'safari' in window
&& typeof window.safari === 'object'
&& 'pushNotification' in (window.safari as any)
)
type Task = (state: State) => void

const DEFAULT_MIN_TASK_TIME = 0
/**
* Returns true if the IdleDeadline object exists and the remaining time is
* less or equal to than the minTaskTime. Otherwise returns false.
Expand All @@ -33,36 +23,27 @@ function shouldYield(deadline?: IdleDeadline, minTaskTime?: number) {
return false
}

interface State {
time: number
visibilityState: DocumentVisibilityState
}
type Task = (state: State) => void

/**
* A class wraps a queue of requestIdleCallback functions for two reasons:
* 1. So other callers can know whether or not the queue is empty.
* 2. So we can provide some guarantees that the queued functions will
* run in unload-type situations.
* This class manages a queue of tasks designed to execute during idle browser time.
*
* It allows checking whether tasks are pending and ensures task execution even
* in situations like page unload.
*/
export class IdleQueue {
private idleCallbackHandle_: number | null
private taskQueue_: { state: State, task: Task, minTaskTime: number }[]
private idleCallbackHandle_: number | null = null
private taskQueue_: { state: State, task: Task, minTaskTime: number }[] = []
private isProcessing_ = false

private state_: State | null
private defaultMinTaskTime_: number
private ensureTasksRun_: boolean
private state_: State | null = null
private defaultMinTaskTime_: number = DEFAULT_MIN_TASK_TIME
private ensureTasksRun_: boolean = false
private queueMicrotask?: (callback: VoidFunction) => void

/**
* Creates the IdleQueue instance and adds lifecycle event listeners to
* run the queue if the page is hidden (with fallback behavior for Safari).
*/
constructor({ ensureTasksRun = false, defaultMinTaskTime = DEFAULT_MIN_TASK_TIME } = {}) {
this.idleCallbackHandle_ = null
this.taskQueue_ = []
this.state_ = null
this.defaultMinTaskTime_ = defaultMinTaskTime
this.ensureTasksRun_ = ensureTasksRun

Expand All @@ -74,15 +55,13 @@ export class IdleQueue {
if (isBrowser && this.ensureTasksRun_) {
addEventListener('visibilitychange', this.onVisibilityChange_, true)

// Safari does not reliably fire the `pagehide` or `visibilitychange`
// events when closing a tab, so we have to use `beforeunload` with a
// timeout to check whether the default action was prevented.
// - https://bugs.webkit.org/show_bug.cgi?id=151610
// - https://bugs.webkit.org/show_bug.cgi?id=151234
// NOTE: we only add this to Safari because adding it to Firefox would
// prevent the page from being eligible for bfcache.
if (isSafari_)
if (isSafari) {
// Safari workaround: Due to unreliable event behavior, we use 'beforeunload'
// to ensure tasks run if a tab/window is closed unexpectedly.
// NOTE: we only add this to Safari because adding it to Firefox would
// prevent the page from being eligible for bfcache.
addEventListener('beforeunload', this.runTasksImmediately, true)
}
}
}

Expand Down Expand Up @@ -134,14 +113,7 @@ export class IdleQueue {
if (isBrowser && this.ensureTasksRun_) {
removeEventListener('visibilitychange', this.onVisibilityChange_, true)

// Safari does not reliably fire the `pagehide` or `visibilitychange`
// events when closing a tab, so we have to use `beforeunload` with a
// timeout to check whether the default action was prevented.
// - https://bugs.webkit.org/show_bug.cgi?id=151610
// - https://bugs.webkit.org/show_bug.cgi?id=151234
// NOTE: we only add this to Safari because adding it to Firefox would
// prevent the page from being eligible for bfcache.
if (isSafari_)
if (isSafari)
removeEventListener('beforeunload', this.runTasksImmediately, true)
}
}
Expand All @@ -156,10 +128,12 @@ export class IdleQueue {
visibilityState: isBrowser ? document.visibilityState : 'visible',
}

const minTaskTime = Math.max(0, options?.minTaskTime || this.defaultMinTaskTime_)

arrayMethod.call(this.taskQueue_, {
state,
task,
minTaskTime: options?.minTaskTime || this.defaultMinTaskTime_,
minTaskTime,
})

this.scheduleTasksToRun_()
Expand Down Expand Up @@ -205,7 +179,13 @@ export class IdleQueue {

this.state_ = state

task(state)
try {
task(state)
}
catch (error) {
console.error('Error running IdleQueue Task: ', error)
}

this.state_ = null
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import { IdleQueue } from './idleQueue'

export { IdleQueue }
export { IdleQueue } from './idleQueue'
41 changes: 0 additions & 41 deletions src/queueMicrotask.ts

This file was deleted.

16 changes: 16 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare global {
interface Window {
safari: any
}
}

export const isBrowser: boolean
= typeof window !== 'undefined'
&& typeof document !== 'undefined'
&& typeof navigator !== 'undefined'

export const isSafari: boolean
= isBrowser
&& 'safari' in window
&& typeof window.safari === 'object'
&& 'pushNotification' in window.safari
3 changes: 3 additions & 0 deletions src/utils/now.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function now(): number {
return Date.now()
}
41 changes: 41 additions & 0 deletions src/utils/queueMicrotask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { isBrowser } from './env'

type Microtask = () => void

function createQueueMicrotaskViaPromises(): (microtask: Microtask) => void {
return (microtask: Microtask) => {
Promise.resolve().then(microtask)
}
}

function createQueueMicrotaskViaMutationObserver(): (microtask: Microtask) => void {
let mutationCounter = 0
let microtaskQueue: Microtask[] = []
const observer = new MutationObserver(() => {
microtaskQueue.forEach(microtask => microtask())
microtaskQueue = []
})
const node = document.createTextNode('')
observer.observe(node, { characterData: true })

return (microtask: Microtask) => {
microtaskQueue.push(microtask)

// MutationObserver is a fallback for when native microtasks are unavailable
node.data = String(++mutationCounter % 2)
}
}

/**
/**
* Schedules a microtask using the best available browser method
* (queueMicrotask, Promises, or MutationObserver).
*/
export function createQueueMicrotask(): (microtask: Microtask) => void {
if (isBrowser && typeof queueMicrotask === 'function')
return queueMicrotask.bind(window)
else if (typeof Promise === 'function' && Promise.toString().includes('[native code]'))
return createQueueMicrotaskViaPromises()
else
return createQueueMicrotaskViaMutationObserver()
}

0 comments on commit de26785

Please sign in to comment.