Skip to content

Commit d7be18e

Browse files
author
Lasim
committed
feat(docs): enhance documentation for Server-Sent Events, global settings caching, frontend component organization, and introduce DsMeter component
1 parent 720be0b commit d7be18e

File tree

6 files changed

+689
-13
lines changed

6 files changed

+689
-13
lines changed

development/backend/api/sse.mdx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,95 @@ export default async function sseRoute(server: FastifyInstance) {
200200
}
201201
```
202202

203+
## Polling Pattern with Async Operations
204+
205+
When using `setInterval` with async database queries or API calls, you **must** check connection state **after** the async operation completes to prevent crashes:
206+
207+
```typescript
208+
export default async function pollingStreamRoute(server: FastifyInstance) {
209+
server.get('/metrics/stream', {
210+
sse: true,
211+
preValidation: requirePermission('metrics.view'),
212+
schema: {
213+
tags: ['Metrics'],
214+
summary: 'Stream metrics via SSE with polling',
215+
security: [{ cookieAuth: [] }]
216+
}
217+
}, async (request, reply) => {
218+
const userId = request.user!.id
219+
let pollInterval: NodeJS.Timeout | null = null
220+
221+
// Keep connection open
222+
reply.sse.keepAlive()
223+
224+
// Send initial snapshot
225+
const initialData = await fetchMetrics(userId)
226+
reply.sse.send({ event: 'snapshot', data: initialData })
227+
228+
// Poll for updates every 3 seconds
229+
pollInterval = setInterval(async () => {
230+
// Check #1: Before starting async work
231+
if (!reply.sse.isConnected) {
232+
if (pollInterval) clearInterval(pollInterval)
233+
return
234+
}
235+
236+
try {
237+
// Async database query or API call
238+
const data = await fetchMetrics(userId)
239+
240+
// ⚠️ CRITICAL: Check #2 after async operation completes
241+
if (!reply.sse.isConnected) {
242+
if (pollInterval) clearInterval(pollInterval)
243+
return
244+
}
245+
246+
// If sending multiple items in a loop, check before each send
247+
for (const item of data) {
248+
if (!reply.sse.isConnected) {
249+
if (pollInterval) clearInterval(pollInterval)
250+
return
251+
}
252+
reply.sse.send({ event: 'update', data: item })
253+
}
254+
} catch (error) {
255+
server.log.error(error, 'Failed to fetch metrics')
256+
// Don't crash - just log the error
257+
}
258+
}, 3000)
259+
260+
// Cleanup on disconnect
261+
reply.sse.onClose(() => {
262+
if (pollInterval) {
263+
clearInterval(pollInterval)
264+
pollInterval = null
265+
}
266+
server.log.debug({ userId }, 'Metrics stream closed')
267+
})
268+
})
269+
}
270+
```
271+
272+
### Why Multiple Checks Are Needed
273+
274+
**Without the second check after async operations:**
275+
```
276+
Time 0ms: setInterval fires → Check isConnected ✅ (connected)
277+
Time 5ms: Start database query (async)
278+
Time 50ms: Client disconnects → isConnected = false
279+
Time 100ms: Query completes → Call send() → CRASH! ❌
280+
```
281+
282+
**With proper checks:**
283+
```
284+
Time 0ms: setInterval fires → Check #1 ✅ (connected)
285+
Time 5ms: Start database query (async)
286+
Time 50ms: Client disconnects → isConnected = false
287+
Time 100ms: Query completes → Check #2 ✅ (disconnected) → Return early ✅
288+
```
289+
290+
The client can disconnect **during** async operations, so checking only at the start of the interval is insufficient.
291+
203292
## Frontend Client
204293

205294
```javascript

