Skip to content

Commit b8cafd8

Browse files
authored
perf: replace p-queue with evan/concurrency (#143)
* perf: replace p-queue with evan/concurrency * refactor: unify queue error handling
1 parent 52574b8 commit b8cafd8

File tree

7 files changed

+239
-234
lines changed

7 files changed

+239
-234
lines changed

bun.lockb

-1.05 KB
Binary file not shown.

package.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "google-font-metadata",
33
"description": "A metadata generator for Google Fonts.",
4-
"version": "6.0.2",
4+
"version": "6.0.3",
55
"author": "Ayuhito <[email protected]>",
66
"main": "./dist/index.js",
77
"module": "./dist/index.mjs",
@@ -25,14 +25,13 @@
2525
"unicode range"
2626
],
2727
"dependencies": {
28+
"@evan/concurrency": "^0.0.3",
2829
"@octokit/core": "^6.1.2",
29-
"@types/stylis": "^4.2.7",
3030
"cac": "^6.7.14",
3131
"consola": "^3.3.3",
3232
"deepmerge": "^4.3.1",
3333
"json-stringify-pretty-compact": "^4.0.0",
3434
"linkedom": "^0.18.6",
35-
"p-queue": "^8.0.1",
3635
"pathe": "^1.1.2",
3736
"picocolors": "^1.1.1",
3837
"playwright": "^1.49.1",
@@ -41,9 +40,9 @@
4140
},
4241
"devDependencies": {
4342
"@biomejs/biome": "1.9.4",
44-
"@playwright/test": "^1.49.1",
4543
"@types/bun": "latest",
4644
"@types/node": "^22.10.2",
45+
"@types/stylis": "^4.2.7",
4746
"c8": "^10.1.3",
4847
"magic-string": "^0.30.17",
4948
"msw": "^2.7.0",
@@ -58,8 +57,7 @@
5857
"test": "vitest",
5958
"test:generate-fixtures": "bun run ./tests/utils/generate-css-fixtures",
6059
"coverage": "vitest --coverage",
61-
"format": "biome format --fix",
62-
"lint": "biome lint --fix",
60+
"lint": "biome check --fix",
6361
"prepublishOnly": "bunx biome ci && bun run build"
6462
},
6563
"files": ["dist/*", "data/*"],

src/api-parser-v1.ts

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as fs from 'node:fs/promises';
22
import { fileURLToPath } from 'node:url';
33

4+
import { Limiter } from '@evan/concurrency';
45
import { consola } from 'consola';
56
import stringify from 'json-stringify-pretty-compact';
6-
import PQueue from 'p-queue';
77
import { dirname, join } from 'pathe';
88
import { compile } from 'stylis';
99

1010
import { apiv1 as userAgents } from '../data/user-agents.json';
1111
import { APIDirect, APIv1 } from './data';
12+
import { LOOP_LIMIT, addError, checkErrors } from './errors';
1213
import type { APIResponse, FontObjectV1 } from './types';
1314
import { orderObject, weightListGen } from './utils';
1415
import { validate } from './validate';
1516

1617
const baseurl = 'https://fonts.googleapis.com/css?subset=';
18+
const queue = Limiter(18);
19+
20+
const results: FontObjectV1[] = [];
1721

