@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.
Files changed (62) hide show
  1. package/.editorconfig +29 -0
  2. package/.github/workflows/ci.yml +27 -0
  3. package/.github/workflows/gh-pages-main.yml +40 -0
  4. package/.github/workflows/pr-preview.yml +63 -0
  5. package/.husky/pre-commit +4 -0
  6. package/.prettierignore +1 -0
  7. package/.vscode/extensions.json +12 -0
  8. package/.vscode/tasks.json +12 -0
  9. package/LICENSE +661 -0
  10. package/README.md +82 -0
  11. package/coverage/.gitignore +3 -0
  12. package/coverage/.placeholder +0 -0
  13. package/demo/app-root.ts +84 -0
  14. package/demo/index.html +27 -0
  15. package/dist/demo/app-root.d.ts +12 -0
  16. package/dist/demo/app-root.js +90 -0
  17. package/dist/demo/app-root.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/src/fetch-handler-interface.d.ts +33 -0
  22. package/dist/src/fetch-handler-interface.js +2 -0
  23. package/dist/src/fetch-handler-interface.js.map +1 -0
  24. package/dist/src/ia-fetch-handler.d.ts +36 -0
  25. package/dist/src/ia-fetch-handler.js +70 -0
  26. package/dist/src/ia-fetch-handler.js.map +1 -0
  27. package/dist/src/utils/fetch-retrier.d.ts +36 -0
  28. package/dist/src/utils/fetch-retrier.js +94 -0
  29. package/dist/src/utils/fetch-retrier.js.map +1 -0
  30. package/dist/src/utils/promised-sleep.d.ts +13 -0
  31. package/dist/src/utils/promised-sleep.js +16 -0
  32. package/dist/src/utils/promised-sleep.js.map +1 -0
  33. package/dist/test/fetch-retrier.test.d.ts +1 -0
  34. package/dist/test/fetch-retrier.test.js +139 -0
  35. package/dist/test/fetch-retrier.test.js.map +1 -0
  36. package/dist/test/ia-fetch-handler.test.d.ts +1 -0
  37. package/dist/test/ia-fetch-handler.test.js +50 -0
  38. package/dist/test/ia-fetch-handler.test.js.map +1 -0
  39. package/dist/test/mocks/mock-analytics-handler.d.ts +20 -0
  40. package/dist/test/mocks/mock-analytics-handler.js +28 -0
  41. package/dist/test/mocks/mock-analytics-handler.js.map +1 -0
  42. package/dist/vite.config.d.ts +2 -0
  43. package/dist/vite.config.js +25 -0
  44. package/dist/vite.config.js.map +1 -0
  45. package/eslint.config.mjs +53 -0
  46. package/index.ts +2 -0
  47. package/package.json +74 -0
  48. package/renovate.json +6 -0
  49. package/screenshot/gh-pages-settings.png +0 -0
  50. package/src/fetch-handler-interface.ts +39 -0
  51. package/src/ia-fetch-handler.ts +94 -0
  52. package/src/utils/fetch-retrier.ts +141 -0
  53. package/src/utils/promised-sleep.ts +15 -0
  54. package/ssl/server.crt +22 -0
  55. package/ssl/server.key +28 -0
  56. package/test/fetch-retrier.test.ts +173 -0
  57. package/test/ia-fetch-handler.test.ts +66 -0
  58. package/test/mocks/mock-analytics-handler.ts +43 -0
  59. package/tsconfig.json +31 -0
  60. package/vite.config.ts +25 -0
  61. package/web-dev-server.config.mjs +32 -0
  62. package/web-test-runner.config.mjs +41 -0
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ ![Build Status](https://github.com/internetarchive/iaux-fetch-handler-service/actions/workflows/ci.yml/badge.svg) [![codecov](https://codecov.io/gh/internetarchive/iaux-fetch-handler-service/graph/badge.svg?token=ZOYRJ2BV9W)](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
+ ```
@@ -0,0 +1,3 @@
1
+ *
2
+ !.placeholder
3
+ !.gitignore
File without changes
@@ -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
+ }
@@ -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"]}
@@ -0,0 +1,2 @@
1
+ export { IaFetchHandler } from './src/ia-fetch-handler';
2
+ export { FetchHandlerInterface } from './src/fetch-handler-interface';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { IaFetchHandler } from './src/ia-fetch-handler';
2
+ //# sourceMappingURL=index.js.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fetch-handler-interface.js.map
@@ -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":"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","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>;