import type { ChatMessage } from './runtime'; export type StreamplaceChatOptions = { endpoint?: string; wantedCollections?: string[]; wantedDids?: string[]; WebSocketImpl?: typeof WebSocket; }; export type StreamplaceChatEvent = { source: 'jetstream'; raw: unknown; message: ChatMessage; }; type CommitEnvelope = { kind?: string; did?: string; time_us?: number; commit?: { operation?: string; collection?: string; record?: { text?: string; message?: string; createdAt?: string; }; }; }; const DEFAULT_ENDPOINT = 'wss://jetstream2.us-east.bsky.network/subscribe'; export class StreamplaceChatAdapter { private readonly endpoint: string; private readonly wantedCollections: string[]; private readonly wantedDids: string[]; private readonly WebSocketImpl: typeof WebSocket; constructor(options: StreamplaceChatOptions = {}) { this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT; this.wantedCollections = options.wantedCollections ?? [process.env.STREAMPLACE_CHAT_NSID ?? 'app.bsky.feed.post']; this.wantedDids = options.wantedDids ?? []; this.WebSocketImpl = options.WebSocketImpl ?? WebSocket; } createUrl() { const url = new URL(this.endpoint); for (const collection of this.wantedCollections) { url.searchParams.append('wantedCollections', collection); } for (const did of this.wantedDids) { url.searchParams.append('wantedDids', did); } return url.toString(); } extractChatEvent(payload: string): StreamplaceChatEvent | undefined { const data = JSON.parse(payload) as CommitEnvelope; if (data.kind !== 'commit') return undefined; if (data.commit?.operation !== 'create') return undefined; const collection = data.commit?.collection; if (!collection || !this.wantedCollections.includes(collection)) return undefined; const text = (data.commit.record?.text ?? data.commit.record?.message)?.trim(); if (!text) return undefined; return { source: 'jetstream', raw: data, message: { author: data.did ?? 'unknown', text, timestamp: data.commit.record?.createdAt ?? (typeof data.time_us === 'number' ? new Date(Math.floor(data.time_us / 1000)).toISOString() : new Date().toISOString()), }, }; } subscribe(onEvent: (event: StreamplaceChatEvent) => void) { const ws = new this.WebSocketImpl(this.createUrl()); ws.onmessage = (event) => { const text = typeof event.data === 'string' ? event.data : String(event.data); const parsed = this.extractChatEvent(text); if (parsed) onEvent(parsed); }; return ws; } }