From c1c849a61cf4368fd314dbc8028cb64b55482ff6 Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Wed, 29 Jun 2022 09:45:30 +0300 Subject: [PATCH] Reuse storage for API data, clean up build scripts --- components/ember/build.js | 21 +-- components/react/build.js | 22 +-- components/svelte/build.js | 22 +-- components/svg-framework/build.js | 21 +-- components/vue/build.js | 21 +-- components/vue2/build.js | 29 +-- iconify-icon/icon/build.js | 21 +-- packages/core/package.json | 4 + packages/core/src/api/callbacks.ts | 112 +++--------- packages/core/src/api/icons.ts | 203 +++++---------------- packages/core/src/api/types.ts | 60 ++++++ packages/core/src/browser-storage/data.ts | 5 +- packages/core/src/browser-storage/index.ts | 13 +- packages/core/src/browser-storage/store.ts | 3 - packages/core/src/browser-storage/types.ts | 3 - packages/core/tests/api/callbacks-test.ts | 125 +++++-------- 16 files changed, 207 insertions(+), 478 deletions(-) create mode 100644 packages/core/src/api/types.ts diff --git a/components/ember/build.js b/components/ember/build.js index 301a4cf..7129ecc 100644 --- a/components/ember/build.js +++ b/components/ember/build.js @@ -1,16 +1,12 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Parse command line const compile = { - core: false, lib: true, rollup: true, api: true, @@ -58,22 +54,9 @@ const fileExists = (file) => { return true; }; -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { - if (key !== 'core' && compile[key]) { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/components/react/build.js b/components/react/build.js index 23b2647..7893c7c 100644 --- a/components/react/build.js +++ b/components/react/build.js @@ -1,17 +1,12 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Build process const compile = { - // Compile @iconify/core - core: false, // Compile TypeScript src -> lib lib: true, // Fix types for icon components @@ -72,22 +67,9 @@ if (compile.api && !fileExists('./lib/icon.d.ts')) { compile.lib = true; } -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { - if (key !== 'core' && compile[key]) { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/components/svelte/build.js b/components/svelte/build.js index 4d1240e..4f8e53b 100644 --- a/components/svelte/build.js +++ b/components/svelte/build.js @@ -1,9 +1,6 @@ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; @@ -13,7 +10,6 @@ const extractor = (name) => // Parse command line const compile = { - core: false, tsc: true, bundles: true, api: true, @@ -61,28 +57,12 @@ const fileExists = (file) => { return true; }; -if (compile.dist && !fileExists(coreDir + '/lib/modules.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { if (!compile[key]) { return; } switch (key) { - case 'core': - break; - case 'api': apiFiles().forEach((name) => { const cmd = extractor(name).split(' '); diff --git a/components/svg-framework/build.js b/components/svg-framework/build.js index db6a885..715561d 100644 --- a/components/svg-framework/build.js +++ b/components/svg-framework/build.js @@ -1,16 +1,12 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Parse command line const compile = { - core: false, lib: true, dist: true, api: true, @@ -65,27 +61,14 @@ if (compile.api && !fileExists('./lib/iconify.d.ts')) { compile.lib = true; } -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - // Add api2 if (compile.api) { compile.api2 = true; } -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { - if (key !== 'core' && compile[key]) { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/components/vue/build.js b/components/vue/build.js index 5a2500e..4721fec 100644 --- a/components/vue/build.js +++ b/components/vue/build.js @@ -1,16 +1,12 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Parse command line const compile = { - core: false, lib: true, dist: true, api: true, @@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/IconifyIcon.d.ts')) { compile.lib = true; } -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { - if (key !== 'core' && compile[key]) { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/components/vue2/build.js b/components/vue2/build.js index b3c48a1..4721fec 100644 --- a/components/vue2/build.js +++ b/components/vue2/build.js @@ -1,21 +1,17 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Parse command line const compile = { - core: false, lib: true, dist: true, api: true, }; -process.argv.slice(2).forEach(cmd => { +process.argv.slice(2).forEach((cmd) => { if (cmd.slice(0, 2) !== '--') { return; } @@ -39,7 +35,7 @@ process.argv.slice(2).forEach(cmd => { case 'only': // disable other modules - Object.keys(compile).forEach(key2 => { + Object.keys(compile).forEach((key2) => { compile[key2] = key2 === key; }); break; @@ -48,7 +44,7 @@ process.argv.slice(2).forEach(cmd => { }); // Check if required modules in same monorepo are available -const fileExists = file => { +const fileExists = (file) => { try { fs.statSync(file); } catch (e) { @@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/IconifyIcon.d.ts')) { compile.lib = true; } -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages -Object.keys(compile).forEach(key => { - if (key !== 'core' && compile[key]) { +// Compile packages +Object.keys(compile).forEach((key) => { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/iconify-icon/icon/build.js b/iconify-icon/icon/build.js index 649f287..20707e3 100644 --- a/iconify-icon/icon/build.js +++ b/iconify-icon/icon/build.js @@ -1,16 +1,12 @@ /* eslint-disable */ const fs = require('fs'); -const path = require('path'); const child_process = require('child_process'); -const coreDir = path.dirname(require.resolve('@iconify/core/package.json')); - // List of commands to run const commands = []; // Parse command line const compile = { - core: false, lib: true, dist: true, api: true, @@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/index.d.ts')) { compile.lib = true; } -if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { - compile.core = true; -} - -// Compile core before compiling this package -if (compile.core) { - commands.push({ - cmd: 'npm', - args: ['run', 'build'], - cwd: coreDir, - }); -} - -// Compile other packages +// Compile packages Object.keys(compile).forEach((key) => { - if (key !== 'core' && compile[key]) { + if (compile[key]) { commands.push({ cmd: 'npm', args: ['run', 'build:' + key], diff --git a/packages/core/package.json b/packages/core/package.json index b6d9dbb..36aa008 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,6 +57,10 @@ "require": "./lib/api/query.cjs", "import": "./lib/api/query.mjs" }, + "./lib/api/types": { + "require": "./lib/api/types.cjs", + "import": "./lib/api/types.mjs" + }, "./lib/browser-storage/config": { "require": "./lib/browser-storage/config.cjs", "import": "./lib/browser-storage/config.mjs" diff --git a/packages/core/src/api/callbacks.ts b/packages/core/src/api/callbacks.ts index 228d6eb..f2b64ee 100644 --- a/packages/core/src/api/callbacks.ts +++ b/packages/core/src/api/callbacks.ts @@ -2,59 +2,17 @@ import type { IconifyIconLoaderCallback, IconifyIconLoaderAbort, } from './icons'; -import { getStorage } from '../storage/storage'; import type { SortedIcons } from '../icon/sort'; -import type { IconifyIconSource } from '@iconify/utils/lib/icon/name'; - -/** - * Storage for callbacks - */ -interface CallbackItem { - // id - id: number; - - // Icons - icons: SortedIcons; - - // Callback to call on any update - callback: IconifyIconLoaderCallback; - - // Callback to call to remove item from queue - abort: IconifyIconLoaderAbort; -} - -type PrefixCallbackItems = CallbackItem[]; -type ProviderCallbackItems = Record; - -// Records sorted by provider and prefix -// This export is only for unit testing, should not be used -export const callbacks = Object.create(null) as Record< - string, - ProviderCallbackItems ->; - -// List of provider/prefix combinations that need to be updated -type ProviderPendingUpdates = Record; -const pendingUpdates = Object.create(null) as Record< - string, - ProviderPendingUpdates ->; +import type { APICallbackItem, IconStorageWithIcons } from './types'; /** * Remove callback */ -function removeCallback(sources: IconifyIconSource[], id: number): void { - sources.forEach((source) => { - const provider = source.provider; - if (callbacks[provider] === void 0) { - return; - } - const providerCallbacks = callbacks[provider]; - - const prefix = source.prefix; - const items = providerCallbacks[prefix]; +function removeCallback(storages: IconStorageWithIcons[], id: number): void { + storages.forEach((storage) => { + const items = storage.loaderCallbacks; if (items) { - providerCallbacks[prefix] = items.filter((row) => row.id !== id); + storage.loaderCallbacks = items.filter((row) => row.id !== id); } }); } @@ -62,37 +20,26 @@ function removeCallback(sources: IconifyIconSource[], id: number): void { /** * Update all callbacks for provider and prefix */ -export function updateCallbacks(provider: string, prefix: string): void { - if (pendingUpdates[provider] === void 0) { - pendingUpdates[provider] = Object.create( - null - ) as ProviderPendingUpdates; - } - const providerPendingUpdates = pendingUpdates[provider]; - - if (!providerPendingUpdates[prefix]) { - providerPendingUpdates[prefix] = true; +export function updateCallbacks(storage: IconStorageWithIcons): void { + if (!storage.pendingCallbacksFlag) { + storage.pendingCallbacksFlag = true; setTimeout(() => { - providerPendingUpdates[prefix] = false; - - if ( - callbacks[provider] === void 0 || - callbacks[provider][prefix] === void 0 - ) { - return; - } + storage.pendingCallbacksFlag = false; // Get all items - const items = callbacks[provider][prefix].slice(0); + const items = storage.loaderCallbacks + ? storage.loaderCallbacks.slice(0) + : []; if (!items.length) { return; } - const storage = getStorage(provider, prefix); - // Check each item for changes let hasPending = false; - items.forEach((item: CallbackItem) => { + const provider = storage.provider; + const prefix = storage.prefix; + + items.forEach((item) => { const icons = item.icons; const oldLength = icons.pending.length; icons.pending = icons.pending.filter((icon) => { @@ -129,15 +76,7 @@ export function updateCallbacks(provider: string, prefix: string): void { if (icons.pending.length !== oldLength) { if (!hasPending) { // All icons have been loaded - remove callback from prefix - removeCallback( - [ - { - provider, - prefix, - }, - ], - item.id - ); + removeCallback([storage], item.id); } item.callback( icons.loaded.slice(0), @@ -162,7 +101,7 @@ let idCounter = 0; export function storeCallback( callback: IconifyIconLoaderCallback, icons: SortedIcons, - pendingSources: IconifyIconSource[] + pendingSources: IconStorageWithIcons[] ): IconifyIconLoaderAbort { // Create unique id and abort function const id = idCounter++; @@ -174,24 +113,15 @@ export function storeCallback( } // Create item and store it for all pending prefixes - const item: CallbackItem = { + const item: APICallbackItem = { id, icons, callback, abort: abort, }; - pendingSources.forEach((source) => { - const provider = source.provider; - const prefix = source.prefix; - if (callbacks[provider] === void 0) { - callbacks[provider] = Object.create(null) as ProviderCallbackItems; - } - const providerCallbacks = callbacks[provider]; - if (providerCallbacks[prefix] === void 0) { - providerCallbacks[prefix] = []; - } - providerCallbacks[prefix].push(item); + pendingSources.forEach((storage) => { + (storage.loaderCallbacks || (storage.loaderCallbacks = [])).push(item); }); return abort; diff --git a/packages/core/src/api/icons.ts b/packages/core/src/api/icons.ts index 3bf4dcb..ed2e8ff 100644 --- a/packages/core/src/api/icons.ts +++ b/packages/core/src/api/icons.ts @@ -1,9 +1,5 @@ import type { IconifyIcon, IconifyJSON } from '@iconify/types'; -import { - IconifyIconName, - IconifyIconSource, - stringToIcon, -} from '@iconify/utils/lib/icon/name'; +import { IconifyIconName, stringToIcon } from '@iconify/utils/lib/icon/name'; import type { SortedIcons } from '../icon/sort'; import { sortIcons } from '../icon/sort'; import { storeCallback, updateCallbacks } from './callbacks'; @@ -13,6 +9,7 @@ import { listToIcons } from '../icon/list'; import { allowSimpleNames, getIconData } from '../storage/functions'; import { sendAPIQuery } from './query'; import { storeInBrowserStorage } from '../browser-storage/store'; +import type { IconStorageWithIcons } from './types'; // Empty abort callback for loadIcons() function emptyCallback(): void { @@ -49,126 +46,46 @@ export type IconifyLoadIcons = ( */ export type IsPending = (icon: IconifyIconName) => boolean; -/** - * List of icons that are being loaded. - * - * Icons are added to this list when they are being checked and - * removed from this list when they are added to storage as - * either an icon or a missing icon. This way same icon should - * never be requested twice. - * - * [provider][prefix][icon] = time when icon was added to queue - */ -type PrefixPendingIcons = Record; -type ProviderPendingIcons = Record; - -const pendingIcons = Object.create(null) as Record< - string, - ProviderPendingIcons ->; - -/** - * List of icons that are waiting to be loaded. - * - * List is passed to API module, then cleared. - * - * This list should not be used for any checks, use pendingIcons to check - * if icons is being loaded. - * - * [provider][prefix] = array of icon names - */ -type IconsToLoadPrefixItem = string[]; -type IconsToLoadProviderItem = Record; - -const iconsToLoad = Object.create(null) as Record< - string, - IconsToLoadProviderItem ->; - -// Flags to merge multiple synchronous icon requests in one asynchronous request -type FlagsItem = Record; - -const loaderFlags = Object.create(null) as Record; -const queueFlags = Object.create(null) as Record; - /** * Function called when new icons have been loaded */ -function loadedNewIcons(provider: string, prefix: string): void { +function loadedNewIcons(storage: IconStorageWithIcons): void { // Run only once per tick, possibly joining multiple API responses in one call - if (loaderFlags[provider] === void 0) { - loaderFlags[provider] = Object.create(null) as FlagsItem; - } - const providerLoaderFlags = loaderFlags[provider]; - if (!providerLoaderFlags[prefix]) { - providerLoaderFlags[prefix] = true; + if (!storage.iconsLoaderFlag) { + storage.iconsLoaderFlag = true; setTimeout(() => { - providerLoaderFlags[prefix] = false; - updateCallbacks(provider, prefix); + storage.iconsLoaderFlag = false; + updateCallbacks(storage); }); } } -// Storage for errors for loadNewIcons(). Used to avoid spamming log with identical errors. -const errorsCache = Object.create(null) as Record; - /** * Load icons */ -function loadNewIcons(provider: string, prefix: string, icons: string[]): void { - function err(): void { - const key = (provider === '' ? '' : '@' + provider + ':') + prefix; - const time = Math.floor(Date.now() / 60000); // log once in a minute - if (errorsCache[key] < time) { - errorsCache[key] = time; - console.error( - 'Unable to retrieve icons for "' + - key + - '" because API is not configured properly.' - ); - } - } - - // Create nested objects if needed - if (iconsToLoad[provider] === void 0) { - iconsToLoad[provider] = Object.create(null) as IconsToLoadProviderItem; - } - const providerIconsToLoad = iconsToLoad[provider]; - - if (queueFlags[provider] === void 0) { - queueFlags[provider] = Object.create(null) as FlagsItem; - } - const providerQueueFlags = queueFlags[provider]; - - if (pendingIcons[provider] === void 0) { - pendingIcons[provider] = Object.create(null) as ProviderPendingIcons; - } - const providerPendingIcons = pendingIcons[provider]; - +function loadNewIcons(storage: IconStorageWithIcons, icons: string[]): void { // Add icons to queue - if (providerIconsToLoad[prefix] === void 0) { - providerIconsToLoad[prefix] = icons; + if (!storage.iconsToLoad) { + storage.iconsToLoad = icons; } else { - providerIconsToLoad[prefix] = providerIconsToLoad[prefix] - .concat(icons) - .sort(); + storage.iconsToLoad = storage.iconsToLoad.concat(icons).sort(); } // Trigger update on next tick, mering multiple synchronous requests into one asynchronous request - if (!providerQueueFlags[prefix]) { - providerQueueFlags[prefix] = true; + if (!storage.iconsQueueFlag) { + storage.iconsQueueFlag = true; setTimeout(() => { - providerQueueFlags[prefix] = false; + storage.iconsQueueFlag = false; + const { provider, prefix } = storage; // Get icons and delete queue - const icons = providerIconsToLoad[prefix]; - delete providerIconsToLoad[prefix]; + const icons = storage.iconsToLoad; + delete storage.iconsToLoad; // Get API module - const api = getAPIModule(provider); - if (!api) { - // No way to load icons! - err(); + let api: ReturnType; + if (!icons || !(api = getAPIModule(provider))) { + // No icons or no way to load icons! return; } @@ -176,8 +93,6 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void { const params = api.prepare(provider, prefix, icons); params.forEach((item) => { sendAPIQuery(provider, item, (data, error) => { - const storage = getStorage(provider, prefix); - // Check for error if (typeof data !== 'object') { if (error !== 404) { @@ -201,10 +116,12 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void { } // Remove added icons from pending list - const pending = providerPendingIcons[prefix]; - parsed.forEach((name) => { - delete pending[name]; - }); + const pending = storage.pendingIcons; + if (pending) { + parsed.forEach((name) => { + pending.delete(name); + }); + } // Cache API response storeInBrowserStorage( @@ -217,7 +134,7 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void { } // Trigger update on next tick - loadedNewIcons(provider, prefix); + loadedNewIcons(storage); }); }); }); @@ -228,13 +145,12 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void { * Check if icon is being loaded */ export const isPending: IsPending = (icon: IconifyIconName): boolean => { - const provider = icon.provider; - const prefix = icon.prefix; - return ( - pendingIcons[provider] && - pendingIcons[provider][prefix] && - pendingIcons[provider][prefix][icon.name] !== void 0 - ); + const storage = getStorage( + icon.provider, + icon.prefix + ) as IconStorageWithIcons; + const pending = storage.pendingIcons; + return !!(pending && pending.has(icon.name)); }; /** @@ -280,59 +196,41 @@ export const loadIcons: IconifyLoadIcons = ( string, ProviderNewIconsList >; - const sources: IconifyIconSource[] = []; + const sources: IconStorageWithIcons[] = []; let lastProvider: string, lastPrefix: string; sortedIcons.pending.forEach((icon) => { - const provider = icon.provider; - const prefix = icon.prefix; + const { provider, prefix } = icon; if (prefix === lastPrefix && provider === lastProvider) { return; } lastProvider = provider; lastPrefix = prefix; - sources.push({ - provider, - prefix, - }); + sources.push(getStorage(provider, prefix)); - if (pendingIcons[provider] === void 0) { - pendingIcons[provider] = Object.create( - null - ) as ProviderPendingIcons; - } - const providerPendingIcons = pendingIcons[provider]; - if (providerPendingIcons[prefix] === void 0) { - providerPendingIcons[prefix] = Object.create( - null - ) as PrefixPendingIcons; - } - - if (newIcons[provider] === void 0) { - newIcons[provider] = Object.create(null) as ProviderNewIconsList; - } - const providerNewIcons = newIcons[provider]; - if (providerNewIcons[prefix] === void 0) { + const providerNewIcons = + newIcons[provider] || + (newIcons[provider] = Object.create(null) as ProviderNewIconsList); + if (!providerNewIcons[prefix]) { providerNewIcons[prefix] = []; } }); // List of new icons - const time = Date.now(); - // Filter pending icons list: find icons that are not being loaded yet // If icon was called before, it must exist in pendingIcons or storage, but because this // function is called right after sortIcons() that checks storage, icon is definitely not in storage. sortedIcons.pending.forEach((icon) => { - const provider = icon.provider; - const prefix = icon.prefix; - const name = icon.name; + const { provider, prefix, name } = icon; - const pendingQueue = pendingIcons[provider][prefix]; - if (pendingQueue[name] === void 0) { + const storage = getStorage(provider, prefix) as IconStorageWithIcons; + const pendingQueue = + storage.pendingIcons || (storage.pendingIcons = new Set()); + + if (!pendingQueue.has(name)) { // New icon - add to pending queue to mark it as being loaded - pendingQueue[name] = time; + pendingQueue.add(name); // Add it to new icons list to pass it to API module for loading newIcons[provider][prefix].push(name); } @@ -340,11 +238,10 @@ export const loadIcons: IconifyLoadIcons = ( // Load icons on next tick to make sure result is not returned before callback is stored and // to consolidate multiple synchronous loadIcons() calls into one asynchronous API call - sources.forEach((source) => { - const provider = source.provider; - const prefix = source.prefix; + sources.forEach((storage) => { + const { provider, prefix } = storage; if (newIcons[provider][prefix].length) { - loadNewIcons(provider, prefix, newIcons[provider][prefix]); + loadNewIcons(storage, newIcons[provider][prefix]); } }); diff --git a/packages/core/src/api/types.ts b/packages/core/src/api/types.ts new file mode 100644 index 0000000..cbd9ff0 --- /dev/null +++ b/packages/core/src/api/types.ts @@ -0,0 +1,60 @@ +import type { + IconifyIconLoaderCallback, + IconifyIconLoaderAbort, +} from './icons'; +import type { SortedIcons } from '../icon/sort'; +import type { IconStorage } from '../storage/storage'; + +/** + * Storage for callbacks + */ +export interface APICallbackItem { + // id + id: number; + + // Icons + icons: SortedIcons; + + // Callback to call on any update + callback: IconifyIconLoaderCallback; + + // Callback to call to remove item from queue + abort: IconifyIconLoaderAbort; +} + +/** + * Add custom stuff to storage + */ +export interface IconStorageWithIcons extends IconStorage { + /** + * List of icons that are being loaded, added to storage + * + * Icons are added to this list when they are being checked and + * removed from this list when they are added to storage as + * either an icon or a missing icon. This way same icon should + * never be requested twice. + */ + pendingIcons?: Set; + + /** + * List of icons that are waiting to be loaded. + * + * List is passed to API module, then cleared. + * + * This list should not be used for any checks, use pendingIcons to check + * if icons is being loaded. + * + * [provider][prefix] = array of icon names + */ + iconsToLoad?: string[]; + + // Flags to merge multiple synchronous icon requests in one asynchronous request + iconsLoaderFlag?: boolean; + iconsQueueFlag?: boolean; + + // Loader callbacks + loaderCallbacks?: APICallbackItem[]; + + // Pending callbacks update + pendingCallbacksFlag?: boolean; +} diff --git a/packages/core/src/browser-storage/data.ts b/packages/core/src/browser-storage/data.ts index b5cb0d5..65d4e23 100644 --- a/packages/core/src/browser-storage/data.ts +++ b/packages/core/src/browser-storage/data.ts @@ -2,7 +2,6 @@ import type { BrowserStorageConfig, BrowserStorageCount, BrowserStorageEmptyList, - BrowserStorageStatus, } from './types'; /** @@ -32,8 +31,8 @@ export const browserStorageEmptyItems: BrowserStorageEmptyList = { /** * Flag to check if storage has been loaded */ -export let browserStorageStatus: BrowserStorageStatus = false; +export let browserStorageStatus = false; -export function setBrowserStorageStatus(status: BrowserStorageStatus) { +export function setBrowserStorageStatus(status: boolean) { browserStorageStatus = status; } diff --git a/packages/core/src/browser-storage/index.ts b/packages/core/src/browser-storage/index.ts index f08a7ad..cc659bf 100644 --- a/packages/core/src/browser-storage/index.ts +++ b/packages/core/src/browser-storage/index.ts @@ -23,10 +23,10 @@ import type { BrowserStorageConfig, BrowserStorageItem } from './types'; * Load icons from cache */ export function initBrowserStorage() { - if (browserStorageStatus === true) { + if (browserStorageStatus) { return; } - setBrowserStorageStatus('loading'); + setBrowserStorageStatus(true); // Minimum time const minTime = @@ -66,10 +66,12 @@ export function initBrowserStorage() { valid = false; } else { // Add icon set + const iconSet = data.data; + const provider = data.provider; - const prefix = data.data.prefix; + const prefix = iconSet.prefix; const storage = getStorage(provider, prefix); - valid = addIconSet(storage, data.data).length > 0; + valid = addIconSet(storage, iconSet).length > 0; } } catch (err) { valid = false; @@ -129,9 +131,10 @@ export function initBrowserStorage() { } } + // Load each storage for (const key in browserStorageConfig) { load(key as keyof BrowserStorageConfig); } - setBrowserStorageStatus(true); + // Check for update } diff --git a/packages/core/src/browser-storage/store.ts b/packages/core/src/browser-storage/store.ts index 76b8d65..8c2f7a4 100644 --- a/packages/core/src/browser-storage/store.ts +++ b/packages/core/src/browser-storage/store.ts @@ -18,9 +18,6 @@ export function storeInBrowserStorage(provider: string, data: IconifyJSON) { if (!browserStorageStatus) { initBrowserStorage(); } - if (browserStorageStatus === 'loading') { - return; - } function store(key: BrowserStorageType): true | undefined { if (!browserStorageConfig[key]) { diff --git a/packages/core/src/browser-storage/types.ts b/packages/core/src/browser-storage/types.ts index 303820e..4dffa50 100644 --- a/packages/core/src/browser-storage/types.ts +++ b/packages/core/src/browser-storage/types.ts @@ -18,6 +18,3 @@ export interface BrowserStorageItem { provider: string; data: IconifyJSON; } - -// Status: not loaded, loading, loaded -export type BrowserStorageStatus = false | 'loading' | true; diff --git a/packages/core/tests/api/callbacks-test.ts b/packages/core/tests/api/callbacks-test.ts index 71a6cc6..7d5ecf2 100644 --- a/packages/core/tests/api/callbacks-test.ts +++ b/packages/core/tests/api/callbacks-test.ts @@ -1,8 +1,5 @@ -import { - callbacks, - updateCallbacks, - storeCallback, -} from '../../lib/api/callbacks'; +import { updateCallbacks, storeCallback } from '../../lib/api/callbacks'; +import type { IconStorageWithIcons } from '../../lib/api/types'; import { sortIcons } from '../../lib/icon/sort'; import { getStorage, addIconSet } from '../../lib/storage/storage'; @@ -22,7 +19,7 @@ describe('Testing API callbacks', () => { const prefix = nextPrefix(); let counter = 0; - const storage = getStorage(provider, prefix); + const storage = getStorage(provider, prefix) as IconStorageWithIcons; const abort = storeCallback( (loaded, missing, pending, unsubscribe) => { expect(unsubscribe).toBe(abort); @@ -52,7 +49,7 @@ describe('Testing API callbacks', () => { name: 'icon2', }, ]); - expect(callbacks[provider][prefix].length).toBe(1); + expect(storage.loaderCallbacks?.length).toBe(1); // Add icon2 and trigger update addIconSet(storage, { @@ -64,7 +61,7 @@ describe('Testing API callbacks', () => { }, }); - updateCallbacks(provider, prefix); + updateCallbacks(storage); return; case 2: @@ -89,7 +86,7 @@ describe('Testing API callbacks', () => { }, ]); expect(pending).toEqual([]); - expect(callbacks[provider][prefix].length).toBe(0); + expect(storage.loaderCallbacks?.length).toBe(0); done(); } }, @@ -110,19 +107,14 @@ describe('Testing API callbacks', () => { name: 'icon3', }, ]), - [ - { - provider, - prefix, - }, - ] + [storage] ); // Test callbacks - expect(callbacks[provider][prefix].length).toBe(1); + expect(storage.loaderCallbacks?.length).toBe(1); // Test update - should do nothing - updateCallbacks(provider, prefix); + updateCallbacks(storage); // Wait for tick because updateCallbacks will use one setTimeout(() => { @@ -139,7 +131,7 @@ describe('Testing API callbacks', () => { }, not_found: ['icon3'], }); - updateCallbacks(provider, prefix); + updateCallbacks(storage); }); }); @@ -147,7 +139,7 @@ describe('Testing API callbacks', () => { const provider = ''; const prefix = nextPrefix(); - const storage = getStorage(provider, prefix); + const storage = getStorage(provider, prefix) as IconStorageWithIcons; addIconSet(storage, { prefix, icons: { @@ -182,16 +174,11 @@ describe('Testing API callbacks', () => { name: 'icon3', }, ]), - [ - { - provider, - prefix, - }, - ] + [storage] ); // callbacks should not have been initialised - expect(callbacks[prefix]).toBeUndefined(); + expect(storage.loaderCallbacks).toBeUndefined(); }); it('Cancel callback', (done) => { @@ -199,7 +186,7 @@ describe('Testing API callbacks', () => { const prefix = nextPrefix(); let counter = 0; - const storage = getStorage(provider, prefix); + const storage = getStorage(provider, prefix) as IconStorageWithIcons; const abort = storeCallback( (loaded, missing, pending, unsubscribe) => { expect(unsubscribe).toBe(abort); @@ -229,7 +216,7 @@ describe('Testing API callbacks', () => { name: 'icon2', }, ]); - expect(callbacks[provider][prefix].length).toBe(1); + expect(storage.loaderCallbacks?.length).toBe(1); // Add icon2 and trigger update addIconSet(storage, { @@ -241,11 +228,11 @@ describe('Testing API callbacks', () => { }, }); - updateCallbacks(provider, prefix); + updateCallbacks(storage); // Unsubscribe and set timer to call done() unsubscribe(); - expect(callbacks[provider][prefix].length).toBe(0); + expect(storage.loaderCallbacks?.length).toBe(0); setTimeout(done); }, sortIcons([ @@ -265,19 +252,14 @@ describe('Testing API callbacks', () => { name: 'icon3', }, ]), - [ - { - provider, - prefix, - }, - ] + [storage] ); // Test callbacks - expect(callbacks[provider][prefix].length).toBe(1); + expect(storage.loaderCallbacks?.length).toBe(1); // Test update - should do nothing - updateCallbacks(provider, prefix); + updateCallbacks(storage); // Wait for tick because updateCallbacks will use one setTimeout(() => { @@ -294,7 +276,7 @@ describe('Testing API callbacks', () => { }, not_found: ['icon3'], }); - updateCallbacks(provider, prefix); + updateCallbacks(storage); }); }); @@ -304,8 +286,8 @@ describe('Testing API callbacks', () => { const prefix2 = nextPrefix(); let counter = 0; - const storage1 = getStorage(provider, prefix1); - const storage2 = getStorage(provider, prefix2); + const storage1 = getStorage(provider, prefix1) as IconStorageWithIcons; + const storage2 = getStorage(provider, prefix2) as IconStorageWithIcons; const abort = storeCallback( (loaded, missing, pending, unsubscribe) => { @@ -336,8 +318,8 @@ describe('Testing API callbacks', () => { name: 'icon2', }, ]); - expect(callbacks[provider][prefix1].length).toBe(0); - expect(callbacks[provider][prefix2].length).toBe(1); + expect(storage1.loaderCallbacks?.length).toBe(0); + expect(storage2.loaderCallbacks?.length).toBe(1); // Add icon2 and trigger update addIconSet(storage2, { @@ -349,13 +331,13 @@ describe('Testing API callbacks', () => { }, }); - updateCallbacks(provider, prefix2); + updateCallbacks(storage2); break; case 2: // Second run - icon2 should be loaded - expect(callbacks[provider][prefix1].length).toBe(0); - expect(callbacks[provider][prefix2].length).toBe(0); + expect(storage1.loaderCallbacks?.length).toBe(0); + expect(storage2.loaderCallbacks?.length).toBe(0); done(); break; @@ -380,18 +362,15 @@ describe('Testing API callbacks', () => { name: 'icon3', }, ]), - [ - { provider, prefix: prefix1 }, - { provider, prefix: prefix2 }, - ] + [storage1, storage2] ); // Test callbacks - expect(callbacks[provider][prefix1].length).toBe(1); - expect(callbacks[provider][prefix2].length).toBe(1); + expect(storage1.loaderCallbacks?.length).toBe(1); + expect(storage2.loaderCallbacks?.length).toBe(1); // Test update - should do nothing - updateCallbacks(provider, prefix1); + updateCallbacks(storage1); // Wait for tick because updateCallbacks will use one setTimeout(() => { @@ -408,7 +387,7 @@ describe('Testing API callbacks', () => { }, not_found: ['icon3'], }); - updateCallbacks(provider, prefix1); + updateCallbacks(storage1); }); }); @@ -419,8 +398,8 @@ describe('Testing API callbacks', () => { const prefix2 = nextPrefix(); let counter = 0; - const storage1 = getStorage(provider1, prefix1); - const storage2 = getStorage(provider2, prefix2); + const storage1 = getStorage(provider1, prefix1) as IconStorageWithIcons; + const storage2 = getStorage(provider2, prefix2) as IconStorageWithIcons; const abort = storeCallback( (loaded, missing, pending, unsubscribe) => { @@ -451,12 +430,8 @@ describe('Testing API callbacks', () => { name: 'icon2', }, ]); - expect(callbacks[provider1][prefix1].length).toBe(0); - expect(callbacks[provider2][prefix2].length).toBe(1); - - // Make sure providers/prefixes aren't mixed - expect(callbacks[provider1][prefix2]).toBeUndefined(); - expect(callbacks[provider2][prefix1]).toBeUndefined(); + expect(storage1.loaderCallbacks?.length).toBe(0); + expect(storage2.loaderCallbacks?.length).toBe(1); // Add icon2 and trigger update addIconSet(storage2, { @@ -468,17 +443,13 @@ describe('Testing API callbacks', () => { }, }); - updateCallbacks(provider2, prefix2); + updateCallbacks(storage2); break; case 2: // Second run - icon2 should be loaded - expect(callbacks[provider1][prefix1].length).toBe(0); - expect(callbacks[provider2][prefix2].length).toBe(0); - - // Make sure providers/prefixes aren't mixed - expect(callbacks[provider1][prefix2]).toBeUndefined(); - expect(callbacks[provider2][prefix1]).toBeUndefined(); + expect(storage1.loaderCallbacks?.length).toBe(0); + expect(storage2.loaderCallbacks?.length).toBe(0); done(); break; @@ -504,21 +475,15 @@ describe('Testing API callbacks', () => { name: 'icon3', }, ]), - [ - { provider: provider1, prefix: prefix1 }, - { provider: provider2, prefix: prefix2 }, - ] + [storage1, storage2] ); // Test callbacks - expect(callbacks[provider1][prefix1].length).toBe(1); - expect(callbacks[provider2][prefix2].length).toBe(1); - - expect(callbacks[provider1][prefix2]).toBeUndefined(); - expect(callbacks[provider2][prefix1]).toBeUndefined(); + expect(storage1.loaderCallbacks?.length).toBe(1); + expect(storage2.loaderCallbacks?.length).toBe(1); // Test update - should do nothing - updateCallbacks(provider1, prefix1); + updateCallbacks(storage1); // Wait for tick because updateCallbacks will use one setTimeout(() => { @@ -535,7 +500,7 @@ describe('Testing API callbacks', () => { }, not_found: ['icon3'], }); - updateCallbacks(provider1, prefix1); + updateCallbacks(storage1); }); }); });