development/backend/global-settings.mdx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -904,11 +904,70 @@ const maxUploadSize = parseInt((await GlobalSettingsService.get('system.max_uplo
904904
- **Regular audits**: Review settings periodically for unused or outdated values
905905
- **Environment separation**: Use different encryption secrets for different environments
906906

907-
### Performance Considerations
907+
### Performance: In-Memory Cache
908+
909+
The global settings system uses an **in-memory cache** to eliminate database queries for read operations. This is critical for high-frequency endpoints like health checks (`/`) and Swagger documentation (`/documentation`).
910+
911+
#### How It Works
912+
913+
```
914+
┌─────────────────────────────────────────────────────────────┐
915+
│ Architecture │
916+
├─────────────────────────────────────────────────────────────┤
917+
│ Server Start → GlobalSettingsCache.load() → Memory Cache │
918+
│ │
919+
│ READ: GlobalSettings.get() → Memory (instant, no DB) │
920+
│ │
921+
│ WRITE: GlobalSettingsService.set/update/delete() │
922+
│ → DB + Cache Invalidation (reload from DB) │
923+
└─────────────────────────────────────────────────────────────┘
924+
```
925+
926+
#### Cache Lifecycle
927+
928+
1. **Startup**: After global settings initialization, all settings are loaded into memory
929+
2. **Reads**: All `GlobalSettings.get*()` calls read from memory (no database query)
930+
3. **Writes**: When settings are modified via `GlobalSettingsService`, the cache is automatically invalidated and reloaded
931+
4. **Fallback**: If cache is not initialized, reads fall back to database queries
932+
933+
#### When Cache Is Invalidated
934+
935+
The cache automatically reloads from the database after:
936+
937+
- `GlobalSettingsService.set()` - Create or update a setting
938+
- `GlobalSettingsService.setTyped()` - Create or update with type
939+
- `GlobalSettingsService.update()` - Update an existing setting
940+
- `GlobalSettingsService.delete()` - Delete a setting
941+
942+
#### Manual Cache Refresh
943+
944+
If you modify settings outside of `GlobalSettingsService` (not recommended), refresh the cache manually:
945+
946+
```typescript
947+
import { GlobalSettings } from '../global-settings';
948+
949+
await GlobalSettings.refreshCaches();
950+
```
951+
952+
#### Single-Instance Limitation
953+
954+
The current cache implementation is designed for **single-instance deployments**. Multi-instance support (horizontal scaling) with shared cache synchronization is planned for a future release.
955+
956+
#### Monitoring Cache Status
957+
958+
```typescript
959+
import { GlobalSettingsCache } from '../global-settings';
960+
961+
// Check if cache is ready
962+
if (GlobalSettingsCache.isInitialized()) {
963+
console.log(`Cache has ${GlobalSettingsCache.size()} settings`);
964+
}
965+
```
966+
967+
### Additional Performance Tips
908968

909-
- **Cache frequently accessed settings**: Consider caching non-sensitive, frequently used settings
910969
- **Batch operations**: Use bulk endpoints when creating multiple related settings
911-
- **Minimize database calls**: Retrieve settings by group when you need multiple related values
970+
- **Group retrieval**: Use `getGroupValues()` to fetch all settings in a group at once
912971

913972
### Error Handling
914973

development/frontend/architecture.mdx

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,72 @@ views/
6565

6666
Components are **reusable UI building blocks** that encapsulate specific functionality and presentation logic.
6767

68+
#### Component Organization Structure
69+
70+
**MANDATORY**: All feature-specific components must follow a hierarchical directory structure that mirrors the view organization. This creates clear ownership boundaries and improves code discoverability.
71+
72+
```
73+
components/
74+
├── ui/ # Design system components (shadcn-vue)
75+
│ ├── button/
76+
│ ├── card/
77+
│ └── input/
78+
├── [feature]/ # Feature-specific components
79+
│ ├── [sub-feature]/ # Nested feature organization
80+
│ │ ├── ComponentName.vue # Individual components
81+
│ │ ├── AnotherComponent.vue
82+
│ │ └── index.ts # Barrel exports (mandatory)
83+
│ └── index.ts
84+
├── AppSidebar.vue # Top-level shared components
85+
└── NavbarLayout.vue
86+
```
87+
88+
#### Real-World Example: MCP Server Components
89+
90+
```
91+
components/
92+
├── mcp-server/
93+
│ ├── installation/
94+
│ │ ├── InstallationInfo.vue
95+
│ │ ├── InstallationTabs.vue
96+
│ │ ├── McpToolsTab.vue
97+
│ │ ├── TeamConfiguration.vue
98+
│ │ ├── UserConfiguration.vue
99+
│ │ ├── DangerZone.vue
100+
│ │ └── index.ts # Export all installation components
101+
│ └── catalog/
102+
│ ├── ServerCard.vue
103+
│ ├── ServerFilters.vue
104+
│ └── index.ts
105+
```
106+
107+
#### Barrel Export Pattern (Mandatory)
108+
109+
Every feature component directory must include an `index.ts` file that exports all components. This creates a clean import API and makes refactoring easier.
110+
111+
```typescript
112+
// components/mcp-server/installation/index.ts
113+
export { default as InstallationInfo } from './InstallationInfo.vue'
114+
export { default as InstallationTabs } from './InstallationTabs.vue'
115+
export { default as McpToolsTab } from './McpToolsTab.vue'
116+
export { default as TeamConfiguration } from './TeamConfiguration.vue'
117+
export { default as UserConfiguration } from './UserConfiguration.vue'
118+
export { default as DangerZone } from './DangerZone.vue'
119+
```
120+
121+
#### Usage in Views
122+
123+
```vue
124+
<script setup lang="ts">
125+
// Clean, single-line imports for all related components
126+
import {
127+
InstallationInfo,
128+
InstallationTabs,
129+
DangerZone
130+
} from '@/components/mcp-server/installation'
131+
</script>
132+
```
133+
68134
#### Component Categories
69135

70136
1. **UI Components** (`/ui`): Generic, design-system components - read the [UI Design System](/development/frontend/ui/)
@@ -74,16 +140,34 @@ Components are **reusable UI building blocks** that encapsulate specific functio
74140
- Follow shadcn-vue patterns
75141
- No business logic
76142
- Highly reusable across features
77-
- Follow shadcn-vue patterns
78143

79-
2. **Feature Components** (`/components/[feature]`): Domain-specific components
144+
2. **Feature Components** (`/components/[feature]/[sub-feature]`): Domain-specific components
145+
- Must follow hierarchical organization matching views
80146
- Contain feature-specific logic
81147
- Reusable within their domain
148+
- Must include barrel exports via `index.ts`
82149
- May compose UI components
83150

84151
3. **Shared Components** (`/components`): Cross-feature components
85152
- Used across multiple features
86-
- Examples: `AppSidebar`, `DashboardLayout`
153+
- Examples: `AppSidebar`, `NavbarLayout`
154+
- Only place components here if they truly span features
155+
156+
#### Component Organization Rules
157+
158+
1. **Mirror View Structure**: Component organization should parallel view hierarchy
159+
2. **Feature Isolation**: Keep feature components within their feature directory
160+
3. **Mandatory Barrel Exports**: Every feature directory must export via `index.ts`
161+
4. **No Deep Nesting**: Maximum 3 levels deep (feature/sub-feature/component)
162+
5. **Colocation**: Related components stay together
163+
164+
#### When to Create a New Feature Directory
165+
166+
Create a new feature component directory when:
167+
- You have 3+ components related to the same feature
168+
- The components are reused across multiple views within the feature
169+
- The feature has clear boundaries and ownership
170+
- The components share common types or logic
87171

88172
#### Component Design Rules
89173

@@ -92,6 +176,13 @@ Components are **reusable UI building blocks** that encapsulate specific functio
92176
3. **Composition Pattern**: Break complex components into smaller parts
93177
4. **Self-Contained**: Components should work in isolation
94178

179+
#### Exceptions to the Structure
180+
181+
You may deviate from the structure for:
182+
- **One-off components**: Components used in a single view can stay in the view file
183+
- **UI library components**: shadcn-vue components in `/ui` follow their own structure
184+
- **Extremely small features**: Features with only 1-2 simple components
185+
95186
### Services Layer (`/services`)
96187

97188
Services handle **all external communication and business logic processing**. They act as the bridge between the frontend and backend APIs.
@@ -115,9 +206,85 @@ Services handle **all external communication and business logic processing**. Th
115206

116207
Composables are **reusable logic units** that leverage Vue's Composition API to share stateful logic across components.
117208

209+
#### Composable Organization Structure
210+
211+
**MANDATORY**: Feature-specific composables must follow a hierarchical directory structure that mirrors the component and view organization. This creates consistency across the codebase and makes related logic easy to find.
212+
213+
```
214+
composables/
215+
├── [feature]/ # Feature-specific composables
216+
│ ├── [sub-feature]/ # Nested feature organization
217+
│ │ ├── useFeatureLogic.ts # Individual composables
218+
│ │ ├── useAnotherLogic.ts
219+
│ │ └── index.ts # Barrel exports (mandatory)
220+
│ └── index.ts
221+
├── useAuth.ts # Top-level shared composables
222+
├── useEventBus.ts
223+
└── useBreadcrumbs.ts
224+
```
225+
226+
#### Real-World Example: MCP Server Composables
227+
228+
```
229+
composables/
230+
├── mcp-server/
231+
│ ├── installation/
232+
│ │ ├── useInstallationCache.ts
233+
│ │ ├── useInstallationForm.ts
234+
│ │ └── index.ts # Export all installation composables
235+
│ └── catalog/
236+
│ ├── useCatalogFilters.ts
237+
│ ├── useCatalogSearch.ts
238+
│ └── index.ts
239+
├── useAuth.ts
240+
└── useEventBus.ts
241+
```
242+
243+
#### Barrel Export Pattern (Mandatory)
244+
245+
Every feature composable directory must include an `index.ts` file that exports all composables. This mirrors the component structure and provides a clean import API.
246+
247+
```typescript
248+
// composables/mcp-server/installation/index.ts
249+
export { useMcpInstallationCache } from './useInstallationCache'
250+
export { useInstallationForm } from './useInstallationForm'
251+
```
252+
253+
#### Usage in Components
254+
255+
```vue
256+
<script setup lang="ts">
257+
// Clean imports matching the component import pattern
258+
import { useMcpInstallationCache } from '@/composables/mcp-server/installation'
259+
import { InstallationTabs } from '@/components/mcp-server/installation'
260+
261+
const {
262+
installation,
263+
isLoading,
264+
loadInstallation
265+
} = useMcpInstallationCache()
266+
</script>
267+
```
268+
269+
#### Composable Organization Rules
270+
271+
1. **Mirror Component Structure**: Composable organization should parallel component hierarchy
272+
2. **Feature Isolation**: Keep feature composables within their feature directory
273+
3. **Mandatory Barrel Exports**: Every feature directory must export via `index.ts`
274+
4. **No Deep Nesting**: Maximum 3 levels deep (feature/sub-feature/composable)
275+
5. **Colocation**: Keep related composables together
276+
277+
#### When to Create a Feature Composable Directory
278+
279+
Create a new feature composable directory when:
280+
- You have 2+ composables related to the same feature
281+
- The composables are reused across multiple components within the feature
282+
- The feature has clear boundaries and ownership
283+
- The composables share common types or state
284+
118285
#### Composable Design Patterns
119286

120-
1. **Naming Convention**: Always prefix with `use` (e.g., `useAuth`, `useEventBus`)
287+
1. **Naming Convention**: Always prefix with `use` (e.g., `useAuth`, `useEventBus`, `useInstallationCache`)
121288
2. **Single Purpose**: Each composable solves one specific problem
122289
3. **Return Interface**: Clearly define what's returned (state, methods, computed)
123290
4. **Lifecycle Awareness**: Handle setup/cleanup in lifecycle hooks
@@ -127,7 +294,14 @@ Composables are **reusable logic units** that leverage Vue's Composition API to
127294
- **Data Fetching**: `useAsyncData`, `usePagination`
128295
- **Form Handling**: `useForm`, `useValidation`
129296
- **UI State**: `useModal`, `useToast`
130-
- **Feature Logic**: `useTeamManagement`, `useCredentials`
297+
- **Feature Logic**: `useTeamManagement`, `useCredentials`, `useInstallationCache`
298+
299+
#### Exceptions to the Structure
300+
301+
You may deviate from the structure for:
302+
- **Global utilities**: Composables used across all features (e.g., `useAuth`, `useEventBus`)
303+
- **Single composable features**: Features with only one composable can stay at the root
304+
- **Third-party integrations**: External library wrappers may have their own structure
131305

132306
### Stores Layer (`/stores`)
133307

0 commit comments

Comments
 (0)