Initial Commit
This commit is contained in:
162
src/services/sync-client.ts
Normal file
162
src/services/sync-client.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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)}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user