@api-client/ui 0.1.3 → 0.1.4
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.
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
import type { Exception } from '@api-client/core/exceptions/exception.js';
|
|
2
|
+
/**
|
|
3
|
+
* Defines the options for the retry mechanism.
|
|
4
|
+
*/
|
|
5
|
+
export interface RetryOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The maximum number of retry attempts.
|
|
8
|
+
* Defaults to 3.
|
|
9
|
+
*/
|
|
10
|
+
retries?: number;
|
|
11
|
+
/**
|
|
12
|
+
* The delay in milliseconds before the next retry.
|
|
13
|
+
* Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
|
|
14
|
+
* Defaults to 1000ms.
|
|
15
|
+
*/
|
|
16
|
+
delayMs?: number | ((attempt: number) => number);
|
|
17
|
+
/**
|
|
18
|
+
* An optional function that determines if a retry should be attempted based on the error.
|
|
19
|
+
* If not provided, all errors will trigger a retry up to the maximum number of retries.
|
|
20
|
+
* @param error The error that occurred.
|
|
21
|
+
* @returns True if a retry should be attempted, false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
shouldRetry?: (error: Error) => boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
27
|
+
* The delay doubles with each attempt.
|
|
28
|
+
*
|
|
29
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
30
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
31
|
+
* @returns The calculated delay in milliseconds.
|
|
32
|
+
*/
|
|
33
|
+
export declare function exponentialBackoffDelay(attempt: number, initialDelay?: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Checks if the API error should be retried based on its status code.
|
|
36
|
+
* If the status code is 404, it returns false (do not retry).
|
|
37
|
+
* For other status codes, it returns true (retry).
|
|
38
|
+
* @param error The error to check.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shouldRetryApiError(error: Exception): boolean;
|
|
1
41
|
/**
|
|
2
42
|
* The base class for models.
|
|
3
43
|
*
|
|
@@ -36,6 +76,10 @@ export declare abstract class Model<T> extends EventTarget {
|
|
|
36
76
|
* @type A promise resolved when the debounced task finished.
|
|
37
77
|
*/
|
|
38
78
|
get taskComplete(): Promise<void> | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* A getter for the raw value.
|
|
81
|
+
*/
|
|
82
|
+
get value(): T | undefined;
|
|
39
83
|
/**
|
|
40
84
|
* @param obj The source object to use.
|
|
41
85
|
*/
|
|
@@ -53,5 +97,28 @@ export declare abstract class Model<T> extends EventTarget {
|
|
|
53
97
|
debounce(callback: (...args: unknown[]) => unknown): void;
|
|
54
98
|
notifyError(error: Error): void;
|
|
55
99
|
protected resolveTaskPromise(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
102
|
+
* @param operation A function that returns a Promise to be executed.
|
|
103
|
+
* @param options Optional retry configurations.
|
|
104
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
108
|
+
* const operation = async (): Promise<UserData> => {
|
|
109
|
+
* // ... perform API call to create user
|
|
110
|
+
* }
|
|
111
|
+
* const retryOptions: RetryOptions = {
|
|
112
|
+
* retries: 5,
|
|
113
|
+
* delayMs: exponentialBackoffDelay,
|
|
114
|
+
* shouldRetry: shouldRetryApiError,
|
|
115
|
+
* }
|
|
116
|
+
* this.raw = await this.retry(operation)
|
|
117
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
118
|
+
* return this.raw;
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
protected retry<R>(operation: () => Promise<R>, options?: RetryOptions): Promise<R>;
|
|
56
123
|
}
|
|
57
124
|
//# sourceMappingURL=Model.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0CAA0C,CAAA;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAChD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,SAAO,GAAG,MAAM,CAIpF;AACD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAO7D;AACD;;;;;;;;;GASG;AACH,8BAAsB,KAAK,CAAC,CAAC,CAAE,SAAQ,WAAW;;IAChD;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,CAAC,CAAA;IAEP;;;;OAIG;IACH,eAAe,SAAM;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;IAExC;;OAEG;IACH,KAAK,SAAK;IAiBV;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAE5C;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAEzB;IAED;;OAEG;gBACS,GAAG,CAAC,EAAE,CAAC;IAMnB;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI;IAqBzD,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAe/B,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAWpC;;;;;;;;;;;;;;;;;;;;;OAqBG;cACa,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC;CAmC9F"}
|
package/build/src/core/Model.js
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
/**
|
|
3
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
4
|
+
* The delay doubles with each attempt.
|
|
5
|
+
*
|
|
6
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
7
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
8
|
+
* @returns The calculated delay in milliseconds.
|
|
9
|
+
*/
|
|
10
|
+
export function exponentialBackoffDelay(attempt, initialDelay = 1000) {
|
|
11
|
+
if (attempt <= 0)
|
|
12
|
+
return initialDelay;
|
|
13
|
+
// For attempt 1, delay is initialDelay * 2^0 = initialDelay
|
|
14
|
+
return initialDelay * Math.pow(2, attempt - 1);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Checks if the API error should be retried based on its status code.
|
|
18
|
+
* If the status code is 404, it returns false (do not retry).
|
|
19
|
+
* For other status codes, it returns true (retry).
|
|
20
|
+
* @param error The error to check.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldRetryApiError(error) {
|
|
23
|
+
const { status } = error;
|
|
24
|
+
if (status === 404) {
|
|
25
|
+
// Do not retry on 404 errors
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* The base class for models.
|
|
4
32
|
*
|
|
@@ -50,6 +78,12 @@ export class Model extends EventTarget {
|
|
|
50
78
|
get taskComplete() {
|
|
51
79
|
return this.#taskComplete;
|
|
52
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A getter for the raw value.
|
|
83
|
+
*/
|
|
84
|
+
get value() {
|
|
85
|
+
return this.raw;
|
|
86
|
+
}
|
|
53
87
|
/**
|
|
54
88
|
* @param obj The source object to use.
|
|
55
89
|
*/
|
|
@@ -113,5 +147,60 @@ export class Model extends EventTarget {
|
|
|
113
147
|
resolver();
|
|
114
148
|
}
|
|
115
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
152
|
+
* @param operation A function that returns a Promise to be executed.
|
|
153
|
+
* @param options Optional retry configurations.
|
|
154
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
158
|
+
* const operation = async (): Promise<UserData> => {
|
|
159
|
+
* // ... perform API call to create user
|
|
160
|
+
* }
|
|
161
|
+
* const retryOptions: RetryOptions = {
|
|
162
|
+
* retries: 5,
|
|
163
|
+
* delayMs: exponentialBackoffDelay,
|
|
164
|
+
* shouldRetry: shouldRetryApiError,
|
|
165
|
+
* }
|
|
166
|
+
* this.raw = await this.retry(operation)
|
|
167
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
168
|
+
* return this.raw;
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
async retry(operation, options = {}) {
|
|
173
|
+
const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options;
|
|
174
|
+
let lastError;
|
|
175
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
return await operation();
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
lastError = e;
|
|
181
|
+
if (!shouldRetry(lastError) || attempt === retries - 1) {
|
|
182
|
+
// Do not retry if shouldRetry returns false or if it's the last attempt
|
|
183
|
+
this.notifyError(lastError);
|
|
184
|
+
throw lastError;
|
|
185
|
+
}
|
|
186
|
+
const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs;
|
|
187
|
+
if (delay > 0) {
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// This part should ideally not be reached if retries > 0,
|
|
193
|
+
// as the loop would throw on the last attempt.
|
|
194
|
+
// However, to satisfy TypeScript and guard against retries = 0:
|
|
195
|
+
if (lastError) {
|
|
196
|
+
this.notifyError(lastError);
|
|
197
|
+
throw lastError;
|
|
198
|
+
}
|
|
199
|
+
// Should not happen if operation is called at least once.
|
|
200
|
+
// Adding a generic error for completeness.
|
|
201
|
+
const err = new Error('Retry operation failed without a specific error.');
|
|
202
|
+
this.notifyError(err);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
116
205
|
}
|
|
117
206
|
//# sourceMappingURL=Model.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD;;;;;;;;;GASG;AACH,MAAM,OAAgB,KAAS,SAAQ,WAAW;IAChD;;;;;;OAMG;IACH,GAAG,CAAI;IAEP;;;;OAIG;IACH,eAAe,GAAG,GAAG,CAAA;IAErB;;OAEG;IACH,cAAc,CAA0B;IAExC;;OAEG;IACH,KAAK,GAAG,EAAE,CAAA;IAEV;;OAEG;IACH,sBAAsB,GAAG,KAAK,CAAA;IAE9B;;OAEG;IACH,aAAa,CAAgB;IAE7B;;OAEG;IACH,aAAa,CAAa;IAE1B;;OAEG;IACH,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,YAAY,GAAO;QACjB,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,iBAAiB,EAAE,CAAA;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAgB;QACrB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,QAAQ,CAAC,QAAyC;QAChD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QACnC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC1B,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YAC1C,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;YAC/B,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,CAAU,CAAA;gBACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;gBACrB,MAAM,GAAG,CAAA;YACX,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,kBAAkB,EAAE,CAAA;YAC3B,CAAC;QACH,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;IAC1B,CAAC;IAED,WAAW,CAAC,KAAY;QACtB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAQ,OAAO,EAAE;YAC9B,MAAM,EAAE,KAAK;SACd,CAAC,CACH,CAAA;IACH,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,aAAa,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACjD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAA;YAC5B,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC;IAES,kBAAkB;QAC1B,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,OAAM;QACR,CAAC;QACD,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAA;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAA;QACnC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,EAAE,CAAA;QACZ,CAAC;IACH,CAAC;CACF","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n\nimport type { Exception } from '@api-client/core/exceptions/exception.js'\n\n/**\n * Defines the options for the retry mechanism.\n */\nexport interface RetryOptions {\n /**\n * The maximum number of retry attempts.\n * Defaults to 3.\n */\n retries?: number\n /**\n * The delay in milliseconds before the next retry.\n * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).\n * Defaults to 1000ms.\n */\n delayMs?: number | ((attempt: number) => number)\n /**\n * An optional function that determines if a retry should be attempted based on the error.\n * If not provided, all errors will trigger a retry up to the maximum number of retries.\n * @param error The error that occurred.\n * @returns True if a retry should be attempted, false otherwise.\n */\n shouldRetry?: (error: Error) => boolean\n}\n\n/**\n * Calculates an exponential backoff delay for retry attempts.\n * The delay doubles with each attempt.\n *\n * @param attempt The current retry attempt number (should be 1-indexed).\n * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.\n * @returns The calculated delay in milliseconds.\n */\nexport function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {\n if (attempt <= 0) return initialDelay\n // For attempt 1, delay is initialDelay * 2^0 = initialDelay\n return initialDelay * Math.pow(2, attempt - 1)\n}\n/**\n * Checks if the API error should be retried based on its status code.\n * If the status code is 404, it returns false (do not retry).\n * For other status codes, it returns true (retry).\n * @param error The error to check.\n */\nexport function shouldRetryApiError(error: Exception): boolean {\n const { status } = error\n if (status === 404) {\n // Do not retry on 404 errors\n return false\n }\n return true\n}\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * A getter for the raw value.\n */\n get value(): T | undefined {\n return this.raw\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n\n /**\n * Executes an asynchronous operation with a retry mechanism.\n * @param operation A function that returns a Promise to be executed.\n * @param options Optional retry configurations.\n * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.\n * @example\n * ```typescript\n * override async create(data: Partial<IUser>): Promise<IUser> {\n * const operation = async (): Promise<UserData> => {\n * // ... perform API call to create user\n * }\n * const retryOptions: RetryOptions = {\n * retries: 5,\n * delayMs: exponentialBackoffDelay,\n * shouldRetry: shouldRetryApiError,\n * }\n * this.raw = await this.retry(operation)\n * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))\n * return this.raw;\n * }\n * ```\n */\n protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {\n const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options\n\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < retries; attempt++) {\n try {\n return await operation()\n } catch (e) {\n lastError = e as Error\n if (!shouldRetry(lastError) || attempt === retries - 1) {\n // Do not retry if shouldRetry returns false or if it's the last attempt\n this.notifyError(lastError)\n throw lastError\n }\n\n const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n }\n }\n // This part should ideally not be reached if retries > 0,\n // as the loop would throw on the last attempt.\n // However, to satisfy TypeScript and guard against retries = 0:\n if (lastError) {\n this.notifyError(lastError)\n throw lastError\n }\n // Should not happen if operation is called at least once.\n // Adding a generic error for completeness.\n const err = new Error('Retry operation failed without a specific error.')\n this.notifyError(err)\n throw err\n }\n}\n"]}
|
package/package.json
CHANGED
package/src/core/Model.ts
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
|
|
3
|
+
import type { Exception } from '@api-client/core/exceptions/exception.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines the options for the retry mechanism.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The maximum number of retry attempts.
|
|
11
|
+
* Defaults to 3.
|
|
12
|
+
*/
|
|
13
|
+
retries?: number
|
|
14
|
+
/**
|
|
15
|
+
* The delay in milliseconds before the next retry.
|
|
16
|
+
* Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
|
|
17
|
+
* Defaults to 1000ms.
|
|
18
|
+
*/
|
|
19
|
+
delayMs?: number | ((attempt: number) => number)
|
|
20
|
+
/**
|
|
21
|
+
* An optional function that determines if a retry should be attempted based on the error.
|
|
22
|
+
* If not provided, all errors will trigger a retry up to the maximum number of retries.
|
|
23
|
+
* @param error The error that occurred.
|
|
24
|
+
* @returns True if a retry should be attempted, false otherwise.
|
|
25
|
+
*/
|
|
26
|
+
shouldRetry?: (error: Error) => boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
31
|
+
* The delay doubles with each attempt.
|
|
32
|
+
*
|
|
33
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
34
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
35
|
+
* @returns The calculated delay in milliseconds.
|
|
36
|
+
*/
|
|
37
|
+
export function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {
|
|
38
|
+
if (attempt <= 0) return initialDelay
|
|
39
|
+
// For attempt 1, delay is initialDelay * 2^0 = initialDelay
|
|
40
|
+
return initialDelay * Math.pow(2, attempt - 1)
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks if the API error should be retried based on its status code.
|
|
44
|
+
* If the status code is 404, it returns false (do not retry).
|
|
45
|
+
* For other status codes, it returns true (retry).
|
|
46
|
+
* @param error The error to check.
|
|
47
|
+
*/
|
|
48
|
+
export function shouldRetryApiError(error: Exception): boolean {
|
|
49
|
+
const { status } = error
|
|
50
|
+
if (status === 404) {
|
|
51
|
+
// Do not retry on 404 errors
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
2
56
|
/**
|
|
3
57
|
* The base class for models.
|
|
4
58
|
*
|
|
@@ -58,6 +112,13 @@ export abstract class Model<T> extends EventTarget {
|
|
|
58
112
|
return this.#taskComplete
|
|
59
113
|
}
|
|
60
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A getter for the raw value.
|
|
117
|
+
*/
|
|
118
|
+
get value(): T | undefined {
|
|
119
|
+
return this.raw
|
|
120
|
+
}
|
|
121
|
+
|
|
61
122
|
/**
|
|
62
123
|
* @param obj The source object to use.
|
|
63
124
|
*/
|
|
@@ -126,4 +187,62 @@ export abstract class Model<T> extends EventTarget {
|
|
|
126
187
|
resolver()
|
|
127
188
|
}
|
|
128
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
193
|
+
* @param operation A function that returns a Promise to be executed.
|
|
194
|
+
* @param options Optional retry configurations.
|
|
195
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
199
|
+
* const operation = async (): Promise<UserData> => {
|
|
200
|
+
* // ... perform API call to create user
|
|
201
|
+
* }
|
|
202
|
+
* const retryOptions: RetryOptions = {
|
|
203
|
+
* retries: 5,
|
|
204
|
+
* delayMs: exponentialBackoffDelay,
|
|
205
|
+
* shouldRetry: shouldRetryApiError,
|
|
206
|
+
* }
|
|
207
|
+
* this.raw = await this.retry(operation)
|
|
208
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
209
|
+
* return this.raw;
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {
|
|
214
|
+
const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options
|
|
215
|
+
|
|
216
|
+
let lastError: Error | undefined
|
|
217
|
+
|
|
218
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
219
|
+
try {
|
|
220
|
+
return await operation()
|
|
221
|
+
} catch (e) {
|
|
222
|
+
lastError = e as Error
|
|
223
|
+
if (!shouldRetry(lastError) || attempt === retries - 1) {
|
|
224
|
+
// Do not retry if shouldRetry returns false or if it's the last attempt
|
|
225
|
+
this.notifyError(lastError)
|
|
226
|
+
throw lastError
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs
|
|
230
|
+
if (delay > 0) {
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// This part should ideally not be reached if retries > 0,
|
|
236
|
+
// as the loop would throw on the last attempt.
|
|
237
|
+
// However, to satisfy TypeScript and guard against retries = 0:
|
|
238
|
+
if (lastError) {
|
|
239
|
+
this.notifyError(lastError)
|
|
240
|
+
throw lastError
|
|
241
|
+
}
|
|
242
|
+
// Should not happen if operation is called at least once.
|
|
243
|
+
// Adding a generic error for completeness.
|
|
244
|
+
const err = new Error('Retry operation failed without a specific error.')
|
|
245
|
+
this.notifyError(err)
|
|
246
|
+
throw err
|
|
247
|
+
}
|
|
129
248
|
}
|