@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":"AACA;;;;;;;;;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;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;CAUrC"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
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
  }