- apps/web: Vite + React 19 + TanStack Router/Query + shadcn/ui - apps/api: Hono + Bun on :3001 with /api/health, /api/auth/*, /mcp stubs - apps/worker: Bun worker stub, DB connection, graceful SIGTERM - apps/web-legacy/: old Next.js code moved aside (preserved for T2-T8 reference) - Dockerfiles: api (Bun), worker (Bun), spa (multi-stage Caddy) - Caddyfile: serves dist + reverse-proxies /api/* + /mcp to api - docker-compose.yml: 4-service target (api, spa, db, worker) - packages/db/src/client.ts: shared Drizzle client for api + worker - db/client.ts: root-level alias for convenience Parent: t_e1cbd87d -> t_24c9c3fd (T0)
95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useCallback,
|
|
useState,
|
|
useRef,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import { useRealtime, type RealtimeEvent } from '@/hooks/use-realtime';
|
|
|
|
type EventCallback = (event: RealtimeEvent) => void;
|
|
|
|
interface RealtimeContextType {
|
|
connected: boolean;
|
|
error: string | null;
|
|
reconnect: () => void;
|
|
disconnect: () => void;
|
|
subscribe: (
|
|
collections: string[],
|
|
callback: EventCallback
|
|
) => () => void;
|
|
}
|
|
|
|
const RealtimeContext = createContext<RealtimeContextType | null>(null);
|
|
|
|
export function RealtimeProvider({ children }: { children: ReactNode }) {
|
|
const subscribersRef = useRef<Map<string, Set<EventCallback>>>(new Map());
|
|
const [, forceRender] = useState(0);
|
|
|
|
const handleEvent = useCallback((event: RealtimeEvent) => {
|
|
const collection = event.collection || '*';
|
|
const collectionSubs = subscribersRef.current.get(collection);
|
|
const globalSubs = subscribersRef.current.get('*');
|
|
|
|
if (collectionSubs) {
|
|
collectionSubs.forEach((cb) => cb(event));
|
|
}
|
|
if (globalSubs) {
|
|
globalSubs.forEach((cb) => cb(event));
|
|
}
|
|
}, []);
|
|
|
|
const { connected, error, reconnect, disconnect } = useRealtime({
|
|
onEvent: handleEvent,
|
|
});
|
|
|
|
const subscribe = useCallback(
|
|
(collections: string[], callback: EventCallback) => {
|
|
for (const collection of collections) {
|
|
if (!subscribersRef.current.has(collection)) {
|
|
subscribersRef.current.set(collection, new Set());
|
|
}
|
|
subscribersRef.current.get(collection)!.add(callback);
|
|
}
|
|
// Also register as a global subscriber
|
|
if (!subscribersRef.current.has('*')) {
|
|
subscribersRef.current.set('*', new Set());
|
|
}
|
|
subscribersRef.current.get('*')!.add(callback);
|
|
|
|
forceRender((n) => n + 1);
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
for (const collection of collections) {
|
|
subscribersRef.current.get(collection)?.delete(callback);
|
|
}
|
|
subscribersRef.current.get('*')?.delete(callback);
|
|
forceRender((n) => n + 1);
|
|
};
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<RealtimeContext.Provider
|
|
value={{ connected, error, reconnect, disconnect, subscribe }}
|
|
>
|
|
{children}
|
|
</RealtimeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useRealtimeContext() {
|
|
const context = useContext(RealtimeContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
'useRealtimeContext must be used within a RealtimeProvider'
|
|
);
|
|
}
|
|
return context;
|
|
}
|