// --------------------------------------------------------------------------- // OddSockets Client — realtime pub/sub + presence for generated sites // --------------------------------------------------------------------------- // The realtime leg of the tyga backend bundle (alongside lib/anydb.ts data + // lib/tygarun.ts SaaS). Channels, presence and history via OddSockets. // // This stub ships UNCONFIGURED. When the project connects OddSockets, the // builder replaces ODDSOCKETS_CONFIG with real values (like lib/tygarun.ts) // and this file is swapped for the real client (oddsocketsService.getClientCode, // which imports `oddsockets-js`). Until then isConfigured() is false and the // hooks fall back to a LOCAL in-memory demo bus so the UI is alive out of the // box. The demo bus is clearly ephemeral — a broken real connect must surface // as an error, never be masked by the demo state. // --------------------------------------------------------------------------- const ODDSOCKETS_CONFIG = { // Always connect the manager LB — it assigns the worker and the SDK follows // the handoff. NEVER hardcode a worker URL. connectUrl: "__ODDSOCKETS_CONNECT_URL__", // e.g. https://connect.oddsockets.tyga.network // Per-site key (Authorization: Bearer ak_live_...). Isolation boundary = the // key owner (account). Provisioned per site by our reseller rail on connect. apiKey: "__ODDSOCKETS_KEY__", }; export function isConfigured(): boolean { return !ODDSOCKETS_CONFIG.apiKey.startsWith("__"); } export const CONNECT_URL = ODDSOCKETS_CONFIG.connectUrl; export interface PresenceMember { id: string; state?: Record; } export interface ChannelMessage { id: string; data: any; at: string; } export interface OddSocketsChannel { subscribe(cb: (msg: ChannelMessage) => void, opts?: Record): () => void; publish(data: any, opts?: Record): Promise; getHistory(opts?: { count?: number; start?: string; end?: string }): Promise; getPresence(): Promise; updateState(state: Record): Promise; unsubscribe(): void; } export interface OddSocketsClient { channel(name: string): OddSocketsChannel; connected: boolean; } // ── Local in-memory demo bus (used until OddSockets is connected) ──────────── // Simulates pub/sub + presence within the tab so realtime UIs render live in // demo mode. Not shared across clients — purely local, seed/demo only. type Listener = (msg: ChannelMessage) => void; function createDemoBus(): OddSocketsClient { const listeners: Record> = {}; const history: Record = {}; const presence: Record> = {}; const selfId = `demo_${Math.random().toString(36).slice(2, 8)}`; function channel(name: string): OddSocketsChannel { listeners[name] = listeners[name] || new Set(); history[name] = history[name] || []; presence[name] = presence[name] || {}; return { subscribe(cb, _opts) { listeners[name].add(cb); // announce demo self-presence presence[name][selfId] = { id: selfId, state: {} }; return () => listeners[name].delete(cb); }, async publish(data, _opts) { const msg: ChannelMessage = { id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, data, at: new Date().toISOString(), }; history[name].push(msg); if (history[name].length > 200) history[name].shift(); listeners[name].forEach((cb) => cb(msg)); }, async getHistory(opts = {}) { const all = history[name] || []; return opts.count ? all.slice(-opts.count) : all.slice(); }, async getPresence() { return Object.values(presence[name] || {}); }, async updateState(state) { presence[name][selfId] = { id: selfId, state }; }, unsubscribe() { listeners[name].clear(); delete presence[name][selfId]; }, }; } return { channel, connected: false }; } // Singleton client — demo bus in the stub; the real client (oddsockets-js) // replaces this whole file on connect via oddsocketsService.getClientCode. const client: OddSocketsClient = createDemoBus(); export default client; // Usage: // import oddsockets, { isConfigured } from '@/lib/oddsockets'; // const ch = oddsockets.channel('room:lobby'); // const off = ch.subscribe((m) => console.log(m.data)); // await ch.publish({ text: 'hello' }); // await ch.updateState({ typing: true }); // const members = await ch.getPresence();