Skip to content

Commit fd5f6eb

Browse files
authored
Merge pull request #63 from sparksuite/timeout-option
Added timeout option
2 parents 76e6da0 + ec58099 commit fd5f6eb

File tree

7 files changed

+82
-17
lines changed

7 files changed

+82
-17
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "w3c-css-validator",
3-
"version": "1.0.3",
3+
"version": "1.1.0",
44
"description": "Easily validate CSS using W3C's public CSS validator service",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/retrieve-validation/browser.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,36 @@
22
import { W3CCSSValidatorResponse } from '.';
33

44
// Utility function for retrieving response from W3C CSS Validator in a browser environment
5-
const retrieveInBrowser = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
6-
const res = await fetch(url);
5+
const retrieveInBrowser = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
6+
// Initialize controller who's signal will abort the fetch
7+
const controller = new AbortController();
8+
9+
// Start timeout
10+
setTimeout(() => {
11+
controller.abort();
12+
}, timeout);
13+
14+
// Attempt to fetch CSS validation, catching the abort error to handle specially
15+
let res: Response | null = null;
16+
17+
try {
18+
res = await fetch(url, { signal: controller.signal });
19+
} catch (err: unknown) {
20+
if (err instanceof Error && err.name === 'AbortError') {
21+
throw new Error(`The request took longer than ${timeout}ms`);
22+
}
23+
24+
throw err;
25+
}
26+
27+
if (!res) {
28+
throw new Error('Response expected');
29+
}
30+
31+
// Parse JSON
732
const data = (await res.json()) as W3CCSSValidatorResponse;
833

34+
// Return
935
return data.cssvalidation;
1036
};
1137

src/retrieve-validation/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ export interface W3CCSSValidatorResponse {
1919
}
2020

2121
// Function that detects the appropriate HTTP request client and returns a response accordingly
22-
const retrieveValidation = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
22+
const retrieveValidation = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
2323
if (typeof window !== 'undefined' && typeof window?.fetch === 'function') {
24-
return await retrieveInBrowser(url);
24+
return await retrieveInBrowser(url, timeout);
2525
}
2626

27-
return await retrieveInNode(url);
27+
return await retrieveInNode(url, timeout);
2828
};
2929

3030
export default retrieveValidation;

src/retrieve-validation/node.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,30 @@ import * as https from 'https';
33
import { W3CCSSValidatorResponse } from '.';
44

55
// Utility function for retrieving response from W3C CSS Validator in a Node.js environment
6-
const retrieveInNode = async (url: string): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
7-
return new Promise((resolve) => {
8-
https.get(url, (res) => {
9-
let data = '';
6+
const retrieveInNode = async (url: string, timeout: number): Promise<W3CCSSValidatorResponse['cssvalidation']> => {
7+
return new Promise((resolve, reject) => {
8+
// Attempt to fetch CSS validation
9+
const req = https.get(
10+
url,
11+
{
12+
timeout,
13+
},
14+
(res) => {
15+
let data = '';
1016

11-
res.on('data', (chunk) => {
12-
data += chunk;
13-
});
17+
res.on('data', (chunk) => {
18+
data += chunk;
19+
});
1420

15-
res.on('end', () => {
16-
resolve((JSON.parse(data) as W3CCSSValidatorResponse).cssvalidation);
17-
});
21+
res.on('end', () => {
22+
resolve((JSON.parse(data) as W3CCSSValidatorResponse).cssvalidation);
23+
});
24+
}
25+
);
26+
27+
// Listen for timeout event and reject in callback
28+
req.on('timeout', () => {
29+
reject(new Error(`The request took longer than ${timeout}ms`));
1830
});
1931
});
2032
};

src/validate-text.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ export default function testValidateText(validateText: ValidateText): void {
8989
);
9090
});
9191

92+
it('Complains about negative timeout', async () => {
93+
await expect(validateText('abc', { timeout: -1 })).rejects.toThrow('The timeout must be a positive integer');
94+
});
95+
96+
it('Complains about non-integer times', async () => {
97+
await expect(validateText('abc', { timeout: Infinity })).rejects.toThrow('The timeout must be an integer');
98+
await expect(validateText('abc', { timeout: 400.1 })).rejects.toThrow('The timeout must be an integer');
99+
});
100+
101+
it('Throws when the timeout is passed', async () => {
102+
await expect(validateText('abc', { timeout: 1 })).rejects.toThrow('The request took longer than 1ms');
103+
});
104+
92105
it('Parses out unwanted characters from error messages', async () => {
93106
const result = await validateText('.foo { foo: bar; }');
94107

src/validate-text.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import retrieveValidation from './retrieve-validation';
44
// Define types
55
interface ValidateTextOptionsBase {
66
medium?: 'all' | 'braille' | 'embossed' | 'handheld' | 'print' | 'projection' | 'screen' | 'speech' | 'tty' | 'tv';
7+
timeout?: number;
78
}
89

910
interface ValidateTextOptionsWithoutWarnings extends ValidateTextOptionsBase {
@@ -58,6 +59,7 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
5859
}
5960

6061
if (options) {
62+
// Validate medium option
6163
const allowedMediums: typeof options['medium'][] = [
6264
'all',
6365
'braille',
@@ -80,6 +82,15 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
8082
if (options.warningLevel && !allowedWarningLevels.includes(options.warningLevel)) {
8183
throw new Error(`The warning level must be one of the following: ${allowedWarningLevels.join(', ')}`);
8284
}
85+
86+
// Validate timeout option
87+
if (options.timeout !== undefined && !Number.isInteger(options.timeout)) {
88+
throw new Error('The timeout must be an integer');
89+
}
90+
91+
if (options.timeout && options.timeout < 0) {
92+
throw new Error('The timeout must be a positive integer');
93+
}
8394
}
8495

8596
// Build URL for fetching
@@ -96,7 +107,7 @@ async function validateText(textToBeValidated: string, options?: ValidateTextOpt
96107
.join('&')}`;
97108

98109
// Call W3C CSS Validator API and store response
99-
const cssValidationResponse = await retrieveValidation(url);
110+
const cssValidationResponse = await retrieveValidation(url, options?.timeout ?? 10000);
100111

101112
// Build result
102113
const base: ValidateTextResultBase = {

website/docs/functions/validate-text.md

+3
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@ Option | Default | Possible values
1212
:--- | :--- | :---
1313
`medium` | `all` | `all`, `braille`, `embossed`, `handheld`, `print`, `projection`, `screen`, `speech`, `tty`, `tv`
1414
`warningLevel` | `0` | `0`, `1`, `2`, `3`
15+
`timeout` | `10000` | `integer`
1516

1617
Option | Explanation
1718
:--- | :---
1819
`medium` | The equivalent of the `@media` rule, applied to all of the CSS
1920
`warningLevel` | `0` means don’t return any warnings; `1`, `2`, `3` will return warnings (if any), with higher numbers corresponding to more warnings
21+
`timeout` | The time in milliseconds after which the request to the W3C API will be terminated and an error will be thrown
2022

2123
```ts
2224
const result = await cssValidator.validateText(css, {
2325
medium: 'print',
2426
warningLevel: 3,
27+
timeout: 3000,
2528
});
2629
```
2730

0 commit comments

Comments
 (0)