create-fluxstack 1.7.5 → 1.8.3
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/.dockerignore +82 -0
- package/.env.example +19 -0
- package/Dockerfile +70 -0
- package/README.md +6 -3
- package/app/client/SIMPLIFICATION.md +140 -0
- package/app/client/frontend-only.ts +1 -1
- package/app/client/src/App.tsx +148 -283
- package/app/client/src/index.css +5 -20
- package/app/client/src/lib/eden-api.ts +53 -220
- package/app/client/src/main.tsx +2 -3
- package/app/server/app.ts +20 -5
- package/app/server/backend-only.ts +15 -12
- package/app/server/controllers/users.controller.ts +57 -31
- package/app/server/index.ts +86 -96
- package/app/server/live/register-components.ts +18 -7
- package/app/server/routes/env-test.ts +110 -0
- package/app/server/routes/index.ts +1 -8
- package/app/server/routes/users.routes.ts +192 -91
- package/config/app.config.ts +2 -54
- package/config/client.config.ts +95 -0
- package/config/fluxstack.config.ts +2 -2
- package/config/index.ts +57 -22
- package/config/monitoring.config.ts +114 -0
- package/config/plugins.config.ts +80 -0
- package/config/runtime.config.ts +0 -17
- package/config/server.config.ts +50 -30
- package/core/build/bundler.ts +17 -16
- package/core/build/flux-plugins-generator.ts +34 -23
- package/core/build/index.ts +32 -31
- package/core/build/live-components-generator.ts +44 -30
- package/core/build/optimizer.ts +37 -17
- package/core/cli/command-registry.ts +4 -14
- package/core/cli/commands/plugin-deps.ts +8 -8
- package/core/cli/generators/component.ts +3 -3
- package/core/cli/generators/controller.ts +4 -4
- package/core/cli/generators/index.ts +8 -8
- package/core/cli/generators/interactive.ts +4 -4
- package/core/cli/generators/plugin.ts +3 -3
- package/core/cli/generators/prompts.ts +1 -1
- package/core/cli/generators/route.ts +27 -11
- package/core/cli/generators/service.ts +5 -5
- package/core/cli/generators/template-engine.ts +1 -1
- package/core/cli/generators/types.ts +1 -1
- package/core/cli/index.ts +158 -189
- package/core/cli/plugin-discovery.ts +3 -3
- package/core/client/hooks/index.ts +2 -2
- package/core/client/hooks/state-validator.ts +1 -1
- package/core/client/hooks/useAuth.ts +1 -1
- package/core/client/hooks/useChunkedUpload.ts +1 -1
- package/core/client/hooks/useHybridLiveComponent.ts +1 -1
- package/core/client/hooks/useWebSocket.ts +1 -1
- package/core/config/env.ts +5 -1
- package/core/config/runtime-config.ts +12 -10
- package/core/config/schema.ts +33 -2
- package/core/framework/server.ts +30 -14
- package/core/framework/types.ts +2 -2
- package/core/index.ts +31 -23
- package/core/live/ComponentRegistry.ts +1 -1
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1 -1
- package/core/plugins/built-in/live-components/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +65 -161
- package/core/plugins/built-in/static/index.ts +75 -277
- package/core/plugins/built-in/swagger/index.ts +301 -231
- package/core/plugins/built-in/vite/index.ts +342 -377
- package/core/plugins/config.ts +2 -2
- package/core/plugins/dependency-manager.ts +2 -2
- package/core/plugins/discovery.ts +1 -1
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/manager.ts +19 -4
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +25 -21
- package/core/plugins/types.ts +147 -5
- package/core/server/backend-entry.ts +51 -0
- package/core/server/framework.ts +2 -2
- package/core/server/live/ComponentRegistry.ts +9 -26
- package/core/server/live/FileUploadManager.ts +1 -1
- package/core/server/live/auto-generated-components.ts +26 -0
- package/core/server/live/websocket-plugin.ts +211 -19
- package/core/server/middleware/errorHandling.ts +1 -1
- package/core/server/middleware/index.ts +4 -4
- package/core/server/plugins/database.ts +1 -2
- package/core/server/plugins/static-files-plugin.ts +259 -231
- package/core/server/plugins/swagger.ts +1 -1
- package/core/server/services/BaseService.ts +1 -1
- package/core/server/services/ServiceContainer.ts +1 -1
- package/core/server/services/index.ts +4 -4
- package/core/server/standalone.ts +16 -1
- package/core/testing/index.ts +1 -1
- package/core/testing/setup.ts +1 -1
- package/core/types/plugin.ts +6 -0
- package/core/utils/build-logger.ts +324 -0
- package/core/utils/config-schema.ts +2 -6
- package/core/utils/helpers.ts +14 -9
- package/core/utils/logger/startup-banner.ts +7 -33
- package/core/utils/regenerate-files.ts +69 -0
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +68 -25
- package/fluxstack.config.ts +138 -252
- package/package.json +3 -18
- package/plugins/crypto-auth/index.ts +52 -47
- package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/helpers.ts +16 -1
- package/vitest.config.ts +17 -26
- package/app/client/src/App.css +0 -883
- package/app/client/src/components/ErrorBoundary.tsx +0 -107
- package/app/client/src/components/ErrorDisplay.css +0 -365
- package/app/client/src/components/ErrorDisplay.tsx +0 -258
- package/app/client/src/components/FluxStackConfig.tsx +0 -1321
- package/app/client/src/components/HybridLiveCounter.tsx +0 -140
- package/app/client/src/components/LiveClock.tsx +0 -286
- package/app/client/src/components/MainLayout.tsx +0 -388
- package/app/client/src/components/SidebarNavigation.tsx +0 -391
- package/app/client/src/components/StateDemo.tsx +0 -178
- package/app/client/src/components/SystemMonitor.tsx +0 -1044
- package/app/client/src/components/UserProfile.tsx +0 -809
- package/app/client/src/hooks/useAuth.ts +0 -39
- package/app/client/src/hooks/useNotifications.ts +0 -56
- package/app/client/src/lib/errors.ts +0 -340
- package/app/client/src/lib/hooks/useErrorHandler.ts +0 -258
- package/app/client/src/lib/index.ts +0 -45
- package/app/client/src/pages/ApiDocs.tsx +0 -182
- package/app/client/src/pages/CryptoAuthPage.tsx +0 -394
- package/app/client/src/pages/Demo.tsx +0 -174
- package/app/client/src/pages/HybridLive.tsx +0 -263
- package/app/client/src/pages/Overview.tsx +0 -155
- package/app/client/src/store/README.md +0 -43
- package/app/client/src/store/index.ts +0 -16
- package/app/client/src/store/slices/uiSlice.ts +0 -151
- package/app/client/src/store/slices/userSlice.ts +0 -161
- package/app/client/src/test/README.md +0 -257
- package/app/client/src/test/setup.ts +0 -70
- package/app/client/src/test/types.ts +0 -12
- package/app/server/live/CounterComponent.ts +0 -191
- package/app/server/live/FluxStackConfig.ts +0 -534
- package/app/server/live/SidebarNavigation.ts +0 -157
- package/app/server/live/SystemMonitor.ts +0 -595
- package/app/server/live/SystemMonitorIntegration.ts +0 -151
- package/app/server/live/UserProfileComponent.ts +0 -141
- package/app/server/middleware/auth.ts +0 -136
- package/app/server/middleware/errorHandling.ts +0 -252
- package/app/server/middleware/index.ts +0 -10
- package/app/server/middleware/rateLimit.ts +0 -193
- package/app/server/middleware/requestLogging.ts +0 -215
- package/app/server/middleware/validation.ts +0 -270
- package/app/server/routes/config.ts +0 -145
- package/app/server/routes/crypto-auth-demo.routes.ts +0 -167
- package/app/server/routes/example-with-crypto-auth.routes.ts +0 -235
- package/app/server/routes/exemplo-posts.routes.ts +0 -161
- package/app/server/routes/upload.ts +0 -92
- package/app/server/services/NotificationService.ts +0 -302
- package/app/server/services/UserService.ts +0 -222
- package/app/server/services/index.ts +0 -46
- package/app/server/types/index.ts +0 -1
- package/config/build.config.ts +0 -24
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication hooks
|
|
3
|
-
* App-specific authentication hook using FluxStack core
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useUserStore } from '../store/slices/userSlice'
|
|
7
|
-
|
|
8
|
-
export function useAuth() {
|
|
9
|
-
const {
|
|
10
|
-
currentUser,
|
|
11
|
-
isAuthenticated,
|
|
12
|
-
isLoading,
|
|
13
|
-
error,
|
|
14
|
-
login,
|
|
15
|
-
register,
|
|
16
|
-
logout,
|
|
17
|
-
updateProfile,
|
|
18
|
-
clearError
|
|
19
|
-
} = useUserStore()
|
|
20
|
-
|
|
21
|
-
// Computed values
|
|
22
|
-
const isAdmin = currentUser?.role === 'admin'
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
// State
|
|
26
|
-
currentUser,
|
|
27
|
-
isAuthenticated,
|
|
28
|
-
isLoading,
|
|
29
|
-
error,
|
|
30
|
-
isAdmin,
|
|
31
|
-
|
|
32
|
-
// Actions
|
|
33
|
-
login,
|
|
34
|
-
register,
|
|
35
|
-
logout,
|
|
36
|
-
updateProfile,
|
|
37
|
-
clearError
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Notification management hooks
|
|
3
|
-
* Provides easy-to-use hooks for managing notifications using Zustand
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useCallback } from 'react'
|
|
7
|
-
import { useUIStore } from '../store/slices/uiSlice'
|
|
8
|
-
|
|
9
|
-
export function useNotifications() {
|
|
10
|
-
const {
|
|
11
|
-
notifications,
|
|
12
|
-
addNotification,
|
|
13
|
-
removeNotification,
|
|
14
|
-
clearNotifications
|
|
15
|
-
} = useUIStore()
|
|
16
|
-
|
|
17
|
-
// Convenience methods for different notification types
|
|
18
|
-
const success = useCallback(
|
|
19
|
-
(title: string, message: string, duration?: number) => {
|
|
20
|
-
addNotification({ type: 'success', title, message, duration })
|
|
21
|
-
},
|
|
22
|
-
[addNotification]
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const error = useCallback(
|
|
26
|
-
(title: string, message: string, duration?: number) => {
|
|
27
|
-
addNotification({ type: 'error', title, message, duration })
|
|
28
|
-
},
|
|
29
|
-
[addNotification]
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const warning = useCallback(
|
|
33
|
-
(title: string, message: string, duration?: number) => {
|
|
34
|
-
addNotification({ type: 'warning', title, message, duration })
|
|
35
|
-
},
|
|
36
|
-
[addNotification]
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
const info = useCallback(
|
|
40
|
-
(title: string, message: string, duration?: number) => {
|
|
41
|
-
addNotification({ type: 'info', title, message, duration })
|
|
42
|
-
},
|
|
43
|
-
[addNotification]
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
notifications,
|
|
48
|
-
addNotification,
|
|
49
|
-
removeNotification,
|
|
50
|
-
clearNotifications,
|
|
51
|
-
success,
|
|
52
|
-
error,
|
|
53
|
-
warning,
|
|
54
|
-
info
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
// Client-side error handling utilities
|
|
2
|
-
export interface ClientError {
|
|
3
|
-
message: string
|
|
4
|
-
code: string
|
|
5
|
-
statusCode: number
|
|
6
|
-
details?: any
|
|
7
|
-
timestamp: string
|
|
8
|
-
correlationId?: string
|
|
9
|
-
userMessage?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface RetryOptions {
|
|
13
|
-
maxRetries: number
|
|
14
|
-
baseDelay: number
|
|
15
|
-
maxDelay: number
|
|
16
|
-
backoffFactor: number
|
|
17
|
-
retryableStatusCodes: number[]
|
|
18
|
-
retryableErrorCodes: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface FallbackOptions<T> {
|
|
22
|
-
fallbackValue?: T
|
|
23
|
-
fallbackFunction?: () => T | Promise<T>
|
|
24
|
-
showFallbackMessage?: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class ClientAPIError extends Error {
|
|
28
|
-
public readonly code: string
|
|
29
|
-
public readonly statusCode: number
|
|
30
|
-
public readonly details?: any
|
|
31
|
-
public readonly timestamp: Date
|
|
32
|
-
public readonly correlationId?: string
|
|
33
|
-
public readonly userMessage?: string
|
|
34
|
-
public readonly isRetryable: boolean
|
|
35
|
-
|
|
36
|
-
constructor(
|
|
37
|
-
message: string,
|
|
38
|
-
code: string,
|
|
39
|
-
statusCode: number,
|
|
40
|
-
details?: any,
|
|
41
|
-
correlationId?: string,
|
|
42
|
-
userMessage?: string
|
|
43
|
-
) {
|
|
44
|
-
super(message)
|
|
45
|
-
this.name = 'ClientAPIError'
|
|
46
|
-
this.code = code
|
|
47
|
-
this.statusCode = statusCode
|
|
48
|
-
this.details = details
|
|
49
|
-
this.timestamp = new Date()
|
|
50
|
-
this.correlationId = correlationId
|
|
51
|
-
this.userMessage = userMessage
|
|
52
|
-
this.isRetryable = this.determineRetryability()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private determineRetryability(): boolean {
|
|
56
|
-
// Network errors and server errors are generally retryable
|
|
57
|
-
const retryableStatusCodes = [408, 429, 500, 502, 503, 504]
|
|
58
|
-
const retryableErrorCodes = [
|
|
59
|
-
'NETWORK_ERROR',
|
|
60
|
-
'TIMEOUT_ERROR',
|
|
61
|
-
'EXTERNAL_SERVICE_ERROR',
|
|
62
|
-
'DATABASE_ERROR',
|
|
63
|
-
'SERVICE_UNAVAILABLE',
|
|
64
|
-
'RATE_LIMIT_EXCEEDED'
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
return retryableStatusCodes.includes(this.statusCode) ||
|
|
68
|
-
retryableErrorCodes.includes(this.code)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
toJSON(): ClientError {
|
|
72
|
-
return {
|
|
73
|
-
message: this.message,
|
|
74
|
-
code: this.code,
|
|
75
|
-
statusCode: this.statusCode,
|
|
76
|
-
details: this.details,
|
|
77
|
-
timestamp: this.timestamp.toISOString(),
|
|
78
|
-
correlationId: this.correlationId,
|
|
79
|
-
userMessage: this.userMessage
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
getUserFriendlyMessage(): string {
|
|
84
|
-
if (this.userMessage) {
|
|
85
|
-
return this.userMessage
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return getDefaultUserMessage(this.code, this.statusCode)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export class NetworkError extends ClientAPIError {
|
|
93
|
-
constructor(message: string = 'Network connection failed', correlationId?: string) {
|
|
94
|
-
super(
|
|
95
|
-
message,
|
|
96
|
-
'NETWORK_ERROR',
|
|
97
|
-
0,
|
|
98
|
-
undefined,
|
|
99
|
-
correlationId,
|
|
100
|
-
'Unable to connect to the server. Please check your internet connection and try again.'
|
|
101
|
-
)
|
|
102
|
-
this.name = 'NetworkError'
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export class TimeoutError extends ClientAPIError {
|
|
107
|
-
constructor(timeout: number, correlationId?: string) {
|
|
108
|
-
super(
|
|
109
|
-
`Request timed out after ${timeout}ms`,
|
|
110
|
-
'TIMEOUT_ERROR',
|
|
111
|
-
408,
|
|
112
|
-
{ timeout },
|
|
113
|
-
correlationId,
|
|
114
|
-
'The request is taking longer than expected. Please try again.'
|
|
115
|
-
)
|
|
116
|
-
this.name = 'TimeoutError'
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Default user-friendly messages
|
|
121
|
-
export function getDefaultUserMessage(code: string, statusCode: number): string {
|
|
122
|
-
// First check by error code
|
|
123
|
-
const codeMessages: Record<string, string> = {
|
|
124
|
-
'VALIDATION_ERROR': 'Please check your input and try again.',
|
|
125
|
-
'INVALID_INPUT': 'The information provided is not valid.',
|
|
126
|
-
'MISSING_REQUIRED_FIELD': 'Please fill in all required fields.',
|
|
127
|
-
'UNAUTHORIZED': 'Please log in to access this resource.',
|
|
128
|
-
'INVALID_TOKEN': 'Your session has expired. Please log in again.',
|
|
129
|
-
'TOKEN_EXPIRED': 'Your session has expired. Please log in again.',
|
|
130
|
-
'FORBIDDEN': 'You do not have permission to perform this action.',
|
|
131
|
-
'INSUFFICIENT_PERMISSIONS': 'You do not have the required permissions.',
|
|
132
|
-
'NOT_FOUND': 'The requested resource could not be found.',
|
|
133
|
-
'RESOURCE_NOT_FOUND': 'The requested item could not be found.',
|
|
134
|
-
'ENDPOINT_NOT_FOUND': 'The requested service is not available.',
|
|
135
|
-
'CONFLICT': 'There was a conflict with the current state.',
|
|
136
|
-
'RESOURCE_ALREADY_EXISTS': 'This item already exists.',
|
|
137
|
-
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again later.',
|
|
138
|
-
'INTERNAL_SERVER_ERROR': 'An unexpected error occurred. Please try again later.',
|
|
139
|
-
'DATABASE_ERROR': 'A database error occurred. Please try again later.',
|
|
140
|
-
'EXTERNAL_SERVICE_ERROR': 'An external service is currently unavailable.',
|
|
141
|
-
'SERVICE_UNAVAILABLE': 'The service is temporarily unavailable.',
|
|
142
|
-
'MAINTENANCE_MODE': 'The service is under maintenance. Please try again later.',
|
|
143
|
-
'NETWORK_ERROR': 'Unable to connect to the server. Please check your connection.',
|
|
144
|
-
'TIMEOUT_ERROR': 'The request timed out. Please try again.'
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (codeMessages[code]) {
|
|
148
|
-
return codeMessages[code]
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Fallback to status code messages
|
|
152
|
-
const statusMessages: Record<number, string> = {
|
|
153
|
-
400: 'Invalid request. Please check your input.',
|
|
154
|
-
401: 'Authentication required. Please log in.',
|
|
155
|
-
403: 'Access denied. You do not have permission.',
|
|
156
|
-
404: 'Resource not found.',
|
|
157
|
-
409: 'Conflict with existing data.',
|
|
158
|
-
422: 'Invalid data provided.',
|
|
159
|
-
429: 'Too many requests. Please try again later.',
|
|
160
|
-
500: 'Server error. Please try again later.',
|
|
161
|
-
502: 'Service temporarily unavailable.',
|
|
162
|
-
503: 'Service temporarily unavailable.',
|
|
163
|
-
504: 'Request timeout. Please try again.'
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return statusMessages[statusCode] || 'An unexpected error occurred.'
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Retry utility with exponential backoff
|
|
170
|
-
export async function withRetry<T>(
|
|
171
|
-
operation: () => Promise<T>,
|
|
172
|
-
options: Partial<RetryOptions> = {}
|
|
173
|
-
): Promise<T> {
|
|
174
|
-
const config: RetryOptions = {
|
|
175
|
-
maxRetries: 3,
|
|
176
|
-
baseDelay: 1000,
|
|
177
|
-
maxDelay: 10000,
|
|
178
|
-
backoffFactor: 2,
|
|
179
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
180
|
-
retryableErrorCodes: [
|
|
181
|
-
'NETWORK_ERROR',
|
|
182
|
-
'TIMEOUT_ERROR',
|
|
183
|
-
'EXTERNAL_SERVICE_ERROR',
|
|
184
|
-
'DATABASE_ERROR',
|
|
185
|
-
'SERVICE_UNAVAILABLE',
|
|
186
|
-
'RATE_LIMIT_EXCEEDED'
|
|
187
|
-
],
|
|
188
|
-
...options
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let lastError: Error | undefined
|
|
192
|
-
|
|
193
|
-
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
194
|
-
try {
|
|
195
|
-
return await operation()
|
|
196
|
-
} catch (error) {
|
|
197
|
-
lastError = error as Error
|
|
198
|
-
|
|
199
|
-
// Don't retry on the last attempt
|
|
200
|
-
if (attempt === config.maxRetries) {
|
|
201
|
-
break
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Check if error is retryable
|
|
205
|
-
if (error instanceof ClientAPIError) {
|
|
206
|
-
const isRetryableStatus = config.retryableStatusCodes.includes(error.statusCode)
|
|
207
|
-
const isRetryableCode = config.retryableErrorCodes.includes(error.code)
|
|
208
|
-
|
|
209
|
-
if (!isRetryableStatus && !isRetryableCode) {
|
|
210
|
-
break // Don't retry non-retryable errors
|
|
211
|
-
}
|
|
212
|
-
} else if (!(error instanceof NetworkError || error instanceof TimeoutError)) {
|
|
213
|
-
break // Don't retry unknown errors
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Calculate delay with exponential backoff
|
|
217
|
-
const delay = Math.min(
|
|
218
|
-
config.baseDelay * Math.pow(config.backoffFactor, attempt),
|
|
219
|
-
config.maxDelay
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
// Add jitter to prevent thundering herd
|
|
223
|
-
const jitteredDelay = delay + Math.random() * 1000
|
|
224
|
-
|
|
225
|
-
await new Promise(resolve => setTimeout(resolve, jitteredDelay))
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
throw lastError!
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Fallback utility
|
|
233
|
-
export async function withFallback<T>(
|
|
234
|
-
operation: () => Promise<T>,
|
|
235
|
-
fallbackOptions: FallbackOptions<T>
|
|
236
|
-
): Promise<T> {
|
|
237
|
-
try {
|
|
238
|
-
return await operation()
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.warn('Operation failed, using fallback:', error)
|
|
241
|
-
|
|
242
|
-
if (fallbackOptions.fallbackFunction) {
|
|
243
|
-
return await fallbackOptions.fallbackFunction()
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (fallbackOptions.fallbackValue !== undefined) {
|
|
247
|
-
return fallbackOptions.fallbackValue
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
throw error // Re-throw if no fallback provided
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Circuit breaker pattern for preventing cascading failures
|
|
255
|
-
export class CircuitBreaker {
|
|
256
|
-
private failures = 0
|
|
257
|
-
private lastFailureTime = 0
|
|
258
|
-
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'
|
|
259
|
-
|
|
260
|
-
private failureThreshold: number
|
|
261
|
-
private recoveryTimeout: number
|
|
262
|
-
|
|
263
|
-
constructor(
|
|
264
|
-
failureThreshold: number = 5,
|
|
265
|
-
recoveryTimeout: number = 60000
|
|
266
|
-
) {
|
|
267
|
-
this.failureThreshold = failureThreshold
|
|
268
|
-
this.recoveryTimeout = recoveryTimeout
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
272
|
-
if (this.state === 'OPEN') {
|
|
273
|
-
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
|
|
274
|
-
this.state = 'HALF_OPEN'
|
|
275
|
-
} else {
|
|
276
|
-
throw new ClientAPIError(
|
|
277
|
-
'Circuit breaker is open',
|
|
278
|
-
'CIRCUIT_BREAKER_OPEN',
|
|
279
|
-
503,
|
|
280
|
-
undefined,
|
|
281
|
-
undefined,
|
|
282
|
-
'Service is temporarily unavailable due to repeated failures'
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const result = await operation()
|
|
289
|
-
this.onSuccess()
|
|
290
|
-
return result
|
|
291
|
-
} catch (error) {
|
|
292
|
-
this.onFailure()
|
|
293
|
-
throw error
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private onSuccess(): void {
|
|
298
|
-
this.failures = 0
|
|
299
|
-
this.state = 'CLOSED'
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private onFailure(): void {
|
|
303
|
-
this.failures++
|
|
304
|
-
this.lastFailureTime = Date.now()
|
|
305
|
-
|
|
306
|
-
if (this.failures >= this.failureThreshold) {
|
|
307
|
-
this.state = 'OPEN'
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
getState(): string {
|
|
312
|
-
return this.state
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
reset(): void {
|
|
316
|
-
this.failures = 0
|
|
317
|
-
this.lastFailureTime = 0
|
|
318
|
-
this.state = 'CLOSED'
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Error boundary helper for React components
|
|
323
|
-
export interface ErrorInfo {
|
|
324
|
-
error: Error
|
|
325
|
-
errorInfo: { componentStack: string }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function logClientError(error: Error, errorInfo?: { componentStack: string }): void {
|
|
329
|
-
console.error('Client error:', {
|
|
330
|
-
message: error.message,
|
|
331
|
-
stack: error.stack,
|
|
332
|
-
componentStack: errorInfo?.componentStack,
|
|
333
|
-
timestamp: new Date().toISOString(),
|
|
334
|
-
userAgent: navigator.userAgent,
|
|
335
|
-
url: window.location.href
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
// In a real application, you might want to send this to an error tracking service
|
|
339
|
-
// Example: Sentry, LogRocket, etc.
|
|
340
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef } from 'react'
|
|
2
|
-
import { logClientError } from '../errors'
|
|
3
|
-
import { getErrorMessage, isRetryableError, shouldShowErrorToUser } from '../eden-api'
|
|
4
|
-
|
|
5
|
-
export interface ErrorState {
|
|
6
|
-
error: Error | null
|
|
7
|
-
isRetrying: boolean
|
|
8
|
-
retryCount: number
|
|
9
|
-
canRetry: boolean
|
|
10
|
-
userMessage: string | null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface UseErrorHandlerOptions {
|
|
14
|
-
maxRetries?: number
|
|
15
|
-
showUserFriendlyMessages?: boolean
|
|
16
|
-
logErrors?: boolean
|
|
17
|
-
onError?: (error: Error) => void
|
|
18
|
-
onRetry?: (retryCount: number) => void
|
|
19
|
-
onMaxRetriesReached?: (error: Error) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function useErrorHandler(options: UseErrorHandlerOptions = {}) {
|
|
23
|
-
const {
|
|
24
|
-
maxRetries = 3,
|
|
25
|
-
showUserFriendlyMessages = true,
|
|
26
|
-
logErrors = true,
|
|
27
|
-
onError,
|
|
28
|
-
onRetry,
|
|
29
|
-
onMaxRetriesReached
|
|
30
|
-
} = options
|
|
31
|
-
|
|
32
|
-
const [errorState, setErrorState] = useState<ErrorState>({
|
|
33
|
-
error: null,
|
|
34
|
-
isRetrying: false,
|
|
35
|
-
retryCount: 0,
|
|
36
|
-
canRetry: false,
|
|
37
|
-
userMessage: null
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const retryTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
41
|
-
|
|
42
|
-
const handleError = useCallback((error: Error) => {
|
|
43
|
-
if (logErrors) {
|
|
44
|
-
logClientError(error, undefined)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
setErrorState(prevState => {
|
|
48
|
-
const currentRetryCount = prevState?.retryCount || 0
|
|
49
|
-
const canRetry = isRetryableError(error) && currentRetryCount < maxRetries
|
|
50
|
-
const userMessage = showUserFriendlyMessages && shouldShowErrorToUser(error)
|
|
51
|
-
? getErrorMessage(error)
|
|
52
|
-
: null
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
error,
|
|
56
|
-
isRetrying: false,
|
|
57
|
-
retryCount: currentRetryCount,
|
|
58
|
-
canRetry,
|
|
59
|
-
userMessage
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
onError?.(error)
|
|
64
|
-
}, [maxRetries, showUserFriendlyMessages, logErrors, onError])
|
|
65
|
-
|
|
66
|
-
const retry = useCallback(async (operation: () => Promise<any>) => {
|
|
67
|
-
if (!errorState.canRetry || errorState.isRetrying) {
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const newRetryCount = (errorState?.retryCount || 0) + 1
|
|
72
|
-
|
|
73
|
-
setErrorState(prev => ({
|
|
74
|
-
...prev,
|
|
75
|
-
isRetrying: true,
|
|
76
|
-
retryCount: newRetryCount
|
|
77
|
-
}))
|
|
78
|
-
|
|
79
|
-
onRetry?.(newRetryCount)
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// Add exponential backoff delay
|
|
83
|
-
const delay = Math.min(1000 * Math.pow(2, newRetryCount - 1), 10000)
|
|
84
|
-
await new Promise(resolve => {
|
|
85
|
-
retryTimeoutRef.current = setTimeout(resolve, delay)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const result = await operation()
|
|
89
|
-
|
|
90
|
-
// Clear error state on successful retry
|
|
91
|
-
setErrorState({
|
|
92
|
-
error: null,
|
|
93
|
-
isRetrying: false,
|
|
94
|
-
retryCount: 0,
|
|
95
|
-
canRetry: false,
|
|
96
|
-
userMessage: null
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
return result
|
|
100
|
-
} catch (error) {
|
|
101
|
-
const canRetryAgain = isRetryableError(error as Error) && newRetryCount < maxRetries
|
|
102
|
-
|
|
103
|
-
setErrorState(prev => ({
|
|
104
|
-
...prev,
|
|
105
|
-
error: error as Error,
|
|
106
|
-
isRetrying: false,
|
|
107
|
-
retryCount: newRetryCount,
|
|
108
|
-
canRetry: canRetryAgain,
|
|
109
|
-
userMessage: showUserFriendlyMessages && shouldShowErrorToUser(error)
|
|
110
|
-
? getErrorMessage(error)
|
|
111
|
-
: null
|
|
112
|
-
}))
|
|
113
|
-
|
|
114
|
-
if (!canRetryAgain) {
|
|
115
|
-
onMaxRetriesReached?.(error as Error)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw error
|
|
119
|
-
}
|
|
120
|
-
}, [errorState?.retryCount, errorState?.canRetry, errorState?.isRetrying, maxRetries, showUserFriendlyMessages, onRetry, onMaxRetriesReached])
|
|
121
|
-
|
|
122
|
-
const clearError = useCallback(() => {
|
|
123
|
-
if (retryTimeoutRef.current) {
|
|
124
|
-
clearTimeout(retryTimeoutRef.current)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
setErrorState({
|
|
128
|
-
error: null,
|
|
129
|
-
isRetrying: false,
|
|
130
|
-
retryCount: 0,
|
|
131
|
-
canRetry: false,
|
|
132
|
-
userMessage: null
|
|
133
|
-
})
|
|
134
|
-
}, [])
|
|
135
|
-
|
|
136
|
-
const executeWithErrorHandling = useCallback(async <T>(
|
|
137
|
-
operation: () => Promise<T>
|
|
138
|
-
): Promise<T | null> => {
|
|
139
|
-
try {
|
|
140
|
-
clearError()
|
|
141
|
-
return await operation()
|
|
142
|
-
} catch (error) {
|
|
143
|
-
handleError(error as Error)
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
}, [handleError, clearError])
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
...errorState,
|
|
150
|
-
handleError,
|
|
151
|
-
retry,
|
|
152
|
-
clearError,
|
|
153
|
-
executeWithErrorHandling
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Hook for handling API calls with automatic error handling
|
|
158
|
-
export function useApiCall<T>(
|
|
159
|
-
apiCall: () => Promise<T>,
|
|
160
|
-
options: UseErrorHandlerOptions & {
|
|
161
|
-
immediate?: boolean
|
|
162
|
-
dependencies?: any[]
|
|
163
|
-
} = {}
|
|
164
|
-
) {
|
|
165
|
-
const { immediate = false, dependencies = [], ...errorOptions } = options
|
|
166
|
-
|
|
167
|
-
const [data, setData] = useState<T | null>(null)
|
|
168
|
-
const [loading, setLoading] = useState(immediate)
|
|
169
|
-
|
|
170
|
-
const errorHandler = useErrorHandler(errorOptions)
|
|
171
|
-
|
|
172
|
-
const execute = useCallback(async (): Promise<T | null> => {
|
|
173
|
-
setLoading(true)
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const result = await apiCall()
|
|
177
|
-
setData(result)
|
|
178
|
-
errorHandler.clearError()
|
|
179
|
-
return result
|
|
180
|
-
} catch (error) {
|
|
181
|
-
errorHandler.handleError(error as Error)
|
|
182
|
-
return null
|
|
183
|
-
} finally {
|
|
184
|
-
setLoading(false)
|
|
185
|
-
}
|
|
186
|
-
}, [apiCall, errorHandler])
|
|
187
|
-
|
|
188
|
-
const retryCall = useCallback(async () => {
|
|
189
|
-
return errorHandler.retry(async () => {
|
|
190
|
-
setLoading(true)
|
|
191
|
-
try {
|
|
192
|
-
const result = await apiCall()
|
|
193
|
-
setData(result)
|
|
194
|
-
return result
|
|
195
|
-
} finally {
|
|
196
|
-
setLoading(false)
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
}, [apiCall, errorHandler])
|
|
200
|
-
|
|
201
|
-
// Execute immediately if requested
|
|
202
|
-
React.useEffect(() => {
|
|
203
|
-
if (immediate) {
|
|
204
|
-
execute()
|
|
205
|
-
}
|
|
206
|
-
}, [immediate, ...dependencies])
|
|
207
|
-
|
|
208
|
-
const { retry: errorHandlerRetry, ...restErrorHandler } = errorHandler
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
data,
|
|
212
|
-
loading,
|
|
213
|
-
execute,
|
|
214
|
-
retry: retryCall,
|
|
215
|
-
...restErrorHandler
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Hook for form submission with error handling
|
|
220
|
-
export function useFormSubmission<T>(
|
|
221
|
-
submitFunction: (data: any) => Promise<T>,
|
|
222
|
-
options: UseErrorHandlerOptions = {}
|
|
223
|
-
) {
|
|
224
|
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
225
|
-
const [submitData, setSubmitData] = useState<T | null>(null)
|
|
226
|
-
|
|
227
|
-
const errorHandler = useErrorHandler(options)
|
|
228
|
-
|
|
229
|
-
const submit = useCallback(async (formData: any): Promise<T | null> => {
|
|
230
|
-
setIsSubmitting(true)
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const result = await submitFunction(formData)
|
|
234
|
-
setSubmitData(result)
|
|
235
|
-
errorHandler.clearError()
|
|
236
|
-
return result
|
|
237
|
-
} catch (error) {
|
|
238
|
-
errorHandler.handleError(error as Error)
|
|
239
|
-
return null
|
|
240
|
-
} finally {
|
|
241
|
-
setIsSubmitting(false)
|
|
242
|
-
}
|
|
243
|
-
}, [submitFunction, errorHandler])
|
|
244
|
-
|
|
245
|
-
const retrySubmit = useCallback(async (formData: any) => {
|
|
246
|
-
return errorHandler.retry(() => submit(formData))
|
|
247
|
-
}, [submit, errorHandler])
|
|
248
|
-
|
|
249
|
-
const { retry: errorHandlerRetry, ...restErrorHandler } = errorHandler
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
submit,
|
|
253
|
-
retry: retrySubmit,
|
|
254
|
-
isSubmitting,
|
|
255
|
-
submitData,
|
|
256
|
-
...restErrorHandler
|
|
257
|
-
}
|
|
258
|
-
}
|