Skip to content

Commit

Permalink
chore: concurrent downloading from Figma API
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberalien committed Nov 26, 2023
1 parent 8dc70ab commit 76cdeac
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 73 deletions.
5 changes: 5 additions & 0 deletions @iconify/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@
"require": "./lib/download/api/index.cjs",
"import": "./lib/download/api/index.mjs"
},
"./lib/download/api/queue": {
"types": "./lib/download/api/queue.d.ts",
"require": "./lib/download/api/queue.cjs",
"import": "./lib/download/api/queue.mjs"
},
"./lib/download/api/types": {
"types": "./lib/download/api/types.d.ts",
"require": "./lib/download/api/types.cjs",
Expand Down
113 changes: 113 additions & 0 deletions @iconify/tools/src/download/api/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Concurrent queries limit
*/
let queriesLimit = 5;

/**
* Concurrent queries retries count
*/
let maxRetries = 3;

/**
* Set concurrent queries default limit
*/
export function setConcurrentQueriesDefaultLimit(value: number) {
queriesLimit = value;
}

/**
* Set concurrent queries default retries count
*/
export function setConcurrentQueriesDefaultRetries(value: number) {
maxRetries = value;
}

/**
* Callback to get query
*/
export type GetConcurrentQueryCallback<T> = (index: number) => Promise<T>;

/**
* Runs concurrent async operations
*/
export function runConcurrentQueries<T>(
count: number,
callback: GetConcurrentQueryCallback<T>,
limit = 0,
retries = 0
): Promise<T[]> {
// Set limit and retries count
limit = Math.max(1, Math.min(limit || queriesLimit, count));
retries = Math.max(1, retries || maxRetries);

// Results
const results: T[] = Array<T>(count).fill(null as unknown as T);

// Queue
let nextIndex = 0;
const resolving = new Set<number>();
let rejected = false;
let resolved = false;

return new Promise((resolve, reject) => {
// Function to call after item is resolved
function resolvedItem() {
if (rejected || resolved) {
return;
}

if (!resolving.size && nextIndex > count) {
resolved = true;
resolve(results);
return;
}

if (resolving.size < limit && nextIndex <= count) {
startNext();
}
}

// Run item
function run(index: number, retry: number) {
// Mark as resolving
resolving.add(index);

// Get promise and run it
const p = callback(index);
p.then((value) => {
resolving.delete(index);
results[index] = value;
resolvedItem();
}).catch((err) => {
if (retry < retries) {
// try again on next tick
setTimeout(() => {
run(index, retry + 1);
});
} else if (!rejected) {
rejected = true;
reject(err);
}
});
}

// Start next item
function startNext() {
// Get next item
const index = nextIndex++;
if (index >= count) {
// Out of queue items
resolvedItem();
return;
}

// Run query
run(index, 0);
}

// Queue items up to a limit
for (let i = 0; i < limit; i++) {
startNext();
}
});
}
184 changes: 111 additions & 73 deletions @iconify/tools/src/import/figma/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
clearAPICache,
getAPICache,
} from '../../download/api/cache';
import { runConcurrentQueries } from '../../download/api/queue';
import type { APICacheOptions, APIQueryParams } from '../../download/api/types';
import type { DocumentNotModified } from '../../download/types/modified';
import type {
Expand All @@ -16,7 +17,7 @@ import type {
FigmaFilesQueryOptions,
FigmaImagesQueryOptions,
} from './types/options';
import type { FigmaNodesImportResult } from './types/result';
import type { FigmaIconNode, FigmaNodesImportResult } from './types/result';

/**
* Compare last modified dates
Expand Down Expand Up @@ -180,77 +181,97 @@ export async function figmaImagesQuery(
const maxLength = 2048 - uri.length;
const svgOptions = options.svgOptions || {};

let ids: string[] = [];
let idsLength = 0;
let lastError: number | undefined;
let found = 0;

// Send query
const query = async () => {
const params = new URLSearchParams({
ids: ids.join(','),
format: 'svg',
});
if (options.version) {
params.set('version', options.version);
}
if (svgOptions.includeID) {
params.set('svg_include_id', 'true');
}
if (svgOptions.simplifyStroke) {
params.set('svg_simplify_stroke', 'true');
}
if (svgOptions.useAbsoluteBounds) {
params.set('use_absolute_bounds', 'true');
}
const query = (ids: string[]): Promise<FigmaAPIImagesResponse> => {
return new Promise((resolve, reject) => {
const params = new URLSearchParams({
ids: ids.join(','),
format: 'svg',
});
if (options.version) {
params.set('version', options.version);
}
if (svgOptions.includeID) {
params.set('svg_include_id', 'true');
}
if (svgOptions.simplifyStroke) {
params.set('svg_simplify_stroke', 'true');
}
if (svgOptions.useAbsoluteBounds) {
params.set('use_absolute_bounds', 'true');
}

const data = await sendAPIQuery(
{
uri,
params,
headers: {
'X-FIGMA-TOKEN': options.token,
sendAPIQuery(
{
uri,
params,
headers: {
'X-FIGMA-TOKEN': options.token,
},
},
},
cache
);
if (typeof data === 'number') {
lastError = data;
return;
}
try {
const parsedData = JSON.parse(data) as FigmaAPIImagesResponse;
const images = parsedData.images;
for (const id in images) {
const node = nodes.icons[id];
const target = images[id];
if (node && target) {
node.url = target;
found++;
}
}
} catch (err) {
return;
}
cache
)
.then((data) => {
if (typeof data === 'number') {
reject(data);
return;
}

let parsedData: FigmaAPIImagesResponse;
try {
parsedData = JSON.parse(data) as FigmaAPIImagesResponse;
} catch {
reject('Bad API response');
return;
}

resolve(parsedData);
})
.catch(reject);
});
};

// Loop all ids
// Generate queue
let ids: string[] = [];
let idsLength = 0;
const allKeys = Object.keys(nodes.icons);
const queue: string[][] = [];
for (let i = 0; i < allKeys.length; i++) {
const id = allKeys[i];
ids.push(id);
idsLength += id.length + 1;
if (idsLength >= maxLength) {
await query();
queue.push(ids.slice(0));
ids = [];
idsLength = 0;
}
}
if (idsLength) {
await query();
queue.push(ids.slice(0));
}

// Check data
// Get data
const results = await runConcurrentQueries(queue.length, (index) =>
query(queue[index])
);

// Parse data
results.forEach((data) => {
const images = data.images;
for (const id in images) {
const node = nodes.icons[id];
const target = images[id];
if (node && target) {
node.url = target;
found++;
}
}
});

// Validate results
if (!found) {
if (lastError) {
throw new Error(
Expand All @@ -276,37 +297,54 @@ export async function figmaDownloadImages(
const icons = nodes.icons;
const ids = Object.keys(icons);
let count = 0;
let lastError: number | undefined;

// Filter data
interface FigmaIconNodeWithURL extends FigmaIconNode {
url: string;
}
const filtered = Object.create(null) as Record<
string,
FigmaIconNodeWithURL
>;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const item = icons[id];
if (!item.url) {
continue;
}
const result = await sendAPIQuery(
{
uri: item.url,
},
cache
);
if (typeof result === 'number') {
lastError = result;
continue;
}
if (typeof result === 'string') {
count++;
item.content = result;
if (item.url) {
filtered[id] = item as FigmaIconNodeWithURL;
}
}
const keys = Object.keys(filtered);

// Download everything
await runConcurrentQueries(keys.length, (index) => {
return new Promise((resolve, reject) => {
const id = keys[index];
const item = filtered[id];
sendAPIQuery(
{
uri: item.url,
},
cache
)
.then((data) => {
if (typeof data === 'string') {
count++;
item.content = data;
resolve(true);
} else {
reject(data);
}
})
.catch(reject);
});
});

// Make sure something was downloaded
if (!count) {
throw new Error(
`Error retrieving images${
lastError ? ': ' + lastError.toString() : ''
}`
);
throw new Error('Error retrieving images');
}

// Update counter and return node
nodes.downloadedIconsCount = count;
return nodes;
}
Loading

0 comments on commit 76cdeac

Please sign in to comment.