Skip to content

Commit ead10d6

Browse files
authored
feat(loader): add retry logic (#12)
1 parent d804d14 commit ead10d6

File tree

5 files changed

+111
-22
lines changed

5 files changed

+111
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# hCaptcha Loader
22

3-
This is a JavaScript library to easily configure the loading of the [hCaptcha](https://www.hcaptcha.com) JS client SDK with built-in error handling.
3+
This is a JavaScript library to easily configure the loading of the [hCaptcha](https://www.hcaptcha.com) JS client SDK with built-in error handling. It also includes a retry mechanism that will attempt to load the hCaptcha script several times in the event if fails to load due to a network or unforeseen issue.
44

55
> [hCaptcha](https://www.hcaptcha.com) is a drop-replacement for reCAPTCHA that protects user privacy.
66
>

lib/__test__/loader.test.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { waitFor } from '@testing-library/dom';
33

44
import { fetchScript } from "../src/script";
55
import { hCaptchaLoader, hCaptchaScripts } from "../src/loader";
6-
import { HCAPTCHA_LOAD_FN_NAME, SCRIPT_COMPLETE, SCRIPT_ERROR} from "../src/constants";
6+
import { HCAPTCHA_LOAD_FN_NAME, SCRIPT_COMPLETE, SCRIPT_ERROR } from "../src/constants";
77

88
jest.mock('../src/script');
99

@@ -38,7 +38,7 @@ describe('hCaptchaLoader', () => {
3838
const promise = hCaptchaLoader({ sentry: false });
3939

4040
await waitFor(() => {
41-
expect(mockFetchScript).toHaveBeenCalled();
41+
expect(mockFetchScript).toHaveBeenCalledTimes(1);
4242

4343
// Trigger script onload callback to resolve promise
4444
window[HCAPTCHA_LOAD_FN_NAME]();
@@ -47,26 +47,80 @@ describe('hCaptchaLoader', () => {
4747
});
4848

4949
it('should not fetch script since it was already loaded', async () => {
50-
const result = await hCaptchaLoader({ sentry: false });
51-
expect(result).toEqual(window.hcaptcha);
52-
expect(mockFetchScript).not.toHaveBeenCalled();
50+
const result = await hCaptchaLoader({ sentry: false });
51+
expect(result).toEqual(window.hcaptcha);
52+
expect(mockFetchScript).not.toHaveBeenCalled();
5353
});
54-
5554
});
5655

56+
describe('script retry', () => {
57+
58+
beforeAll(() => {
59+
window.hcaptcha = 'hcaptcha-test';
60+
})
61+
62+
afterEach(() => {
63+
jest.resetAllMocks();
64+
cleanupScripts();
65+
});
66+
67+
it('should retry and load after fetch script error', async () => {
68+
mockFetchScript.mockRejectedValueOnce(SCRIPT_ERROR);
69+
mockFetchScript.mockResolvedValueOnce(SCRIPT_COMPLETE);
70+
71+
const promise = hCaptchaLoader({ sentry: false });
72+
73+
await waitFor(() => {
74+
expect(mockFetchScript).toHaveBeenCalledTimes(2);
75+
76+
// Trigger script onload callback to resolve promise
77+
window[HCAPTCHA_LOAD_FN_NAME]();
78+
expect(promise).resolves.toEqual(window.hcaptcha);
79+
});
80+
});
81+
82+
it('should try loading 2 times and succeed on final try', async () => {
83+
mockFetchScript.mockRejectedValueOnce(SCRIPT_ERROR);
84+
mockFetchScript.mockRejectedValueOnce(SCRIPT_ERROR);
85+
mockFetchScript.mockResolvedValueOnce(SCRIPT_COMPLETE);
86+
87+
const promise = hCaptchaLoader({ sentry: false });
88+
89+
await waitFor(() => {
90+
expect(mockFetchScript).toHaveBeenCalledTimes(3);
91+
92+
// Trigger script onload callback to resolve promise
93+
window[HCAPTCHA_LOAD_FN_NAME]();
94+
expect(promise).resolves.toEqual(window.hcaptcha);
95+
});
96+
});
97+
98+
it('should try loading 3 times and throw', async () => {
99+
mockFetchScript.mockRejectedValue('test error');
100+
101+
try {
102+
await hCaptchaLoader({ sentry: false, cleanup: true });
103+
} catch (error) {
104+
expect(mockFetchScript).toBeCalledTimes(3);
105+
expect(error.message).toBe(SCRIPT_ERROR);
106+
}
107+
});
108+
})
109+
57110
describe('script error', () => {
58111

59112
afterEach(() => {
60113
cleanupScripts();
114+
jest.resetAllMocks();
61115
})
62116

63117
it('should reject with script-error when error while loading occurs', async () => {
64-
mockFetchScript.mockRejectedValueOnce(SCRIPT_ERROR);
118+
mockFetchScript.mockRejectedValue(SCRIPT_ERROR);
65119

66120
try {
67121
await hCaptchaLoader({ sentry: false });
68122
} catch(error) {
69-
expect(error.message).toEqual(SCRIPT_ERROR)
123+
expect(error.message).toEqual(SCRIPT_ERROR);
70124
}
71125
});
72126

lib/src/constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export const SCRIPT_ID = 'hCaptcha-script';
22
export const HCAPTCHA_LOAD_FN_NAME = 'hCaptchaOnLoad';
33
export const SCRIPT_ERROR = 'script-error';
4-
export const SCRIPT_COMPLETE = 'script-loaded';
4+
export const SCRIPT_COMPLETE = 'script-loaded';
5+
6+
export const SENTRY_TAG = '@hCaptcha/loader';
7+
8+
export const MAX_RETRIES = 2;

lib/src/loader.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { generateQuery, getFrame, getMountElement } from './utils';
2-
import { HCAPTCHA_LOAD_FN_NAME, SCRIPT_ERROR } from './constants';
2+
import { HCAPTCHA_LOAD_FN_NAME, MAX_RETRIES, SCRIPT_ERROR, SENTRY_TAG } from './constants';
33
import { initSentry } from './sentry';
44
import { fetchScript } from './script';
55

6-
import type { ILoaderParams } from './types';
6+
import type { ILoaderParams, SentryHub } from './types';
77

88
// Prevent loading API script multiple times
99
export const hCaptchaScripts = [];
1010

1111
// Generate hCaptcha API script
12-
export function hCaptchaLoader(params: ILoaderParams = { cleanup: true }): Promise<any> {
13-
const sentry = initSentry(params.sentry);
12+
export function hCaptchaApi(params: ILoaderParams = { cleanup: true }, sentry: SentryHub): Promise<any> {
1413

1514
try {
1615

1716
sentry.addBreadcrumb({
18-
category: 'script',
17+
category: SENTRY_TAG,
1918
message: 'hCaptcha loader params',
2019
data: params,
2120
});
@@ -26,7 +25,7 @@ export function hCaptchaLoader(params: ILoaderParams = { cleanup: true }): Promi
2625

2726
if (script) {
2827
sentry.addBreadcrumb({
29-
category: 'script',
28+
category: SENTRY_TAG,
3029
message: 'hCaptcha already loaded',
3130
});
3231

@@ -42,7 +41,7 @@ export function hCaptchaLoader(params: ILoaderParams = { cleanup: true }): Promi
4241
// Create global onload callback for the hCaptcha library to call
4342
frame.window[HCAPTCHA_LOAD_FN_NAME] = () => {
4443
sentry.addBreadcrumb({
45-
category: 'hCaptcha:script',
44+
category: SENTRY_TAG,
4645
message: 'hCaptcha script called onload function',
4746
});
4847

@@ -66,12 +65,15 @@ export function hCaptchaLoader(params: ILoaderParams = { cleanup: true }): Promi
6665
await fetchScript({ query, ...params });
6766

6867
sentry.addBreadcrumb({
69-
category: 'hCaptcha:script',
68+
category: SENTRY_TAG,
7069
message: 'hCaptcha loaded',
70+
data: script
7171
});
72+
73+
hCaptchaScripts.push({ promise, scope: frame.window });
7274
} catch(error) {
7375
sentry.addBreadcrumb({
74-
category: 'hCaptcha:script',
76+
category: SENTRY_TAG,
7577
message: 'hCaptcha failed to load',
7678
data: error,
7779
});
@@ -82,11 +84,40 @@ export function hCaptchaLoader(params: ILoaderParams = { cleanup: true }): Promi
8284
}
8385
);
8486

85-
hCaptchaScripts.push({ promise, scope: frame.window });
86-
8787
return promise;
8888
} catch (error) {
8989
sentry.captureException(error);
9090
return Promise.reject(new Error(SCRIPT_ERROR));
9191
}
9292
}
93+
94+
export async function loadScript(params, retries = 0) {
95+
const message = retries < MAX_RETRIES ? 'Retry loading hCaptcha Api' : 'Exceeded maximum retries';
96+
97+
const sentry = initSentry(params.sentry);
98+
99+
try {
100+
101+
return await hCaptchaApi(params, sentry);
102+
} catch (error) {
103+
104+
sentry.addBreadcrumb({
105+
SENTRY_SOURCE: SENTRY_TAG,
106+
message,
107+
data: { error }
108+
});
109+
110+
if (retries >= MAX_RETRIES) {
111+
sentry.captureException(error);
112+
return Promise.reject(error);
113+
} else {
114+
retries += 1;
115+
return loadScript(params, retries);
116+
}
117+
}
118+
}
119+
120+
121+
export function hCaptchaLoader(params) {
122+
return loadScript(params);
123+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@hcaptcha/loader",
33
"description": "This is a JavaScript library to easily configure the loading of the hCaptcha JS client SDK with built-in error handling.",
4-
"version": "1.0.10",
4+
"version": "1.1.0",
55
"author": "hCaptcha team and contributors",
66
"license": "MIT",
77
"keywords": [

0 commit comments

Comments
 (0)