@thorprovider/adapters 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/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +1 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +271 -0
- package/README.md +634 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2524 -0
- package/dist/index.d.ts +2524 -0
- package/dist/index.js +8136 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8045 -0
- package/dist/index.mjs.map +1 -0
- package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
- package/docs/authentication.md +165 -0
- package/docs/capabilities-system.md +143 -0
- package/docs/category-customization.md +309 -0
- package/docs/fetch-reliability-solution.md +399 -0
- package/docs/getting-started.md +154 -0
- package/docs/multi-tenant-channel-scoping.md +225 -0
- package/docs/profile-management.md +147 -0
- package/docs/providers-overview.md +85 -0
- package/docs/services-overview.md +135 -0
- package/docs/update-documentation.md +113 -0
- package/package.json +43 -0
- package/publish.log +93 -0
- package/quick-reference.md +169 -0
- package/src/admin/AdminAuthService.ts +88 -0
- package/src/admin/AdminSessionService.ts +91 -0
- package/src/admin/MetricsService.ts +145 -0
- package/src/admin/index.ts +13 -0
- package/src/backend/BackendMetadataAdapter.ts +88 -0
- package/src/backend/factory.ts +107 -0
- package/src/backend/index.ts +12 -0
- package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
- package/src/core/errors.js +161 -0
- package/src/core/errors.ts +132 -0
- package/src/core/fetch-utils.js +240 -0
- package/src/core/fetch-utils.ts +227 -0
- package/src/core/index.ts +8 -0
- package/src/core/logger.js +63 -0
- package/src/core/logger.ts +37 -0
- package/src/factory/index.ts +102 -0
- package/src/index.ts +205 -0
- package/src/lib/money-transform.ts +80 -0
- package/src/metadata/channel-scope-validation.js +136 -0
- package/src/metadata/channel-scope-validation.ts +151 -0
- package/src/metadata/channel-scope.js +174 -0
- package/src/metadata/channel-scope.ts +215 -0
- package/src/metadata/index.js +31 -0
- package/src/metadata/index.ts +37 -0
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +22 -0
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.js +601 -0
- package/src/providers/medusa/auth.ts +496 -0
- package/src/providers/medusa/fulfillment-planner.ts +225 -0
- package/src/providers/medusa/index.js +2485 -0
- package/src/providers/medusa/index.ts +4239 -0
- package/src/providers/medusa/medusa.ts +8 -0
- package/src/providers/medusa/payment.js +265 -0
- package/src/providers/medusa/payment.ts +269 -0
- package/src/providers/medusa/transforms.js +505 -0
- package/src/providers/medusa/transforms.ts +625 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +102 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +270 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/resilience/README.md +299 -0
- package/src/resilience/circuit-breaker.ts +222 -0
- package/src/resilience/index.ts +10 -0
- package/src/types/capabilities.types.js +141 -0
- package/src/types/capabilities.types.ts +366 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider.js +9 -0
- package/src/types/provider.ts +9 -0
- package/src/types/ui-responses.ts +261 -0
- package/src/utils/city-coordinates.ts +40 -0
- package/src/utils/geolocation.ts +52 -0
- package/src/utils/payment-utils.js +152 -0
- package/src/utils/payment-utils.ts +203 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Resilience Patterns
|
|
2
|
+
|
|
3
|
+
**Framework-agnostic resilience patterns for robust backend integration.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 Overview
|
|
8
|
+
|
|
9
|
+
This module provides enterprise-grade resilience patterns for handling backend failures gracefully. All patterns are framework-agnostic and can be used in any JavaScript/TypeScript environment.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📦 Export
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { CircuitBreaker, backendCircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🔌 Circuit Breaker Pattern
|
|
22
|
+
|
|
23
|
+
The Circuit Breaker pattern prevents cascading failures when the backend is unavailable by failing fast after a threshold of consecutive failures.
|
|
24
|
+
|
|
25
|
+
### How It Works
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌──────────┐
|
|
29
|
+
│ CLOSED │ ◄─── Normal operation (requests pass through)
|
|
30
|
+
└────┬─────┘
|
|
31
|
+
│ 5 failures
|
|
32
|
+
↓
|
|
33
|
+
┌──────────┐
|
|
34
|
+
│ OPEN │ ◄─── Failing fast (requests rejected immediately)
|
|
35
|
+
└────┬─────┘
|
|
36
|
+
│ 60 seconds timeout
|
|
37
|
+
↓
|
|
38
|
+
┌──────────┐
|
|
39
|
+
│ HALF_OPEN│ ◄─── Testing recovery (1 request allowed)
|
|
40
|
+
└────┬─────┘
|
|
41
|
+
│ Success? → CLOSED
|
|
42
|
+
│ Failure? → OPEN
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### States
|
|
46
|
+
|
|
47
|
+
| State | Behavior | Purpose |
|
|
48
|
+
|-------|----------|---------|
|
|
49
|
+
| **CLOSED** | All requests pass through normally | Normal operation |
|
|
50
|
+
| **OPEN** | Requests fail immediately with fallback | Prevent hammering failed backend |
|
|
51
|
+
| **HALF_OPEN** | Single test request allowed | Check if backend recovered |
|
|
52
|
+
|
|
53
|
+
### Basic Usage
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { CircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
57
|
+
|
|
58
|
+
const breaker = new CircuitBreaker({
|
|
59
|
+
failureThreshold: 5, // Open after 5 failures
|
|
60
|
+
resetTimeout: 60000, // Try recovery after 1 minute
|
|
61
|
+
name: 'my-api'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// With fallback
|
|
65
|
+
const data = await breaker.execute(
|
|
66
|
+
() => fetch('/api/data'),
|
|
67
|
+
{ default: 'value' } // Fallback if circuit is open
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Without fallback (throws on failure)
|
|
71
|
+
try {
|
|
72
|
+
const result = await breaker.execute(() => apiCall());
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Circuit is open or API call failed
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Configuration Options
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface CircuitBreakerConfig {
|
|
82
|
+
/** Number of failures before opening circuit */
|
|
83
|
+
failureThreshold: number;
|
|
84
|
+
|
|
85
|
+
/** Time in ms to wait before attempting recovery */
|
|
86
|
+
resetTimeout: number;
|
|
87
|
+
|
|
88
|
+
/** Name for logging/监控 */
|
|
89
|
+
name?: string;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Monitoring
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Get current state
|
|
97
|
+
console.log(breaker.state); // 'CLOSED' | 'OPEN' | 'HALF_OPEN'
|
|
98
|
+
|
|
99
|
+
// Get statistics
|
|
100
|
+
const stats = breaker.stats;
|
|
101
|
+
console.log({
|
|
102
|
+
state: stats.state,
|
|
103
|
+
failureCount: stats.failureCount,
|
|
104
|
+
totalRequests: stats.totalRequests,
|
|
105
|
+
totalFailures: stats.totalFailures,
|
|
106
|
+
totalSuccesses: stats.totalSuccesses
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Manual reset (force recovery)
|
|
110
|
+
breaker.manualReset();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🌐 Global Backend Circuit Breaker
|
|
116
|
+
|
|
117
|
+
A pre-configured global instance for backend API calls:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
121
|
+
|
|
122
|
+
// Use in adapters
|
|
123
|
+
async function getProducts() {
|
|
124
|
+
return backendCircuitBreaker.execute(
|
|
125
|
+
() => fetch('/api/products'),
|
|
126
|
+
[] // Empty array fallback
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Configuration:**
|
|
132
|
+
- `failureThreshold`: 5 failures
|
|
133
|
+
- `resetTimeout`: 60,000ms (1 minute)
|
|
134
|
+
- `name`: 'backend-api'
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🎨 Usage Patterns
|
|
139
|
+
|
|
140
|
+
### Pattern 1: L2 Adapter with Graceful Degradation
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// packages/adapters/src/medusa/product-adapter.ts
|
|
144
|
+
import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
145
|
+
|
|
146
|
+
export async function getProducts(options: GetProductsOptions) {
|
|
147
|
+
return backendCircuitBreaker.execute(
|
|
148
|
+
() => medusaClient.products.list(options),
|
|
149
|
+
[] // Return empty array if backend is down
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Benefits:**
|
|
155
|
+
- ✅ Frontend stays functional with empty state
|
|
156
|
+
- ✅ User sees "No products" instead of error page
|
|
157
|
+
- ✅ Backend gets time to recover
|
|
158
|
+
|
|
159
|
+
### Pattern 2: L5 Server Component with Fallback UI
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// packages/core/app/products/page.tsx
|
|
163
|
+
import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
164
|
+
import { getProducts } from '@thorprovider/adapters/medusa';
|
|
165
|
+
|
|
166
|
+
export default async function ProductsPage() {
|
|
167
|
+
const products = await backendCircuitBreaker.execute(
|
|
168
|
+
() => getProducts(),
|
|
169
|
+
[] // Fallback to empty array
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (products.length === 0) {
|
|
173
|
+
return <EmptyState message="Products temporarily unavailable" />;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return <ProductsGrid products={products} />;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Pattern 3: Critical Operations (No Fallback)
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Critical operations should fail explicitly
|
|
184
|
+
async function completeCheckout(data: CheckoutData) {
|
|
185
|
+
try {
|
|
186
|
+
return await backendCircuitBreaker.execute(
|
|
187
|
+
() => medusaClient.carts.complete(data)
|
|
188
|
+
// NO fallback - we want this to fail if backend is down
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// Show error message to user
|
|
192
|
+
throw new Error('Checkout unavailable. Please try again later.');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 🧪 Testing
|
|
200
|
+
|
|
201
|
+
### Unit Test Example
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { CircuitBreaker } from '@thorprovider/adapters/resilience';
|
|
205
|
+
|
|
206
|
+
describe('CircuitBreaker', () => {
|
|
207
|
+
it('opens after threshold failures', async () => {
|
|
208
|
+
const breaker = new CircuitBreaker({
|
|
209
|
+
failureThreshold: 3,
|
|
210
|
+
resetTimeout: 1000,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
|
|
214
|
+
|
|
215
|
+
// 3 failures
|
|
216
|
+
await expect(breaker.execute(mockFn)).rejects.toThrow();
|
|
217
|
+
await expect(breaker.execute(mockFn)).rejects.toThrow();
|
|
218
|
+
await expect(breaker.execute(mockFn)).rejects.toThrow();
|
|
219
|
+
|
|
220
|
+
expect(breaker.state).toBe('OPEN');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('uses fallback when circuit is open', async () => {
|
|
224
|
+
const breaker = new CircuitBreaker({
|
|
225
|
+
failureThreshold: 1,
|
|
226
|
+
resetTimeout: 1000,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
|
|
230
|
+
|
|
231
|
+
// Open circuit
|
|
232
|
+
await breaker.execute(mockFn, 'fallback').catch(() => {});
|
|
233
|
+
|
|
234
|
+
// Should return fallback immediately
|
|
235
|
+
const result = await breaker.execute(mockFn, 'fallback');
|
|
236
|
+
expect(result).toBe('fallback');
|
|
237
|
+
expect(mockFn).toHaveBeenCalledTimes(1); // Not called again
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 📐 Architecture Decisions
|
|
245
|
+
|
|
246
|
+
### Why L2 (Adapters)?
|
|
247
|
+
|
|
248
|
+
1. **Framework Agnostic**: Works in Next.js, Remix, Vite, Node.js
|
|
249
|
+
2. **Reusable**: All starters can use the same circuit breaker
|
|
250
|
+
3. **No Dependencies**: Zero external dependencies (pure TypeScript)
|
|
251
|
+
4. **Type Safe**: Full TypeScript support
|
|
252
|
+
|
|
253
|
+
### Why Not L5 (Starters)?
|
|
254
|
+
|
|
255
|
+
- ❌ Not reusable across starters
|
|
256
|
+
- ❌ Mixed with framework-specific code
|
|
257
|
+
- ❌ Harder to test in isolation
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 🚀 Best Practices
|
|
262
|
+
|
|
263
|
+
### DO ✅
|
|
264
|
+
|
|
265
|
+
- Use for all backend API calls
|
|
266
|
+
- Provide sensible fallbacks for non-critical operations
|
|
267
|
+
- Monitor circuit breaker stats in production
|
|
268
|
+
- Use separate instances for different services
|
|
269
|
+
|
|
270
|
+
### DON'T ❌
|
|
271
|
+
|
|
272
|
+
- Don't use for client-side only operations
|
|
273
|
+
- Don't ignore OPEN state errors silently
|
|
274
|
+
- Don't set failureThreshold too low (causes flapping)
|
|
275
|
+
- Don't set resetTimeout too high (slow recovery)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 🔮 Future Enhancements
|
|
280
|
+
|
|
281
|
+
- [ ] **Retry Pattern**: Exponential backoff for transient failures
|
|
282
|
+
- [ ] **Bulkhead Pattern**: Isolate resource pools
|
|
283
|
+
- [ ] **Rate Limiting**: Prevent API abuse
|
|
284
|
+
- [ ] **Metrics Export**: Prometheus/Datadog integration
|
|
285
|
+
- [ ] **Health Check Integration**: Auto-open circuit on health check fail
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 📚 References
|
|
290
|
+
|
|
291
|
+
- [Martin Fowler - Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
|
|
292
|
+
- [Microsoft - Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
|
|
293
|
+
- [Netflix Hystrix](https://github.com/Netflix/Hystrix/wiki)
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
**Last Updated**: March 24, 2026
|
|
298
|
+
**Status**: ✅ Active
|
|
299
|
+
**Layer**: L2 (Adapters)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker Pattern Implementation
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures when the backend is unavailable.
|
|
5
|
+
* Implements the Circuit Breaker pattern as described by Martin Fowler.
|
|
6
|
+
*
|
|
7
|
+
* States:
|
|
8
|
+
* - CLOSED: Normal operation, requests pass through
|
|
9
|
+
* - OPEN: Too many failures, requests fail fast
|
|
10
|
+
* - HALF_OPEN: Testing if backend has recovered
|
|
11
|
+
*
|
|
12
|
+
* @see https://martinfowler.com/bliki/CircuitBreaker.html
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
16
|
+
|
|
17
|
+
export interface CircuitBreakerConfig {
|
|
18
|
+
/** Number of failures before opening circuit */
|
|
19
|
+
failureThreshold: number;
|
|
20
|
+
/** Time in ms to wait before attempting recovery */
|
|
21
|
+
resetTimeout: number;
|
|
22
|
+
/** Name for logging/monitoring */
|
|
23
|
+
name?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CircuitBreakerStats {
|
|
27
|
+
state: CircuitState;
|
|
28
|
+
failureCount: number;
|
|
29
|
+
lastFailureTime: number | null;
|
|
30
|
+
totalRequests: number;
|
|
31
|
+
totalFailures: number;
|
|
32
|
+
totalSuccesses: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Circuit Breaker Class
|
|
37
|
+
*
|
|
38
|
+
* Monitors backend calls and prevents cascading failures by
|
|
39
|
+
* failing fast when the backend is unhealthy.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const breaker = new CircuitBreaker({
|
|
44
|
+
* failureThreshold: 3,
|
|
45
|
+
* resetTimeout: 60000, // 1 minute
|
|
46
|
+
* name: 'backend-api'
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* const result = await breaker.execute(
|
|
50
|
+
* () => fetch('/api/data'),
|
|
51
|
+
* fallbackData
|
|
52
|
+
* );
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export class CircuitBreaker {
|
|
56
|
+
private failureCount = 0;
|
|
57
|
+
private lastFailureTime: number | null = null;
|
|
58
|
+
private totalRequests = 0;
|
|
59
|
+
private totalFailures = 0;
|
|
60
|
+
private totalSuccesses = 0;
|
|
61
|
+
|
|
62
|
+
constructor(private config: CircuitBreakerConfig) {
|
|
63
|
+
this.config = {
|
|
64
|
+
failureThreshold: config.failureThreshold || 5,
|
|
65
|
+
resetTimeout: config.resetTimeout || 60000, // 1 minute default
|
|
66
|
+
name: config.name || 'circuit-breaker',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get current circuit state
|
|
72
|
+
*/
|
|
73
|
+
get state(): CircuitState {
|
|
74
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
75
|
+
// Circuit is open (failing fast)
|
|
76
|
+
if (this.shouldAttemptReset()) {
|
|
77
|
+
return 'HALF_OPEN'; // Try to recover
|
|
78
|
+
}
|
|
79
|
+
return 'OPEN';
|
|
80
|
+
}
|
|
81
|
+
return 'CLOSED'; // Normal operation
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get circuit breaker statistics
|
|
86
|
+
*/
|
|
87
|
+
get stats(): CircuitBreakerStats {
|
|
88
|
+
return {
|
|
89
|
+
state: this.state,
|
|
90
|
+
failureCount: this.failureCount,
|
|
91
|
+
lastFailureTime: this.lastFailureTime,
|
|
92
|
+
totalRequests: this.totalRequests,
|
|
93
|
+
totalFailures: this.totalFailures,
|
|
94
|
+
totalSuccesses: this.totalSuccesses,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute a function with circuit breaker protection
|
|
100
|
+
*
|
|
101
|
+
* @param fn - Function to execute
|
|
102
|
+
* @param fallback - Fallback value if circuit is open
|
|
103
|
+
* @returns Result from fn or fallback
|
|
104
|
+
*/
|
|
105
|
+
async execute<T>(
|
|
106
|
+
fn: () => Promise<T>,
|
|
107
|
+
fallback?: T
|
|
108
|
+
): Promise<T> {
|
|
109
|
+
this.totalRequests++;
|
|
110
|
+
|
|
111
|
+
const currentState = this.state;
|
|
112
|
+
|
|
113
|
+
// If circuit is OPEN and we have a fallback, return it immediately
|
|
114
|
+
if (currentState === 'OPEN') {
|
|
115
|
+
this.log('Circuit is OPEN - failing fast');
|
|
116
|
+
if (fallback !== undefined) {
|
|
117
|
+
return fallback;
|
|
118
|
+
}
|
|
119
|
+
throw new CircuitBreakerOpenError(
|
|
120
|
+
`Circuit breaker "${this.config.name}" is OPEN - backend may be unavailable`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try to execute the function
|
|
125
|
+
try {
|
|
126
|
+
const result = await fn();
|
|
127
|
+
this.onSuccess();
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.onFailure();
|
|
131
|
+
|
|
132
|
+
// If we have a fallback, use it
|
|
133
|
+
if (fallback !== undefined) {
|
|
134
|
+
this.log('Returning fallback due to error:', error);
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Otherwise, re-throw the error
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if circuit should attempt to reset
|
|
145
|
+
*/
|
|
146
|
+
private shouldAttemptReset(): boolean {
|
|
147
|
+
if (!this.lastFailureTime) return false;
|
|
148
|
+
|
|
149
|
+
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
150
|
+
return timeSinceLastFailure >= this.config.resetTimeout;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Record a successful call
|
|
155
|
+
*/
|
|
156
|
+
private onSuccess(): void {
|
|
157
|
+
this.totalSuccesses++;
|
|
158
|
+
this.reset();
|
|
159
|
+
this.log('Request succeeded - circuit reset');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Record a failed call
|
|
164
|
+
*/
|
|
165
|
+
private onFailure(): void {
|
|
166
|
+
this.failureCount++;
|
|
167
|
+
this.totalFailures++;
|
|
168
|
+
this.lastFailureTime = Date.now();
|
|
169
|
+
|
|
170
|
+
if (this.state === 'OPEN') {
|
|
171
|
+
this.log(`Circuit OPENED after ${this.failureCount} failures`);
|
|
172
|
+
} else {
|
|
173
|
+
this.log(`Failure recorded (${this.failureCount}/${this.config.failureThreshold})`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reset circuit to CLOSED state
|
|
179
|
+
*/
|
|
180
|
+
private reset(): void {
|
|
181
|
+
this.failureCount = 0;
|
|
182
|
+
this.lastFailureTime = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Manually reset the circuit breaker
|
|
187
|
+
*/
|
|
188
|
+
public manualReset(): void {
|
|
189
|
+
this.reset();
|
|
190
|
+
this.log('Circuit manually reset');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Log messages (only in development)
|
|
195
|
+
*/
|
|
196
|
+
private log(...args: unknown[]): void {
|
|
197
|
+
if (process.env.NODE_ENV === 'development') {
|
|
198
|
+
console.log(`[CircuitBreaker:${this.config.name}]`, ...args);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Custom error for when circuit is open
|
|
205
|
+
*/
|
|
206
|
+
export class CircuitBreakerOpenError extends Error {
|
|
207
|
+
constructor(message: string) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = 'CircuitBreakerOpenError';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Global circuit breaker instance for backend API calls
|
|
215
|
+
*
|
|
216
|
+
* Can be imported and used throughout the application
|
|
217
|
+
*/
|
|
218
|
+
export const backendCircuitBreaker = new CircuitBreaker({
|
|
219
|
+
failureThreshold: 5, // Open after 5 failures
|
|
220
|
+
resetTimeout: 60000, // Try to recover after 1 minute
|
|
221
|
+
name: 'backend-api',
|
|
222
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @thorprovider/adapters v2.0
|
|
4
|
+
* Backend Capabilities System
|
|
5
|
+
*
|
|
6
|
+
* Defines what operations each backend platform supports.
|
|
7
|
+
* This is a compile-time capability detection system, NOT runtime feature flags.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: BackendCapabilities interface is defined in @thorprovider/types (L1: Foundation).
|
|
10
|
+
* This file re-exports it and provides platform-specific implementations.
|
|
11
|
+
*
|
|
12
|
+
* @see packages/core/docs/BACKEND_CAPABILITIES_ANALYSIS.md
|
|
13
|
+
* @see packages/core/docs/COMMERCE_PROVIDER_ARCHITECTURE.md
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.SHOPIFY_CAPABILITIES = exports.MEDUSA_CAPABILITIES = void 0;
|
|
17
|
+
exports.isBackendCapabilities = isBackendCapabilities;
|
|
18
|
+
exports.getCapabilityDescription = getCapabilityDescription;
|
|
19
|
+
/**
|
|
20
|
+
* Medusa backend capabilities
|
|
21
|
+
*
|
|
22
|
+
* @see https://docs.medusajs.com/api/store
|
|
23
|
+
*/
|
|
24
|
+
exports.MEDUSA_CAPABILITIES = {
|
|
25
|
+
// Profile Management
|
|
26
|
+
updateProfile: true,
|
|
27
|
+
updateEmail: false, // Security restriction
|
|
28
|
+
changePasswordDirect: false, // Email flow only
|
|
29
|
+
changePasswordViaEmail: true,
|
|
30
|
+
deleteAccount: true, // Implemented via ThorMedusaClient
|
|
31
|
+
// Avatar
|
|
32
|
+
uploadAvatar: false,
|
|
33
|
+
avatarViaMetadata: true,
|
|
34
|
+
// Custom Attributes
|
|
35
|
+
customAttributes: true,
|
|
36
|
+
// Orders
|
|
37
|
+
fetchOrders: true,
|
|
38
|
+
cancelOrder: true,
|
|
39
|
+
// Addresses
|
|
40
|
+
manageAddresses: true,
|
|
41
|
+
defaultAddress: true,
|
|
42
|
+
// Cart/Checkout
|
|
43
|
+
applyDiscounts: true,
|
|
44
|
+
shippingCalculation: true,
|
|
45
|
+
guestCheckout: true,
|
|
46
|
+
savedPaymentMethods: false, // Depends on payment provider
|
|
47
|
+
listPaymentProviders: true,
|
|
48
|
+
regionSpecificPayments: true,
|
|
49
|
+
// Wishlist
|
|
50
|
+
wishlist: false, // Requires custom implementation
|
|
51
|
+
// Search
|
|
52
|
+
advancedSearch: true,
|
|
53
|
+
facetedFilters: true,
|
|
54
|
+
// Reviews
|
|
55
|
+
productReviews: false, // Requires custom implementation
|
|
56
|
+
// Loyalty
|
|
57
|
+
loyaltyProgram: false, // Requires custom implementation
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Shopify backend capabilities
|
|
61
|
+
*
|
|
62
|
+
* @see https://shopify.dev/docs/api/storefront
|
|
63
|
+
*/
|
|
64
|
+
exports.SHOPIFY_CAPABILITIES = {
|
|
65
|
+
// Profile Management
|
|
66
|
+
updateProfile: true,
|
|
67
|
+
updateEmail: true,
|
|
68
|
+
changePasswordDirect: true,
|
|
69
|
+
changePasswordViaEmail: true,
|
|
70
|
+
deleteAccount: false, // Requires admin API
|
|
71
|
+
// Avatar
|
|
72
|
+
uploadAvatar: false,
|
|
73
|
+
avatarViaMetadata: true,
|
|
74
|
+
// Custom Attributes
|
|
75
|
+
customAttributes: true,
|
|
76
|
+
// Orders
|
|
77
|
+
fetchOrders: true,
|
|
78
|
+
cancelOrder: true,
|
|
79
|
+
// Addresses
|
|
80
|
+
manageAddresses: true,
|
|
81
|
+
defaultAddress: true,
|
|
82
|
+
// Cart/Checkout
|
|
83
|
+
applyDiscounts: true,
|
|
84
|
+
shippingCalculation: true,
|
|
85
|
+
guestCheckout: true,
|
|
86
|
+
savedPaymentMethods: true,
|
|
87
|
+
listPaymentProviders: true,
|
|
88
|
+
regionSpecificPayments: false, // Global payment methods
|
|
89
|
+
// Wishlist
|
|
90
|
+
wishlist: false, // Requires app
|
|
91
|
+
// Search
|
|
92
|
+
advancedSearch: true,
|
|
93
|
+
facetedFilters: true,
|
|
94
|
+
// Reviews
|
|
95
|
+
productReviews: false, // Requires app
|
|
96
|
+
// Loyalty
|
|
97
|
+
loyaltyProgram: false, // Requires app
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Type guard to check if an object is a valid BackendCapabilities
|
|
101
|
+
*/
|
|
102
|
+
function isBackendCapabilities(obj) {
|
|
103
|
+
if (typeof obj !== 'object' || obj === null)
|
|
104
|
+
return false;
|
|
105
|
+
var capabilities = obj;
|
|
106
|
+
// Check required profile management capabilities
|
|
107
|
+
return (typeof capabilities.updateProfile === 'boolean' &&
|
|
108
|
+
typeof capabilities.updateEmail === 'boolean' &&
|
|
109
|
+
typeof capabilities.changePasswordDirect === 'boolean');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get capability description for UI display
|
|
113
|
+
*/
|
|
114
|
+
function getCapabilityDescription(capability) {
|
|
115
|
+
var descriptions = {
|
|
116
|
+
updateProfile: 'Update profile information (name, phone)',
|
|
117
|
+
updateEmail: 'Change email address',
|
|
118
|
+
changePasswordDirect: 'Change password directly',
|
|
119
|
+
changePasswordViaEmail: 'Request password reset via email',
|
|
120
|
+
deleteAccount: 'Delete account (self-service)',
|
|
121
|
+
uploadAvatar: 'Upload profile picture',
|
|
122
|
+
avatarViaMetadata: 'Store avatar URL in metadata',
|
|
123
|
+
customAttributes: 'Custom customer attributes',
|
|
124
|
+
fetchOrders: 'View order history',
|
|
125
|
+
cancelOrder: 'Cancel orders',
|
|
126
|
+
manageAddresses: 'Add/update/delete addresses',
|
|
127
|
+
defaultAddress: 'Set default address',
|
|
128
|
+
applyDiscounts: 'Apply discount codes',
|
|
129
|
+
shippingCalculation: 'Calculate shipping rates',
|
|
130
|
+
guestCheckout: 'Checkout without account',
|
|
131
|
+
savedPaymentMethods: 'Save payment methods',
|
|
132
|
+
listPaymentProviders: 'List available payment providers',
|
|
133
|
+
regionSpecificPayments: 'Region-specific payment methods',
|
|
134
|
+
wishlist: 'Wishlist/favorites',
|
|
135
|
+
advancedSearch: 'Advanced product search',
|
|
136
|
+
facetedFilters: 'Filter by product attributes',
|
|
137
|
+
productReviews: 'Product reviews and ratings',
|
|
138
|
+
loyaltyProgram: 'Loyalty/rewards program',
|
|
139
|
+
};
|
|
140
|
+
return descriptions[capability] || capability;
|
|
141
|
+
}
|