diff --git a/src/lib/browser.ts b/src/lib/browser.ts new file mode 100644 index 0000000..8393887 --- /dev/null +++ b/src/lib/browser.ts @@ -0,0 +1,11 @@ +/** + * Checks if the current environment is a browser. + * + * This function verifies whether the code is being executed in a browser environment. + * It checks for the presence of the `window` object, which is unique to browsers. + * + * @returns {boolean} - Returns `true` if the current environment is a browser, otherwise `false`. + */ +export const isBrowser = () => { + return typeof window !== 'undefined' && typeof window.document !== 'undefined'; +}; diff --git a/src/lib/extension-load-queue.ts b/src/lib/extension-load-queue.ts index 0a4fe24..e31d46d 100644 --- a/src/lib/extension-load-queue.ts +++ b/src/lib/extension-load-queue.ts @@ -1,63 +1,171 @@ +/** + * This file provides utilities for managing and processing load queues in a browser environment. + * + * It includes: + * - A system for creating and managing load queues identified by unique symbols. + * - A script store that holds callbacks associated with controllers. + * - A React hook for using and managing controllers within a script store. + * + * Example usage: + * + * const MY_QUEUE_SYMBOL = Symbol.for('my-queue'); + * const MY_STORE_SYMBOL = Symbol.for('my-controllers-store'); + * + * const myStore = getScriptStore(MY_STORE_SYMBOL); + * + * createLoadQueue({ + * store: myStore, + * createController: () => new MyController(), + * queueKey: MY_QUEUE_SYMBOL, + * }); + * + * const controller = useController(myStore); + */ + import {useEffect, useState} from 'react'; +import {isBrowser} from './browser'; + export type ControllerLoadedCallback = (controller: T) => void; -export const QUEUE_SYMBOL = Symbol.for('queue'); +export const QUEUES_SYMBOL = Symbol.for('extension-load-queues'); +export const SINGLE_QUEUE_SYMBOL = Symbol.for('single-queue'); export type ScriptStore = ControllerLoadedCallback[]; +/** + * Interface for creating a load queue. + * + * @template T - Type of the controller created in the queue. + */ export interface CreateLoadQueueArgs { + /** + * The store where callbacks are saved. + * + * @type {ScriptStore} + */ store: ScriptStore; + + /** + * Function that creates a controller. + * + * @returns {T} A new controller instance. + */ createController: () => T; + + /** + * Flag to check if the queue is already created. + * + * @type {boolean} [isQueueCreated] + */ isQueueCreated?: boolean; + + /** + * Callback to handle queue creation. + * + * @param {boolean} created - Indicates if the queue was successfully created. + */ onQueueCreated?: (created: boolean) => void; -} -// TODO: this function is a very frequently used utility, -// so we should place it in a separate top-level file and document it. -export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'; + /** + * Symbol key for identifying the queue. + * + * @type {symbol} [queueKey] + */ + queueKey?: symbol; +} -// TODO: window casts are weird — fix encounter any issues with declaration merging -export const getScriptStore = (key: symbol): ScriptStore => { +/** + * Retrieves or initializes a script store for the given symbol key. + * + * @param {symbol} storeKey - The key associated with the script store. + * @returns {ScriptStore} The script store associated with the given key. + */ +export const getScriptStore = (storeKey: symbol): ScriptStore => { if (isBrowser()) { - (window as Window)[key] = (window as Window)[key] || []; - return (window as Window)[key]; + // Initialize the store if it does not exist or check that it is an array + if (!window[storeKey]) { + window[storeKey] = []; + } else if (!Array.isArray(window[storeKey])) { + throw new Error(`Expected window[${String(storeKey)}] to be an array`); + } + + return window[storeKey] as ScriptStore; } else { - throw new Error('This functionality should be employed on the client-side.'); + throw new Error('Cannot initialize QueueStore in a non-browser environment.'); } }; -export const getQueueStore = () => { +/** + * Ensures that the queues are initialized as an object in the global window. + * + * @returns {void} + */ +const ensureQueuesSymbolInitialized = (): void => { if (isBrowser()) { - (window as Window)[QUEUE_SYMBOL] = (window as Window)[QUEUE_SYMBOL] || false; - return (window as Window)[QUEUE_SYMBOL]; + if (typeof window[QUEUES_SYMBOL] !== 'object' || window[QUEUES_SYMBOL] === null) { + window[QUEUES_SYMBOL] = {}; + } + } else { + throw new Error('Cannot initialize QueueStore in a non-browser environment.'); } +}; - return null; +/** + * Retrieves the status of whether the queue associated with the queueKey is created. + * + * @param {symbol} queueKey - The key associated with the queue. + * @returns {boolean} Whether the queue is created. + */ +export const getQueueStore = (queueKey: symbol): boolean => { + ensureQueuesSymbolInitialized(); + return window[QUEUES_SYMBOL]?.[queueKey] || false; }; -export const handleQueueCreated = (created: boolean) => { - (window as Window)[QUEUE_SYMBOL] = created; +/** + * Returns a function to mark the queue as created for a given queueKey. + * + * @param {symbol} queueKey - The key associated with the queue. + * @returns {function(boolean): void} A function that takes a boolean `created` + * and marks the queue as created. + */ +const createHandleQueueCreated = (queueKey: symbol): ((created: boolean) => void) => { + ensureQueuesSymbolInitialized(); + return (created: boolean) => { + window[QUEUES_SYMBOL][queueKey] = created; + }; }; +/** + * Creates and manages a load queue. + * + * @param {CreateLoadQueueArgs} args - The arguments required to create a load queue. + * @returns {void} + */ export const createLoadQueue = ({ store, createController, - isQueueCreated = getQueueStore(), - onQueueCreated = handleQueueCreated, -}: CreateLoadQueueArgs) => { + queueKey = SINGLE_QUEUE_SYMBOL, + isQueueCreated = getQueueStore(queueKey), + onQueueCreated = createHandleQueueCreated(queueKey), +}: CreateLoadQueueArgs): void => { if (!store || isQueueCreated) { return; } + + // Mark the queue as created onQueueCreated(true); const controller = createController(); const queue = store.splice(0, store.length); + // Override the store.push method to add callbacks to the queue and start processing store.push = function (...args) { args.forEach((callback) => { queue.push(callback); + + // Start processing the queue unqueue(); }); @@ -66,12 +174,14 @@ export const createLoadQueue = ({ let processing = false; + // Function to start processing the next callback in the queue function unqueue() { if (!processing) { next(); } } + // Function to process callbacks in the queue one by one async function next(): Promise { processing = true; @@ -81,30 +191,33 @@ export const createLoadQueue = ({ return next(); } + // Mark the processing as complete processing = false; } + // Start the queue processing unqueue(); }; -const noop = () => {}; - -export function useController(store: ScriptStore) { +/** + * React hook to manage and use a controller with a script store. + * + * @param {ScriptStore} store - The store where the controller is managed. + * @returns {T | null} The current controller or null if not available. + */ +export function useController(store: ScriptStore): T | null { const [controller, setController] = useState(null); useEffect(() => { - if (store) { - store.push(setController); - - return () => { - const index = store.indexOf(setController); - if (index > -1) { - store.splice(index, 1); - } - }; - } - - return noop; + store.push(setController); // Add setController to the store + + return () => { + const index = store.indexOf(setController); + if (index > -1) { + // Remove setController when unmounting + store.splice(index, 1); + } + }; }, []); return controller; @@ -112,8 +225,7 @@ export function useController(store: ScriptStore) { declare global { interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: symbol]: any; // TODO: sub-optimal, fix any - QUEUE_SYMBOL: boolean; + [key: symbol]: ScriptStore; + [QUEUES_SYMBOL]: Record; } } diff --git a/src/lib/index.ts b/src/lib/index.ts index b0c79bf..4aeb25f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -6,19 +6,10 @@ import { createLoadQueue, getQueueStore, getScriptStore, - handleQueueCreated, - isBrowser, useController, } from './extension-load-queue'; +import {isBrowser} from './browser'; -export { - AttrsParser, - createLoadQueue, - getQueueStore, - getScriptStore, - handleQueueCreated, - isBrowser, - useController, -}; +export {AttrsParser, createLoadQueue, getQueueStore, getScriptStore, isBrowser, useController}; export type {ControllerLoadedCallback, CreateLoadQueueArgs, ScriptStore};