Skip to content

Commit b476f5a

Browse files
Merge pull request #251 from SheetMetalConnect/claude/improve-maintainability-scalability-01QQFLjFdCTSx1L2KxgjCzdx
Improve maintainability and scalability
2 parents d8b6e62 + 55561c7 commit b476f5a

File tree

10 files changed

+2423
-456
lines changed

10 files changed

+2423
-456
lines changed

docs/CODING_PATTERNS.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,172 @@ import Auth from "./pages/auth/Auth";
271271

272272
---
273273

274+
## Error Handling with AppError
275+
276+
Use the centralized error utilities for consistent error handling:
277+
278+
```tsx
279+
import { AppError, ErrorCode, getErrorMessage, fromSupabaseError } from '@/lib/errors';
280+
import { logger } from '@/lib/logger';
281+
282+
// Convert Supabase errors to AppError
283+
const { data, error } = await supabase.from('jobs').select('*');
284+
if (error) {
285+
throw fromSupabaseError(error, { operation: 'fetchJobs' });
286+
}
287+
288+
// Safe error message extraction
289+
try {
290+
await someOperation();
291+
} catch (error) {
292+
const message = getErrorMessage(error); // Always returns string
293+
logger.error('Operation failed', error, { operation: 'myOperation' });
294+
toast.error(message);
295+
}
296+
297+
// User-friendly error messages
298+
const appError = fromSupabaseError(error);
299+
toast.error(appError.toUserMessage()); // "The requested resource was not found."
300+
```
301+
302+
For detailed error handling patterns, see [docs/ERROR_HANDLING.md](./ERROR_HANDLING.md).
303+
304+
---
305+
306+
## Structured Logging
307+
308+
Use the logger for consistent, context-aware logging:
309+
310+
```tsx
311+
import { logger, createScopedLogger } from '@/lib/logger';
312+
313+
// Basic logging with context
314+
logger.info('Job created', {
315+
operation: 'createJob',
316+
entityType: 'job',
317+
entityId: jobId,
318+
tenantId,
319+
});
320+
321+
logger.error('Failed to update operation', error, {
322+
operation: 'updateOperation',
323+
entityId: operationId,
324+
});
325+
326+
// Scoped logger for hooks/components
327+
const log = createScopedLogger({
328+
operation: 'useJobOperations',
329+
entityType: 'job',
330+
});
331+
332+
log.info('Fetching job data');
333+
log.error('Failed to fetch', error);
334+
335+
// Performance timing
336+
const data = await logger.timedAsync('fetchMetrics', async () => {
337+
return await fetchAllMetrics();
338+
});
339+
```
340+
341+
---
342+
343+
## Realtime Subscriptions with Filtering
344+
345+
**Always filter subscriptions to reduce scope and prevent cascade refetches:**
346+
347+
```tsx
348+
import { useDebouncedCallback } from '@/hooks/useDebounce';
349+
350+
// GOOD - filtered by tenant_id with debouncing
351+
useEffect(() => {
352+
if (!tenantId) return;
353+
354+
const debouncedFetch = useDebouncedCallback(fetchData, 200);
355+
356+
const channel = supabase
357+
.channel(`operations-${tenantId}`)
358+
.on('postgres_changes', {
359+
event: '*',
360+
schema: 'public',
361+
table: 'operations',
362+
filter: `tenant_id=eq.${tenantId}`, // Filter to reduce scope
363+
}, () => {
364+
debouncedFetch(); // Debounce to prevent cascade
365+
})
366+
.subscribe();
367+
368+
return () => channel.unsubscribe();
369+
}, [tenantId]);
370+
371+
// BAD - no filter, triggers on ALL operations across all tenants
372+
.on('postgres_changes', {
373+
event: '*',
374+
schema: 'public',
375+
table: 'operations',
376+
// No filter - will trigger on every operation change!
377+
}, () => fetchData())
378+
```
379+
380+
**Or use the reusable subscription hook:**
381+
382+
```tsx
383+
import { useRealtimeSubscription, useTenantSubscription } from '@/hooks/useRealtimeSubscription';
384+
385+
// Tenant-scoped subscription
386+
useTenantSubscription(
387+
'operations',
388+
tenantId,
389+
() => refetch(),
390+
{ debounceMs: 200 }
391+
);
392+
393+
// Multiple tables
394+
useRealtimeSubscription({
395+
channelName: 'production-updates',
396+
tables: [
397+
{ table: 'operations', filter: `cell_id=eq.${cellId}` },
398+
{ table: 'time_entries', filter: `tenant_id=eq.${tenantId}` },
399+
],
400+
onDataChange: handleUpdate,
401+
debounceMs: 300,
402+
});
403+
```
404+
405+
---
406+
407+
## QueryKeys and Cache Management
408+
409+
Use the QueryKeys factory for consistent cache keys:
410+
411+
```tsx
412+
import { QueryKeys, StaleTime, CacheTime } from '@/lib/queryClient';
413+
import { invalidateOperationCaches } from '@/lib/cacheInvalidation';
414+
415+
// Query with standard key
416+
const { data } = useQuery({
417+
queryKey: QueryKeys.jobs.all(tenantId),
418+
queryFn: fetchJobs,
419+
staleTime: StaleTime.SHORT, // 30 seconds
420+
});
421+
422+
// Invalidate related caches on mutation
423+
const mutation = useMutation({
424+
mutationFn: updateOperation,
425+
onSuccess: () => {
426+
invalidateOperationCaches(queryClient, tenantId, operationId, cellId);
427+
},
428+
});
429+
```
430+
431+
**Stale time presets:**
432+
- `StaleTime.VERY_SHORT` - 10s (active operations, work queue)
433+
- `StaleTime.SHORT` - 30s (job lists, default)
434+
- `StaleTime.MEDIUM` - 2min (cell configurations)
435+
- `StaleTime.LONG` - 5min (user profiles)
436+
- `StaleTime.VERY_LONG` - 15min (app settings)
437+
438+
---
439+
274440
## Quick Reference Commands
275441

276442
```bash

0 commit comments

Comments
 (0)