bake canonical build harness (deterministic /ship for forks) [restaurant]
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<string, any>;
|
||||
}
|
||||
|
||||
export interface ChannelMessage {
|
||||
id: string;
|
||||
data: any;
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface OddSocketsChannel {
|
||||
subscribe(cb: (msg: ChannelMessage) => void, opts?: Record<string, any>): () => void;
|
||||
publish(data: any, opts?: Record<string, any>): Promise<void>;
|
||||
getHistory(opts?: { count?: number; start?: string; end?: string }): Promise<ChannelMessage[]>;
|
||||
getPresence(): Promise<PresenceMember[]>;
|
||||
updateState(state: Record<string, any>): Promise<void>;
|
||||
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<string, Set<Listener>> = {};
|
||||
const history: Record<string, ChannelMessage[]> = {};
|
||||
const presence: Record<string, Record<string, PresenceMember>> = {};
|
||||
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();
|
||||
Reference in New Issue
Block a user