@fjell/registry 4.4.23 → 4.4.24

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.
@@ -0,0 +1,485 @@
1
+ # Network Errors and Timeouts
2
+
3
+ Handle connection failures, DNS issues, and request timeouts with automatic retry logic.
4
+
5
+ ## Overview
6
+
7
+ Network errors are among the most common issues in distributed systems. The Fjell Client API automatically handles these transient failures with intelligent retry logic and exponential backoff.
8
+
9
+ ## Error Types
10
+
11
+ ### NetworkError
12
+ - **Code**: `NETWORK_ERROR`
13
+ - **Retryable**: ✅ Yes
14
+ - **Common Causes**: Connection refused, DNS resolution failure, network unreachable
15
+ - **HTTP Equivalents**: Connection timeouts, DNS errors, network unavailable
16
+
17
+ ### TimeoutError
18
+ - **Code**: `TIMEOUT_ERROR`
19
+ - **Retryable**: ✅ Yes
20
+ - **Common Causes**: Request takes longer than configured timeout
21
+ - **HTTP Equivalents**: Request timeout, gateway timeout
22
+
23
+ ## Automatic Retry Behavior
24
+
25
+ Network errors are automatically retried with exponential backoff:
26
+
27
+ ```typescript
28
+ // This will automatically retry on network failures
29
+ try {
30
+ const user = await userApi.get(userKey);
31
+ console.log('Success (possibly after retries):', user.name);
32
+ } catch (error) {
33
+ // Only reaches here after all retry attempts failed
34
+ console.error('Network completely unavailable:', error.message);
35
+ }
36
+ ```
37
+
38
+ ### Default Retry Configuration
39
+
40
+ ```typescript
41
+ const defaultRetryConfig = {
42
+ maxRetries: 3, // Maximum retry attempts
43
+ initialDelayMs: 1000, // Initial delay: 1 second
44
+ maxDelayMs: 30000, // Maximum delay: 30 seconds
45
+ backoffMultiplier: 2, // Double delay each time
46
+ enableJitter: true // Add randomness to prevent thundering herd
47
+ };
48
+ ```
49
+
50
+ ### Retry Sequence Example
51
+
52
+ For a network error with default configuration:
53
+
54
+ 1. **Initial attempt**: Fails with `ECONNREFUSED`
55
+ 2. **Retry 1**: Wait ~1000ms (with jitter: 500-1000ms), retry
56
+ 3. **Retry 2**: Wait ~2000ms (with jitter: 1000-2000ms), retry
57
+ 4. **Retry 3**: Wait ~4000ms (with jitter: 2000-4000ms), retry
58
+ 5. **Final failure**: If still failing, throw error
59
+
60
+ ## Configuration
61
+
62
+ ### Basic Network Resilience
63
+
64
+ ```typescript
65
+ const config = {
66
+ baseUrl: 'https://api.example.com',
67
+ retryConfig: {
68
+ maxRetries: 5, // More retries for critical operations
69
+ initialDelayMs: 500, // Faster initial retry
70
+ maxDelayMs: 60000, // Allow longer delays
71
+ enableJitter: true // Prevent retry storms
72
+ }
73
+ };
74
+
75
+ const userApi = createPItemApi<User, 'user'>('user', ['users'], config);
76
+ ```
77
+
78
+ ### Custom Network Error Handling
79
+
80
+ ```typescript
81
+ const networkResilientConfig = {
82
+ baseUrl: 'https://api.example.com',
83
+ retryConfig: {
84
+ maxRetries: 3,
85
+ initialDelayMs: 1000,
86
+ maxDelayMs: 30000,
87
+ backoffMultiplier: 2,
88
+ enableJitter: true,
89
+
90
+ // Custom retry logic for network errors
91
+ shouldRetry: (error, attemptNumber) => {
92
+ // Always retry network and timeout errors
93
+ if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT_ERROR') {
94
+ return attemptNumber < 5; // Allow up to 5 retries for network issues
95
+ }
96
+
97
+ // Default retry logic for other errors
98
+ return error.isRetryable && attemptNumber < 3;
99
+ },
100
+
101
+ // Monitor retry attempts
102
+ onRetry: (error, attemptNumber, delay) => {
103
+ console.log(`Network retry ${attemptNumber + 1}: ${error.message} (delay: ${delay}ms)`);
104
+
105
+ // Send metrics to monitoring service
106
+ metrics.increment('api.network.retry', {
107
+ error_code: error.code,
108
+ attempt: attemptNumber + 1
109
+ });
110
+ }
111
+ }
112
+ };
113
+ ```
114
+
115
+ ## Error Handling Patterns
116
+
117
+ ### Basic Network Error Handling
118
+
119
+ ```typescript
120
+ async function fetchUserSafely(userKey: PriKey<'user'>): Promise<User | null> {
121
+ try {
122
+ return await userApi.get(userKey);
123
+ } catch (error) {
124
+ if (error.code === 'NETWORK_ERROR') {
125
+ console.log('Network issue resolved automatically or exhausted retries');
126
+
127
+ // Could show user-friendly message
128
+ showNotification('Network connection issues. Please try again later.');
129
+ return null;
130
+ }
131
+
132
+ if (error.code === 'TIMEOUT_ERROR') {
133
+ console.log('Request timed out after retries');
134
+
135
+ // Could suggest checking connection
136
+ showNotification('Request timed out. Please check your connection.');
137
+ return null;
138
+ }
139
+
140
+ // Re-throw unexpected errors
141
+ throw error;
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Graceful Degradation
147
+
148
+ ```typescript
149
+ async function getUserWithFallback(userKey: PriKey<'user'>): Promise<User | null> {
150
+ try {
151
+ // Try primary API with retry
152
+ return await userApi.get(userKey);
153
+
154
+ } catch (error) {
155
+ if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT_ERROR') {
156
+ console.log('Primary API unavailable, trying alternatives...');
157
+
158
+ // Try cached data
159
+ try {
160
+ const cachedUser = await cache.get(`user:${userKey.pk}`);
161
+ if (cachedUser) {
162
+ console.log('Serving from cache');
163
+ return cachedUser;
164
+ }
165
+ } catch (cacheError) {
166
+ console.warn('Cache also unavailable:', cacheError.message);
167
+ }
168
+
169
+ // Try backup API
170
+ try {
171
+ const backupUser = await backupUserApi.get(userKey);
172
+ console.log('Served from backup API');
173
+ return backupUser;
174
+ } catch (backupError) {
175
+ console.warn('Backup API also failed:', backupError.message);
176
+ }
177
+
178
+ // All sources failed
179
+ console.error('All data sources unavailable');
180
+ return null;
181
+ }
182
+
183
+ // Non-network errors
184
+ throw error;
185
+ }
186
+ }
187
+ ```
188
+
189
+ ### Circuit Breaker Pattern
190
+
191
+ ```typescript
192
+ class CircuitBreaker {
193
+ private failures = 0;
194
+ private lastFailureTime = 0;
195
+ private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
196
+
197
+ constructor(
198
+ private failureThreshold = 5,
199
+ private recoveryTimeMs = 30000
200
+ ) {}
201
+
202
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
203
+ if (this.state === 'OPEN') {
204
+ if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
205
+ this.state = 'HALF_OPEN';
206
+ } else {
207
+ throw new Error('Circuit breaker is OPEN');
208
+ }
209
+ }
210
+
211
+ try {
212
+ const result = await operation();
213
+
214
+ // Success - reset circuit breaker
215
+ if (this.state === 'HALF_OPEN') {
216
+ this.state = 'CLOSED';
217
+ this.failures = 0;
218
+ }
219
+
220
+ return result;
221
+
222
+ } catch (error) {
223
+ this.failures++;
224
+ this.lastFailureTime = Date.now();
225
+
226
+ if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT_ERROR') {
227
+ if (this.failures >= this.failureThreshold) {
228
+ this.state = 'OPEN';
229
+ console.log('Circuit breaker opened due to network failures');
230
+ }
231
+ }
232
+
233
+ throw error;
234
+ }
235
+ }
236
+ }
237
+
238
+ // Usage
239
+ const circuitBreaker = new CircuitBreaker(3, 10000); // 3 failures, 10s recovery
240
+
241
+ async function resilientApiCall(userKey: PriKey<'user'>): Promise<User | null> {
242
+ try {
243
+ return await circuitBreaker.execute(() => userApi.get(userKey));
244
+ } catch (error) {
245
+ if (error.message === 'Circuit breaker is OPEN') {
246
+ console.log('Circuit breaker preventing calls, using fallback');
247
+ return await getFallbackUser(userKey);
248
+ }
249
+ throw error;
250
+ }
251
+ }
252
+ ```
253
+
254
+ ## Monitoring and Observability
255
+
256
+ ### Network Error Metrics
257
+
258
+ ```typescript
259
+ const networkMetrics = {
260
+ // Track network error rates
261
+ recordNetworkError: (error: NetworkError, context: any) => {
262
+ metrics.increment('api.network.errors', {
263
+ error_type: error.code,
264
+ endpoint: context.url,
265
+ method: context.method
266
+ });
267
+ },
268
+
269
+ // Track retry success rates
270
+ recordRetrySuccess: (attemptNumber: number, context: any) => {
271
+ metrics.increment('api.network.retry_success', {
272
+ attempt_number: attemptNumber,
273
+ endpoint: context.url
274
+ });
275
+
276
+ metrics.timing('api.network.recovery_time', context.duration, {
277
+ attempts: attemptNumber
278
+ });
279
+ },
280
+
281
+ // Track connection health
282
+ recordConnectionHealth: (isHealthy: boolean) => {
283
+ metrics.gauge('api.network.health', isHealthy ? 1 : 0);
284
+ }
285
+ };
286
+
287
+ // Custom error handler with metrics
288
+ const config = {
289
+ errorHandler: (error, context) => {
290
+ if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT_ERROR') {
291
+ networkMetrics.recordNetworkError(error, context);
292
+
293
+ // Alert on high network error rates
294
+ if (context.totalAttempts >= 3) {
295
+ alerting.sendAlert({
296
+ severity: 'warning',
297
+ message: `High network error rate: ${error.message}`,
298
+ context
299
+ });
300
+ }
301
+ }
302
+ }
303
+ };
304
+ ```
305
+
306
+ ### Health Checks
307
+
308
+ ```typescript
309
+ async function performHealthCheck(): Promise<boolean> {
310
+ try {
311
+ // Simple health check endpoint
312
+ await healthApi.get({ keyType: 'health', pk: 'status' });
313
+ networkMetrics.recordConnectionHealth(true);
314
+ return true;
315
+ } catch (error) {
316
+ if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT_ERROR') {
317
+ networkMetrics.recordConnectionHealth(false);
318
+ console.warn('Health check failed:', error.message);
319
+ return false;
320
+ }
321
+
322
+ // API is reachable but returned an error
323
+ networkMetrics.recordConnectionHealth(true);
324
+ return true;
325
+ }
326
+ }
327
+
328
+ // Run health checks periodically
329
+ setInterval(performHealthCheck, 30000); // Every 30 seconds
330
+ ```
331
+
332
+ ## Testing Network Errors
333
+
334
+ ### Unit Testing
335
+
336
+ ```typescript
337
+ describe('Network Error Handling', () => {
338
+ it('should retry on network errors', async () => {
339
+ const mockApi = createMockApi();
340
+
341
+ // First call fails, second succeeds
342
+ mockApi.httpGet
343
+ .mockRejectedValueOnce(new NetworkError('ECONNREFUSED'))
344
+ .mockResolvedValueOnce({ id: 'user-123' });
345
+
346
+ const user = await userApi.get(userKey);
347
+
348
+ expect(user.id).toBe('user-123');
349
+ expect(mockApi.httpGet).toHaveBeenCalledTimes(2);
350
+ });
351
+
352
+ it('should respect max retries', async () => {
353
+ const mockApi = createMockApi();
354
+ mockApi.httpGet.mockRejectedValue(new NetworkError('ECONNREFUSED'));
355
+
356
+ const config = { retryConfig: { maxRetries: 2 } };
357
+ const api = createPItemApi<User, 'user'>('user', ['users'], config);
358
+
359
+ await expect(api.get(userKey)).rejects.toThrow('ECONNREFUSED');
360
+ expect(mockApi.httpGet).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
361
+ });
362
+
363
+ it('should apply exponential backoff', async () => {
364
+ const delays: number[] = [];
365
+ const mockSetTimeout = jest.spyOn(global, 'setTimeout').mockImplementation((fn, delay) => {
366
+ delays.push(delay);
367
+ return setTimeout(fn, 0); // Execute immediately for testing
368
+ });
369
+
370
+ const mockApi = createMockApi();
371
+ mockApi.httpGet.mockRejectedValue(new NetworkError('ECONNREFUSED'));
372
+
373
+ try {
374
+ await userApi.get(userKey);
375
+ } catch (error) {
376
+ // Expected to fail after retries
377
+ }
378
+
379
+ expect(delays).toHaveLength(3);
380
+ expect(delays[1]).toBeGreaterThan(delays[0]); // Exponential increase
381
+ expect(delays[2]).toBeGreaterThan(delays[1]);
382
+
383
+ mockSetTimeout.mockRestore();
384
+ });
385
+ });
386
+ ```
387
+
388
+ ### Integration Testing
389
+
390
+ ```typescript
391
+ describe('Network Resilience Integration', () => {
392
+ it('should handle intermittent network issues', async () => {
393
+ let callCount = 0;
394
+
395
+ server.use(
396
+ rest.get('/api/users/:id', (req, res, ctx) => {
397
+ callCount++;
398
+
399
+ // Fail first two calls, succeed on third
400
+ if (callCount <= 2) {
401
+ return res.networkError('Connection failed');
402
+ }
403
+
404
+ return res(ctx.json({ id: 'user-123' }));
405
+ })
406
+ );
407
+
408
+ const user = await userApi.get(userKey);
409
+ expect(user.id).toBe('user-123');
410
+ expect(callCount).toBe(3);
411
+ });
412
+ });
413
+ ```
414
+
415
+ ## Best Practices
416
+
417
+ ### 1. **Configure Appropriate Timeouts**
418
+
419
+ ```typescript
420
+ const config = {
421
+ baseUrl: 'https://api.example.com',
422
+ timeout: 10000, // 10 second timeout
423
+ retryConfig: {
424
+ maxRetries: 3,
425
+ initialDelayMs: 1000,
426
+ maxDelayMs: 15000
427
+ }
428
+ };
429
+ ```
430
+
431
+ ### 2. **Implement Progressive Degradation**
432
+
433
+ ```typescript
434
+ async function getDataWithDegradation(key: string) {
435
+ try {
436
+ // Try full data
437
+ return await api.getFullData(key);
438
+ } catch (error) {
439
+ if (error.code === 'NETWORK_ERROR') {
440
+ try {
441
+ // Try summary data (smaller payload)
442
+ return await api.getSummaryData(key);
443
+ } catch (summaryError) {
444
+ // Use cached minimal data
445
+ return await cache.getMinimalData(key);
446
+ }
447
+ }
448
+ throw error;
449
+ }
450
+ }
451
+ ```
452
+
453
+ ### 3. **User Experience Considerations**
454
+
455
+ ```typescript
456
+ async function fetchDataWithUI(key: string) {
457
+ const loadingIndicator = showLoadingSpinner();
458
+
459
+ try {
460
+ const data = await api.getData(key);
461
+ hideLoadingSpinner(loadingIndicator);
462
+ return data;
463
+
464
+ } catch (error) {
465
+ hideLoadingSpinner(loadingIndicator);
466
+
467
+ if (error.code === 'NETWORK_ERROR') {
468
+ showNotification('Connection issue. Retrying automatically...', 'info');
469
+
470
+ // Give user option to retry manually
471
+ showRetryButton(() => fetchDataWithUI(key));
472
+ } else if (error.code === 'TIMEOUT_ERROR') {
473
+ showNotification('Request timed out. Please try again.', 'warning');
474
+ }
475
+
476
+ throw error;
477
+ }
478
+ }
479
+ ```
480
+
481
+ ## Related Documentation
482
+
483
+ - [Error Handling Overview](./README.md) - Complete error handling guide
484
+ - [Server Errors](./server-errors.md) - Handle 5xx server errors
485
+ - [Configuration](../configuration.md) - Configure retry behavior
@@ -0,0 +1,121 @@
1
+ # API Operations
2
+
3
+ This section provides comprehensive documentation for all available operations in the Fjell Client API. Each operation supports both Primary Items (PItemApi) and Contained Items (CItemApi) with location-aware variants.
4
+
5
+ ## Quick Reference
6
+
7
+ ### Core CRUD Operations
8
+ - **[`all`](./all.md)** - Retrieve multiple items with query support
9
+ - **[`create`](./create.md)** - Create new items with validation
10
+ - **[`get`](./get.md)** - Retrieve single items by key
11
+ - **[`update`](./update.md)** - Update existing items with partial data
12
+ - **[`remove`](./remove.md)** - Delete items safely
13
+ - **[`one`](./one.md)** - Find single item with query conditions
14
+
15
+ ### Business Logic Operations
16
+ - **[`action`](./action.md)** - Execute business actions on items
17
+ - **[`allAction`](./allAction.md)** - Execute batch actions on collections
18
+ - **[`facet`](./facet.md)** - Retrieve analytics and computed data for items
19
+ - **[`allFacet`](./allFacet.md)** - Retrieve analytics for collections
20
+
21
+ ### Query Operations
22
+ - **[`find`](./find.md)** - Find items using custom finders
23
+ - **[`findOne`](./findOne.md)** - Find single item using custom finders
24
+
25
+ ## Operation Categories
26
+
27
+ ### Primary Item Operations (PItemApi)
28
+ Operations for independent entities that exist at the top level of your API hierarchy:
29
+
30
+ ```typescript
31
+ const userApi = createPItemApi<User, 'user'>('user', ['users'], config);
32
+
33
+ // Basic CRUD
34
+ const users = await userApi.all({ active: true });
35
+ const user = await userApi.create({ name: 'John', email: 'john@example.com' });
36
+ const foundUser = await userApi.get(userKey);
37
+ const updatedUser = await userApi.update(userKey, { name: 'John Smith' });
38
+ await userApi.remove(userKey);
39
+
40
+ // Business logic
41
+ await userApi.action(userKey, 'activate', { reason: 'manual' });
42
+ const analytics = await userApi.facet(userKey, 'activity-stats');
43
+ ```
44
+
45
+ ### Contained Item Operations (CItemApi)
46
+ Operations for hierarchical entities that require location context:
47
+
48
+ ```typescript
49
+ const taskApi = createCItemApi<Task, 'task', 'user'>('task', ['users', 'tasks'], config);
50
+
51
+ // Location-aware CRUD
52
+ const userTasks = await taskApi.all({ status: 'pending' }, [userId]);
53
+ const newTask = await taskApi.create({ title: 'Complete project' }, [userId]);
54
+
55
+ // Location-aware business logic
56
+ await taskApi.allAction('bulk-update', { priority: 'high' }, [userId]);
57
+ const taskMetrics = await taskApi.allFacet('completion-metrics', {}, [userId]);
58
+ ```
59
+
60
+ ## Error Handling
61
+
62
+ All operations include comprehensive error handling with:
63
+ - **Automatic retry logic** for transient failures
64
+ - **Custom error types** for different scenarios
65
+ - **Enhanced error context** for debugging
66
+ - **Configurable retry behavior**
67
+
68
+ See the [Error Handling Documentation](../error-handling/README.md) for complete details.
69
+
70
+ ## Performance Considerations
71
+
72
+ ### Pagination
73
+ Use `limit` and `offset` in queries for large datasets:
74
+
75
+ ```typescript
76
+ const users = await userApi.all({
77
+ limit: 50,
78
+ offset: 100,
79
+ sort: 'createdAt:desc'
80
+ });
81
+ ```
82
+
83
+ ### Batch Operations
84
+ Use `allAction` and `allFacet` for efficient batch processing:
85
+
86
+ ```typescript
87
+ // Efficient batch update
88
+ await userApi.allAction('bulk-update', {
89
+ lastLoginAt: new Date()
90
+ }, { filter: 'active:true' });
91
+ ```
92
+
93
+ ### Caching Strategies
94
+ Implement client-side caching for frequently accessed data:
95
+
96
+ ```typescript
97
+ const cachedUser = await cache.get(userKey) || await userApi.get(userKey);
98
+ ```
99
+
100
+ ## Type Safety
101
+
102
+ All operations are fully typed with TypeScript generics:
103
+
104
+ ```typescript
105
+ interface User extends Item<'user'> {
106
+ name: string;
107
+ email: string;
108
+ active: boolean;
109
+ }
110
+
111
+ // Type-safe operations
112
+ const userApi = createPItemApi<User, 'user'>('user', ['users'], config);
113
+ const user: User = await userApi.create({ name: 'John', email: 'john@example.com' });
114
+ ```
115
+
116
+ ## Next Steps
117
+
118
+ - Review individual operation documentation for detailed examples
119
+ - Check the [Configuration Guide](../configuration.md) for setup options
120
+ - Explore [Error Handling](../error-handling/README.md) for resilience patterns
121
+ - See [Examples](../../examples-README.md) for complete usage scenarios