import type { XOInvitation } from "@xo-cash/types"; import { EventEmitter } from "./event-emitter.js"; import { SSESession, type SSEvent } from "./sse-client.js"; import { decodeExtendedJson, encodeExtendedJson } from "./ext-json.js"; export type SyncServerEventMap = { connected: void; disconnected: void; error: Error; message: SSEvent; }; export class SyncServer extends EventEmitter { static async from( baseUrl: string, invitationIdentifier: string, ): Promise { const server = new SyncServer(baseUrl, invitationIdentifier); await server.connect(); return server; } private sse: SSESession; constructor( private readonly baseUrl: string, private readonly invitationIdentifier: string, ) { super(); // Create an SSE Session this.sse = new SSESession( `${baseUrl}/invitations?invitationIdentifier=${invitationIdentifier}`, { method: "GET", headers: { Accept: "text/event-stream", }, // Create our event bubblers onMessage: (event: SSEvent) => this.emit("message", event), onError: (error: unknown) => this.emit( "error", error instanceof Error ? error : new Error(String(error)), ), onDisconnected: () => this.emit("disconnected", undefined), onConnected: () => this.emit("connected", undefined), }, ); } /** * Connect to the sync server. */ async connect(): Promise { // Connect to the SSE Session await this.sse.connect(); } /** * Disconnect from the sync server. */ async disconnect(): Promise { // Disconnect from the SSE Session this.sse.close(); } /** * Get the invitation by identifier. * @param identifier - The invitation identifier. * @returns The invitation. */ async getInvitation(identifier: string): Promise { // Send a GET request to the sync server const response = await fetch( `${this.baseUrl}/invitations?invitationIdentifier=${identifier}`, ); if (!response.ok) { throw new Error(`Failed to get invitation: ${response.statusText}`); } const invitation = decodeExtendedJson(await response.text()) as | XOInvitation | undefined; return invitation; } /** * Publish an invitation. * @param invitation - The invitation to create. * @returns The invitation. */ async publishInvitation(invitation: XOInvitation): Promise { // Send a POST request to the sync server const response = await fetch(`${this.baseUrl}/invitations`, { method: "POST", body: encodeExtendedJson(invitation), headers: { "Content-Type": "application/json", }, }); // Throw is there was an issue with the request if (!response.ok) { throw new Error(`Failed to publish invitation: ${response.statusText}`); } // Read the returned JSON // TODO: This should use zod to verify the response const data = decodeExtendedJson(await response.text()) as XOInvitation; return data; } }