163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
/**
|
|
* Sync Server Client - HTTP client for sync-server communication.
|
|
*
|
|
* Handles:
|
|
* - Creating/updating invitations on the server
|
|
* - Fetching invitations by ID
|
|
* - ExtJSON encoding/decoding for data transfer
|
|
*/
|
|
|
|
import type { XOInvitation } from '@xo-cash/types';
|
|
import { encodeExtendedJson, decodeExtendedJson } from '../utils/ext-json.js';
|
|
|
|
/**
|
|
* Response from the sync server.
|
|
*/
|
|
export interface SyncServerResponse<T> {
|
|
success: boolean;
|
|
data?: T;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* HTTP client for sync-server communication.
|
|
*/
|
|
export class SyncClient {
|
|
/** Base URL of the sync server */
|
|
private baseUrl: string;
|
|
|
|
/**
|
|
* Creates a new sync client.
|
|
* @param baseUrl - Base URL of the sync server (e.g., http://localhost:3000)
|
|
*/
|
|
constructor(baseUrl: string) {
|
|
// Remove trailing slash if present
|
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
}
|
|
|
|
/**
|
|
* Makes an HTTP request to the sync server.
|
|
* @param method - HTTP method
|
|
* @param path - Request path
|
|
* @param body - Optional request body
|
|
* @returns Response data
|
|
*/
|
|
private async request<T>(
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
const options: RequestInit = {
|
|
method,
|
|
headers,
|
|
};
|
|
|
|
if (body !== undefined) {
|
|
// Encode body using ExtJSON for proper BigInt and Uint8Array serialization
|
|
options.body = encodeExtendedJson(body);
|
|
}
|
|
|
|
const response = await fetch(url, options);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
|
|
// Return empty object if no response body
|
|
if (!responseText) {
|
|
return {} as T;
|
|
}
|
|
|
|
// Decode response using ExtJSON
|
|
return decodeExtendedJson(responseText) as T;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Invitation Operations
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Posts an invitation to the sync server (create or update).
|
|
* @param invitation - Invitation to post
|
|
* @returns The stored invitation
|
|
*/
|
|
async postInvitation(invitation: XOInvitation): Promise<XOInvitation> {
|
|
return this.request<XOInvitation>('POST', '/invitations', invitation);
|
|
}
|
|
|
|
/**
|
|
* Gets an invitation from the sync server.
|
|
* @param invitationIdentifier - Invitation ID to fetch
|
|
* @returns The invitation or undefined if not found
|
|
*/
|
|
async getInvitation(invitationIdentifier: string): Promise<XOInvitation | undefined> {
|
|
try {
|
|
// Use query parameter for GET request (can't have body)
|
|
const response = await this.request<XOInvitation>(
|
|
'GET',
|
|
`/invitations?invitationIdentifier=${encodeURIComponent(invitationIdentifier)}`
|
|
);
|
|
return response;
|
|
} catch (error) {
|
|
// Return undefined if not found (404)
|
|
if (error instanceof Error && error.message.includes('404')) {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates an invitation on the sync server.
|
|
* @param invitation - Updated invitation
|
|
* @returns The updated invitation
|
|
*/
|
|
async updateInvitation(invitation: XOInvitation): Promise<XOInvitation> {
|
|
// Uses the same POST endpoint which handles both create and update
|
|
return this.postInvitation(invitation);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Health Check
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Checks if the sync server is healthy.
|
|
* @returns True if server is healthy
|
|
*/
|
|
async isHealthy(): Promise<boolean> {
|
|
try {
|
|
const response = await this.request<{ status: string }>('GET', '/health');
|
|
return response.status === 'ok';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the base URL of the sync server.
|
|
*/
|
|
getBaseUrl(): string {
|
|
return this.baseUrl;
|
|
}
|
|
|
|
/**
|
|
* Gets the SSE endpoint URL for an invitation.
|
|
* @param invitationId - Invitation ID to subscribe to
|
|
* @returns SSE endpoint URL
|
|
*/
|
|
getSSEUrl(invitationIdentifier: string): string {
|
|
return `${this.baseUrl}/invitations?invitationIdentifier=${encodeURIComponent(invitationIdentifier)}`;
|
|
}
|
|
}
|