@private.me/xbind 3.0.1 → 3.0.2
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/README.md +55 -14
- package/dist-standalone/_deps/mldsa-wasm/dist/mldsa.js +1920 -1
- package/dist-standalone/_deps/shared/cjs/errors.js +729 -1
- package/dist-standalone/_deps/shared/cjs/index.js +463 -1
- package/dist-standalone/_deps/shared/cjs/types.js +315 -1
- package/dist-standalone/_deps/shared/errors.js +244 -1
- package/dist-standalone/_deps/shared/index.js +72 -1
- package/dist-standalone/_deps/shared/types.js +86 -1
- package/dist-standalone/_deps/ux-helpers/cjs/errors.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/index.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/pagination.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/progress.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/search.js +1 -1
- package/dist-standalone/_deps/ux-helpers/cjs/types.js +1 -1
- package/dist-standalone/_deps/ux-helpers/errors.js +1 -1
- package/dist-standalone/_deps/ux-helpers/index.js +1 -1
- package/dist-standalone/_deps/ux-helpers/pagination.js +1 -1
- package/dist-standalone/_deps/ux-helpers/progress.js +1 -1
- package/dist-standalone/_deps/ux-helpers/search.js +1 -1
- package/dist-standalone/_deps/xchange/auto-accept.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/auto-accept.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/errors.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/index.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/invite-client.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/lazy-init.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/trust-integration.js +1 -1
- package/dist-standalone/_deps/xchange/cjs/xchange.js +1 -1
- package/dist-standalone/_deps/xchange/errors.js +1 -1
- package/dist-standalone/_deps/xchange/index.js +1 -1
- package/dist-standalone/_deps/xchange/invite-client.js +1 -1
- package/dist-standalone/_deps/xchange/lazy-init.js +1 -1
- package/dist-standalone/_deps/xchange/trust-integration.js +1 -1
- package/dist-standalone/_deps/xchange/xchange.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/discovery.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/errors.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/index.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/registry.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/schema.js +1 -1
- package/dist-standalone/_deps/xregistry/cjs/types.js +1 -1
- package/dist-standalone/_deps/xregistry/discovery.js +1 -1
- package/dist-standalone/_deps/xregistry/errors.js +1 -1
- package/dist-standalone/_deps/xregistry/index.js +1 -1
- package/dist-standalone/_deps/xregistry/registry.js +1 -1
- package/dist-standalone/_deps/xregistry/schema.js +1 -1
- package/dist-standalone/_deps/xregistry/types.js +1 -1
- package/dist-standalone/agent-call.js +659 -1
- package/dist-standalone/agent-sdk.js +328 -1
- package/dist-standalone/agent.js +1800 -1
- package/dist-standalone/approval.js +193 -1
- package/dist-standalone/async-iterators.js +382 -1
- package/dist-standalone/auth.js +219 -1
- package/dist-standalone/auto-accept.js +229 -1
- package/dist-standalone/backup-config.js +201 -1
- package/dist-standalone/backup.js +326 -1
- package/dist-standalone/batch-operations.js +388 -1
- package/dist-standalone/cancellation.js +477 -1
- package/dist-standalone/checkpoint.js +186 -1
- package/dist-standalone/circuit-breaker.js +468 -1
- package/dist-standalone/cjs/agent-call.js +701 -1
- package/dist-standalone/cjs/agent-sdk.js +332 -1
- package/dist-standalone/cjs/agent.js +1837 -1
- package/dist-standalone/cjs/approval.js +199 -1
- package/dist-standalone/cjs/async-iterators.js +392 -1
- package/dist-standalone/cjs/auth.js +225 -1
- package/dist-standalone/cjs/auto-accept.js +233 -1
- package/dist-standalone/cjs/backup-config.js +207 -1
- package/dist-standalone/cjs/backup.js +330 -1
- package/dist-standalone/cjs/batch-operations.js +397 -1
- package/dist-standalone/cjs/cancellation.js +490 -1
- package/dist-standalone/cjs/checkpoint.js +193 -1
- package/dist-standalone/cjs/circuit-breaker.js +476 -1
- package/dist-standalone/cjs/cli/init.js +492 -1
- package/dist-standalone/cjs/config-validation.js +522 -1
- package/dist-standalone/cjs/connect.js +312 -1
- package/dist-standalone/cjs/connection-pool.js +506 -1
- package/dist-standalone/cjs/correlation-id.js +339 -1
- package/dist-standalone/cjs/crypto-utils.js +176 -1
- package/dist-standalone/cjs/debug-mode.js +534 -1
- package/dist-standalone/cjs/did-document.js +101 -1
- package/dist-standalone/cjs/did-privateme.js +130 -1
- package/dist-standalone/cjs/did-web.js +201 -1
- package/dist-standalone/cjs/discovery.js +462 -1
- package/dist-standalone/cjs/dual-mode.js +251 -1
- package/dist-standalone/cjs/email-templates.js +313 -1
- package/dist-standalone/cjs/email-transport.js +239 -1
- package/dist-standalone/cjs/envelope.js +538 -1
- package/dist-standalone/cjs/errors.js +913 -1
- package/dist-standalone/cjs/event-emitter.js +461 -1
- package/dist-standalone/cjs/gateway-state.js +55 -1
- package/dist-standalone/cjs/gateway-transport.js +120 -1
- package/dist-standalone/cjs/graceful-degradation.js +403 -1
- package/dist-standalone/cjs/guardrails.js +223 -1
- package/dist-standalone/cjs/health-check.js +336 -1
- package/dist-standalone/cjs/http-compat.js +272 -1
- package/dist-standalone/cjs/http-status-map.js +571 -1
- package/dist-standalone/cjs/identity.js +645 -1
- package/dist-standalone/cjs/index.js +406 -1
- package/dist-standalone/cjs/invitation.js +421 -1
- package/dist-standalone/cjs/invite.js +328 -1
- package/dist-standalone/cjs/key-agreement.js +335 -1
- package/dist-standalone/cjs/lazy-init.js +300 -1
- package/dist-standalone/cjs/logger.js +291 -1
- package/dist-standalone/cjs/mdns-discovery.js +202 -1
- package/dist-standalone/cjs/nonce-store.js +80 -1
- package/dist-standalone/cjs/pairing-manager.js +223 -1
- package/dist-standalone/cjs/plugin-system.js +264 -1
- package/dist-standalone/cjs/plugins/logging.js +168 -1
- package/dist-standalone/cjs/plugins/metrics.js +181 -1
- package/dist-standalone/cjs/plugins/validation.js +302 -1
- package/dist-standalone/cjs/policy.js +320 -1
- package/dist-standalone/cjs/progress-callbacks.js +583 -1
- package/dist-standalone/cjs/redis-nonce-store.js +76 -1
- package/dist-standalone/cjs/registry-middleware.js +50 -1
- package/dist-standalone/cjs/retry-strategies.js +544 -1
- package/dist-standalone/cjs/retry-transport.js +102 -1
- package/dist-standalone/cjs/runtime/browser.js +533 -1
- package/dist-standalone/cjs/runtime/edge.js +526 -1
- package/dist-standalone/cjs/runtime/react-native.js +394 -1
- package/dist-standalone/cjs/security-policy.js +245 -1
- package/dist-standalone/cjs/serialization.js +1040 -1
- package/dist-standalone/cjs/split-channel.js +225 -1
- package/dist-standalone/cjs/subscription-proof.js +230 -1
- package/dist-standalone/cjs/succession.js +148 -1
- package/dist-standalone/cjs/timeouts.js +412 -1
- package/dist-standalone/cjs/trace-context.js +424 -1
- package/dist-standalone/cjs/trace-spans.js +495 -1
- package/dist-standalone/cjs/transport.js +63 -1
- package/dist-standalone/cjs/trust-registry.js +991 -1
- package/dist-standalone/cjs/types/error-response.js +56 -1
- package/dist-standalone/cjs/vault-auth.js +178 -1
- package/dist-standalone/cjs/vault-store-loader.js +194 -1
- package/dist-standalone/cjs/verify.js +25 -1
- package/dist-standalone/cjs/version-info.js +543 -1
- package/dist-standalone/cjs/xfetch.js +340 -1
- package/dist-standalone/cli/init.js +455 -1
- package/dist-standalone/cli/setup.js +514 -1
- package/dist-standalone/cli/types.js +27 -1
- package/dist-standalone/cli/xbind.js +148 -1
- package/dist-standalone/config-validation.js +513 -1
- package/dist-standalone/connect.js +274 -1
- package/dist-standalone/connection-pool.js +500 -1
- package/dist-standalone/correlation-id.js +326 -1
- package/dist-standalone/crypto-utils.js +157 -1
- package/dist-standalone/debug-mode.js +510 -1
- package/dist-standalone/did-document.js +96 -1
- package/dist-standalone/did-privateme.js +121 -1
- package/dist-standalone/did-web.js +196 -1
- package/dist-standalone/discovery.js +458 -1
- package/dist-standalone/dual-mode.js +247 -1
- package/dist-standalone/email-templates.js +309 -1
- package/dist-standalone/email-transport.js +232 -1
- package/dist-standalone/envelope.js +525 -1
- package/dist-standalone/errors.js +896 -1
- package/dist-standalone/event-emitter.js +456 -1
- package/dist-standalone/gateway-state.js +51 -1
- package/dist-standalone/gateway-transport.js +116 -1
- package/dist-standalone/graceful-degradation.js +396 -1
- package/dist-standalone/guardrails.js +216 -1
- package/dist-standalone/health-check.js +332 -1
- package/dist-standalone/http-compat.js +267 -1
- package/dist-standalone/http-status-map.js +561 -1
- package/dist-standalone/identity.js +619 -1
- package/dist-standalone/index.js +78 -1
- package/dist-standalone/invitation.js +415 -1
- package/dist-standalone/invite.js +324 -1
- package/dist-standalone/key-agreement.js +325 -1
- package/dist-standalone/lazy-init.js +295 -1
- package/dist-standalone/logger.js +285 -1
- package/dist-standalone/mdns-discovery.js +195 -1
- package/dist-standalone/nonce-store.js +76 -1
- package/dist-standalone/pairing-manager.js +219 -1
- package/dist-standalone/plugin-system.js +257 -1
- package/dist-standalone/plugins/logging.js +163 -1
- package/dist-standalone/plugins/metrics.js +176 -1
- package/dist-standalone/plugins/validation.js +297 -1
- package/dist-standalone/policy.js +315 -1
- package/dist-standalone/progress-callbacks.js +576 -1
- package/dist-standalone/redis-nonce-store.js +72 -1
- package/dist-standalone/registry-middleware.js +47 -1
- package/dist-standalone/retry-strategies.js +534 -1
- package/dist-standalone/retry-transport.js +98 -1
- package/dist-standalone/runtime/browser.js +516 -1
- package/dist-standalone/runtime/edge.js +511 -1
- package/dist-standalone/runtime/react-native.js +383 -1
- package/dist-standalone/security-policy.js +239 -1
- package/dist-standalone/serialization.js +1031 -1
- package/dist-standalone/split-channel.js +219 -1
- package/dist-standalone/subscription-proof.js +224 -1
- package/dist-standalone/succession.js +142 -1
- package/dist-standalone/timeouts.js +398 -1
- package/dist-standalone/trace-context.js +414 -1
- package/dist-standalone/trace-spans.js +488 -1
- package/dist-standalone/transport.js +59 -1
- package/dist-standalone/trust-registry.js +950 -1
- package/dist-standalone/types/error-response.js +52 -1
- package/dist-standalone/vault-auth.js +174 -1
- package/dist-standalone/vault-store-loader.js +187 -1
- package/dist-standalone/verify.js +16 -1
- package/dist-standalone/version-info.js +530 -1
- package/dist-standalone/xfetch.js +335 -1
- package/package.json +4 -13
- package/share1.dat +0 -0
- package/dist-standalone/_deps/mldsa-wasm/LICENSE +0 -24
- package/dist-standalone/_deps/mldsa-wasm/package.json +0 -46
- package/dist-standalone/_deps/shared/cjs/package.json +0 -1
- package/dist-standalone/_deps/ux-helpers/cjs/package.json +0 -1
- package/dist-standalone/_deps/xchange/cjs/package.json +0 -1
- package/dist-standalone/_deps/xregistry/cjs/package.json +0 -1
- package/dist-standalone/cjs/package.json +0 -3
- package/dist-standalone/package.json +0 -10
|
@@ -1 +1,534 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module retry-strategies
|
|
3
|
+
* Configurable retry strategies for enhanced reliability.
|
|
4
|
+
*
|
|
5
|
+
* Provides flexible retry policies with exponential backoff, jitter, circuit breakers,
|
|
6
|
+
* and error-specific retry logic. Designed for production-grade fault tolerance.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RetryStrategy, CircuitBreaker } from '@private.me/xbind/retry-strategies';
|
|
11
|
+
*
|
|
12
|
+
* // Exponential backoff with circuit breaker
|
|
13
|
+
* const strategy = RetryStrategy.exponentialBackoff({
|
|
14
|
+
* maxAttempts: 5,
|
|
15
|
+
* initialDelayMs: 1000,
|
|
16
|
+
* maxDelayMs: 30000,
|
|
17
|
+
* multiplier: 2,
|
|
18
|
+
* jitter: true
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* const circuitBreaker = new CircuitBreaker({
|
|
22
|
+
* failureThreshold: 5,
|
|
23
|
+
* resetTimeoutMs: 60000
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { err } from"./_deps/shared/index.js";
|
|
28
|
+
/* ── Retry Strategy Implementations ── */
|
|
29
|
+
/**
|
|
30
|
+
* Exponential backoff retry strategy.
|
|
31
|
+
*
|
|
32
|
+
* Delay formula: min(initialDelay * multiplier^attempt, maxDelay) ± jitter
|
|
33
|
+
*
|
|
34
|
+
* Best for: Network requests, API calls, database operations.
|
|
35
|
+
* Avoids overwhelming failing services with exponentially increasing delays.
|
|
36
|
+
*/
|
|
37
|
+
export class ExponentialBackoffStrategy {
|
|
38
|
+
name = 'ExponentialBackoff';
|
|
39
|
+
maxAttempts;
|
|
40
|
+
initialDelayMs;
|
|
41
|
+
maxDelayMs;
|
|
42
|
+
multiplier;
|
|
43
|
+
jitter;
|
|
44
|
+
jitterFactor;
|
|
45
|
+
retryableErrors;
|
|
46
|
+
constructor(config = {}) {
|
|
47
|
+
this.maxAttempts = config.maxAttempts ?? 3;
|
|
48
|
+
this.initialDelayMs = config.initialDelayMs ?? 1000;
|
|
49
|
+
this.maxDelayMs = config.maxDelayMs ?? 30000;
|
|
50
|
+
this.multiplier = config.multiplier ?? 2;
|
|
51
|
+
this.jitter = config.jitter ?? true;
|
|
52
|
+
this.jitterFactor = config.jitterFactor ?? 0.2;
|
|
53
|
+
this.retryableErrors = config.retryableErrors
|
|
54
|
+
? new Set(config.retryableErrors)
|
|
55
|
+
: new Set(['SEND_FAILED', 'NETWORK_ERROR', 'RECIPIENT_UNREACHABLE', 'TIMEOUT']);
|
|
56
|
+
}
|
|
57
|
+
shouldRetry(context) {
|
|
58
|
+
// Check if we've exceeded max attempts
|
|
59
|
+
if (context.attempt >= this.maxAttempts) {
|
|
60
|
+
return {
|
|
61
|
+
shouldRetry: false,
|
|
62
|
+
reason: `Maximum attempts (${this.maxAttempts}) exceeded`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Check if error is retryable
|
|
66
|
+
if (!this.retryableErrors.has(context.error)) {
|
|
67
|
+
return {
|
|
68
|
+
shouldRetry: false,
|
|
69
|
+
reason: `Error type '${context.error}' is not retryable`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Calculate exponential backoff delay
|
|
73
|
+
const baseDelay = this.initialDelayMs * Math.pow(this.multiplier, context.attempt);
|
|
74
|
+
const cappedDelay = Math.min(baseDelay, this.maxDelayMs);
|
|
75
|
+
// Apply jitter if enabled
|
|
76
|
+
const finalDelay = this.jitter ? this.applyJitter(cappedDelay) : cappedDelay;
|
|
77
|
+
return {
|
|
78
|
+
shouldRetry: true,
|
|
79
|
+
delayMs: Math.round(finalDelay),
|
|
80
|
+
reason: `Attempt ${context.attempt + 1}/${this.maxAttempts}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
applyJitter(delayMs) {
|
|
84
|
+
// Use crypto.getRandomValues for secure random jitter (OWASP-compliant)
|
|
85
|
+
const randomArray = new Uint32Array(1);
|
|
86
|
+
crypto.getRandomValues(randomArray);
|
|
87
|
+
const random = randomArray[0] / 0xffffffff; // Normalize to [0, 1]
|
|
88
|
+
// Apply jitter: delay ± (delay * jitterFactor)
|
|
89
|
+
const jitterRange = delayMs * this.jitterFactor;
|
|
90
|
+
const jitter = (random * 2 - 1) * jitterRange; // Range: [-jitterRange, +jitterRange]
|
|
91
|
+
return delayMs + jitter;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Linear backoff retry strategy.
|
|
96
|
+
*
|
|
97
|
+
* Delay formula: min(initialDelay + attempt * increment, maxDelay) ± jitter
|
|
98
|
+
*
|
|
99
|
+
* Best for: Rate-limited APIs, gradual load recovery.
|
|
100
|
+
* More predictable than exponential, suitable when service recovery is linear.
|
|
101
|
+
*/
|
|
102
|
+
export class LinearBackoffStrategy {
|
|
103
|
+
name = 'LinearBackoff';
|
|
104
|
+
maxAttempts;
|
|
105
|
+
delayIncrementMs;
|
|
106
|
+
initialDelayMs;
|
|
107
|
+
maxDelayMs;
|
|
108
|
+
jitter;
|
|
109
|
+
jitterFactor;
|
|
110
|
+
retryableErrors;
|
|
111
|
+
constructor(config = {}) {
|
|
112
|
+
this.maxAttempts = config.maxAttempts ?? 3;
|
|
113
|
+
this.delayIncrementMs = config.delayIncrementMs ?? 1000;
|
|
114
|
+
this.initialDelayMs = config.initialDelayMs ?? 1000;
|
|
115
|
+
this.maxDelayMs = config.maxDelayMs ?? 30000;
|
|
116
|
+
this.jitter = config.jitter ?? true;
|
|
117
|
+
this.jitterFactor = config.jitterFactor ?? 0.1;
|
|
118
|
+
this.retryableErrors = config.retryableErrors
|
|
119
|
+
? new Set(config.retryableErrors)
|
|
120
|
+
: new Set(['SEND_FAILED', 'NETWORK_ERROR', 'RECIPIENT_UNREACHABLE', 'TIMEOUT']);
|
|
121
|
+
}
|
|
122
|
+
shouldRetry(context) {
|
|
123
|
+
if (context.attempt >= this.maxAttempts) {
|
|
124
|
+
return {
|
|
125
|
+
shouldRetry: false,
|
|
126
|
+
reason: `Maximum attempts (${this.maxAttempts}) exceeded`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!this.retryableErrors.has(context.error)) {
|
|
130
|
+
return {
|
|
131
|
+
shouldRetry: false,
|
|
132
|
+
reason: `Error type '${context.error}' is not retryable`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Calculate linear backoff delay
|
|
136
|
+
const baseDelay = this.initialDelayMs + context.attempt * this.delayIncrementMs;
|
|
137
|
+
const cappedDelay = Math.min(baseDelay, this.maxDelayMs);
|
|
138
|
+
// Apply jitter if enabled
|
|
139
|
+
const finalDelay = this.jitter ? this.applyJitter(cappedDelay) : cappedDelay;
|
|
140
|
+
return {
|
|
141
|
+
shouldRetry: true,
|
|
142
|
+
delayMs: Math.round(finalDelay),
|
|
143
|
+
reason: `Attempt ${context.attempt + 1}/${this.maxAttempts}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
applyJitter(delayMs) {
|
|
147
|
+
const randomArray = new Uint32Array(1);
|
|
148
|
+
crypto.getRandomValues(randomArray);
|
|
149
|
+
const random = randomArray[0] / 0xffffffff;
|
|
150
|
+
const jitterRange = delayMs * this.jitterFactor;
|
|
151
|
+
const jitter = (random * 2 - 1) * jitterRange;
|
|
152
|
+
return delayMs + jitter;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fixed delay retry strategy.
|
|
157
|
+
*
|
|
158
|
+
* Delay formula: delayMs ± jitter (constant for all attempts)
|
|
159
|
+
*
|
|
160
|
+
* Best for: Simple retry scenarios, testing.
|
|
161
|
+
* Predictable timing, no backoff complexity.
|
|
162
|
+
*/
|
|
163
|
+
export class FixedDelayStrategy {
|
|
164
|
+
name = 'FixedDelay';
|
|
165
|
+
maxAttempts;
|
|
166
|
+
delayMs;
|
|
167
|
+
jitter;
|
|
168
|
+
jitterFactor;
|
|
169
|
+
retryableErrors;
|
|
170
|
+
constructor(config = {}) {
|
|
171
|
+
this.maxAttempts = config.maxAttempts ?? 3;
|
|
172
|
+
this.delayMs = config.delayMs ?? 1000;
|
|
173
|
+
this.jitter = config.jitter ?? false;
|
|
174
|
+
this.jitterFactor = config.jitterFactor ?? 0.1;
|
|
175
|
+
this.retryableErrors = config.retryableErrors
|
|
176
|
+
? new Set(config.retryableErrors)
|
|
177
|
+
: new Set(['SEND_FAILED', 'NETWORK_ERROR', 'RECIPIENT_UNREACHABLE', 'TIMEOUT']);
|
|
178
|
+
}
|
|
179
|
+
shouldRetry(context) {
|
|
180
|
+
if (context.attempt >= this.maxAttempts) {
|
|
181
|
+
return {
|
|
182
|
+
shouldRetry: false,
|
|
183
|
+
reason: `Maximum attempts (${this.maxAttempts}) exceeded`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!this.retryableErrors.has(context.error)) {
|
|
187
|
+
return {
|
|
188
|
+
shouldRetry: false,
|
|
189
|
+
reason: `Error type '${context.error}' is not retryable`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const finalDelay = this.jitter ? this.applyJitter(this.delayMs) : this.delayMs;
|
|
193
|
+
return {
|
|
194
|
+
shouldRetry: true,
|
|
195
|
+
delayMs: Math.round(finalDelay),
|
|
196
|
+
reason: `Attempt ${context.attempt + 1}/${this.maxAttempts}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
applyJitter(delayMs) {
|
|
200
|
+
const randomArray = new Uint32Array(1);
|
|
201
|
+
crypto.getRandomValues(randomArray);
|
|
202
|
+
const random = randomArray[0] / 0xffffffff;
|
|
203
|
+
const jitterRange = delayMs * this.jitterFactor;
|
|
204
|
+
const jitter = (random * 2 - 1) * jitterRange;
|
|
205
|
+
return delayMs + jitter;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* No retry strategy (fail immediately).
|
|
210
|
+
*
|
|
211
|
+
* Best for: Operations that must succeed on first try, testing.
|
|
212
|
+
*/
|
|
213
|
+
export class NoRetryStrategy {
|
|
214
|
+
name = 'NoRetry';
|
|
215
|
+
shouldRetry(_context) {
|
|
216
|
+
return {
|
|
217
|
+
shouldRetry: false,
|
|
218
|
+
reason: 'No retry policy - fail immediately',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/* ── Circuit Breaker ── */
|
|
223
|
+
/**
|
|
224
|
+
* Circuit breaker for preventing cascading failures.
|
|
225
|
+
*
|
|
226
|
+
* States:
|
|
227
|
+
* - CLOSED: Normal operation, requests pass through
|
|
228
|
+
* - OPEN: Circuit tripped, requests fail immediately
|
|
229
|
+
* - HALF_OPEN: Testing recovery, limited requests allowed
|
|
230
|
+
*
|
|
231
|
+
* Best practices:
|
|
232
|
+
* - Use with retry strategies for comprehensive fault tolerance
|
|
233
|
+
* - Monitor circuit state changes for alerting
|
|
234
|
+
* - Tune thresholds based on service characteristics
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const breaker = new CircuitBreaker({
|
|
239
|
+
* failureThreshold: 5,
|
|
240
|
+
* resetTimeoutMs: 60000,
|
|
241
|
+
* successThreshold: 2
|
|
242
|
+
* });
|
|
243
|
+
*
|
|
244
|
+
* // Check before operation
|
|
245
|
+
* if (breaker.allowRequest()) {
|
|
246
|
+
* const result = await operation();
|
|
247
|
+
* if (result.ok) {
|
|
248
|
+
* breaker.recordSuccess();
|
|
249
|
+
* } else {
|
|
250
|
+
* breaker.recordFailure();
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export class CircuitBreaker {
|
|
256
|
+
state = 'CLOSED';
|
|
257
|
+
consecutiveFailures = 0;
|
|
258
|
+
consecutiveSuccesses = 0;
|
|
259
|
+
openedAt;
|
|
260
|
+
totalRequests = 0;
|
|
261
|
+
totalFailures = 0;
|
|
262
|
+
totalSuccesses = 0;
|
|
263
|
+
failureTimestamps = [];
|
|
264
|
+
failureThreshold;
|
|
265
|
+
resetTimeoutMs;
|
|
266
|
+
successThreshold;
|
|
267
|
+
windowMs;
|
|
268
|
+
constructor(config = {}) {
|
|
269
|
+
this.failureThreshold = config.failureThreshold ?? 5;
|
|
270
|
+
this.resetTimeoutMs = config.resetTimeoutMs ?? 60000;
|
|
271
|
+
this.successThreshold = config.successThreshold ?? 2;
|
|
272
|
+
this.windowMs = config.windowMs ?? 60000;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check if request should be allowed through circuit.
|
|
276
|
+
*
|
|
277
|
+
* @returns True if request should proceed, false if circuit is open
|
|
278
|
+
*/
|
|
279
|
+
allowRequest() {
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
// Clean up old failure timestamps outside the window
|
|
282
|
+
this.failureTimestamps = this.failureTimestamps.filter(timestamp => now - timestamp < this.windowMs);
|
|
283
|
+
// CLOSED: Allow all requests
|
|
284
|
+
if (this.state === 'CLOSED') {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
// OPEN: Check if reset timeout has elapsed
|
|
288
|
+
if (this.state === 'OPEN') {
|
|
289
|
+
if (this.openedAt && now - this.openedAt >= this.resetTimeoutMs) {
|
|
290
|
+
// Transition to HALF_OPEN
|
|
291
|
+
this.state = 'HALF_OPEN';
|
|
292
|
+
this.consecutiveSuccesses = 0;
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false; // Circuit still open
|
|
296
|
+
}
|
|
297
|
+
// HALF_OPEN: Allow limited requests to test recovery
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Record a successful operation.
|
|
302
|
+
*/
|
|
303
|
+
recordSuccess() {
|
|
304
|
+
this.totalRequests++;
|
|
305
|
+
this.totalSuccesses++;
|
|
306
|
+
this.consecutiveFailures = 0;
|
|
307
|
+
if (this.state === 'HALF_OPEN') {
|
|
308
|
+
this.consecutiveSuccesses++;
|
|
309
|
+
// Enough successes to close circuit
|
|
310
|
+
if (this.consecutiveSuccesses >= this.successThreshold) {
|
|
311
|
+
this.state = 'CLOSED';
|
|
312
|
+
this.consecutiveSuccesses = 0;
|
|
313
|
+
this.openedAt = undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Record a failed operation.
|
|
319
|
+
*/
|
|
320
|
+
recordFailure() {
|
|
321
|
+
this.totalRequests++;
|
|
322
|
+
this.totalFailures++;
|
|
323
|
+
this.consecutiveFailures++;
|
|
324
|
+
this.consecutiveSuccesses = 0;
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
this.failureTimestamps.push(now);
|
|
327
|
+
// Trip circuit if threshold exceeded
|
|
328
|
+
if (this.state === 'CLOSED' || this.state === 'HALF_OPEN') {
|
|
329
|
+
// Count failures within the time window
|
|
330
|
+
const recentFailures = this.failureTimestamps.filter(timestamp => now - timestamp < this.windowMs).length;
|
|
331
|
+
if (recentFailures >= this.failureThreshold) {
|
|
332
|
+
this.state = 'OPEN';
|
|
333
|
+
this.openedAt = now;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get current circuit breaker statistics.
|
|
339
|
+
*/
|
|
340
|
+
getStats() {
|
|
341
|
+
return {
|
|
342
|
+
state: this.state,
|
|
343
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
344
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
345
|
+
openedAt: this.openedAt,
|
|
346
|
+
totalRequests: this.totalRequests,
|
|
347
|
+
totalFailures: this.totalFailures,
|
|
348
|
+
totalSuccesses: this.totalSuccesses,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Reset circuit breaker to initial state.
|
|
353
|
+
*/
|
|
354
|
+
reset() {
|
|
355
|
+
this.state = 'CLOSED';
|
|
356
|
+
this.consecutiveFailures = 0;
|
|
357
|
+
this.consecutiveSuccesses = 0;
|
|
358
|
+
this.openedAt = undefined;
|
|
359
|
+
this.totalRequests = 0;
|
|
360
|
+
this.totalFailures = 0;
|
|
361
|
+
this.totalSuccesses = 0;
|
|
362
|
+
this.failureTimestamps = [];
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Force circuit to open (for testing or manual intervention).
|
|
366
|
+
*/
|
|
367
|
+
forceOpen() {
|
|
368
|
+
this.state = 'OPEN';
|
|
369
|
+
this.openedAt = Date.now();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Force circuit to close (for testing or manual recovery).
|
|
373
|
+
*/
|
|
374
|
+
forceClose() {
|
|
375
|
+
this.state = 'CLOSED';
|
|
376
|
+
this.consecutiveFailures = 0;
|
|
377
|
+
this.consecutiveSuccesses = 0;
|
|
378
|
+
this.openedAt = undefined;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/* ── Factory Functions ── */
|
|
382
|
+
/**
|
|
383
|
+
* Factory for creating retry strategies with common configurations.
|
|
384
|
+
*/
|
|
385
|
+
export class RetryStrategy {
|
|
386
|
+
/**
|
|
387
|
+
* Create exponential backoff strategy.
|
|
388
|
+
*
|
|
389
|
+
* @param config - Configuration options
|
|
390
|
+
* @returns Exponential backoff strategy
|
|
391
|
+
*/
|
|
392
|
+
static exponentialBackoff(config) {
|
|
393
|
+
return new ExponentialBackoffStrategy(config);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Create linear backoff strategy.
|
|
397
|
+
*
|
|
398
|
+
* @param config - Configuration options
|
|
399
|
+
* @returns Linear backoff strategy
|
|
400
|
+
*/
|
|
401
|
+
static linearBackoff(config) {
|
|
402
|
+
return new LinearBackoffStrategy(config);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Create fixed delay strategy.
|
|
406
|
+
*
|
|
407
|
+
* @param config - Configuration options
|
|
408
|
+
* @returns Fixed delay strategy
|
|
409
|
+
*/
|
|
410
|
+
static fixedDelay(config) {
|
|
411
|
+
return new FixedDelayStrategy(config);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Create no retry strategy (fail immediately).
|
|
415
|
+
*
|
|
416
|
+
* @returns No retry strategy
|
|
417
|
+
*/
|
|
418
|
+
static noRetry() {
|
|
419
|
+
return new NoRetryStrategy();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Create aggressive retry strategy for critical operations.
|
|
423
|
+
*
|
|
424
|
+
* Config: 5 attempts, 500ms initial, max 10s, exponential backoff.
|
|
425
|
+
*
|
|
426
|
+
* @returns Aggressive retry strategy
|
|
427
|
+
*/
|
|
428
|
+
static aggressive() {
|
|
429
|
+
return new ExponentialBackoffStrategy({
|
|
430
|
+
maxAttempts: 5,
|
|
431
|
+
initialDelayMs: 500,
|
|
432
|
+
maxDelayMs: 10000,
|
|
433
|
+
multiplier: 2,
|
|
434
|
+
jitter: true,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Create conservative retry strategy for non-critical operations.
|
|
439
|
+
*
|
|
440
|
+
* Config: 2 attempts, 2s initial, max 5s, linear backoff.
|
|
441
|
+
*
|
|
442
|
+
* @returns Conservative retry strategy
|
|
443
|
+
*/
|
|
444
|
+
static conservative() {
|
|
445
|
+
return new LinearBackoffStrategy({
|
|
446
|
+
maxAttempts: 2,
|
|
447
|
+
initialDelayMs: 2000,
|
|
448
|
+
delayIncrementMs: 1000,
|
|
449
|
+
maxDelayMs: 5000,
|
|
450
|
+
jitter: true,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Create default retry strategy (balanced).
|
|
455
|
+
*
|
|
456
|
+
* Config: 3 attempts, 1s initial, max 30s, exponential backoff.
|
|
457
|
+
*
|
|
458
|
+
* @returns Default retry strategy
|
|
459
|
+
*/
|
|
460
|
+
static default() {
|
|
461
|
+
return new ExponentialBackoffStrategy({
|
|
462
|
+
maxAttempts: 3,
|
|
463
|
+
initialDelayMs: 1000,
|
|
464
|
+
maxDelayMs: 30000,
|
|
465
|
+
multiplier: 2,
|
|
466
|
+
jitter: true,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/* ── Retry Executor ── */
|
|
471
|
+
/**
|
|
472
|
+
* Execute an operation with retry strategy and optional circuit breaker.
|
|
473
|
+
*
|
|
474
|
+
* @param operation - Async operation to execute
|
|
475
|
+
* @param strategy - Retry strategy to use
|
|
476
|
+
* @param circuitBreaker - Optional circuit breaker
|
|
477
|
+
* @returns Result of the operation
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```typescript
|
|
481
|
+
* const result = await executeWithRetry(
|
|
482
|
+
* async () => transport.send(envelope, did),
|
|
483
|
+
* RetryStrategy.exponentialBackoff(),
|
|
484
|
+
* circuitBreaker
|
|
485
|
+
* );
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
export async function executeWithRetry(operation, strategy, circuitBreaker) {
|
|
489
|
+
const startTime = Date.now();
|
|
490
|
+
let attempt = 0;
|
|
491
|
+
let lastError = 'NETWORK_ERROR'; // Default error
|
|
492
|
+
while (true) {
|
|
493
|
+
// Check circuit breaker
|
|
494
|
+
if (circuitBreaker && !circuitBreaker.allowRequest()) {
|
|
495
|
+
return err('SEND_FAILED');
|
|
496
|
+
}
|
|
497
|
+
// Execute operation
|
|
498
|
+
const result = await operation();
|
|
499
|
+
if (result.ok) {
|
|
500
|
+
// Success - record and return
|
|
501
|
+
circuitBreaker?.recordSuccess();
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
// Failure - record error
|
|
505
|
+
lastError = result.error;
|
|
506
|
+
circuitBreaker?.recordFailure();
|
|
507
|
+
// Check retry decision
|
|
508
|
+
const elapsedMs = Date.now() - startTime;
|
|
509
|
+
const context = {
|
|
510
|
+
attempt,
|
|
511
|
+
error: lastError,
|
|
512
|
+
startTime,
|
|
513
|
+
elapsedMs,
|
|
514
|
+
};
|
|
515
|
+
const decision = strategy.shouldRetry(context);
|
|
516
|
+
if (!decision.shouldRetry) {
|
|
517
|
+
// No more retries - return last error
|
|
518
|
+
return err(lastError);
|
|
519
|
+
}
|
|
520
|
+
// Wait before retry
|
|
521
|
+
if (decision.delayMs && decision.delayMs > 0) {
|
|
522
|
+
await sleep(decision.delayMs);
|
|
523
|
+
}
|
|
524
|
+
attempt++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Sleep for specified milliseconds.
|
|
529
|
+
*
|
|
530
|
+
* @param ms - Duration in milliseconds
|
|
531
|
+
*/
|
|
532
|
+
function sleep(ms) {
|
|
533
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
534
|
+
}
|
|
@@ -1 +1,98 @@
|
|
|
1
|
-
|
|
1
|
+
/* ── Implementation ── */
|
|
2
|
+
/**
|
|
3
|
+
* Decorator that adds exponential backoff retry logic to any transport adapter.
|
|
4
|
+
*
|
|
5
|
+
* Retry delays follow exponential backoff with jitter:
|
|
6
|
+
* - Formula: 2^attempt * baseDelay + jitter
|
|
7
|
+
* - Jitter: Math.random() * maxJitter * 2 - maxJitter
|
|
8
|
+
* - Default delays: 1s, 2s, 4s (with ±200ms jitter)
|
|
9
|
+
*
|
|
10
|
+
* Use case: Push notification delivery failures requiring automatic retry.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const transport = new RetryTransportAdapter(baseTransport, {
|
|
15
|
+
* maxRetries: 3,
|
|
16
|
+
* baseDelayMs: 1000,
|
|
17
|
+
* maxJitterMs: 200
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class RetryTransportAdapter {
|
|
22
|
+
inner;
|
|
23
|
+
maxRetries;
|
|
24
|
+
baseDelayMs;
|
|
25
|
+
maxJitterMs;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new RetryTransportAdapter wrapping an existing transport.
|
|
28
|
+
*
|
|
29
|
+
* @param inner - The transport adapter to wrap with retry logic
|
|
30
|
+
* @param options - Retry configuration options
|
|
31
|
+
*/
|
|
32
|
+
constructor(inner, options = {}) {
|
|
33
|
+
this.inner = inner;
|
|
34
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
35
|
+
this.baseDelayMs = options.baseDelayMs ?? 1000;
|
|
36
|
+
this.maxJitterMs = options.maxJitterMs ?? 200;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Send an envelope with exponential backoff retry logic.
|
|
40
|
+
*
|
|
41
|
+
* Retries on all error types (SEND_FAILED, NETWORK_ERROR, RECIPIENT_UNREACHABLE, TIMEOUT).
|
|
42
|
+
* Throws error after all retries are exhausted.
|
|
43
|
+
*
|
|
44
|
+
* @param envelope - The envelope to send
|
|
45
|
+
* @param recipientDid - The recipient's DID
|
|
46
|
+
* @returns Result with void on success, or TransportError on failure
|
|
47
|
+
* @throws Error if all retry attempts are exhausted
|
|
48
|
+
*/
|
|
49
|
+
async send(envelope, recipientDid) {
|
|
50
|
+
let lastError;
|
|
51
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
52
|
+
const result = await this.inner.send(envelope, recipientDid);
|
|
53
|
+
// Success - return immediately
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
// Store error for final throw
|
|
58
|
+
lastError = result.error;
|
|
59
|
+
// Don't delay after final attempt
|
|
60
|
+
if (attempt < this.maxRetries) {
|
|
61
|
+
// Exponential backoff: 2^attempt * baseDelay + jitter
|
|
62
|
+
const delay = Math.pow(2, attempt) * this.baseDelayMs;
|
|
63
|
+
// SAFETY: Using crypto.getRandomValues for OWASP-compliant secure random jitter
|
|
64
|
+
const jitterArray = new Uint32Array(1);
|
|
65
|
+
crypto.getRandomValues(jitterArray);
|
|
66
|
+
const jitter = (jitterArray[0] / 0xffffffff) * this.maxJitterMs * 2 -
|
|
67
|
+
this.maxJitterMs;
|
|
68
|
+
await this.sleep(delay + jitter);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// All retries exhausted - throw error with clear message
|
|
72
|
+
throw new Error(`Failed after ${this.maxRetries} retries: ${lastError ?? 'unknown error'}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Register a handler for incoming envelopes.
|
|
76
|
+
* Delegates directly to the inner transport.
|
|
77
|
+
*
|
|
78
|
+
* @param handler - The envelope handler function
|
|
79
|
+
*/
|
|
80
|
+
onReceive(handler) {
|
|
81
|
+
this.inner.onReceive(handler);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Shut down the transport.
|
|
85
|
+
* Delegates directly to the inner transport.
|
|
86
|
+
*/
|
|
87
|
+
dispose() {
|
|
88
|
+
this.inner.dispose();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sleep for a specified duration.
|
|
92
|
+
*
|
|
93
|
+
* @param ms - Duration in milliseconds
|
|
94
|
+
*/
|
|
95
|
+
sleep(ms) {
|
|
96
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
97
|
+
}
|
|
98
|
+
}
|