2
0
mirror of https://github.com/iconify/iconify.git synced 2024-11-17 01:55:09 +00:00

Reuse storage for API data, clean up build scripts

This commit is contained in:
Vjacheslav Trushkin 2022-06-29 09:45:30 +03:00
parent 9f71691fd2
commit c1c849a61c
16 changed files with 207 additions and 478 deletions

View File

@ -1,16 +1,12 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
lib: true, lib: true,
rollup: true, rollup: true,
api: true, api: true,
@ -58,22 +54,9 @@ const fileExists = (file) => {
return true; return true;
}; };
if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { // Compile packages
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) => { Object.keys(compile).forEach((key) => {
if (key !== 'core' && compile[key]) { if (compile[key]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -1,17 +1,12 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Build process // Build process
const compile = { const compile = {
// Compile @iconify/core
core: false,
// Compile TypeScript src -> lib // Compile TypeScript src -> lib
lib: true, lib: true,
// Fix types for icon components // Fix types for icon components
@ -72,22 +67,9 @@ if (compile.api && !fileExists('./lib/icon.d.ts')) {
compile.lib = true; compile.lib = true;
} }
if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { // Compile packages
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) => { Object.keys(compile).forEach((key) => {
if (key !== 'core' && compile[key]) { if (compile[key]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -1,9 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
@ -13,7 +10,6 @@ const extractor = (name) =>
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
tsc: true, tsc: true,
bundles: true, bundles: true,
api: true, api: true,
@ -61,28 +57,12 @@ const fileExists = (file) => {
return true; return true;
}; };
if (compile.dist && !fileExists(coreDir + '/lib/modules.mjs')) { // Compile packages
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) => { Object.keys(compile).forEach((key) => {
if (!compile[key]) { if (!compile[key]) {
return; return;
} }
switch (key) { switch (key) {
case 'core':
break;
case 'api': case 'api':
apiFiles().forEach((name) => { apiFiles().forEach((name) => {
const cmd = extractor(name).split(' '); const cmd = extractor(name).split(' ');

View File

@ -1,16 +1,12 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
lib: true, lib: true,
dist: true, dist: true,
api: true, api: true,
@ -65,27 +61,14 @@ if (compile.api && !fileExists('./lib/iconify.d.ts')) {
compile.lib = true; 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 // Add api2
if (compile.api) { if (compile.api) {
compile.api2 = true; compile.api2 = true;
} }
// Compile other packages // Compile packages
Object.keys(compile).forEach((key) => { Object.keys(compile).forEach((key) => {
if (key !== 'core' && compile[key]) { if (compile[key]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -1,16 +1,12 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
lib: true, lib: true,
dist: true, dist: true,
api: true, api: true,
@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/IconifyIcon.d.ts')) {
compile.lib = true; compile.lib = true;
} }
if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { // Compile packages
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) => { Object.keys(compile).forEach((key) => {
if (key !== 'core' && compile[key]) { if (compile[key]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -1,21 +1,17 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
lib: true, lib: true,
dist: true, dist: true,
api: true, api: true,
}; };
process.argv.slice(2).forEach(cmd => { process.argv.slice(2).forEach((cmd) => {
if (cmd.slice(0, 2) !== '--') { if (cmd.slice(0, 2) !== '--') {
return; return;
} }
@ -39,7 +35,7 @@ process.argv.slice(2).forEach(cmd => {
case 'only': case 'only':
// disable other modules // disable other modules
Object.keys(compile).forEach(key2 => { Object.keys(compile).forEach((key2) => {
compile[key2] = key2 === key; compile[key2] = key2 === key;
}); });
break; break;
@ -48,7 +44,7 @@ process.argv.slice(2).forEach(cmd => {
}); });
// Check if required modules in same monorepo are available // Check if required modules in same monorepo are available
const fileExists = file => { const fileExists = (file) => {
try { try {
fs.statSync(file); fs.statSync(file);
} catch (e) { } catch (e) {
@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/IconifyIcon.d.ts')) {
compile.lib = true; compile.lib = true;
} }
if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { // Compile packages
compile.core = true; Object.keys(compile).forEach((key) => {
} if (compile[key]) {
// 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]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -1,16 +1,12 @@
/* eslint-disable */ /* eslint-disable */
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const child_process = require('child_process'); const child_process = require('child_process');
const coreDir = path.dirname(require.resolve('@iconify/core/package.json'));
// List of commands to run // List of commands to run
const commands = []; const commands = [];
// Parse command line // Parse command line
const compile = { const compile = {
core: false,
lib: true, lib: true,
dist: true, dist: true,
api: true, api: true,
@ -65,22 +61,9 @@ if (compile.api && !fileExists('./lib/index.d.ts')) {
compile.lib = true; compile.lib = true;
} }
if (compile.lib && !fileExists(coreDir + '/lib/cache.mjs')) { // Compile packages
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) => { Object.keys(compile).forEach((key) => {
if (key !== 'core' && compile[key]) { if (compile[key]) {
commands.push({ commands.push({
cmd: 'npm', cmd: 'npm',
args: ['run', 'build:' + key], args: ['run', 'build:' + key],

View File

@ -57,6 +57,10 @@
"require": "./lib/api/query.cjs", "require": "./lib/api/query.cjs",
"import": "./lib/api/query.mjs" "import": "./lib/api/query.mjs"
}, },
"./lib/api/types": {
"require": "./lib/api/types.cjs",
"import": "./lib/api/types.mjs"
},
"./lib/browser-storage/config": { "./lib/browser-storage/config": {
"require": "./lib/browser-storage/config.cjs", "require": "./lib/browser-storage/config.cjs",
"import": "./lib/browser-storage/config.mjs" "import": "./lib/browser-storage/config.mjs"

View File

@ -2,59 +2,17 @@ import type {
IconifyIconLoaderCallback, IconifyIconLoaderCallback,
IconifyIconLoaderAbort, IconifyIconLoaderAbort,
} from './icons'; } from './icons';
import { getStorage } from '../storage/storage';
import type { SortedIcons } from '../icon/sort'; import type { SortedIcons } from '../icon/sort';
import type { IconifyIconSource } from '@iconify/utils/lib/icon/name'; import type { APICallbackItem, IconStorageWithIcons } from './types';
/**
* 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<string, PrefixCallbackItems>;
// 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<string, boolean>;
const pendingUpdates = Object.create(null) as Record<
string,
ProviderPendingUpdates
>;
/** /**
* Remove callback * Remove callback
*/ */
function removeCallback(sources: IconifyIconSource[], id: number): void { function removeCallback(storages: IconStorageWithIcons[], id: number): void {
sources.forEach((source) => { storages.forEach((storage) => {
const provider = source.provider; const items = storage.loaderCallbacks;
if (callbacks[provider] === void 0) {
return;
}
const providerCallbacks = callbacks[provider];
const prefix = source.prefix;
const items = providerCallbacks[prefix];
if (items) { 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 * Update all callbacks for provider and prefix
*/ */
export function updateCallbacks(provider: string, prefix: string): void { export function updateCallbacks(storage: IconStorageWithIcons): void {
if (pendingUpdates[provider] === void 0) { if (!storage.pendingCallbacksFlag) {
pendingUpdates[provider] = Object.create( storage.pendingCallbacksFlag = true;
null
) as ProviderPendingUpdates;
}
const providerPendingUpdates = pendingUpdates[provider];
if (!providerPendingUpdates[prefix]) {
providerPendingUpdates[prefix] = true;
setTimeout(() => { setTimeout(() => {
providerPendingUpdates[prefix] = false; storage.pendingCallbacksFlag = false;
if (
callbacks[provider] === void 0 ||
callbacks[provider][prefix] === void 0
) {
return;
}
// Get all items // Get all items
const items = callbacks[provider][prefix].slice(0); const items = storage.loaderCallbacks
? storage.loaderCallbacks.slice(0)
: [];
if (!items.length) { if (!items.length) {
return; return;
} }
const storage = getStorage(provider, prefix);
// Check each item for changes // Check each item for changes
let hasPending = false; let hasPending = false;
items.forEach((item: CallbackItem) => { const provider = storage.provider;
const prefix = storage.prefix;
items.forEach((item) => {
const icons = item.icons; const icons = item.icons;
const oldLength = icons.pending.length; const oldLength = icons.pending.length;
icons.pending = icons.pending.filter((icon) => { icons.pending = icons.pending.filter((icon) => {
@ -129,15 +76,7 @@ export function updateCallbacks(provider: string, prefix: string): void {
if (icons.pending.length !== oldLength) { if (icons.pending.length !== oldLength) {
if (!hasPending) { if (!hasPending) {
// All icons have been loaded - remove callback from prefix // All icons have been loaded - remove callback from prefix
removeCallback( removeCallback([storage], item.id);
[
{
provider,
prefix,
},
],
item.id
);
} }
item.callback( item.callback(
icons.loaded.slice(0), icons.loaded.slice(0),
@ -162,7 +101,7 @@ let idCounter = 0;
export function storeCallback( export function storeCallback(
callback: IconifyIconLoaderCallback, callback: IconifyIconLoaderCallback,
icons: SortedIcons, icons: SortedIcons,
pendingSources: IconifyIconSource[] pendingSources: IconStorageWithIcons[]
): IconifyIconLoaderAbort { ): IconifyIconLoaderAbort {
// Create unique id and abort function // Create unique id and abort function
const id = idCounter++; const id = idCounter++;
@ -174,24 +113,15 @@ export function storeCallback(
} }
// Create item and store it for all pending prefixes // Create item and store it for all pending prefixes
const item: CallbackItem = { const item: APICallbackItem = {
id, id,
icons, icons,
callback, callback,
abort: abort, abort: abort,
}; };
pendingSources.forEach((source) => { pendingSources.forEach((storage) => {
const provider = source.provider; (storage.loaderCallbacks || (storage.loaderCallbacks = [])).push(item);
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);
}); });
return abort; return abort;

View File

@ -1,9 +1,5 @@
import type { IconifyIcon, IconifyJSON } from '@iconify/types'; import type { IconifyIcon, IconifyJSON } from '@iconify/types';
import { import { IconifyIconName, stringToIcon } from '@iconify/utils/lib/icon/name';
IconifyIconName,
IconifyIconSource,
stringToIcon,
} from '@iconify/utils/lib/icon/name';
import type { SortedIcons } from '../icon/sort'; import type { SortedIcons } from '../icon/sort';
import { sortIcons } from '../icon/sort'; import { sortIcons } from '../icon/sort';
import { storeCallback, updateCallbacks } from './callbacks'; import { storeCallback, updateCallbacks } from './callbacks';
@ -13,6 +9,7 @@ import { listToIcons } from '../icon/list';
import { allowSimpleNames, getIconData } from '../storage/functions'; import { allowSimpleNames, getIconData } from '../storage/functions';
import { sendAPIQuery } from './query'; import { sendAPIQuery } from './query';
import { storeInBrowserStorage } from '../browser-storage/store'; import { storeInBrowserStorage } from '../browser-storage/store';
import type { IconStorageWithIcons } from './types';
// Empty abort callback for loadIcons() // Empty abort callback for loadIcons()
function emptyCallback(): void { function emptyCallback(): void {
@ -49,126 +46,46 @@ export type IconifyLoadIcons = (
*/ */
export type IsPending = (icon: IconifyIconName) => boolean; 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<string, number>;
type ProviderPendingIcons = Record<string, PrefixPendingIcons>;
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<string, IconsToLoadPrefixItem>;
const iconsToLoad = Object.create(null) as Record<
string,
IconsToLoadProviderItem
>;
// Flags to merge multiple synchronous icon requests in one asynchronous request
type FlagsItem = Record<string, boolean>;
const loaderFlags = Object.create(null) as Record<string, FlagsItem>;
const queueFlags = Object.create(null) as Record<string, FlagsItem>;
/** /**
* Function called when new icons have been loaded * 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 // Run only once per tick, possibly joining multiple API responses in one call
if (loaderFlags[provider] === void 0) { if (!storage.iconsLoaderFlag) {
loaderFlags[provider] = Object.create(null) as FlagsItem; storage.iconsLoaderFlag = true;
}
const providerLoaderFlags = loaderFlags[provider];
if (!providerLoaderFlags[prefix]) {
providerLoaderFlags[prefix] = true;
setTimeout(() => { setTimeout(() => {
providerLoaderFlags[prefix] = false; storage.iconsLoaderFlag = false;
updateCallbacks(provider, prefix); updateCallbacks(storage);
}); });
} }
} }
// Storage for errors for loadNewIcons(). Used to avoid spamming log with identical errors.
const errorsCache = Object.create(null) as Record<string, number>;
/** /**
* Load icons * Load icons
*/ */
function loadNewIcons(provider: string, prefix: string, icons: string[]): void { function loadNewIcons(storage: IconStorageWithIcons, 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];
// Add icons to queue // Add icons to queue
if (providerIconsToLoad[prefix] === void 0) { if (!storage.iconsToLoad) {
providerIconsToLoad[prefix] = icons; storage.iconsToLoad = icons;
} else { } else {
providerIconsToLoad[prefix] = providerIconsToLoad[prefix] storage.iconsToLoad = storage.iconsToLoad.concat(icons).sort();
.concat(icons)
.sort();
} }
// Trigger update on next tick, mering multiple synchronous requests into one asynchronous request // Trigger update on next tick, mering multiple synchronous requests into one asynchronous request
if (!providerQueueFlags[prefix]) { if (!storage.iconsQueueFlag) {
providerQueueFlags[prefix] = true; storage.iconsQueueFlag = true;
setTimeout(() => { setTimeout(() => {
providerQueueFlags[prefix] = false; storage.iconsQueueFlag = false;
const { provider, prefix } = storage;
// Get icons and delete queue // Get icons and delete queue
const icons = providerIconsToLoad[prefix]; const icons = storage.iconsToLoad;
delete providerIconsToLoad[prefix]; delete storage.iconsToLoad;
// Get API module // Get API module
const api = getAPIModule(provider); let api: ReturnType<typeof getAPIModule>;
if (!api) { if (!icons || !(api = getAPIModule(provider))) {
// No way to load icons! // No icons or no way to load icons!
err();
return; return;
} }
@ -176,8 +93,6 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void {
const params = api.prepare(provider, prefix, icons); const params = api.prepare(provider, prefix, icons);
params.forEach((item) => { params.forEach((item) => {
sendAPIQuery(provider, item, (data, error) => { sendAPIQuery(provider, item, (data, error) => {
const storage = getStorage(provider, prefix);
// Check for error // Check for error
if (typeof data !== 'object') { if (typeof data !== 'object') {
if (error !== 404) { if (error !== 404) {
@ -201,10 +116,12 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void {
} }
// Remove added icons from pending list // Remove added icons from pending list
const pending = providerPendingIcons[prefix]; const pending = storage.pendingIcons;
parsed.forEach((name) => { if (pending) {
delete pending[name]; parsed.forEach((name) => {
}); pending.delete(name);
});
}
// Cache API response // Cache API response
storeInBrowserStorage( storeInBrowserStorage(
@ -217,7 +134,7 @@ function loadNewIcons(provider: string, prefix: string, icons: string[]): void {
} }
// Trigger update on next tick // 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 * Check if icon is being loaded
*/ */
export const isPending: IsPending = (icon: IconifyIconName): boolean => { export const isPending: IsPending = (icon: IconifyIconName): boolean => {
const provider = icon.provider; const storage = getStorage(
const prefix = icon.prefix; icon.provider,
return ( icon.prefix
pendingIcons[provider] && ) as IconStorageWithIcons;
pendingIcons[provider][prefix] && const pending = storage.pendingIcons;
pendingIcons[provider][prefix][icon.name] !== void 0 return !!(pending && pending.has(icon.name));
);
}; };
/** /**
@ -280,59 +196,41 @@ export const loadIcons: IconifyLoadIcons = (
string, string,
ProviderNewIconsList ProviderNewIconsList
>; >;
const sources: IconifyIconSource[] = []; const sources: IconStorageWithIcons[] = [];
let lastProvider: string, lastPrefix: string; let lastProvider: string, lastPrefix: string;
sortedIcons.pending.forEach((icon) => { sortedIcons.pending.forEach((icon) => {
const provider = icon.provider; const { provider, prefix } = icon;
const prefix = icon.prefix;
if (prefix === lastPrefix && provider === lastProvider) { if (prefix === lastPrefix && provider === lastProvider) {
return; return;
} }
lastProvider = provider; lastProvider = provider;
lastPrefix = prefix; lastPrefix = prefix;
sources.push({ sources.push(getStorage(provider, prefix));
provider,
prefix,
});
if (pendingIcons[provider] === void 0) { const providerNewIcons =
pendingIcons[provider] = Object.create( newIcons[provider] ||
null (newIcons[provider] = Object.create(null) as ProviderNewIconsList);
) as ProviderPendingIcons; if (!providerNewIcons[prefix]) {
}
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) {
providerNewIcons[prefix] = []; providerNewIcons[prefix] = [];
} }
}); });
// List of new icons // List of new icons
const time = Date.now();
// Filter pending icons list: find icons that are not being loaded yet // 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 // 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. // function is called right after sortIcons() that checks storage, icon is definitely not in storage.
sortedIcons.pending.forEach((icon) => { sortedIcons.pending.forEach((icon) => {
const provider = icon.provider; const { provider, prefix, name } = icon;
const prefix = icon.prefix;
const name = icon.name;
const pendingQueue = pendingIcons[provider][prefix]; const storage = getStorage(provider, prefix) as IconStorageWithIcons;
if (pendingQueue[name] === void 0) { const pendingQueue =
storage.pendingIcons || (storage.pendingIcons = new Set());
if (!pendingQueue.has(name)) {
// New icon - add to pending queue to mark it as being loaded // 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 // Add it to new icons list to pass it to API module for loading
newIcons[provider][prefix].push(name); 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 // 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 // to consolidate multiple synchronous loadIcons() calls into one asynchronous API call
sources.forEach((source) => { sources.forEach((storage) => {
const provider = source.provider; const { provider, prefix } = storage;
const prefix = source.prefix;
if (newIcons[provider][prefix].length) { if (newIcons[provider][prefix].length) {
loadNewIcons(provider, prefix, newIcons[provider][prefix]); loadNewIcons(storage, newIcons[provider][prefix]);
} }
}); });

View File

@ -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<string>;
/**
* 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;
}

View File

@ -2,7 +2,6 @@ import type {
BrowserStorageConfig, BrowserStorageConfig,
BrowserStorageCount, BrowserStorageCount,
BrowserStorageEmptyList, BrowserStorageEmptyList,
BrowserStorageStatus,
} from './types'; } from './types';
/** /**
@ -32,8 +31,8 @@ export const browserStorageEmptyItems: BrowserStorageEmptyList = {
/** /**
* Flag to check if storage has been loaded * 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; browserStorageStatus = status;
} }

View File

@ -23,10 +23,10 @@ import type { BrowserStorageConfig, BrowserStorageItem } from './types';
* Load icons from cache * Load icons from cache
*/ */
export function initBrowserStorage() { export function initBrowserStorage() {
if (browserStorageStatus === true) { if (browserStorageStatus) {
return; return;
} }
setBrowserStorageStatus('loading'); setBrowserStorageStatus(true);
// Minimum time // Minimum time
const minTime = const minTime =
@ -66,10 +66,12 @@ export function initBrowserStorage() {
valid = false; valid = false;
} else { } else {
// Add icon set // Add icon set
const iconSet = data.data;
const provider = data.provider; const provider = data.provider;
const prefix = data.data.prefix; const prefix = iconSet.prefix;
const storage = getStorage(provider, prefix); const storage = getStorage(provider, prefix);
valid = addIconSet(storage, data.data).length > 0; valid = addIconSet(storage, iconSet).length > 0;
} }
} catch (err) { } catch (err) {
valid = false; valid = false;
@ -129,9 +131,10 @@ export function initBrowserStorage() {
} }
} }
// Load each storage
for (const key in browserStorageConfig) { for (const key in browserStorageConfig) {
load(key as keyof BrowserStorageConfig); load(key as keyof BrowserStorageConfig);
} }
setBrowserStorageStatus(true); // Check for update
} }

View File

@ -18,9 +18,6 @@ export function storeInBrowserStorage(provider: string, data: IconifyJSON) {
if (!browserStorageStatus) { if (!browserStorageStatus) {
initBrowserStorage(); initBrowserStorage();
} }
if (browserStorageStatus === 'loading') {
return;
}
function store(key: BrowserStorageType): true | undefined { function store(key: BrowserStorageType): true | undefined {
if (!browserStorageConfig[key]) { if (!browserStorageConfig[key]) {

View File

@ -18,6 +18,3 @@ export interface BrowserStorageItem {
provider: string; provider: string;
data: IconifyJSON; data: IconifyJSON;
} }
// Status: not loaded, loading, loaded
export type BrowserStorageStatus = false | 'loading' | true;

View File

@ -1,8 +1,5 @@
import { import { updateCallbacks, storeCallback } from '../../lib/api/callbacks';
callbacks, import type { IconStorageWithIcons } from '../../lib/api/types';
updateCallbacks,
storeCallback,
} from '../../lib/api/callbacks';
import { sortIcons } from '../../lib/icon/sort'; import { sortIcons } from '../../lib/icon/sort';
import { getStorage, addIconSet } from '../../lib/storage/storage'; import { getStorage, addIconSet } from '../../lib/storage/storage';
@ -22,7 +19,7 @@ describe('Testing API callbacks', () => {
const prefix = nextPrefix(); const prefix = nextPrefix();
let counter = 0; let counter = 0;
const storage = getStorage(provider, prefix); const storage = getStorage(provider, prefix) as IconStorageWithIcons;
const abort = storeCallback( const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => { (loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).toBe(abort); expect(unsubscribe).toBe(abort);
@ -52,7 +49,7 @@ describe('Testing API callbacks', () => {
name: 'icon2', name: 'icon2',
}, },
]); ]);
expect(callbacks[provider][prefix].length).toBe(1); expect(storage.loaderCallbacks?.length).toBe(1);
// Add icon2 and trigger update // Add icon2 and trigger update
addIconSet(storage, { addIconSet(storage, {
@ -64,7 +61,7 @@ describe('Testing API callbacks', () => {
}, },
}); });
updateCallbacks(provider, prefix); updateCallbacks(storage);
return; return;
case 2: case 2:
@ -89,7 +86,7 @@ describe('Testing API callbacks', () => {
}, },
]); ]);
expect(pending).toEqual([]); expect(pending).toEqual([]);
expect(callbacks[provider][prefix].length).toBe(0); expect(storage.loaderCallbacks?.length).toBe(0);
done(); done();
} }
}, },
@ -110,19 +107,14 @@ describe('Testing API callbacks', () => {
name: 'icon3', name: 'icon3',
}, },
]), ]),
[ [storage]
{
provider,
prefix,
},
]
); );
// Test callbacks // Test callbacks
expect(callbacks[provider][prefix].length).toBe(1); expect(storage.loaderCallbacks?.length).toBe(1);
// Test update - should do nothing // Test update - should do nothing
updateCallbacks(provider, prefix); updateCallbacks(storage);
// Wait for tick because updateCallbacks will use one // Wait for tick because updateCallbacks will use one
setTimeout(() => { setTimeout(() => {
@ -139,7 +131,7 @@ describe('Testing API callbacks', () => {
}, },
not_found: ['icon3'], not_found: ['icon3'],
}); });
updateCallbacks(provider, prefix); updateCallbacks(storage);
}); });
}); });
@ -147,7 +139,7 @@ describe('Testing API callbacks', () => {
const provider = ''; const provider = '';
const prefix = nextPrefix(); const prefix = nextPrefix();
const storage = getStorage(provider, prefix); const storage = getStorage(provider, prefix) as IconStorageWithIcons;
addIconSet(storage, { addIconSet(storage, {
prefix, prefix,
icons: { icons: {
@ -182,16 +174,11 @@ describe('Testing API callbacks', () => {
name: 'icon3', name: 'icon3',
}, },
]), ]),
[ [storage]
{
provider,
prefix,
},
]
); );
// callbacks should not have been initialised // callbacks should not have been initialised
expect(callbacks[prefix]).toBeUndefined(); expect(storage.loaderCallbacks).toBeUndefined();
}); });
it('Cancel callback', (done) => { it('Cancel callback', (done) => {
@ -199,7 +186,7 @@ describe('Testing API callbacks', () => {
const prefix = nextPrefix(); const prefix = nextPrefix();
let counter = 0; let counter = 0;
const storage = getStorage(provider, prefix); const storage = getStorage(provider, prefix) as IconStorageWithIcons;
const abort = storeCallback( const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => { (loaded, missing, pending, unsubscribe) => {
expect(unsubscribe).toBe(abort); expect(unsubscribe).toBe(abort);
@ -229,7 +216,7 @@ describe('Testing API callbacks', () => {
name: 'icon2', name: 'icon2',
}, },
]); ]);
expect(callbacks[provider][prefix].length).toBe(1); expect(storage.loaderCallbacks?.length).toBe(1);
// Add icon2 and trigger update // Add icon2 and trigger update
addIconSet(storage, { addIconSet(storage, {
@ -241,11 +228,11 @@ describe('Testing API callbacks', () => {
}, },
}); });
updateCallbacks(provider, prefix); updateCallbacks(storage);
// Unsubscribe and set timer to call done() // Unsubscribe and set timer to call done()
unsubscribe(); unsubscribe();
expect(callbacks[provider][prefix].length).toBe(0); expect(storage.loaderCallbacks?.length).toBe(0);
setTimeout(done); setTimeout(done);
}, },
sortIcons([ sortIcons([
@ -265,19 +252,14 @@ describe('Testing API callbacks', () => {
name: 'icon3', name: 'icon3',
}, },
]), ]),
[ [storage]
{
provider,
prefix,
},
]
); );
// Test callbacks // Test callbacks
expect(callbacks[provider][prefix].length).toBe(1); expect(storage.loaderCallbacks?.length).toBe(1);
// Test update - should do nothing // Test update - should do nothing
updateCallbacks(provider, prefix); updateCallbacks(storage);
// Wait for tick because updateCallbacks will use one // Wait for tick because updateCallbacks will use one
setTimeout(() => { setTimeout(() => {
@ -294,7 +276,7 @@ describe('Testing API callbacks', () => {
}, },
not_found: ['icon3'], not_found: ['icon3'],
}); });
updateCallbacks(provider, prefix); updateCallbacks(storage);
}); });
}); });
@ -304,8 +286,8 @@ describe('Testing API callbacks', () => {
const prefix2 = nextPrefix(); const prefix2 = nextPrefix();
let counter = 0; let counter = 0;
const storage1 = getStorage(provider, prefix1); const storage1 = getStorage(provider, prefix1) as IconStorageWithIcons;
const storage2 = getStorage(provider, prefix2); const storage2 = getStorage(provider, prefix2) as IconStorageWithIcons;
const abort = storeCallback( const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => { (loaded, missing, pending, unsubscribe) => {
@ -336,8 +318,8 @@ describe('Testing API callbacks', () => {
name: 'icon2', name: 'icon2',
}, },
]); ]);
expect(callbacks[provider][prefix1].length).toBe(0); expect(storage1.loaderCallbacks?.length).toBe(0);
expect(callbacks[provider][prefix2].length).toBe(1); expect(storage2.loaderCallbacks?.length).toBe(1);
// Add icon2 and trigger update // Add icon2 and trigger update
addIconSet(storage2, { addIconSet(storage2, {
@ -349,13 +331,13 @@ describe('Testing API callbacks', () => {
}, },
}); });
updateCallbacks(provider, prefix2); updateCallbacks(storage2);
break; break;
case 2: case 2:
// Second run - icon2 should be loaded // Second run - icon2 should be loaded
expect(callbacks[provider][prefix1].length).toBe(0); expect(storage1.loaderCallbacks?.length).toBe(0);
expect(callbacks[provider][prefix2].length).toBe(0); expect(storage2.loaderCallbacks?.length).toBe(0);
done(); done();
break; break;
@ -380,18 +362,15 @@ describe('Testing API callbacks', () => {
name: 'icon3', name: 'icon3',
}, },
]), ]),
[ [storage1, storage2]
{ provider, prefix: prefix1 },
{ provider, prefix: prefix2 },
]
); );
// Test callbacks // Test callbacks
expect(callbacks[provider][prefix1].length).toBe(1); expect(storage1.loaderCallbacks?.length).toBe(1);
expect(callbacks[provider][prefix2].length).toBe(1); expect(storage2.loaderCallbacks?.length).toBe(1);
// Test update - should do nothing // Test update - should do nothing
updateCallbacks(provider, prefix1); updateCallbacks(storage1);
// Wait for tick because updateCallbacks will use one // Wait for tick because updateCallbacks will use one
setTimeout(() => { setTimeout(() => {
@ -408,7 +387,7 @@ describe('Testing API callbacks', () => {
}, },
not_found: ['icon3'], not_found: ['icon3'],
}); });
updateCallbacks(provider, prefix1); updateCallbacks(storage1);
}); });
}); });
@ -419,8 +398,8 @@ describe('Testing API callbacks', () => {
const prefix2 = nextPrefix(); const prefix2 = nextPrefix();
let counter = 0; let counter = 0;
const storage1 = getStorage(provider1, prefix1); const storage1 = getStorage(provider1, prefix1) as IconStorageWithIcons;
const storage2 = getStorage(provider2, prefix2); const storage2 = getStorage(provider2, prefix2) as IconStorageWithIcons;
const abort = storeCallback( const abort = storeCallback(
(loaded, missing, pending, unsubscribe) => { (loaded, missing, pending, unsubscribe) => {
@ -451,12 +430,8 @@ describe('Testing API callbacks', () => {
name: 'icon2', name: 'icon2',
}, },
]); ]);
expect(callbacks[provider1][prefix1].length).toBe(0); expect(storage1.loaderCallbacks?.length).toBe(0);
expect(callbacks[provider2][prefix2].length).toBe(1); expect(storage2.loaderCallbacks?.length).toBe(1);
// Make sure providers/prefixes aren't mixed
expect(callbacks[provider1][prefix2]).toBeUndefined();
expect(callbacks[provider2][prefix1]).toBeUndefined();
// Add icon2 and trigger update // Add icon2 and trigger update
addIconSet(storage2, { addIconSet(storage2, {
@ -468,17 +443,13 @@ describe('Testing API callbacks', () => {
}, },
}); });
updateCallbacks(provider2, prefix2); updateCallbacks(storage2);
break; break;
case 2: case 2:
// Second run - icon2 should be loaded // Second run - icon2 should be loaded
expect(callbacks[provider1][prefix1].length).toBe(0); expect(storage1.loaderCallbacks?.length).toBe(0);
expect(callbacks[provider2][prefix2].length).toBe(0); expect(storage2.loaderCallbacks?.length).toBe(0);
// Make sure providers/prefixes aren't mixed
expect(callbacks[provider1][prefix2]).toBeUndefined();
expect(callbacks[provider2][prefix1]).toBeUndefined();
done(); done();
break; break;
@ -504,21 +475,15 @@ describe('Testing API callbacks', () => {
name: 'icon3', name: 'icon3',
}, },
]), ]),
[ [storage1, storage2]
{ provider: provider1, prefix: prefix1 },
{ provider: provider2, prefix: prefix2 },
]
); );
// Test callbacks // Test callbacks
expect(callbacks[provider1][prefix1].length).toBe(1); expect(storage1.loaderCallbacks?.length).toBe(1);
expect(callbacks[provider2][prefix2].length).toBe(1); expect(storage2.loaderCallbacks?.length).toBe(1);
expect(callbacks[provider1][prefix2]).toBeUndefined();
expect(callbacks[provider2][prefix1]).toBeUndefined();
// Test update - should do nothing // Test update - should do nothing
updateCallbacks(provider1, prefix1); updateCallbacks(storage1);
// Wait for tick because updateCallbacks will use one // Wait for tick because updateCallbacks will use one
setTimeout(() => { setTimeout(() => {
@ -535,7 +500,7 @@ describe('Testing API callbacks', () => {
}, },
not_found: ['icon3'], not_found: ['icon3'],
}); });
updateCallbacks(provider1, prefix1); updateCallbacks(storage1);
}); });
}); });
}); });