@conduit-client/service-retry 3.0.0 → 3.3.0
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/dist/types/v1/__tests__/composed-retry-policy.spec.d.ts +1 -0
- package/dist/types/v1/composed-retry-policy.d.ts +106 -0
- package/dist/types/v1/index.d.ts +1 -0
- package/dist/types/v1/retry-policy.d.ts +10 -2
- package/dist/v1/index.js +108 -2
- package/dist/v1/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { RetryPolicy } from './retry-policy';
|
|
2
|
+
import type { RetryContext } from './retry-service';
|
|
3
|
+
/**
|
|
4
|
+
* Composes multiple retry policies into a single policy.
|
|
5
|
+
*
|
|
6
|
+
* This allows combining different retry strategies (e.g., throttling + CSRF)
|
|
7
|
+
* into one policy that can be used with the RetryService.
|
|
8
|
+
*
|
|
9
|
+
* ## Behavior
|
|
10
|
+
*
|
|
11
|
+
* - **shouldRetry**: Returns true if ANY policy wants to retry (OR logic)
|
|
12
|
+
* - **calculateDelay**: Uses delay from the FIRST policy that wants to retry (order matters!)
|
|
13
|
+
* - **prepareRetry**: Calls prepareRetry ONLY on policies that matched shouldRetry (runs in parallel)
|
|
14
|
+
*
|
|
15
|
+
* ## Important Notes
|
|
16
|
+
*
|
|
17
|
+
* **Policy Order Matters**: The order of policies in the array determines which delay is used
|
|
18
|
+
* when multiple policies want to retry. Place more specific or higher-priority policies first.
|
|
19
|
+
*
|
|
20
|
+
* **prepareRetry Efficiency**: Only policies that matched shouldRetry will have their
|
|
21
|
+
* prepareRetry called. This avoids unnecessary work and side effects for unrelated retry
|
|
22
|
+
* conditions. If multiple policies match, their prepareRetry hooks run in parallel.
|
|
23
|
+
*
|
|
24
|
+
* **Avoid Overlapping Conditions**: If multiple policies have overlapping retry conditions
|
|
25
|
+
* (e.g., both retry on 500 errors), the first policy's delay will always be used. Design
|
|
26
|
+
* policies with distinct, non-overlapping conditions when possible.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const throttling = new FetchThrottlingRetryPolicy();
|
|
31
|
+
* const csrf = new CsrfTokenRetryPolicy();
|
|
32
|
+
* const composed = new ComposedRetryPolicy([throttling, csrf]);
|
|
33
|
+
*
|
|
34
|
+
* const retryService = new RetryService(composed);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare class ComposedRetryPolicy<T> extends RetryPolicy<T> {
|
|
38
|
+
private readonly policies;
|
|
39
|
+
constructor(policies: RetryPolicy<T>[]);
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if any of the composed policies want to retry.
|
|
42
|
+
*
|
|
43
|
+
* Uses OR logic: if ANY policy returns true, this returns true.
|
|
44
|
+
* Policies are checked in order and evaluation short-circuits on the first match.
|
|
45
|
+
*/
|
|
46
|
+
shouldRetry(result: T, context: RetryContext<T>): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the delay from the first policy that wants to retry.
|
|
49
|
+
*
|
|
50
|
+
* If multiple policies want to retry, only the FIRST policy's delay is used.
|
|
51
|
+
* Policy order in the constructor array determines priority.
|
|
52
|
+
* If no policy wants to retry, returns 0.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // If both PolicyA (1000ms) and PolicyB (5000ms) want to retry:
|
|
57
|
+
* const composed = new ComposedRetryPolicy([policyA, policyB]);
|
|
58
|
+
* composed.calculateDelay(result, context); // Returns 1000ms (PolicyA wins)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
calculateDelay(result: T, context: RetryContext<T>): Promise<number>;
|
|
62
|
+
/**
|
|
63
|
+
* Calls prepareRetry on policies that both:
|
|
64
|
+
* 1. Implement the prepareRetry hook, AND
|
|
65
|
+
* 2. Returned true from shouldRetry for this result
|
|
66
|
+
*
|
|
67
|
+
* This allows only the matching policies to perform preparation work (e.g., token refresh).
|
|
68
|
+
* All matching prepareRetry calls run in parallel for efficiency.
|
|
69
|
+
*
|
|
70
|
+
* **Important**: prepareRetry only runs on policies that matched shouldRetry. This ensures
|
|
71
|
+
* you don't perform unnecessary work or side effects for unrelated retry conditions.
|
|
72
|
+
*
|
|
73
|
+
* **Note**: If multiple policies match and have prepareRetry, ensure they don't have
|
|
74
|
+
* conflicting side effects since they run in parallel.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // Status 401 occurs
|
|
79
|
+
* const composed = new ComposedRetryPolicy([
|
|
80
|
+
* authPolicy, // shouldRetry(401) → true, has prepareRetry
|
|
81
|
+
* throttlePolicy, // shouldRetry(401) → false, has prepareRetry
|
|
82
|
+
* ]);
|
|
83
|
+
*
|
|
84
|
+
* await composed.prepareRetry(result, context);
|
|
85
|
+
* // → Only authPolicy.prepareRetry() runs (because it matched)
|
|
86
|
+
* // → throttlePolicy.prepareRetry() does NOT run (didn't match)
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
prepareRetry(result: T, context: RetryContext<T>): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Returns all composed policies.
|
|
92
|
+
* Useful for accessing or configuring individual policies after composition.
|
|
93
|
+
*/
|
|
94
|
+
getPolicies(): RetryPolicy<T>[];
|
|
95
|
+
/**
|
|
96
|
+
* Helper to get a specific policy by type.
|
|
97
|
+
* Useful for calling policy-specific methods after composition.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const csrfPolicy = composedPolicy.getPolicyByType(CsrfTokenRetryPolicy);
|
|
102
|
+
* csrfPolicy?.setRequestContext(mutableRequest);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
getPolicyByType<P extends RetryPolicy<T>>(policyType: new (...args: any[]) => P): P | undefined;
|
|
106
|
+
}
|
package/dist/types/v1/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ import type { NamedService, ServiceDescriptor } from '@conduit-client/utils';
|
|
|
4
4
|
export type NamedRetryService = NamedService<'retry', RetryService<any>>;
|
|
5
5
|
export { RetryService, type RetryContext } from './retry-service';
|
|
6
6
|
export { RetryPolicy } from './retry-policy';
|
|
7
|
+
export { ComposedRetryPolicy } from './composed-retry-policy';
|
|
7
8
|
export type RetryServiceDescriptor = ServiceDescriptor<RetryService<any>, 'retry', '1.0'>;
|
|
8
9
|
export declare function buildServiceDescriptor<T>(defaultRetryPolicy: RetryPolicy<T>): RetryServiceDescriptor;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { RetryContext } from './retry-service';
|
|
2
2
|
export declare abstract class RetryPolicy<T> {
|
|
3
|
-
abstract shouldRetry(result: T, context: RetryContext<T>): boolean
|
|
4
|
-
abstract calculateDelay(result: T, context: RetryContext<T>): number
|
|
3
|
+
abstract shouldRetry(result: T, context: RetryContext<T>): Promise<boolean>;
|
|
4
|
+
abstract calculateDelay(result: T, context: RetryContext<T>): Promise<number>;
|
|
5
|
+
/**
|
|
6
|
+
* Optional hook called before each retry attempt.
|
|
7
|
+
* Allows policies to perform async preparation work before retrying.
|
|
8
|
+
*
|
|
9
|
+
* @param result - The result that triggered the retry
|
|
10
|
+
* @param context - Current retry context including attempt number and elapsed time
|
|
11
|
+
*/
|
|
12
|
+
prepareRetry?(result: T, context: RetryContext<T>): Promise<void>;
|
|
5
13
|
}
|
package/dist/v1/index.js
CHANGED
|
@@ -19,9 +19,12 @@ class RetryService {
|
|
|
19
19
|
totalElapsedMs: Date.now() - startTime,
|
|
20
20
|
lastResult: result
|
|
21
21
|
};
|
|
22
|
-
while (policy.shouldRetry(result, context)) {
|
|
23
|
-
const delay = policy.calculateDelay(result, context);
|
|
22
|
+
while (await policy.shouldRetry(result, context)) {
|
|
23
|
+
const delay = await policy.calculateDelay(result, context);
|
|
24
24
|
await this.delay(delay);
|
|
25
|
+
if (policy.prepareRetry) {
|
|
26
|
+
await policy.prepareRetry(result, context);
|
|
27
|
+
}
|
|
25
28
|
attempt++;
|
|
26
29
|
result = await operation();
|
|
27
30
|
context = {
|
|
@@ -40,6 +43,108 @@ class RetryService {
|
|
|
40
43
|
}
|
|
41
44
|
class RetryPolicy {
|
|
42
45
|
}
|
|
46
|
+
class ComposedRetryPolicy extends RetryPolicy {
|
|
47
|
+
constructor(policies) {
|
|
48
|
+
super();
|
|
49
|
+
this.policies = policies;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if any of the composed policies want to retry.
|
|
53
|
+
*
|
|
54
|
+
* Uses OR logic: if ANY policy returns true, this returns true.
|
|
55
|
+
* Policies are checked in order and evaluation short-circuits on the first match.
|
|
56
|
+
*/
|
|
57
|
+
async shouldRetry(result, context) {
|
|
58
|
+
for (const policy of this.policies) {
|
|
59
|
+
if (await policy.shouldRetry(result, context)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the delay from the first policy that wants to retry.
|
|
67
|
+
*
|
|
68
|
+
* If multiple policies want to retry, only the FIRST policy's delay is used.
|
|
69
|
+
* Policy order in the constructor array determines priority.
|
|
70
|
+
* If no policy wants to retry, returns 0.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* // If both PolicyA (1000ms) and PolicyB (5000ms) want to retry:
|
|
75
|
+
* const composed = new ComposedRetryPolicy([policyA, policyB]);
|
|
76
|
+
* composed.calculateDelay(result, context); // Returns 1000ms (PolicyA wins)
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
async calculateDelay(result, context) {
|
|
80
|
+
for (const policy of this.policies) {
|
|
81
|
+
if (await policy.shouldRetry(result, context)) {
|
|
82
|
+
return policy.calculateDelay(result, context);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Calls prepareRetry on policies that both:
|
|
89
|
+
* 1. Implement the prepareRetry hook, AND
|
|
90
|
+
* 2. Returned true from shouldRetry for this result
|
|
91
|
+
*
|
|
92
|
+
* This allows only the matching policies to perform preparation work (e.g., token refresh).
|
|
93
|
+
* All matching prepareRetry calls run in parallel for efficiency.
|
|
94
|
+
*
|
|
95
|
+
* **Important**: prepareRetry only runs on policies that matched shouldRetry. This ensures
|
|
96
|
+
* you don't perform unnecessary work or side effects for unrelated retry conditions.
|
|
97
|
+
*
|
|
98
|
+
* **Note**: If multiple policies match and have prepareRetry, ensure they don't have
|
|
99
|
+
* conflicting side effects since they run in parallel.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // Status 401 occurs
|
|
104
|
+
* const composed = new ComposedRetryPolicy([
|
|
105
|
+
* authPolicy, // shouldRetry(401) → true, has prepareRetry
|
|
106
|
+
* throttlePolicy, // shouldRetry(401) → false, has prepareRetry
|
|
107
|
+
* ]);
|
|
108
|
+
*
|
|
109
|
+
* await composed.prepareRetry(result, context);
|
|
110
|
+
* // → Only authPolicy.prepareRetry() runs (because it matched)
|
|
111
|
+
* // → throttlePolicy.prepareRetry() does NOT run (didn't match)
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async prepareRetry(result, context) {
|
|
115
|
+
const matchingPolicies = [];
|
|
116
|
+
for (const policy of this.policies) {
|
|
117
|
+
if (policy.prepareRetry !== void 0 && await policy.shouldRetry(result, context)) {
|
|
118
|
+
matchingPolicies.push(policy);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (matchingPolicies.length > 0) {
|
|
122
|
+
await Promise.all(
|
|
123
|
+
matchingPolicies.map((policy) => policy.prepareRetry(result, context))
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Returns all composed policies.
|
|
129
|
+
* Useful for accessing or configuring individual policies after composition.
|
|
130
|
+
*/
|
|
131
|
+
getPolicies() {
|
|
132
|
+
return this.policies;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Helper to get a specific policy by type.
|
|
136
|
+
* Useful for calling policy-specific methods after composition.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const csrfPolicy = composedPolicy.getPolicyByType(CsrfTokenRetryPolicy);
|
|
141
|
+
* csrfPolicy?.setRequestContext(mutableRequest);
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
getPolicyByType(policyType) {
|
|
145
|
+
return this.policies.find((policy) => policy instanceof policyType);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
43
148
|
function buildServiceDescriptor(defaultRetryPolicy) {
|
|
44
149
|
return {
|
|
45
150
|
version: "1.0",
|
|
@@ -48,6 +153,7 @@ function buildServiceDescriptor(defaultRetryPolicy) {
|
|
|
48
153
|
};
|
|
49
154
|
}
|
|
50
155
|
export {
|
|
156
|
+
ComposedRetryPolicy,
|
|
51
157
|
RetryPolicy,
|
|
52
158
|
RetryService,
|
|
53
159
|
buildServiceDescriptor
|
package/dist/v1/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/v1/retry-service.ts","../../src/v1/retry-policy.ts","../../src/v1/index.ts"],"sourcesContent":["import type { RetryPolicy } from './retry-policy';\n\nexport interface RetryContext<T> {\n attempt: number;\n totalElapsedMs: number;\n lastResult?: T;\n}\n\nexport interface IRetryService<T> {\n applyRetry(operation: () => Promise<T>, retryPolicyOverride?: RetryPolicy<T>): Promise<T>;\n}\n\nexport class RetryService<T> implements IRetryService<T> {\n constructor(private readonly defaultRetryPolicy: RetryPolicy<T>) {}\n\n applyRetry(operation: () => Promise<T>, retryPolicyOverride?: RetryPolicy<T>): Promise<T> {\n return this.retry(operation, retryPolicyOverride || this.defaultRetryPolicy);\n }\n\n protected async retry(operation: () => Promise<T>, policy: RetryPolicy<T>): Promise<T> {\n const startTime = Date.now();\n\n let attempt = 0;\n let result: T = await operation();\n let context: RetryContext<T> = {\n attempt,\n totalElapsedMs: Date.now() - startTime,\n lastResult: result,\n };\n\n while (policy.shouldRetry(result, context)) {\n const delay = policy.calculateDelay(result, context);\n await this.delay(delay);\n\n attempt++;\n result = await operation();\n context = {\n attempt,\n totalElapsedMs: Date.now() - startTime,\n lastResult: result,\n };\n }\n\n return result;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n }\n}\n","import type { RetryContext } from './retry-service';\n\nexport abstract class RetryPolicy<T> {\n abstract shouldRetry(result: T, context: RetryContext<T>): boolean;\n abstract calculateDelay(result: T, context: RetryContext<T>): number;\n}\n","import { RetryService } from './retry-service';\nimport type { RetryPolicy } from './retry-policy';\nimport type { NamedService, ServiceDescriptor } from '@conduit-client/utils';\n\nexport type NamedRetryService = NamedService<'retry', RetryService<any>>;\nexport { RetryService, type RetryContext } from './retry-service';\nexport { RetryPolicy } from './retry-policy';\nexport type RetryServiceDescriptor = ServiceDescriptor<RetryService<any>, 'retry', '1.0'>;\n\nexport function buildServiceDescriptor<T>(\n defaultRetryPolicy: RetryPolicy<T>\n): RetryServiceDescriptor {\n return {\n version: '1.0' as const,\n service: new RetryService<T>(defaultRetryPolicy),\n type: 'retry',\n };\n}\n"],"names":[],"mappings":";;;;;AAYO,MAAM,aAA4C;AAAA,EACrD,YAA6B,oBAAoC;AAApC,SAAA,qBAAA;AAAA,EAAqC;AAAA,EAElE,WAAW,WAA6B,qBAAkD;AACtF,WAAO,KAAK,MAAM,WAAW,uBAAuB,KAAK,kBAAkB;AAAA,EAC/E;AAAA,EAEA,MAAgB,MAAM,WAA6B,QAAoC;AACnF,UAAM,YAAY,KAAK,IAAA;AAEvB,QAAI,UAAU;AACd,QAAI,SAAY,MAAM,UAAA;AACtB,QAAI,UAA2B;AAAA,MAC3B;AAAA,MACA,gBAAgB,KAAK,IAAA,IAAQ;AAAA,MAC7B,YAAY;AAAA,IAAA;AAGhB,WAAO,OAAO,YAAY,QAAQ,OAAO,GAAG;AACxC,YAAM,QAAQ,OAAO,eAAe,QAAQ,OAAO;AACnD,YAAM,KAAK,MAAM,KAAK;AAEtB;AACA,eAAS,MAAM,UAAA;AACf,gBAAU;AAAA,QACN;AAAA,QACA,gBAAgB,KAAK,IAAA,IAAQ;AAAA,QAC7B,YAAY;AAAA,MAAA;AAAA,IAEpB;AAEA,WAAO;AAAA,EACX;AAAA,EAEQ,MAAM,IAA2B;AACrC,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,iBAAW,SAAS,EAAE;AAAA,IAC1B,CAAC;AAAA,EACL;AACJ;ACjDO,MAAe,YAAe;AAGrC;ACIO,SAAS,uBACZ,oBACsB;AACtB,SAAO;AAAA,IACH,SAAS;AAAA,IACT,SAAS,IAAI,aAAgB,kBAAkB;AAAA,IAC/C,MAAM;AAAA,EAAA;AAEd;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/v1/retry-service.ts","../../src/v1/retry-policy.ts","../../src/v1/composed-retry-policy.ts","../../src/v1/index.ts"],"sourcesContent":["import type { RetryPolicy } from './retry-policy';\n\nexport interface RetryContext<T> {\n attempt: number;\n totalElapsedMs: number;\n lastResult?: T;\n}\n\nexport interface IRetryService<T> {\n applyRetry(operation: () => Promise<T>, retryPolicyOverride?: RetryPolicy<T>): Promise<T>;\n}\n\nexport class RetryService<T> implements IRetryService<T> {\n constructor(private readonly defaultRetryPolicy: RetryPolicy<T>) {}\n\n applyRetry(operation: () => Promise<T>, retryPolicyOverride?: RetryPolicy<T>): Promise<T> {\n return this.retry(operation, retryPolicyOverride || this.defaultRetryPolicy);\n }\n\n protected async retry(operation: () => Promise<T>, policy: RetryPolicy<T>): Promise<T> {\n const startTime = Date.now();\n\n let attempt = 0;\n let result: T = await operation();\n let context: RetryContext<T> = {\n attempt,\n totalElapsedMs: Date.now() - startTime,\n lastResult: result,\n };\n\n while (await policy.shouldRetry(result, context)) {\n const delay = await policy.calculateDelay(result, context);\n await this.delay(delay);\n\n // Call prepareRetry hook if policy implements it\n if (policy.prepareRetry) {\n await policy.prepareRetry(result, context);\n }\n\n attempt++;\n result = await operation();\n context = {\n attempt,\n totalElapsedMs: Date.now() - startTime,\n lastResult: result,\n };\n }\n\n return result;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n }\n}\n","import type { RetryContext } from './retry-service';\n\nexport abstract class RetryPolicy<T> {\n abstract shouldRetry(result: T, context: RetryContext<T>): Promise<boolean>;\n abstract calculateDelay(result: T, context: RetryContext<T>): Promise<number>;\n\n /**\n * Optional hook called before each retry attempt.\n * Allows policies to perform async preparation work before retrying.\n *\n * @param result - The result that triggered the retry\n * @param context - Current retry context including attempt number and elapsed time\n */\n prepareRetry?(result: T, context: RetryContext<T>): Promise<void>;\n}\n","import { RetryPolicy } from './retry-policy';\nimport type { RetryContext } from './retry-service';\n\n/**\n * Composes multiple retry policies into a single policy.\n *\n * This allows combining different retry strategies (e.g., throttling + CSRF)\n * into one policy that can be used with the RetryService.\n *\n * ## Behavior\n *\n * - **shouldRetry**: Returns true if ANY policy wants to retry (OR logic)\n * - **calculateDelay**: Uses delay from the FIRST policy that wants to retry (order matters!)\n * - **prepareRetry**: Calls prepareRetry ONLY on policies that matched shouldRetry (runs in parallel)\n *\n * ## Important Notes\n *\n * **Policy Order Matters**: The order of policies in the array determines which delay is used\n * when multiple policies want to retry. Place more specific or higher-priority policies first.\n *\n * **prepareRetry Efficiency**: Only policies that matched shouldRetry will have their\n * prepareRetry called. This avoids unnecessary work and side effects for unrelated retry\n * conditions. If multiple policies match, their prepareRetry hooks run in parallel.\n *\n * **Avoid Overlapping Conditions**: If multiple policies have overlapping retry conditions\n * (e.g., both retry on 500 errors), the first policy's delay will always be used. Design\n * policies with distinct, non-overlapping conditions when possible.\n *\n * @example\n * ```typescript\n * const throttling = new FetchThrottlingRetryPolicy();\n * const csrf = new CsrfTokenRetryPolicy();\n * const composed = new ComposedRetryPolicy([throttling, csrf]);\n *\n * const retryService = new RetryService(composed);\n * ```\n */\nexport class ComposedRetryPolicy<T> extends RetryPolicy<T> {\n constructor(private readonly policies: RetryPolicy<T>[]) {\n super();\n }\n\n /**\n * Returns true if any of the composed policies want to retry.\n *\n * Uses OR logic: if ANY policy returns true, this returns true.\n * Policies are checked in order and evaluation short-circuits on the first match.\n */\n async shouldRetry(result: T, context: RetryContext<T>): Promise<boolean> {\n for (const policy of this.policies) {\n if (await policy.shouldRetry(result, context)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Returns the delay from the first policy that wants to retry.\n *\n * If multiple policies want to retry, only the FIRST policy's delay is used.\n * Policy order in the constructor array determines priority.\n * If no policy wants to retry, returns 0.\n *\n * @example\n * ```typescript\n * // If both PolicyA (1000ms) and PolicyB (5000ms) want to retry:\n * const composed = new ComposedRetryPolicy([policyA, policyB]);\n * composed.calculateDelay(result, context); // Returns 1000ms (PolicyA wins)\n * ```\n */\n async calculateDelay(result: T, context: RetryContext<T>): Promise<number> {\n for (const policy of this.policies) {\n if (await policy.shouldRetry(result, context)) {\n return policy.calculateDelay(result, context);\n }\n }\n return 0;\n }\n\n /**\n * Calls prepareRetry on policies that both:\n * 1. Implement the prepareRetry hook, AND\n * 2. Returned true from shouldRetry for this result\n *\n * This allows only the matching policies to perform preparation work (e.g., token refresh).\n * All matching prepareRetry calls run in parallel for efficiency.\n *\n * **Important**: prepareRetry only runs on policies that matched shouldRetry. This ensures\n * you don't perform unnecessary work or side effects for unrelated retry conditions.\n *\n * **Note**: If multiple policies match and have prepareRetry, ensure they don't have\n * conflicting side effects since they run in parallel.\n *\n * @example\n * ```typescript\n * // Status 401 occurs\n * const composed = new ComposedRetryPolicy([\n * authPolicy, // shouldRetry(401) → true, has prepareRetry\n * throttlePolicy, // shouldRetry(401) → false, has prepareRetry\n * ]);\n *\n * await composed.prepareRetry(result, context);\n * // → Only authPolicy.prepareRetry() runs (because it matched)\n * // → throttlePolicy.prepareRetry() does NOT run (didn't match)\n * ```\n */\n async prepareRetry(result: T, context: RetryContext<T>): Promise<void> {\n // Filter policies that have prepareRetry and match shouldRetry\n const matchingPolicies: RetryPolicy<T>[] = [];\n\n for (const policy of this.policies) {\n if (policy.prepareRetry !== undefined && (await policy.shouldRetry(result, context))) {\n matchingPolicies.push(policy);\n }\n }\n\n // Call prepareRetry on all matching policies in parallel\n if (matchingPolicies.length > 0) {\n await Promise.all(\n matchingPolicies.map((policy) => policy.prepareRetry!(result, context))\n );\n }\n }\n\n /**\n * Returns all composed policies.\n * Useful for accessing or configuring individual policies after composition.\n */\n getPolicies(): RetryPolicy<T>[] {\n return this.policies;\n }\n\n /**\n * Helper to get a specific policy by type.\n * Useful for calling policy-specific methods after composition.\n *\n * @example\n * ```typescript\n * const csrfPolicy = composedPolicy.getPolicyByType(CsrfTokenRetryPolicy);\n * csrfPolicy?.setRequestContext(mutableRequest);\n * ```\n */\n getPolicyByType<P extends RetryPolicy<T>>(\n policyType: new (...args: any[]) => P\n ): P | undefined {\n return this.policies.find((policy) => policy instanceof policyType) as P | undefined;\n }\n}\n","import { RetryService } from './retry-service';\nimport type { RetryPolicy } from './retry-policy';\nimport type { NamedService, ServiceDescriptor } from '@conduit-client/utils';\n\nexport type NamedRetryService = NamedService<'retry', RetryService<any>>;\nexport { RetryService, type RetryContext } from './retry-service';\nexport { RetryPolicy } from './retry-policy';\nexport { ComposedRetryPolicy } from './composed-retry-policy';\nexport type RetryServiceDescriptor = ServiceDescriptor<RetryService<any>, 'retry', '1.0'>;\n\nexport function buildServiceDescriptor<T>(\n defaultRetryPolicy: RetryPolicy<T>\n): RetryServiceDescriptor {\n return {\n version: '1.0' as const,\n service: new RetryService<T>(defaultRetryPolicy),\n type: 'retry',\n };\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduit-client/service-retry",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Luvio Retry Service definition",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"watch": "npm run build --watch"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@conduit-client/utils": "3.
|
|
34
|
+
"@conduit-client/utils": "3.3.0"
|
|
35
35
|
},
|
|
36
36
|
"volta": {
|
|
37
37
|
"extends": "../../../../package.json"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"size-limit": [
|
|
40
40
|
{
|
|
41
41
|
"path": "./dist/v1/index.js",
|
|
42
|
-
"limit": "
|
|
42
|
+
"limit": "1.4 kB"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|