From 95afacf690bf58ad01b5eca30183a215ca13f9b8 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Tue, 17 Sep 2024 20:34:44 +0200 Subject: [PATCH 1/3] fix(extension-load-queue): fixed TODO, added multi queues logic, added comments --- src/lib/browser.ts | 11 +++ src/lib/extension-load-queue.ts | 154 ++++++++++++++++++++++++++------ 2 files changed, 140 insertions(+), 25 deletions(-) create mode 100644 src/lib/browser.ts 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..f0d89cd 100644 --- a/src/lib/extension-load-queue.ts +++ b/src/lib/extension-load-queue.ts @@ -1,63 +1,149 @@ +/** + * 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. + */ export interface CreateLoadQueueArgs { + // The store where callbacks are saved store: ScriptStore; + + // Function that creates a controller createController: () => T; + + // Flag to check if the queue is already created isQueueCreated?: boolean; + + // Callback to handle queue creation 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 + 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.'); } }; -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('This functionality should be employed on the client-side.'); } +}; - 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. + */ +export 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 +152,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,27 +169,44 @@ export const createLoadQueue = ({ return next(); } + // Mark the processing as complete processing = false; } + // Start the queue processing unqueue(); }; -const noop = () => {}; - -export function useController(store: ScriptStore) { +/** + * A no-operation function used as a default cleanup function. + * + * @returns {void} + */ +const noop = (): void => {}; + +/** + * 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); + 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); } }; + } else { + // eslint-disable-next-line no-console + console.warn('Store is not provided to useController'); // Replace console.warn with a logging function if necessary } return noop; @@ -112,8 +217,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; } } From 295d8becdf24baf775d4efc9df6261f06d704e24 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Tue, 17 Sep 2024 21:04:11 +0200 Subject: [PATCH 2/3] fix(extension-load-queue): fixed export --- src/lib/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index b0c79bf..bcb6109 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,20 +3,20 @@ import { ControllerLoadedCallback, CreateLoadQueueArgs, ScriptStore, + createHandleQueueCreated, createLoadQueue, getQueueStore, getScriptStore, - handleQueueCreated, - isBrowser, useController, } from './extension-load-queue'; +import {isBrowser} from './browser'; export { AttrsParser, createLoadQueue, getQueueStore, getScriptStore, - handleQueueCreated, + createHandleQueueCreated, isBrowser, useController, }; From b7a48f146d4bd17c6904b41f613e5a4df502a029 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Wed, 25 Sep 2024 12:49:00 +0200 Subject: [PATCH 3/3] fix(extension-load-queue): fixed export --- src/lib/extension-load-queue.ts | 70 ++++++++++++++++++--------------- src/lib/index.ts | 11 +----- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/lib/extension-load-queue.ts b/src/lib/extension-load-queue.ts index f0d89cd..e31d46d 100644 --- a/src/lib/extension-load-queue.ts +++ b/src/lib/extension-load-queue.ts @@ -35,21 +35,43 @@ 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 + /** + * The store where callbacks are saved. + * + * @type {ScriptStore} + */ store: ScriptStore; - // Function that creates a controller + /** + * Function that creates a controller. + * + * @returns {T} A new controller instance. + */ createController: () => T; - // Flag to check if the queue is already created + /** + * Flag to check if the queue is already created. + * + * @type {boolean} [isQueueCreated] + */ isQueueCreated?: boolean; - // Callback to handle queue creation + /** + * Callback to handle queue creation. + * + * @param {boolean} created - Indicates if the queue was successfully created. + */ onQueueCreated?: (created: boolean) => void; - // Symbol key for identifying the queue + /** + * Symbol key for identifying the queue. + * + * @type {symbol} [queueKey] + */ queueKey?: symbol; } @@ -70,7 +92,7 @@ export const getScriptStore = (storeKey: symbol): ScriptStore => { 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.'); } }; @@ -85,7 +107,7 @@ const ensureQueuesSymbolInitialized = (): void => { window[QUEUES_SYMBOL] = {}; } } else { - throw new Error('This functionality should be employed on the client-side.'); + throw new Error('Cannot initialize QueueStore in a non-browser environment.'); } }; @@ -107,7 +129,7 @@ export const getQueueStore = (queueKey: symbol): boolean => { * @returns {function(boolean): void} A function that takes a boolean `created` * and marks the queue as created. */ -export const createHandleQueueCreated = (queueKey: symbol): ((created: boolean) => void) => { +const createHandleQueueCreated = (queueKey: symbol): ((created: boolean) => void) => { ensureQueuesSymbolInitialized(); return (created: boolean) => { window[QUEUES_SYMBOL][queueKey] = created; @@ -177,13 +199,6 @@ export const createLoadQueue = ({ unqueue(); }; -/** - * A no-operation function used as a default cleanup function. - * - * @returns {void} - */ -const noop = (): void => {}; - /** * React hook to manage and use a controller with a script store. * @@ -194,22 +209,15 @@ export function useController(store: ScriptStore): T | null { const [controller, setController] = useState(null); useEffect(() => { - if (store) { - 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); - } - }; - } else { - // eslint-disable-next-line no-console - console.warn('Store is not provided to useController'); // Replace console.warn with a logging function if necessary - } - - 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; diff --git a/src/lib/index.ts b/src/lib/index.ts index bcb6109..4aeb25f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,7 +3,6 @@ import { ControllerLoadedCallback, CreateLoadQueueArgs, ScriptStore, - createHandleQueueCreated, createLoadQueue, getQueueStore, getScriptStore, @@ -11,14 +10,6 @@ import { } from './extension-load-queue'; import {isBrowser} from './browser'; -export { - AttrsParser, - createLoadQueue, - getQueueStore, - getScriptStore, - createHandleQueueCreated, - isBrowser, - useController, -}; +export {AttrsParser, createLoadQueue, getQueueStore, getScriptStore, isBrowser, useController}; export type {ControllerLoadedCallback, CreateLoadQueueArgs, ScriptStore};