import { io, type Socket } from 'socket.io-client'; import { isEditableToolkitResource, isToolkitResourceContent, isToolkitRecordContent, type EditableToolkitResource, type ToolkitRecordContent, type ToolkitResourceContent, } from '../data/toolkitContentResources'; type NodeProcess = { env?: Partial>; }; type BrowserLocation = { hostname: string; protocol: string; }; type BrowserGlobal = typeof globalThis & { location?: BrowserLocation; }; declare const __TOOLKIT_SOCKET_URL__: string | undefined; export type ToolkitContentResource = EditableToolkitResource; export type ToolkitAssetKind = 'audio' | 'image'; export type ToolkitAssetUploadInput = Readonly<{ dataUrl: string; fileName: string; kind: ToolkitAssetKind; mimeType: string; }>; export type ToolkitAssetUploadPayload = Readonly<{ assetPath: string; fileName: string; kind: ToolkitAssetKind; mimeType: string; publicPath: string; }>; export type ToolkitContentPayload = Readonly<{ content: ToolkitResourceContent; resource: ToolkitContentResource; revision: string; }>; export type ToolkitContentErrorPayload = Readonly<{ error: string; resource: ToolkitContentResource; }>; type ToolkitContentSuccessResponse = ToolkitContentPayload & Readonly<{ ok: true; }>; type ToolkitContentErrorResponse = ToolkitContentErrorPayload & Readonly<{ ok: false; }>; type ToolkitContentResponse = | ToolkitContentSuccessResponse | ToolkitContentErrorResponse; type ToolkitAssetUploadSuccessResponse = ToolkitAssetUploadPayload & Readonly<{ ok: true; }>; type ToolkitAssetUploadErrorResponse = Readonly<{ error: string; ok: false; }>; type ToolkitAssetUploadResponse = | ToolkitAssetUploadSuccessResponse | ToolkitAssetUploadErrorResponse; const defaultToolkitSocketUrl = 'http://127.0.0.1:5175'; const deployedToolkitSocketPort = '5174'; const toolkitSocketTimeoutMs = 5000; let toolkitSocket: Socket | null = null; function getMaybeProcess() { return typeof process === 'undefined' ? undefined : (process as NodeProcess); } function getConfiguredToolkitSocketUrl() { if ( typeof __TOOLKIT_SOCKET_URL__ === 'string' && __TOOLKIT_SOCKET_URL__.length > 0 ) { return __TOOLKIT_SOCKET_URL__; } return getMaybeProcess()?.env?.TOOLKIT_SOCKET_URL; } function getBrowserToolkitSocketUrl() { const location = (globalThis as BrowserGlobal).location; if (location == null) { return null; } if (location.hostname === '127.0.0.1' || location.hostname === 'localhost') { return defaultToolkitSocketUrl; } return `${location.protocol}//${location.hostname}:${deployedToolkitSocketPort}`; } export function getToolkitSocketUrl() { return ( getConfiguredToolkitSocketUrl() ?? getBrowserToolkitSocketUrl() ?? defaultToolkitSocketUrl ); } export function getNodeEnvironment() { return getMaybeProcess()?.env?.NODE_ENV; } export function getToolkitSocket() { if (toolkitSocket == null) { toolkitSocket = io(getToolkitSocketUrl(), { autoConnect: false, reconnection: true, reconnectionAttempts: Number.POSITIVE_INFINITY, reconnectionDelay: 750, transports: ['websocket'], }); } return toolkitSocket; } function isToolkitContentPayload( value: unknown, ): value is ToolkitContentPayload { if (value == null || typeof value !== 'object') { return false; } const payload = value as Partial; return ( isEditableToolkitResource(payload.resource) && typeof payload.revision === 'string' && isToolkitResourceContent(payload.resource, payload.content) ); } function parseToolkitContentResponse(response: unknown): ToolkitContentPayload { if (response == null || typeof response !== 'object') { throw new Error('Toolkit content response was empty.'); } const contentResponse = response as Partial; if (contentResponse.ok === false) { throw new Error(contentResponse.error ?? 'Toolkit content request failed.'); } if (contentResponse.ok === true && isToolkitContentPayload(contentResponse)) { return contentResponse; } throw new Error('Toolkit content response was invalid.'); } function isToolkitAssetKind(value: unknown): value is ToolkitAssetKind { return value === 'audio' || value === 'image'; } function isToolkitAssetUploadPayload( value: unknown, ): value is ToolkitAssetUploadPayload { if (value == null || typeof value !== 'object') { return false; } const payload = value as Partial; return ( typeof payload.assetPath === 'string' && typeof payload.fileName === 'string' && isToolkitAssetKind(payload.kind) && typeof payload.mimeType === 'string' && typeof payload.publicPath === 'string' ); } function parseToolkitAssetUploadResponse( response: unknown, ): ToolkitAssetUploadPayload { if (response == null || typeof response !== 'object') { throw new Error('Toolkit asset upload response was empty.'); } const uploadResponse = response as Partial; if (uploadResponse.ok === false) { throw new Error(uploadResponse.error ?? 'Toolkit asset upload failed.'); } if ( uploadResponse.ok === true && isToolkitAssetUploadPayload(uploadResponse) ) { return uploadResponse; } throw new Error('Toolkit asset upload response was invalid.'); } async function emitContentRequest( eventName: | 'toolkit:content:create' | 'toolkit:content:delete' | 'toolkit:content:read' | 'toolkit:content:save' | 'toolkit:content:update', payload: Readonly<{ content?: ToolkitResourceContent; id?: string; record?: ToolkitRecordContent; resource: ToolkitContentResource; }>, ) { const socket = getToolkitSocket(); if (!socket.connected) { throw new Error('Toolkit socket is not connected.'); } const response = await socket .timeout(toolkitSocketTimeoutMs) .emitWithAck(eventName, payload); return parseToolkitContentResponse(response); } export function readToolkitContent(resource: ToolkitContentResource) { return emitContentRequest('toolkit:content:read', { resource }); } export function saveToolkitContent( resource: ToolkitContentResource, content: ToolkitResourceContent, ) { return emitContentRequest('toolkit:content:save', { content, resource, }); } export function createToolkitContentRecord( resource: ToolkitContentResource, record: ToolkitRecordContent, ) { if (!isToolkitRecordContent(record)) { throw new Error('Toolkit content record requires a string id.'); } return emitContentRequest('toolkit:content:create', { record, resource, }); } export function updateToolkitContentRecord( resource: ToolkitContentResource, record: ToolkitRecordContent, ) { if (!isToolkitRecordContent(record)) { throw new Error('Toolkit content record requires a string id.'); } return emitContentRequest('toolkit:content:update', { record, resource, }); } export function deleteToolkitContentRecord( resource: ToolkitContentResource, id: string, ) { return emitContentRequest('toolkit:content:delete', { id, resource, }); } export async function uploadToolkitAsset(payload: ToolkitAssetUploadInput) { const socket = getToolkitSocket(); if (!socket.connected) { throw new Error('Toolkit socket is not connected.'); } const response = await socket .timeout(toolkitSocketTimeoutMs) .emitWithAck('toolkit:asset:upload', payload); return parseToolkitAssetUploadResponse(response); } export function resetToolkitSocketForTests() { toolkitSocket?.disconnect(); toolkitSocket = null; }