create-fluxstack 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/resources/live-auth.md +462 -465
- package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +45 -3
- package/app/client/src/components/AppLayout.tsx +10 -1
- package/app/client/src/components/ErrorBoundary.tsx +117 -0
- package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
- package/app/client/src/components/LiveUploadWidget.tsx +10 -14
- package/app/client/src/lib/eden-api.ts +6 -0
- package/app/client/src/lib/plugin-hooks.ts +82 -0
- package/app/client/src/live/AuthDemo.tsx +0 -1
- package/app/client/src/live/FormDemo.tsx +1 -1
- package/app/client/src/live/PingPongDemo.tsx +4 -1
- package/app/client/src/live/RoomChatDemo.tsx +90 -50
- package/app/client/src/live/SharedCounterDemo.tsx +5 -0
- package/app/server/auth/AuthManager.ts +24 -0
- package/app/server/auth/contracts.ts +12 -1
- package/app/server/auth/errors.ts +84 -0
- package/app/server/auth/guards/TokenGuard.ts +5 -2
- package/app/server/auth/index.ts +1 -1
- package/app/server/auth/providers/InMemoryProvider.ts +1 -1
- package/app/server/index.ts +3 -4
- package/app/server/live/LiveAdminPanel.ts +8 -8
- package/app/server/live/LiveForm.ts +1 -1
- package/app/server/live/LiveProtectedChat.ts +5 -5
- package/app/server/live/LiveRoomChat.ts +50 -28
- package/app/server/live/LiveUpload.ts +17 -3
- package/app/server/live/auto-generated-components.ts +26 -0
- package/app/server/live/rooms/ChatRoom.ts +17 -2
- package/app/server/routes/auth.routes.ts +29 -20
- package/app/server/routes/index.ts +9 -0
- package/app/server/routes/room.routes.ts +6 -6
- package/config/index.ts +3 -3
- package/config/system/app.config.ts +1 -1
- package/config/system/auth.config.ts +1 -1
- package/config/system/build.config.ts +8 -6
- package/config/system/client.config.ts +6 -4
- package/config/system/database.config.ts +1 -1
- package/config/system/logger.config.ts +1 -1
- package/config/system/monitoring.config.ts +6 -4
- package/config/system/plugins.config.ts +1 -1
- package/config/system/runtime.config.ts +1 -1
- package/config/system/server.config.ts +1 -1
- package/config/system/services.config.ts +1 -1
- package/config/system/session.config.ts +3 -3
- package/config/system/system.config.ts +1 -1
- package/core/build/vite-plugins.ts +3 -2
- package/core/cli/generators/plugin.ts +1 -1
- package/core/config/index.ts +8 -1
- package/core/framework/server.ts +9 -5
- package/core/index.ts +1 -1
- package/core/plugins/index.ts +1 -1
- package/core/plugins/manager.ts +5 -1
- package/core/plugins/types.ts +17 -1
- package/core/server/index.ts +5 -2
- package/core/server/live/index.ts +8 -71
- package/core/server/plugin-client-hooks.ts +97 -0
- package/core/types/types.ts +1 -0
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +8 -5
- package/src/client/components/ui/StatusBadge.tsx +23 -0
- package/core/utils/config-schema.ts +0 -480
- package/core/utils/env.ts +0 -305
- package/plugins/crypto-auth/README.md +0 -788
- package/plugins/crypto-auth/ai-context.md +0 -1282
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
- package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
- package/plugins/crypto-auth/client/components/index.ts +0 -12
- package/plugins/crypto-auth/client/index.ts +0 -12
- package/plugins/crypto-auth/config/index.ts +0 -34
- package/plugins/crypto-auth/index.ts +0 -173
- package/plugins/crypto-auth/package.json +0 -66
- package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
- package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
- package/plugins/crypto-auth/server/index.ts +0 -25
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
- package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
- package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
- package/plugins/crypto-auth/server/middlewares.ts +0 -19
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class LiveAdminPanel {
|
|
2
|
+
static componentName = 'LiveAdminPanel'
|
|
3
|
+
static defaultState = {
|
|
4
|
+
users: [
|
|
5
|
+
{ id: '1', name: 'Alice', role: 'admin', createdAt: Date.now() },
|
|
6
|
+
{ id: '2', name: 'Bob', role: 'user', createdAt: Date.now() },
|
|
7
|
+
{ id: '3', name: 'Carol', role: 'moderator', createdAt: Date.now() },
|
|
8
|
+
],
|
|
9
|
+
audit: [],
|
|
10
|
+
currentUser: null,
|
|
11
|
+
currentRoles: [],
|
|
12
|
+
isAdmin: false,
|
|
13
|
+
}
|
|
14
|
+
static publicActions = ['getAuthInfo', 'init', 'listUsers', 'addUser', 'deleteUser', 'clearAudit']
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class LiveRoomChat {
|
|
2
|
+
static componentName = 'LiveRoomChat'
|
|
3
|
+
static defaultState = {
|
|
4
|
+
username: '',
|
|
5
|
+
activeRoom: null,
|
|
6
|
+
rooms: [],
|
|
7
|
+
messages: {},
|
|
8
|
+
customRooms: []
|
|
9
|
+
}
|
|
10
|
+
static publicActions = ['createRoom', 'joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername']
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class LiveUpload {
|
|
2
|
+
static componentName = 'LiveUpload'
|
|
3
|
+
static defaultState = {
|
|
4
|
+
status: 'idle',
|
|
5
|
+
progress: 0,
|
|
6
|
+
fileName: '',
|
|
7
|
+
fileSize: 0,
|
|
8
|
+
fileType: '',
|
|
9
|
+
fileUrl: '',
|
|
10
|
+
bytesUploaded: 0,
|
|
11
|
+
totalBytes: 0,
|
|
12
|
+
error: null
|
|
13
|
+
}
|
|
14
|
+
static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset']
|
|
15
|
+
}
|
package/app/client/src/App.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
2
|
import { Routes, Route } from 'react-router'
|
|
3
3
|
import { api } from './lib/eden-api'
|
|
4
|
-
import { LiveComponentsProvider } from '@/core/client'
|
|
4
|
+
import { LiveComponentsProvider, useLiveComponents } from '@/core/client'
|
|
5
|
+
import { executeHook } from './lib/plugin-hooks'
|
|
5
6
|
import { FormDemo } from './live/FormDemo'
|
|
6
7
|
import { CounterDemo } from './live/CounterDemo'
|
|
7
8
|
import { UploadDemo } from './live/UploadDemo'
|
|
@@ -14,6 +15,46 @@ import { DemoPage } from './components/DemoPage'
|
|
|
14
15
|
import { HomePage } from './pages/HomePage'
|
|
15
16
|
import { ApiTestPage } from './pages/ApiTestPage'
|
|
16
17
|
|
|
18
|
+
function NotFoundPage() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center px-4">
|
|
21
|
+
<h1 className="text-6xl font-black text-white mb-4">404</h1>
|
|
22
|
+
<p className="text-xl text-gray-400 mb-6">Pagina nao encontrada</p>
|
|
23
|
+
<p className="text-sm text-gray-500 mb-8">
|
|
24
|
+
O caminho <code className="text-purple-400">{window.location.pathname}</code> nao existe.
|
|
25
|
+
</p>
|
|
26
|
+
<a
|
|
27
|
+
href="/"
|
|
28
|
+
className="px-6 py-3 rounded-xl bg-purple-500/20 border border-purple-500/30 text-purple-200 hover:bg-purple-500/30 transition-all"
|
|
29
|
+
>
|
|
30
|
+
Voltar ao inicio
|
|
31
|
+
</a>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Executes the 'onLiveConnect' plugin hook once when
|
|
38
|
+
* the LiveComponents WebSocket connection is established.
|
|
39
|
+
*/
|
|
40
|
+
function PluginHookExecutor() {
|
|
41
|
+
const { connected, getWebSocket } = useLiveComponents()
|
|
42
|
+
const firedRef = useRef(false)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (connected && !firedRef.current) {
|
|
46
|
+
firedRef.current = true
|
|
47
|
+
|
|
48
|
+
executeHook('onLiveConnect', { connected, getWebSocket }).catch(() => {})
|
|
49
|
+
}
|
|
50
|
+
if (!connected) {
|
|
51
|
+
firedRef.current = false
|
|
52
|
+
}
|
|
53
|
+
}, [connected, getWebSocket])
|
|
54
|
+
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
17
58
|
function AppContent() {
|
|
18
59
|
const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('checking')
|
|
19
60
|
const [apiResponse, setApiResponse] = useState<string>('')
|
|
@@ -150,7 +191,7 @@ function AppContent() {
|
|
|
150
191
|
</DemoPage>
|
|
151
192
|
}
|
|
152
193
|
/>
|
|
153
|
-
<Route path="*" element={<
|
|
194
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
154
195
|
</Route>
|
|
155
196
|
</Routes>
|
|
156
197
|
)
|
|
@@ -173,6 +214,7 @@ function App() {
|
|
|
173
214
|
heartbeatInterval={30000}
|
|
174
215
|
debug={false}
|
|
175
216
|
>
|
|
217
|
+
<PluginHookExecutor />
|
|
176
218
|
<AppContent />
|
|
177
219
|
</LiveComponentsProvider>
|
|
178
220
|
)
|
|
@@ -28,7 +28,9 @@ const routeFlameHue: Record<string, string> = {
|
|
|
28
28
|
'/api-test': '90deg', // lima
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Cache favicon blob URLs by hue to avoid recreating blobs on every navigation
|
|
31
|
+
// Cache favicon blob URLs by hue to avoid recreating blobs on every navigation.
|
|
32
|
+
// Limited to MAX_FAVICON_CACHE entries; old blob URLs are revoked to prevent leaks.
|
|
33
|
+
const MAX_FAVICON_CACHE = 20
|
|
32
34
|
const faviconUrlCache = new Map<string, string>()
|
|
33
35
|
|
|
34
36
|
export function AppLayout() {
|
|
@@ -43,6 +45,13 @@ export function AppLayout() {
|
|
|
43
45
|
const hue = routeFlameHue[location.pathname] || '0deg'
|
|
44
46
|
let url = faviconUrlCache.get(hue)
|
|
45
47
|
if (!url) {
|
|
48
|
+
// Evict oldest entry if cache is full, revoking blob URL to free memory
|
|
49
|
+
if (faviconUrlCache.size >= MAX_FAVICON_CACHE) {
|
|
50
|
+
const oldestKey = faviconUrlCache.keys().next().value!
|
|
51
|
+
const oldestUrl = faviconUrlCache.get(oldestKey)!
|
|
52
|
+
URL.revokeObjectURL(oldestUrl)
|
|
53
|
+
faviconUrlCache.delete(oldestKey)
|
|
54
|
+
}
|
|
46
55
|
const colored = faviconSvg.replace(
|
|
47
56
|
'<svg ',
|
|
48
57
|
`<svg style="filter: hue-rotate(${hue})" `
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Component, type ReactNode, type ErrorInfo } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ErrorBoundaryProps {
|
|
4
|
+
children: ReactNode
|
|
5
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
|
|
6
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ErrorBoundaryState {
|
|
10
|
+
hasError: boolean
|
|
11
|
+
error: Error | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
15
|
+
state: ErrorBoundaryState = { hasError: false, error: null }
|
|
16
|
+
|
|
17
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
18
|
+
return { hasError: true, error }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
22
|
+
console.error('[FluxStack] Component error:', error, errorInfo.componentStack)
|
|
23
|
+
this.props.onError?.(error, errorInfo)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reset = () => {
|
|
27
|
+
this.setState({ hasError: false, error: null })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
if (this.state.hasError && this.state.error) {
|
|
32
|
+
if (typeof this.props.fallback === 'function') {
|
|
33
|
+
return this.props.fallback(this.state.error, this.reset)
|
|
34
|
+
}
|
|
35
|
+
if (this.props.fallback) {
|
|
36
|
+
return this.props.fallback
|
|
37
|
+
}
|
|
38
|
+
return <DefaultErrorFallback error={this.state.error} onReset={this.reset} />
|
|
39
|
+
}
|
|
40
|
+
return this.props.children
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function DefaultErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {
|
|
45
|
+
const isDev = import.meta.env.DEV
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="relative flex min-h-screen items-center justify-center overflow-hidden px-6 py-10 bg-gradient-to-br from-slate-900 via-purple-900/80 to-slate-900">
|
|
49
|
+
<style>{`
|
|
50
|
+
@keyframes fluxstack-slow-pulse {
|
|
51
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
52
|
+
50% { transform: scale(1.06); opacity: 1; }
|
|
53
|
+
}
|
|
54
|
+
`}</style>
|
|
55
|
+
|
|
56
|
+
<div className="pointer-events-none absolute inset-0">
|
|
57
|
+
<div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-linear-to-r from-purple-500/18 to-indigo-500/14 blur-3xl" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="relative w-full max-w-xl">
|
|
61
|
+
<div className="absolute -inset-2 rounded-3xl bg-linear-to-r from-purple-500/16 via-fuchsia-500/10 to-indigo-500/16 blur-2xl" />
|
|
62
|
+
|
|
63
|
+
<div className="relative rounded-2xl border border-white/[0.06] bg-white/[0.03] p-8 text-center shadow-2xl backdrop-blur-xl sm:p-10">
|
|
64
|
+
<div className="mx-auto mb-6 flex h-18 w-18 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
|
|
65
|
+
<svg
|
|
66
|
+
className="h-9 w-9 text-purple-300"
|
|
67
|
+
style={{ animation: 'fluxstack-slow-pulse 3.6s ease-in-out infinite' }}
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
strokeWidth="1.7"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8.25v4.5" />
|
|
75
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 16.5h.008v.008H12V16.5Z" />
|
|
76
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10.252 3.942 1.93 18.002A1.5 1.5 0 0 0 3.22 20.25h17.56a1.5 1.5 0 0 0 1.29-2.248L13.748 3.942a2 2 0 0 0-3.496 0Z" />
|
|
77
|
+
</svg>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<h2 className="mb-3 text-2xl font-semibold tracking-tight text-white sm:text-3xl">Something went wrong</h2>
|
|
81
|
+
<p className="mx-auto mb-7 max-w-md text-sm leading-6 text-white/60 sm:text-base">
|
|
82
|
+
FluxStack hit an unexpected rendering error in this view. You can safely retry and continue working.
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
{isDev && (
|
|
86
|
+
<details className="mb-7 overflow-hidden rounded-2xl border border-white/[0.06] bg-black/20 text-left group">
|
|
87
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-medium text-white/75 transition-colors hover:text-white">
|
|
88
|
+
<span>Error details</span>
|
|
89
|
+
<svg className="h-4 w-4 text-white/45 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
|
|
90
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m5 7.5 5 5 5-5" />
|
|
91
|
+
</svg>
|
|
92
|
+
</summary>
|
|
93
|
+
<div className="border-t border-white/[0.06] px-4 py-4">
|
|
94
|
+
<pre className="max-h-56 overflow-auto rounded-xl border border-white/[0.06] bg-black/35 p-4 font-mono text-xs leading-6 text-white/70 whitespace-pre-wrap break-words">
|
|
95
|
+
{error.message}
|
|
96
|
+
{error.stack && `\n\n${error.stack}`}
|
|
97
|
+
</pre>
|
|
98
|
+
</div>
|
|
99
|
+
</details>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
onClick={onReset}
|
|
104
|
+
className="inline-flex items-center justify-center rounded-xl bg-linear-to-r from-purple-500 to-indigo-500 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-purple-950/30 transition-transform duration-200 hover:scale-[1.02] hover:from-purple-400 hover:to-indigo-400"
|
|
105
|
+
>
|
|
106
|
+
Try again
|
|
107
|
+
</button>
|
|
108
|
+
|
|
109
|
+
<p className="mt-4 text-xs leading-5 text-white/45 sm:text-sm">
|
|
110
|
+
If the issue persists, refresh the page or check the console for more context.
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { ErrorBoundary } from './ErrorBoundary'
|
|
3
|
+
|
|
4
|
+
function LiveErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {
|
|
5
|
+
const isDev = import.meta.env.DEV
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="relative flex min-h-[60vh] items-center justify-center overflow-hidden px-6 py-10">
|
|
9
|
+
<style>{`
|
|
10
|
+
@keyframes fluxstack-slow-pulse {
|
|
11
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
12
|
+
50% { transform: scale(1.06); opacity: 1; }
|
|
13
|
+
}
|
|
14
|
+
`}</style>
|
|
15
|
+
|
|
16
|
+
<div className="pointer-events-none absolute inset-0">
|
|
17
|
+
<div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-linear-to-r from-amber-500/18 to-orange-500/14 blur-3xl" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="relative mx-auto w-full max-w-xl">
|
|
21
|
+
<div className="absolute -inset-2 rounded-3xl bg-linear-to-r from-amber-500/16 via-orange-500/10 to-amber-500/16 blur-2xl" />
|
|
22
|
+
|
|
23
|
+
<div className="relative rounded-2xl border border-white/[0.06] bg-white/[0.03] p-8 text-center shadow-2xl backdrop-blur-xl sm:p-10">
|
|
24
|
+
<div className="mx-auto mb-6 flex h-18 w-18 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
|
|
25
|
+
<svg
|
|
26
|
+
className="h-9 w-9 text-amber-300"
|
|
27
|
+
style={{ animation: 'fluxstack-slow-pulse 3.6s ease-in-out infinite' }}
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
strokeWidth="1.7"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.5 8.5c5.25-5.25 13.75-5.25 19 0" />
|
|
35
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5.75 11.75c3.45-3.45 9.05-3.45 12.5 0" />
|
|
36
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.25 15.25c1.52-1.52 3.98-1.52 5.5 0" />
|
|
37
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
|
|
38
|
+
</svg>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<h3 className="mb-3 text-2xl font-semibold tracking-tight text-white sm:text-3xl">Live connection interrupted</h3>
|
|
42
|
+
<p className="mx-auto mb-7 max-w-md text-sm leading-6 text-white/60 sm:text-base">
|
|
43
|
+
This real-time FluxStack component lost its live connection or hit a transient runtime error.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
{isDev && (
|
|
47
|
+
<details className="mb-7 overflow-hidden rounded-2xl border border-white/[0.06] bg-black/20 text-left group">
|
|
48
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-medium text-white/75 transition-colors hover:text-white">
|
|
49
|
+
<span>Error details</span>
|
|
50
|
+
<svg className="h-4 w-4 text-white/45 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
|
|
51
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m5 7.5 5 5 5-5" />
|
|
52
|
+
</svg>
|
|
53
|
+
</summary>
|
|
54
|
+
<div className="border-t border-white/[0.06] px-4 py-4">
|
|
55
|
+
<pre className="max-h-56 overflow-auto rounded-xl border border-white/[0.06] bg-black/35 p-4 font-mono text-xs leading-6 text-white/70 whitespace-pre-wrap break-words">
|
|
56
|
+
{error.message}
|
|
57
|
+
{error.stack && `\n\n${error.stack}`}
|
|
58
|
+
</pre>
|
|
59
|
+
</div>
|
|
60
|
+
</details>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<button
|
|
64
|
+
onClick={onReset}
|
|
65
|
+
className="inline-flex items-center justify-center rounded-xl bg-linear-to-r from-amber-500 to-orange-500 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-orange-950/30 transition-transform duration-200 hover:scale-[1.02] hover:from-amber-400 hover:to-orange-400"
|
|
66
|
+
>
|
|
67
|
+
Reconnect
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
<p className="mt-4 text-xs leading-5 text-white/45 sm:text-sm">
|
|
71
|
+
Reconnecting usually restores live updates within a few seconds.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function LiveErrorBoundary({ children }: { children: ReactNode }) {
|
|
80
|
+
return (
|
|
81
|
+
<ErrorBoundary
|
|
82
|
+
fallback={(error, reset) => <LiveErrorFallback error={error} onReset={reset} />}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -2,20 +2,16 @@ import { useEffect, useMemo, useState } from 'react'
|
|
|
2
2
|
import { useLiveChunkedUpload } from '@/core/client'
|
|
3
3
|
import type { LiveChunkedUploadOptions } from '@/core/client'
|
|
4
4
|
import type { FileUploadCompleteResponse } from '@core/types/types'
|
|
5
|
-
|
|
5
|
+
import { LiveUpload } from '@server/live/LiveUpload'
|
|
6
|
+
|
|
7
|
+
// Derive the state type from the actual LiveUpload component to avoid duplication
|
|
8
|
+
type LiveUploadState = typeof LiveUpload.defaultState
|
|
9
|
+
|
|
10
|
+
// Minimal interface for any Live.use() proxy compatible with LiveUpload
|
|
11
|
+
interface LiveUploadProxy {
|
|
6
12
|
$componentId: string | null
|
|
7
13
|
$connected: boolean
|
|
8
|
-
$state:
|
|
9
|
-
status: 'idle' | 'uploading' | 'complete' | 'error'
|
|
10
|
-
progress: number
|
|
11
|
-
fileName: string
|
|
12
|
-
fileSize: number
|
|
13
|
-
fileType: string
|
|
14
|
-
fileUrl: string
|
|
15
|
-
bytesUploaded: number
|
|
16
|
-
totalBytes: number
|
|
17
|
-
error: string | null
|
|
18
|
-
}
|
|
14
|
+
$state: LiveUploadState
|
|
19
15
|
$error?: string | null
|
|
20
16
|
startUpload: (payload: { fileName: string; fileSize: number; fileType: string }) => Promise<any>
|
|
21
17
|
updateProgress: (payload: { progress: number; bytesUploaded: number; totalBytes: number }) => Promise<any>
|
|
@@ -25,7 +21,7 @@ type LiveUploadActions = {
|
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
export interface LiveUploadWidgetProps {
|
|
28
|
-
live:
|
|
24
|
+
live: LiveUploadProxy
|
|
29
25
|
title?: string
|
|
30
26
|
description?: string
|
|
31
27
|
allowPreview?: boolean
|
|
@@ -133,7 +129,7 @@ export function LiveUploadWidget({
|
|
|
133
129
|
type="file"
|
|
134
130
|
onChange={handleSelectFile}
|
|
135
131
|
className="w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-white/10 file:text-white hover:file:bg-white/20"
|
|
136
|
-
disabled={!live.$connected}
|
|
132
|
+
disabled={!live.$connected || uploading}
|
|
137
133
|
/>
|
|
138
134
|
|
|
139
135
|
<div className="flex gap-3">
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createEdenClient, getErrorMessage } from '@/core/client/api'
|
|
10
10
|
import type { App } from '@server/app'
|
|
11
|
+
import { executeHook } from './plugin-hooks'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* API client with full type inference from server routes
|
|
@@ -70,6 +71,11 @@ export const api = createEdenClient<App>({
|
|
|
70
71
|
// enableLogging: true,
|
|
71
72
|
})
|
|
72
73
|
|
|
74
|
+
// Execute plugin hooks after Eden client is created
|
|
75
|
+
executeHook('onEdenInit', { eden: api }).catch(() => {
|
|
76
|
+
// Silently ignore — hooks are best-effort
|
|
77
|
+
})
|
|
78
|
+
|
|
73
79
|
// Re-export utility for convenience
|
|
74
80
|
export { getErrorMessage }
|
|
75
81
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Client Hooks
|
|
3
|
+
*
|
|
4
|
+
* Fetches JavaScript hooks registered by server-side plugins
|
|
5
|
+
* and executes them at specific hook points in the client lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Hooks are cached after the first fetch (one HTTP request per page load).
|
|
8
|
+
* Errors in individual hooks are caught and logged without crashing the app.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let cachedHooks: Record<string, string[]> | null = null
|
|
12
|
+
let loadPromise: Promise<Record<string, string[]>> | null = null
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch plugin hooks from the server. Results are cached so only
|
|
16
|
+
* one HTTP request is made per page load.
|
|
17
|
+
*
|
|
18
|
+
* The server signs the response with HMAC-SHA256 to ensure integrity.
|
|
19
|
+
* The signature is included for auditability — the client trusts the
|
|
20
|
+
* server via same-origin HTTPS.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadPluginHooks(): Promise<Record<string, string[]>> {
|
|
23
|
+
if (cachedHooks) return cachedHooks
|
|
24
|
+
|
|
25
|
+
// Deduplicate concurrent calls during initial load
|
|
26
|
+
if (loadPromise) return loadPromise
|
|
27
|
+
|
|
28
|
+
loadPromise = (async (): Promise<Record<string, string[]>> => {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch('/api/__plugins/client-hooks')
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
console.warn(`[plugin-hooks] Failed to load hooks: HTTP ${res.status}`)
|
|
33
|
+
cachedHooks = {}
|
|
34
|
+
return cachedHooks
|
|
35
|
+
}
|
|
36
|
+
const data = await res.json()
|
|
37
|
+
cachedHooks = data.hooks ?? {}
|
|
38
|
+
return cachedHooks!
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.warn('[plugin-hooks] Failed to fetch hooks:', e)
|
|
41
|
+
cachedHooks = {}
|
|
42
|
+
return cachedHooks!
|
|
43
|
+
}
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
return loadPromise
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Execute all code strings registered for a given hook name.
|
|
51
|
+
*
|
|
52
|
+
* Each code string is wrapped in a `new Function(...)` call with the
|
|
53
|
+
* provided context keys as parameter names, so the code can reference
|
|
54
|
+
* them directly (e.g., `eden`, `connection`).
|
|
55
|
+
*
|
|
56
|
+
* Errors in individual hooks are caught and logged — they never crash
|
|
57
|
+
* the calling code.
|
|
58
|
+
*
|
|
59
|
+
* @param hookName - The hook point to execute (e.g., 'onEdenInit', 'onLiveConnect')
|
|
60
|
+
* @param context - Key/value pairs available as local variables inside the hook code
|
|
61
|
+
*/
|
|
62
|
+
export async function executeHook(
|
|
63
|
+
hookName: string,
|
|
64
|
+
context?: Record<string, unknown>
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const hooks = await loadPluginHooks()
|
|
67
|
+
const codes = hooks[hookName] || []
|
|
68
|
+
|
|
69
|
+
if (codes.length === 0) return
|
|
70
|
+
|
|
71
|
+
const keys = Object.keys(context || {})
|
|
72
|
+
const values = Object.values(context || {})
|
|
73
|
+
|
|
74
|
+
for (const code of codes) {
|
|
75
|
+
try {
|
|
76
|
+
const fn = new Function(...keys, code)
|
|
77
|
+
fn(...values)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn(`[plugin-hooks] Error executing "${hookName}" hook:`, e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import { useState } from 'react'
|
|
12
12
|
import { Live, useLiveComponents } from '@/core/client'
|
|
13
|
-
import type { LiveAuthOptions } from '@/core/client'
|
|
14
13
|
import { LiveCounter } from '@server/live/LiveCounter'
|
|
15
14
|
import { LiveAdminPanel } from '@server/live/LiveAdminPanel'
|
|
16
15
|
|
|
@@ -87,7 +87,7 @@ export function FormDemo() {
|
|
|
87
87
|
alert(err.message || 'Erro ao enviar')
|
|
88
88
|
}
|
|
89
89
|
}}
|
|
90
|
-
disabled={!form.$connected}
|
|
90
|
+
disabled={!form.$connected || form.$loading}
|
|
91
91
|
className="flex-1 px-4 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg font-medium hover:shadow-lg hover:shadow-purple-500/30 transition-all disabled:opacity-50"
|
|
92
92
|
>
|
|
93
93
|
{form.$loading ? 'Enviando...' : 'Enviar'}
|
|
@@ -72,7 +72,10 @@ export function PingPongDemo() {
|
|
|
72
72
|
intervalRef.current = setInterval(sendPing, 500)
|
|
73
73
|
}
|
|
74
74
|
return () => {
|
|
75
|
-
if (intervalRef.current)
|
|
75
|
+
if (intervalRef.current != null) {
|
|
76
|
+
clearInterval(intervalRef.current)
|
|
77
|
+
intervalRef.current = null
|
|
78
|
+
}
|
|
76
79
|
}
|
|
77
80
|
}, [autoPing, live.$connected, sendPing])
|
|
78
81
|
|