1822
export const fetchCSS = async (
1923
font: APIResponse,
@@ -49,10 +53,10 @@ export const fetchCSS = async (
4953
return cssMap.join('');
5054
};
5155

56+
// Download CSS stylesheets for each file format
5257
export const fetchAllCSS = async (
5358
font: APIResponse,
5459
): Promise<[string, string, string]> =>
55-
// Download CSS stylesheets for each file format
5660
await Promise.all([
5761
fetchCSS(font, userAgents.woff2),
5862
fetchCSS(font, userAgents.woff),
@@ -176,69 +180,63 @@ export const processCSS = (
176180
return fontObject;
177181
};
178182

179-
const results: FontObjectV1[] = [];
180-
181-
const processQueue = async (font: APIResponse, force: boolean) => {
182-
const id = font.family.replaceAll(/\s/g, '-').toLowerCase();
183-
184-
// If last-modified matches latest API, skip fetching CSS and processing.
185-
if (
186-
APIv1[id] !== undefined &&
187-
font.lastModified === APIv1[id].lastModified &&
188-
!force
189-
) {
190-
results.push({ [id]: APIv1[id] });
191-
} else {
192-
const css = await fetchAllCSS(font);
193-
const fontObject = processCSS(css, font);
194-
results.push(fontObject);
195-
consola.info(`Updated ${id}`);
183+
const processQueue = async (
184+
font: APIResponse,
185+
force: boolean,
186+
): Promise<void> => {
187+
try {
188+
const id = font.family.replaceAll(/\s/g, '-').toLowerCase();
189+
190+
// If last-modified matches latest API, skip fetching CSS and processing.
191+
if (
192+
APIv1[id] !== undefined &&
193+
font.lastModified === APIv1[id].lastModified &&
194+
!force
195+
) {
196+
results.push({ [id]: APIv1[id] });
197+
} else {
198+
const css = await fetchAllCSS(font);
199+
const fontObject = processCSS(css, font);
200+
results.push(fontObject);
201+
consola.info(`Updated ${id}`);
202+
}
203+
consola.success(`Parsed ${id}`);
204+
} catch (error) {
205+
addError(`${font.family} experienced an error. ${String(error)}`);
196206
}
197-
consola.success(`Parsed ${id}`);
198207
};
199208

200-
// Queue control
201-
const queue = new PQueue({ concurrency: 18 });
202-
203-
// @ts-ignore - rollup-plugin-dts fails to compile this typing
204-
queue.on('error', (error: Error) => {
205-
consola.error(error);
206-
});
207-
208209
/**
209210
* Parses the fetched API data and writes it to the APIv1 JSON dataset.
210211
* @param force - Force update all fonts without using cache.
211212
* @param noValidate - Skip automatic validation of generated data.
212213
*/
213214
export const parsev1 = async (force: boolean, noValidate: boolean) => {
214215
for (const font of APIDirect) {
215-
try {
216-
queue.add(async () => {
217-
await processQueue(font, force);
218-
});
219-
} catch (error) {
220-
throw new Error(`${font.family} experienced an error. ${String(error)}`);
221-
}
216+
checkErrors(LOOP_LIMIT);
217+
queue.add(() => processQueue(font, force));
222218
}
223-
await queue.onIdle().then(async () => {
224-
// Order the font objects alphabetically for consistency and not create huge diffs
225-
const unordered: FontObjectV1 = Object.assign({}, ...results);
226-
const ordered = orderObject(unordered);
227219

228-
if (!noValidate) {
229-
validate('v1', ordered);
230-
}
220+
await queue.flush();
221+
checkErrors();
231222

232-
await fs.writeFile(
233-
join(
234-
dirname(fileURLToPath(import.meta.url)),
235-
'../data/google-fonts-v1.json',
236-
),
237-
stringify(ordered),
238-
);
239-
240-
consola.success(
241-
`All ${results.length} font datapoints using CSS APIv1 have been generated.`,
242-
);
243-
});
223+
// Order the font objects alphabetically for consistency and not create huge diffs
224+
const unordered: FontObjectV1 = Object.assign({}, ...results);
225+
const ordered = orderObject(unordered);
226+
227+
if (!noValidate) {
228+
validate('v1', ordered);
229+
}
230+
231+
await fs.writeFile(
232+
join(
233+
dirname(fileURLToPath(import.meta.url)),
234+
'../data/google-fonts-v1.json',
235+
),
236+
stringify(ordered),
237+
);
238+
239+
consola.success(
240+
`All ${results.length} font datapoints using CSS APIv1 have been generated.`,
241+
);
244242
};

src/api-parser-v2.ts

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as fs from 'node:fs/promises';
22
import { fileURLToPath } from 'node:url';
33

4+
import { Limiter } from '@evan/concurrency';
45
import { consola } from 'consola';
56
import stringify from 'json-stringify-pretty-compact';
6-
import PQueue from 'p-queue';
77
import { dirname, join } from 'pathe';
88
import { compile } from 'stylis';
99

1010
import { apiv2 as userAgents } from '../data/user-agents.json';
1111
import { APIDirect, APIv2 } from './data';
12+
import { LOOP_LIMIT, addError, checkErrors } from './errors';
1213
import type { APIResponse, FontObjectV2 } from './types';
1314
import { orderObject, weightListGen } from './utils';
1415
import { validate } from './validate';
1516

1617
const baseurl = 'https://fonts.googleapis.com/css2?family=';
18+
const queue = Limiter(18);
19+
20+
const results: FontObjectV2[] = [];
1721

1822
export const fetchCSS = async (
1923
fontFamily: string,
@@ -234,69 +238,63 @@ export const processCSS = (
234238
return fontObject;
235239
};
236240

237-
const results: FontObjectV2[] = [];
238-
239-
const processQueue = async (font: APIResponse, force: boolean) => {
240-
const id = font.family.replaceAll(/\s/g, '-').toLowerCase();
241-
242-
// If last-modified matches latest API, skip fetching CSS and processing.
243-
if (
244-
APIv2[id] !== undefined &&
245-
font.lastModified === APIv2[id].lastModified &&
246-
!force
247-
) {
248-
results.push({ [id]: APIv2[id] });
249-
} else {
250-
const css = await fetchAllCSS(font);
251-
const fontObject = processCSS(css, font);
252-
results.push(fontObject);
253-
consola.info(`Updated ${id}`);
241+
const processQueue = async (
242+
font: APIResponse,
243+
force: boolean,
244+
): Promise<void> => {
245+
try {
246+
const id = font.family.replaceAll(/\s/g, '-').toLowerCase();
247+
248+
// If last-modified matches latest API, skip fetching CSS and processing.
249+
if (
250+
APIv2[id] !== undefined &&
251+
font.lastModified === APIv2[id].lastModified &&
252+
!force
253+
) {
254+
results.push({ [id]: APIv2[id] });
255+
} else {
256+
const css = await fetchAllCSS(font);
257+
const fontObject = processCSS(css, font);
258+
results.push(fontObject);
259+
consola.info(`Updated ${id}`);
260+
}
261+
consola.success(`Parsed ${id}`);
262+
} catch (error) {
263+
addError(`${font.family} experienced an error. ${String(error)}`);
254264
}
255-
consola.success(`Parsed ${id}`);
256265
};
257266

258-
// Queue control
259-
const queue = new PQueue({ concurrency: 18 });
260-
261-
// @ts-ignore - rollup-plugin-dts fails to compile this typing
262-
queue.on('error', (error: Error) => {
263-
consola.error(error);
264-
});
265-
266267
/**
267268
* Parses the fetched API and writes it to the APIv2 dataset.
268269
* @param force - Force update all fonts without using cache.
269270
* @param noValidate - Skip automatic validation of generated data.
270271
*/
271272
export const parsev2 = async (force: boolean, noValidate: boolean) => {
272273
for (const font of APIDirect) {
273-
try {
274-
queue.add(async () => {
275-
await processQueue(font, force);
276-
});
277-
} catch (error) {
278-
throw new Error(`${font.family} experienced an error. ${String(error)}`);
279-
}
274+
checkErrors(LOOP_LIMIT);
275+
queue.add(() => processQueue(font, force));
280276
}
281-
await queue.onIdle().then(async () => {
282-
// Order the font objects alphabetically for consistency and not create huge diffs
283-
const unordered: FontObjectV2 = Object.assign({}, ...results);
284-
const ordered = orderObject(unordered);
285277

286-
if (!noValidate) {
287-
validate('v2', ordered);
288-
}
278+
await queue.flush();
279+
checkErrors();
289280

290-
await fs.writeFile(
291-
join(
292-
dirname(fileURLToPath(import.meta.url)),
293-
'../data/google-fonts-v2.json',
294-
),
295-
stringify(ordered),
296-
);
281+
// Order the font objects alphabetically for consistency and not create huge diffs
282+
const unordered: FontObjectV2 = Object.assign({}, ...results);
283+
const ordered = orderObject(unordered);
297284

298-
consola.success(
299-
`All ${results.length} font datapoints using CSS APIv2 have been generated.`,
300-
);
301-
});
285+
if (!noValidate) {
286+
validate('v2', ordered);
287+
}
288+
289+
await fs.writeFile(
290+
join(
291+
dirname(fileURLToPath(import.meta.url)),
292+
'../data/google-fonts-v2.json',
293+
),
294+
stringify(ordered),
295+
);
296+
297+
consola.success(
298+
`All ${results.length} font datapoints using CSS APIv2 have been generated.`,
299+
);
302300
};

src/errors.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import consola from 'consola';
2+
3+
const errs: string[] = [];
4+
5+
export const LOOP_LIMIT = 5;
6+
7+
export const addError = (error: string) => {
8+
errs.push(error);
9+
};
10+
11+
export const checkErrors = (limit = 0) => {
12+
if (errs.length > limit) {
13+
for (const err of errs) {
14+
consola.error(err);
15+
}
16+
17+
if (limit > 0) {
18+
throw new Error('Too many errors occurred during parsing. Stopping...');
19+
}
20+
21+
throw new Error('Some fonts experienced errors during parsing.');
22+
}
23+
};

0 commit comments

Comments
 (0)