@internetarchive/fetch-handler 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +29 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/gh-pages-main.yml +40 -0
- package/.github/workflows/pr-preview.yml +63 -0
- package/.husky/pre-commit +4 -0
- package/.prettierignore +1 -0
- package/.vscode/extensions.json +12 -0
- package/.vscode/tasks.json +12 -0
- package/LICENSE +661 -0
- package/README.md +82 -0
- package/coverage/.gitignore +3 -0
- package/coverage/.placeholder +0 -0
- package/demo/app-root.ts +84 -0
- package/demo/index.html +27 -0
- package/dist/demo/app-root.d.ts +12 -0
- package/dist/demo/app-root.js +90 -0
- package/dist/demo/app-root.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/fetch-handler-interface.d.ts +33 -0
- package/dist/src/fetch-handler-interface.js +2 -0
- package/dist/src/fetch-handler-interface.js.map +1 -0
- package/dist/src/ia-fetch-handler.d.ts +36 -0
- package/dist/src/ia-fetch-handler.js +70 -0
- package/dist/src/ia-fetch-handler.js.map +1 -0
- package/dist/src/utils/fetch-retrier.d.ts +36 -0
- package/dist/src/utils/fetch-retrier.js +94 -0
- package/dist/src/utils/fetch-retrier.js.map +1 -0
- package/dist/src/utils/promised-sleep.d.ts +13 -0
- package/dist/src/utils/promised-sleep.js +16 -0
- package/dist/src/utils/promised-sleep.js.map +1 -0
- package/dist/test/fetch-retrier.test.d.ts +1 -0
- package/dist/test/fetch-retrier.test.js +139 -0
- package/dist/test/fetch-retrier.test.js.map +1 -0
- package/dist/test/ia-fetch-handler.test.d.ts +1 -0
- package/dist/test/ia-fetch-handler.test.js +50 -0
- package/dist/test/ia-fetch-handler.test.js.map +1 -0
- package/dist/test/mocks/mock-analytics-handler.d.ts +20 -0
- package/dist/test/mocks/mock-analytics-handler.js +28 -0
- package/dist/test/mocks/mock-analytics-handler.js.map +1 -0
- package/dist/vite.config.d.ts +2 -0
- package/dist/vite.config.js +25 -0
- package/dist/vite.config.js.map +1 -0
- package/eslint.config.mjs +53 -0
- package/index.ts +2 -0
- package/package.json +74 -0
- package/renovate.json +6 -0
- package/screenshot/gh-pages-settings.png +0 -0
- package/src/fetch-handler-interface.ts +39 -0
- package/src/ia-fetch-handler.ts +94 -0
- package/src/utils/fetch-retrier.ts +141 -0
- package/src/utils/promised-sleep.ts +15 -0
- package/ssl/server.crt +22 -0
- package/ssl/server.key +28 -0
- package/test/fetch-retrier.test.ts +173 -0
- package/test/ia-fetch-handler.test.ts +66 -0
- package/test/mocks/mock-analytics-handler.ts +43 -0
- package/tsconfig.json +31 -0
- package/vite.config.ts +25 -0
- package/web-dev-server.config.mjs +32 -0
- package/web-test-runner.config.mjs +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
 [](https://codecov.io/gh/internetarchive/iaux-fetch-handler-service)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Internet Archive Fetch Handler Service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
A custom service for handling API requests.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @internetarchive/fetch-handler-service
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Sample Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { IaFetchHandler } from '../src/ia-fetch-handler';
|
|
21
|
+
|
|
22
|
+
@property({ type: Object }) data: any = null;
|
|
23
|
+
@property({ type: String }) error: string = '';
|
|
24
|
+
@property({ type: Boolean }) loading: boolean = false;
|
|
25
|
+
|
|
26
|
+
private fetchHandler: IaFetchHandler;
|
|
27
|
+
|
|
28
|
+
this.fetchHandler = new IaFetchHandler({
|
|
29
|
+
iaApiBaseUrl: 'https://archive.org',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async fetchData() {
|
|
34
|
+
this.loading = true;
|
|
35
|
+
this.error = '';
|
|
36
|
+
try {
|
|
37
|
+
const result =
|
|
38
|
+
await this.fetchHandler.fetchIAApiResponse('/metadata/goody');
|
|
39
|
+
this.data = result;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.error = `Error fetching data: ${error}`;
|
|
42
|
+
} finally {
|
|
43
|
+
this.loading = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See the `demo` directory for a more detailed example.
|
|
49
|
+
|
|
50
|
+
## Local Demo with `web-dev-server`
|
|
51
|
+
Add `127.0.0.1 local.archive.org` to your `/etc/hosts` file
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**NOTE** The first time you visit the site, the browser will say the site is insecure because it's using a self-signed certificate. Accept the certificate and visit the site and the browser will then accept the certificate in the future. It may always say the site is insecure, but for the purposes of development, it's secure enough.
|
|
58
|
+
|
|
59
|
+
To run a local development server that serves the basic demo located in `demo/index.html`
|
|
60
|
+
|
|
61
|
+
## Testing with Web Test Runner
|
|
62
|
+
To run the suite of Web Test Runner tests, run
|
|
63
|
+
```bash
|
|
64
|
+
npm run test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To run the tests in watch mode (for <abbr title="test driven development">TDD</abbr>, for example), run
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run test:watch
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Linting with ESLint, Prettier, and Types
|
|
74
|
+
To scan the project for linting errors, run
|
|
75
|
+
```bash
|
|
76
|
+
npm run lint
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
To automatically fix many linting errors, run
|
|
80
|
+
```bash
|
|
81
|
+
npm run format
|
|
82
|
+
```
|
|
File without changes
|
package/demo/app-root.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { html, LitElement, css } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
import { IaFetchHandler } from '../src/ia-fetch-handler';
|
|
5
|
+
|
|
6
|
+
@customElement('app-root')
|
|
7
|
+
export class AppRoot extends LitElement {
|
|
8
|
+
@property({ type: Object }) data: any = null;
|
|
9
|
+
@property({ type: String }) error: string = '';
|
|
10
|
+
@property({ type: Boolean }) loading: boolean = false;
|
|
11
|
+
|
|
12
|
+
private fetchHandler: IaFetchHandler;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.fetchHandler = new IaFetchHandler({
|
|
17
|
+
iaApiBaseUrl: 'https://archive.org',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
super.connectedCallback();
|
|
23
|
+
this.fetchData();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async fetchData() {
|
|
27
|
+
this.loading = true;
|
|
28
|
+
this.error = '';
|
|
29
|
+
try {
|
|
30
|
+
const result =
|
|
31
|
+
await this.fetchHandler.fetchIAApiResponse('/metadata/goody');
|
|
32
|
+
this.data = result;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.error = `Error fetching data: ${error}`;
|
|
35
|
+
} finally {
|
|
36
|
+
this.loading = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render() {
|
|
41
|
+
return html`
|
|
42
|
+
<div class="container">
|
|
43
|
+
<h1>Fetch Data</h1>
|
|
44
|
+
${this.loading
|
|
45
|
+
? html`<p>Loading...</p>`
|
|
46
|
+
: this.error
|
|
47
|
+
? html`<p class="error">${this.error}</p>`
|
|
48
|
+
: this.data
|
|
49
|
+
? html`<pre>${JSON.stringify(this.data, null, 2)}</pre>`
|
|
50
|
+
: html`<p>No data available.</p>`}
|
|
51
|
+
<button @click="${this.fetchData}">Retry</button>
|
|
52
|
+
</div>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static styles = css`
|
|
57
|
+
.container {
|
|
58
|
+
padding: 20px;
|
|
59
|
+
font-family: Arial, sans-serif;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
h1 {
|
|
63
|
+
color: #333;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.error {
|
|
67
|
+
color: red;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
button {
|
|
71
|
+
margin-top: 20px;
|
|
72
|
+
padding: 10px 20px;
|
|
73
|
+
background-color: #007bff;
|
|
74
|
+
color: white;
|
|
75
|
+
border: none;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
border-radius: 5px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
button:hover {
|
|
81
|
+
background-color: #0056b3;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
}
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en-GB">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
background: #fff;
|
|
8
|
+
font-family: sans-serif;
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="demo"></div>
|
|
14
|
+
|
|
15
|
+
<script type="module">
|
|
16
|
+
import { html, render } from 'lit-html';
|
|
17
|
+
import '../dist/demo/app-root.js';
|
|
18
|
+
|
|
19
|
+
render(
|
|
20
|
+
html`
|
|
21
|
+
<app-root></app-root>
|
|
22
|
+
`,
|
|
23
|
+
document.querySelector('#demo')
|
|
24
|
+
);
|
|
25
|
+
</script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
export declare class AppRoot extends LitElement {
|
|
3
|
+
data: any;
|
|
4
|
+
error: string;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
private fetchHandler;
|
|
7
|
+
constructor();
|
|
8
|
+
connectedCallback(): void;
|
|
9
|
+
fetchData(): Promise<void>;
|
|
10
|
+
render(): import("lit").TemplateResult<1>;
|
|
11
|
+
static styles: import("lit").CSSResult;
|
|
12
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { html, LitElement, css } from 'lit';
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import { IaFetchHandler } from '../src/ia-fetch-handler';
|
|
5
|
+
let AppRoot = class AppRoot extends LitElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.data = null;
|
|
9
|
+
this.error = '';
|
|
10
|
+
this.loading = false;
|
|
11
|
+
this.fetchHandler = new IaFetchHandler({
|
|
12
|
+
iaApiBaseUrl: 'https://archive.org',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
super.connectedCallback();
|
|
17
|
+
this.fetchData();
|
|
18
|
+
}
|
|
19
|
+
async fetchData() {
|
|
20
|
+
this.loading = true;
|
|
21
|
+
this.error = '';
|
|
22
|
+
try {
|
|
23
|
+
const result = await this.fetchHandler.fetchIAApiResponse('/metadata/goody');
|
|
24
|
+
this.data = result;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
this.error = `Error fetching data: ${error}`;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
this.loading = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
return html `
|
|
35
|
+
<div class="container">
|
|
36
|
+
<h1>Fetch Data</h1>
|
|
37
|
+
${this.loading
|
|
38
|
+
? html `<p>Loading...</p>`
|
|
39
|
+
: this.error
|
|
40
|
+
? html `<p class="error">${this.error}</p>`
|
|
41
|
+
: this.data
|
|
42
|
+
? html `<pre>${JSON.stringify(this.data, null, 2)}</pre>`
|
|
43
|
+
: html `<p>No data available.</p>`}
|
|
44
|
+
<button @click="${this.fetchData}">Retry</button>
|
|
45
|
+
</div>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
AppRoot.styles = css `
|
|
50
|
+
.container {
|
|
51
|
+
padding: 20px;
|
|
52
|
+
font-family: Arial, sans-serif;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h1 {
|
|
56
|
+
color: #333;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.error {
|
|
60
|
+
color: red;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
button {
|
|
64
|
+
margin-top: 20px;
|
|
65
|
+
padding: 10px 20px;
|
|
66
|
+
background-color: #007bff;
|
|
67
|
+
color: white;
|
|
68
|
+
border: none;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
border-radius: 5px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
button:hover {
|
|
74
|
+
background-color: #0056b3;
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
__decorate([
|
|
78
|
+
property({ type: Object })
|
|
79
|
+
], AppRoot.prototype, "data", void 0);
|
|
80
|
+
__decorate([
|
|
81
|
+
property({ type: String })
|
|
82
|
+
], AppRoot.prototype, "error", void 0);
|
|
83
|
+
__decorate([
|
|
84
|
+
property({ type: Boolean })
|
|
85
|
+
], AppRoot.prototype, "loading", void 0);
|
|
86
|
+
AppRoot = __decorate([
|
|
87
|
+
customElement('app-root')
|
|
88
|
+
], AppRoot);
|
|
89
|
+
export { AppRoot };
|
|
90
|
+
//# sourceMappingURL=app-root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-root.js","sourceRoot":"","sources":["../../demo/app-root.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGlD,IAAM,OAAO,GAAb,MAAM,OAAQ,SAAQ,UAAU;IAOrC;QACE,KAAK,EAAE,CAAC;QAPkB,SAAI,GAAQ,IAAI,CAAC;QACjB,UAAK,GAAW,EAAE,CAAC;QAClB,YAAO,GAAY,KAAK,CAAC;QAMpD,IAAI,CAAC,YAAY,GAAG,IAAI,cAAc,CAAC;YACrC,YAAY,EAAE,qBAAqB;SACpC,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,SAAS;QACb,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,MAAM,GACV,MAAM,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;YAChE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,GAAG,wBAAwB,KAAK,EAAE,CAAC;QAC/C,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;;UAGL,IAAI,CAAC,OAAO;YACZ,CAAC,CAAC,IAAI,CAAA,mBAAmB;YACzB,CAAC,CAAC,IAAI,CAAC,KAAK;gBACV,CAAC,CAAC,IAAI,CAAA,oBAAoB,IAAI,CAAC,KAAK,MAAM;gBAC1C,CAAC,CAAC,IAAI,CAAC,IAAI;oBACT,CAAC,CAAC,IAAI,CAAA,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ;oBACxD,CAAC,CAAC,IAAI,CAAA,2BAA2B;0BACrB,IAAI,CAAC,SAAS;;KAEnC,CAAC;IACJ,CAAC;;AAEM,cAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BlB,AA3BY,CA2BX;AA3E0B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;qCAAkB;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;sCAAoB;AAClB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;wCAA0B;AAH3C,OAAO;IADnB,aAAa,CAAC,UAAU,CAAC;GACb,OAAO,CA6EnB","sourcesContent":["import { html, LitElement, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\n\nimport { IaFetchHandler } from '../src/ia-fetch-handler';\n\n@customElement('app-root')\nexport class AppRoot extends LitElement {\n @property({ type: Object }) data: any = null;\n @property({ type: String }) error: string = '';\n @property({ type: Boolean }) loading: boolean = false;\n\n private fetchHandler: IaFetchHandler;\n\n constructor() {\n super();\n this.fetchHandler = new IaFetchHandler({\n iaApiBaseUrl: 'https://archive.org',\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.fetchData();\n }\n\n async fetchData() {\n this.loading = true;\n this.error = '';\n try {\n const result =\n await this.fetchHandler.fetchIAApiResponse('/metadata/goody');\n this.data = result;\n } catch (error) {\n this.error = `Error fetching data: ${error}`;\n } finally {\n this.loading = false;\n }\n }\n\n render() {\n return html`\n <div class=\"container\">\n <h1>Fetch Data</h1>\n ${this.loading\n ? html`<p>Loading...</p>`\n : this.error\n ? html`<p class=\"error\">${this.error}</p>`\n : this.data\n ? html`<pre>${JSON.stringify(this.data, null, 2)}</pre>`\n : html`<p>No data available.</p>`}\n <button @click=\"${this.fetchData}\">Retry</button>\n </div>\n `;\n }\n\n static styles = css`\n .container {\n padding: 20px;\n font-family: Arial, sans-serif;\n }\n\n h1 {\n color: #333;\n }\n\n .error {\n color: red;\n }\n\n button {\n margin-top: 20px;\n padding: 10px 20px;\n background-color: #007bff;\n color: white;\n border: none;\n cursor: pointer;\n border-radius: 5px;\n }\n\n button:hover {\n background-color: #0056b3;\n }\n `;\n}\n"]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["export { IaFetchHandler } from './src/ia-fetch-handler';\nexport { FetchHandlerInterface } from './src/fetch-handler-interface';\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface FetchHandlerInterface {
|
|
2
|
+
/**
|
|
3
|
+
* Generic fetch function that handles retries and common IA parameters like `reCache=1`
|
|
4
|
+
*
|
|
5
|
+
* @param input RequestInfo
|
|
6
|
+
* @param init RequestInit
|
|
7
|
+
*/
|
|
8
|
+
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
|
9
|
+
/**
|
|
10
|
+
* A helper function to fetch a response from an API and get a JSON object
|
|
11
|
+
*
|
|
12
|
+
* @param path string
|
|
13
|
+
* @param options?: { includeCredentials?: boolean }
|
|
14
|
+
*/
|
|
15
|
+
fetchApiResponse<T>(url: string, options?: {
|
|
16
|
+
includeCredentials?: boolean;
|
|
17
|
+
method?: string;
|
|
18
|
+
body?: BodyInit;
|
|
19
|
+
headers?: HeadersInit;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A helper function to fetch a response from the IA API and get a JSON object
|
|
23
|
+
*
|
|
24
|
+
* This allows you to just pass the path to the API and get the response instead
|
|
25
|
+
* of the full URL. If you need a full URL, use `fetchApiResponse` instead.
|
|
26
|
+
*
|
|
27
|
+
* @param path string
|
|
28
|
+
* @param options?: { includeCredentials?: boolean }
|
|
29
|
+
*/
|
|
30
|
+
fetchIAApiResponse<T>(path: string, options?: {
|
|
31
|
+
includeCredentials?: boolean;
|
|
32
|
+
}): Promise<T>;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-handler-interface.js","sourceRoot":"","sources":["../../src/fetch-handler-interface.ts"],"names":[],"mappings":"","sourcesContent":["export interface FetchHandlerInterface {\n /**\n * Generic fetch function that handles retries and common IA parameters like `reCache=1`\n *\n * @param input RequestInfo\n * @param init RequestInit\n */\n fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;\n\n /**\n * A helper function to fetch a response from an API and get a JSON object\n *\n * @param path string\n * @param options?: { includeCredentials?: boolean }\n */\n fetchApiResponse<T>(\n url: string,\n options?: {\n includeCredentials?: boolean;\n method?: string;\n body?: BodyInit;\n headers?: HeadersInit;\n },\n ): Promise<T>;\n\n /**\n * A helper function to fetch a response from the IA API and get a JSON object\n *\n * This allows you to just pass the path to the API and get the response instead\n * of the full URL. If you need a full URL, use `fetchApiResponse` instead.\n *\n * @param path string\n * @param options?: { includeCredentials?: boolean }\n */\n fetchIAApiResponse<T>(\n path: string,\n options?: { includeCredentials?: boolean },\n ): Promise<T>;\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FetchRetrierInterface } from './utils/fetch-retrier';
|
|
2
|
+
import type { FetchHandlerInterface } from './fetch-handler-interface';
|
|
3
|
+
/**
|
|
4
|
+
* The FetchHandler adds some common helpers:
|
|
5
|
+
* - retry the request if it fails
|
|
6
|
+
* - add `reCache=1` to the request if it's in the current url so the backend sees it
|
|
7
|
+
* - add convenience method for fetching/decoding an API response by just the path
|
|
8
|
+
*/
|
|
9
|
+
export declare class IaFetchHandler implements FetchHandlerInterface {
|
|
10
|
+
private iaApiBaseUrl?;
|
|
11
|
+
private fetchRetrier;
|
|
12
|
+
private searchParams?;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
iaApiBaseUrl?: string;
|
|
15
|
+
fetchRetrier?: FetchRetrierInterface;
|
|
16
|
+
searchParams?: string;
|
|
17
|
+
});
|
|
18
|
+
/** @inheritdoc */
|
|
19
|
+
fetchIAApiResponse<T>(path: string, options?: {
|
|
20
|
+
includeCredentials?: boolean;
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
/** @inheritdoc */
|
|
23
|
+
fetchApiResponse<T>(url: string, options?: {
|
|
24
|
+
includeCredentials?: boolean;
|
|
25
|
+
method?: string;
|
|
26
|
+
body?: BodyInit;
|
|
27
|
+
headers?: HeadersInit;
|
|
28
|
+
}): Promise<T>;
|
|
29
|
+
/** @inheritdoc */
|
|
30
|
+
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
|
31
|
+
/**
|
|
32
|
+
* Since RequestInfo can be either a `Request` or `string`, we need to change
|
|
33
|
+
* the way we add search params to it depending on the input.
|
|
34
|
+
*/
|
|
35
|
+
private addSearchParams;
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FetchRetrier } from './utils/fetch-retrier';
|
|
2
|
+
/**
|
|
3
|
+
* The FetchHandler adds some common helpers:
|
|
4
|
+
* - retry the request if it fails
|
|
5
|
+
* - add `reCache=1` to the request if it's in the current url so the backend sees it
|
|
6
|
+
* - add convenience method for fetching/decoding an API response by just the path
|
|
7
|
+
*/
|
|
8
|
+
export class IaFetchHandler {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.fetchRetrier = new FetchRetrier();
|
|
11
|
+
if (options === null || options === void 0 ? void 0 : options.iaApiBaseUrl)
|
|
12
|
+
this.iaApiBaseUrl = options.iaApiBaseUrl;
|
|
13
|
+
if (options === null || options === void 0 ? void 0 : options.fetchRetrier)
|
|
14
|
+
this.fetchRetrier = options.fetchRetrier;
|
|
15
|
+
if (options === null || options === void 0 ? void 0 : options.searchParams) {
|
|
16
|
+
this.searchParams = options.searchParams;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
this.searchParams = window.location.search;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** @inheritdoc */
|
|
23
|
+
async fetchIAApiResponse(path, options) {
|
|
24
|
+
const url = `${this.iaApiBaseUrl}${path}`;
|
|
25
|
+
return this.fetchApiResponse(url, options);
|
|
26
|
+
}
|
|
27
|
+
/** @inheritdoc */
|
|
28
|
+
async fetchApiResponse(url, options) {
|
|
29
|
+
const requestInit = {};
|
|
30
|
+
if (options === null || options === void 0 ? void 0 : options.includeCredentials)
|
|
31
|
+
requestInit.credentials = 'include';
|
|
32
|
+
if (options === null || options === void 0 ? void 0 : options.method)
|
|
33
|
+
requestInit.method = options.method;
|
|
34
|
+
if (options === null || options === void 0 ? void 0 : options.body)
|
|
35
|
+
requestInit.body = options.body;
|
|
36
|
+
if (options === null || options === void 0 ? void 0 : options.headers)
|
|
37
|
+
requestInit.headers = options.headers;
|
|
38
|
+
const response = await this.fetch(url, requestInit);
|
|
39
|
+
const json = await response.json();
|
|
40
|
+
return json;
|
|
41
|
+
}
|
|
42
|
+
/** @inheritdoc */
|
|
43
|
+
async fetch(input, init) {
|
|
44
|
+
let finalInput = input;
|
|
45
|
+
const urlParams = new URLSearchParams(this.searchParams);
|
|
46
|
+
if (urlParams.get('reCache') === '1') {
|
|
47
|
+
finalInput = this.addSearchParams(input, { reCache: '1' });
|
|
48
|
+
}
|
|
49
|
+
return this.fetchRetrier.fetchRetry(finalInput, init);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Since RequestInfo can be either a `Request` or `string`, we need to change
|
|
53
|
+
* the way we add search params to it depending on the input.
|
|
54
|
+
*/
|
|
55
|
+
addSearchParams(input, params) {
|
|
56
|
+
const urlString = typeof input === 'string' ? input : input.url;
|
|
57
|
+
const url = new URL(urlString, window.location.href);
|
|
58
|
+
for (const [key, value] of Object.entries(params)) {
|
|
59
|
+
url.searchParams.set(key, value);
|
|
60
|
+
}
|
|
61
|
+
if (typeof input === 'string') {
|
|
62
|
+
return url.href;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const newRequest = new Request(url.href, input);
|
|
66
|
+
return newRequest;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=ia-fetch-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ia-fetch-handler.js","sourceRoot":"","sources":["../../src/ia-fetch-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import { FetchRetrier, FetchRetrierInterface } from './utils/fetch-retrier';\nimport type { FetchHandlerInterface } from './fetch-handler-interface';\n\n/**\n * The FetchHandler adds some common helpers:\n * - retry the request if it fails\n * - add `reCache=1` to the request if it's in the current url so the backend sees it\n * - add convenience method for fetching/decoding an API response by just the path\n */\nexport class IaFetchHandler implements FetchHandlerInterface {\n private iaApiBaseUrl?: string;\n\n private fetchRetrier: FetchRetrierInterface = new FetchRetrier();\n\n private searchParams?: string;\n\n constructor(options?: {\n iaApiBaseUrl?: string;\n fetchRetrier?: FetchRetrierInterface;\n searchParams?: string;\n }) {\n if (options?.iaApiBaseUrl) this.iaApiBaseUrl = options.iaApiBaseUrl;\n if (options?.fetchRetrier) this.fetchRetrier = options.fetchRetrier;\n if (options?.searchParams) {\n this.searchParams = options.searchParams;\n } else {\n this.searchParams = window.location.search;\n }\n }\n\n /** @inheritdoc */\n async fetchIAApiResponse<T>(\n path: string,\n options?: {\n includeCredentials?: boolean;\n },\n ): Promise<T> {\n const url = `${this.iaApiBaseUrl}${path}`;\n return this.fetchApiResponse(url, options);\n }\n\n /** @inheritdoc */\n async fetchApiResponse<T>(\n url: string,\n options?: {\n includeCredentials?: boolean;\n method?: string;\n body?: BodyInit;\n headers?: HeadersInit;\n },\n ): Promise<T> {\n const requestInit: RequestInit = {};\n if (options?.includeCredentials) requestInit.credentials = 'include';\n if (options?.method) requestInit.method = options.method;\n if (options?.body) requestInit.body = options.body;\n if (options?.headers) requestInit.headers = options.headers;\n const response = await this.fetch(url, requestInit);\n const json = await response.json();\n return json as T;\n }\n\n /** @inheritdoc */\n async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {\n let finalInput = input;\n const urlParams = new URLSearchParams(this.searchParams);\n if (urlParams.get('reCache') === '1') {\n finalInput = this.addSearchParams(input, { reCache: '1' });\n }\n return this.fetchRetrier.fetchRetry(finalInput, init);\n }\n\n /**\n * Since RequestInfo can be either a `Request` or `string`, we need to change\n * the way we add search params to it depending on the input.\n */\n private addSearchParams(\n input: RequestInfo,\n params: Record<string, string>,\n ): RequestInfo {\n const urlString = typeof input === 'string' ? input : input.url;\n const url = new URL(urlString, window.location.href);\n\n for (const [key, value] of Object.entries(params)) {\n url.searchParams.set(key, value);\n }\n\n if (typeof input === 'string') {\n return url.href;\n } else {\n const newRequest = new Request(url.href, input);\n return newRequest;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';
|
|
2
|
+
/**
|
|
3
|
+
* A class that retries a fetch request.
|
|
4
|
+
*/
|
|
5
|
+
export interface FetchRetrierInterface {
|
|
6
|
+
/**
|
|
7
|
+
* Execute a fetch with retry.
|
|
8
|
+
*
|
|
9
|
+
* @param requestInfo RequestInfo
|
|
10
|
+
* @param init Optional RequestInit
|
|
11
|
+
* @param retries Optional number of retries to attempt
|
|
12
|
+
* @returns Promise<Response>
|
|
13
|
+
*/
|
|
14
|
+
fetchRetry(requestInfo: RequestInfo, init?: RequestInit, retries?: number): Promise<Response>;
|
|
15
|
+
}
|
|
16
|
+
/** @inheritdoc */
|
|
17
|
+
export declare class FetchRetrier implements FetchRetrierInterface {
|
|
18
|
+
private analyticsHandler;
|
|
19
|
+
private sleep;
|
|
20
|
+
private retryCount;
|
|
21
|
+
private retryDelay;
|
|
22
|
+
constructor(options?: {
|
|
23
|
+
analyticsHandler?: AnalyticsHandlerInterface;
|
|
24
|
+
retryCount?: number;
|
|
25
|
+
retryDelay?: number;
|
|
26
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
27
|
+
});
|
|
28
|
+
/** @inheritdoc */
|
|
29
|
+
fetchRetry(requestInfo: RequestInfo, init?: RequestInit, retries?: number): Promise<Response>;
|
|
30
|
+
private isContentBlockerError;
|
|
31
|
+
private readonly eventCategory;
|
|
32
|
+
private logRetryEvent;
|
|
33
|
+
private logFailureEvent;
|
|
34
|
+
private log404Event;
|
|
35
|
+
private logContentBlockingEvent;
|
|
36
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AnalyticsHandler } from '@internetarchive/analytics-manager';
|
|
2
|
+
import { promisedSleep } from './promised-sleep';
|
|
3
|
+
/** @inheritdoc */
|
|
4
|
+
export class FetchRetrier {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.analyticsHandler = new AnalyticsHandler({ enableAnalytics: true });
|
|
7
|
+
this.sleep = promisedSleep; // default sleep function
|
|
8
|
+
this.retryCount = 2;
|
|
9
|
+
this.retryDelay = 1000;
|
|
10
|
+
this.eventCategory = 'offshootFetchRetry';
|
|
11
|
+
if (options === null || options === void 0 ? void 0 : options.analyticsHandler)
|
|
12
|
+
this.analyticsHandler = options.analyticsHandler;
|
|
13
|
+
if (options === null || options === void 0 ? void 0 : options.retryCount)
|
|
14
|
+
this.retryCount = options.retryCount;
|
|
15
|
+
if (options === null || options === void 0 ? void 0 : options.retryDelay)
|
|
16
|
+
this.retryDelay = options.retryDelay;
|
|
17
|
+
if (options === null || options === void 0 ? void 0 : options.sleepFn)
|
|
18
|
+
this.sleep = options.sleepFn; // override if provided
|
|
19
|
+
}
|
|
20
|
+
/** @inheritdoc */
|
|
21
|
+
async fetchRetry(requestInfo, init, retries = this.retryCount) {
|
|
22
|
+
const urlString = typeof requestInfo === 'string' ? requestInfo : requestInfo.url;
|
|
23
|
+
const retryNumber = this.retryCount - retries + 1;
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(requestInfo, init);
|
|
26
|
+
if (response.ok)
|
|
27
|
+
return response;
|
|
28
|
+
// don't retry on a 404 since this will never succeed
|
|
29
|
+
if (response.status === 404) {
|
|
30
|
+
this.log404Event(urlString);
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
if (retries > 0) {
|
|
34
|
+
await this.sleep(this.retryDelay);
|
|
35
|
+
this.logRetryEvent(urlString, retryNumber, response.statusText, response.status);
|
|
36
|
+
return this.fetchRetry(requestInfo, init, retries - 1);
|
|
37
|
+
}
|
|
38
|
+
this.logFailureEvent(urlString, response.status);
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// if a content blocker is detected, log it and don't retry
|
|
43
|
+
if (this.isContentBlockerError(error)) {
|
|
44
|
+
this.logContentBlockingEvent(urlString, error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
if (retries > 0) {
|
|
48
|
+
await this.sleep(this.retryDelay);
|
|
49
|
+
// intentionally duplicating the error message here since we want something
|
|
50
|
+
// in the status code even though we won't have an actual code
|
|
51
|
+
this.logRetryEvent(urlString, retryNumber, error, error);
|
|
52
|
+
return this.fetchRetry(requestInfo, init, retries - 1);
|
|
53
|
+
}
|
|
54
|
+
this.logFailureEvent(urlString, error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
isContentBlockerError(error) {
|
|
59
|
+
// all of the content blocker errors are `TypeError`
|
|
60
|
+
if (!(error instanceof TypeError))
|
|
61
|
+
return false;
|
|
62
|
+
const message = error.message.toLowerCase();
|
|
63
|
+
return message.includes('content blocker');
|
|
64
|
+
}
|
|
65
|
+
logRetryEvent(urlString, retryNumber, status, code) {
|
|
66
|
+
this.analyticsHandler.sendEvent({
|
|
67
|
+
category: this.eventCategory,
|
|
68
|
+
action: 'retryingFetch',
|
|
69
|
+
label: `retryNumber: ${retryNumber} / ${this.retryCount}, code: ${code}, status: ${status}, url: ${urlString}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
logFailureEvent(urlString, error) {
|
|
73
|
+
this.analyticsHandler.sendEvent({
|
|
74
|
+
category: this.eventCategory,
|
|
75
|
+
action: 'fetchFailed',
|
|
76
|
+
label: `error: ${error}, url: ${urlString}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
log404Event(urlString) {
|
|
80
|
+
this.analyticsHandler.sendEvent({
|
|
81
|
+
category: this.eventCategory,
|
|
82
|
+
action: 'status404NotRetrying',
|
|
83
|
+
label: `url: ${urlString}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
logContentBlockingEvent(urlString, error) {
|
|
87
|
+
this.analyticsHandler.sendEvent({
|
|
88
|
+
category: this.eventCategory,
|
|
89
|
+
action: 'contentBlockerDetectedNotRetrying',
|
|
90
|
+
label: `error: ${error}, url: ${urlString}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=fetch-retrier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-retrier.js","sourceRoot":"","sources":["../../../src/utils/fetch-retrier.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAqBjD,kBAAkB;AAClB,MAAM,OAAO,YAAY;IAQvB,YAAY,OAKX;QAZO,qBAAgB,GAAG,IAAI,gBAAgB,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,UAAK,GAAG,aAAa,CAAC,CAAC,yBAAyB;QAEhD,eAAU,GAAG,CAAC,CAAC;QAEf,eAAU,GAAG,IAAI,CAAC;QAwET,kBAAa,GAAG,oBAAoB,CAAC;QAhEpD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,gBAAgB;YAC3B,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACnD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QAC9D,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QAC9D,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO;YAAE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,uBAAuB;IAC7E,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,UAAU,CACrB,WAAwB,EACxB,IAAkB,EAClB,UAAkB,IAAI,CAAC,UAAU;QAEjC,MAAM,SAAS,GACb,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC;QAClE,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,GAAG,OAAO,GAAG,CAAC,CAAC;QAElD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAChD,IAAI,QAAQ,CAAC,EAAE;gBAAE,OAAO,QAAQ,CAAC;YACjC,qDAAqD;YACrD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;gBAC5B,OAAO,QAAQ,CAAC;YAClB,CAAC;YACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAClC,IAAI,CAAC,aAAa,CAChB,SAAS,EACT,WAAW,EACX,QAAQ,CAAC,UAAU,EACnB,QAAQ,CAAC,MAAM,CAChB,CAAC;gBACF,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;YACzD,CAAC;YACD,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjD,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,2DAA2D;YAC3D,IAAI,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtC,IAAI,CAAC,uBAAuB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;gBAC/C,MAAM,KAAK,CAAC;YACd,CAAC;YAED,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAClC,2EAA2E;gBAC3E,8DAA8D;gBAC9D,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;gBACzD,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;YACzD,CAAC;YAED,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACvC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAEO,qBAAqB,CAAC,KAAc;QAC1C,oDAAoD;QACpD,IAAI,CAAC,CAAC,KAAK,YAAY,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC5C,OAAO,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IAC7C,CAAC;IAIO,aAAa,CACnB,SAAiB,EACjB,WAAmB,EACnB,MAAe,EACf,IAAa;QAEb,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAC9B,QAAQ,EAAE,IAAI,CAAC,aAAa;YAC5B,MAAM,EAAE,eAAe;YACvB,KAAK,EAAE,gBAAgB,WAAW,MAAM,IAAI,CAAC,UAAU,WAAW,IAAI,aAAa,MAAM,UAAU,SAAS,EAAE;SAC/G,CAAC,CAAC;IACL,CAAC;IAEO,eAAe,CAAC,SAAiB,EAAE,KAAc;QACvD,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAC9B,QAAQ,EAAE,IAAI,CAAC,aAAa;YAC5B,MAAM,EAAE,aAAa;YACrB,KAAK,EAAE,UAAU,KAAK,UAAU,SAAS,EAAE;SAC5C,CAAC,CAAC;IACL,CAAC;IAEO,WAAW,CAAC,SAAiB;QACnC,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAC9B,QAAQ,EAAE,IAAI,CAAC,aAAa;YAC5B,MAAM,EAAE,sBAAsB;YAC9B,KAAK,EAAE,QAAQ,SAAS,EAAE;SAC3B,CAAC,CAAC;IACL,CAAC;IAEO,uBAAuB,CAAC,SAAiB,EAAE,KAAc;QAC/D,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAC9B,QAAQ,EAAE,IAAI,CAAC,aAAa;YAC5B,MAAM,EAAE,mCAAmC;YAC3C,KAAK,EAAE,UAAU,KAAK,UAAU,SAAS,EAAE;SAC5C,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';\nimport { AnalyticsHandler } from '@internetarchive/analytics-manager';\nimport { promisedSleep } from './promised-sleep';\n\n/**\n * A class that retries a fetch request.\n */\nexport interface FetchRetrierInterface {\n /**\n * Execute a fetch with retry.\n *\n * @param requestInfo RequestInfo\n * @param init Optional RequestInit\n * @param retries Optional number of retries to attempt\n * @returns Promise<Response>\n */\n fetchRetry(\n requestInfo: RequestInfo,\n init?: RequestInit,\n retries?: number,\n ): Promise<Response>;\n}\n\n/** @inheritdoc */\nexport class FetchRetrier implements FetchRetrierInterface {\n private analyticsHandler = new AnalyticsHandler({ enableAnalytics: true });\n private sleep = promisedSleep; // default sleep function\n\n private retryCount = 2;\n\n private retryDelay = 1000;\n\n constructor(options?: {\n analyticsHandler?: AnalyticsHandlerInterface;\n retryCount?: number;\n retryDelay?: number;\n sleepFn?: (ms: number) => Promise<void>; // <-- add this!\n }) {\n if (options?.analyticsHandler)\n this.analyticsHandler = options.analyticsHandler;\n if (options?.retryCount) this.retryCount = options.retryCount;\n if (options?.retryDelay) this.retryDelay = options.retryDelay;\n if (options?.sleepFn) this.sleep = options.sleepFn; // override if provided\n }\n\n /** @inheritdoc */\n public async fetchRetry(\n requestInfo: RequestInfo,\n init?: RequestInit,\n retries: number = this.retryCount,\n ): Promise<Response> {\n const urlString =\n typeof requestInfo === 'string' ? requestInfo : requestInfo.url;\n const retryNumber = this.retryCount - retries + 1;\n\n try {\n const response = await fetch(requestInfo, init);\n if (response.ok) return response;\n // don't retry on a 404 since this will never succeed\n if (response.status === 404) {\n this.log404Event(urlString);\n return response;\n }\n if (retries > 0) {\n await this.sleep(this.retryDelay);\n this.logRetryEvent(\n urlString,\n retryNumber,\n response.statusText,\n response.status,\n );\n return this.fetchRetry(requestInfo, init, retries - 1);\n }\n this.logFailureEvent(urlString, response.status);\n return response;\n } catch (error) {\n // if a content blocker is detected, log it and don't retry\n if (this.isContentBlockerError(error)) {\n this.logContentBlockingEvent(urlString, error);\n throw error;\n }\n\n if (retries > 0) {\n await this.sleep(this.retryDelay);\n // intentionally duplicating the error message here since we want something\n // in the status code even though we won't have an actual code\n this.logRetryEvent(urlString, retryNumber, error, error);\n return this.fetchRetry(requestInfo, init, retries - 1);\n }\n\n this.logFailureEvent(urlString, error);\n throw error;\n }\n }\n\n private isContentBlockerError(error: unknown): boolean {\n // all of the content blocker errors are `TypeError`\n if (!(error instanceof TypeError)) return false;\n const message = error.message.toLowerCase();\n return message.includes('content blocker');\n }\n\n private readonly eventCategory = 'offshootFetchRetry';\n\n private logRetryEvent(\n urlString: string,\n retryNumber: number,\n status: unknown,\n code: unknown,\n ) {\n this.analyticsHandler.sendEvent({\n category: this.eventCategory,\n action: 'retryingFetch',\n label: `retryNumber: ${retryNumber} / ${this.retryCount}, code: ${code}, status: ${status}, url: ${urlString}`,\n });\n }\n\n private logFailureEvent(urlString: string, error: unknown) {\n this.analyticsHandler.sendEvent({\n category: this.eventCategory,\n action: 'fetchFailed',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n\n private log404Event(urlString: string) {\n this.analyticsHandler.sendEvent({\n category: this.eventCategory,\n action: 'status404NotRetrying',\n label: `url: ${urlString}`,\n });\n }\n\n private logContentBlockingEvent(urlString: string, error: unknown) {\n this.analyticsHandler.sendEvent({\n category: this.eventCategory,\n action: 'contentBlockerDetectedNotRetrying',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously pause execution for a given number of milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* Used for waiting for some asynchronous updates.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* await promisedSleep(100)
|
|
8
|
+
*
|
|
9
|
+
* @export
|
|
10
|
+
* @param {number} ms
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
export declare function promisedSleep(ms: number): Promise<void>;
|