Files
xo-cli/src/utils/sync-server.ts

117 lines
3.1 KiB
TypeScript

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<SyncServerEventMap> {
static async from(
baseUrl: string,
invitationIdentifier: string,
): Promise<SyncServer> {
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<void> {
// Connect to the SSE Session
await this.sse.connect();
}
/**
* Disconnect from the sync server.
*/
async disconnect(): Promise<void> {
// 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<XOInvitation | undefined> {
// 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<XOInvitation> {
// 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;
}
}