Files
blb-throne-of-the-bone-king/src/socket/toolkitSocketClient.ts
T

145 lines
3.5 KiB
TypeScript
Raw Normal View History

2026-06-12 08:19:40 -05:00
import { io, type Socket } from 'socket.io-client';
import { isCreditsContent, type CreditsContent } from '../data';
type NodeProcess = {
env?: Partial<Record<string, string>>;
};
export type ToolkitContentResource = 'credits';
export type ToolkitContentPayload = Readonly<{
content: CreditsContent;
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;
const defaultToolkitSocketUrl = 'http://127.0.0.1:5174';
const toolkitSocketTimeoutMs = 5000;
let toolkitSocket: Socket | null = null;
function getMaybeProcess() {
return typeof process === 'undefined' ? undefined : (process as NodeProcess);
}
export function getToolkitSocketUrl() {
return (
getMaybeProcess()?.env?.TOOLKIT_SOCKET_URL ?? 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 isToolkitContentResource(value: unknown): value is ToolkitContentResource {
return value === 'credits';
}
function isToolkitContentPayload(value: unknown): value is ToolkitContentPayload {
if (value == null || typeof value !== 'object') {
return false;
}
const payload = value as Partial<ToolkitContentPayload>;
return (
isToolkitContentResource(payload.resource) &&
typeof payload.revision === 'string' &&
isCreditsContent(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<ToolkitContentResponse>;
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.');
}
async function emitContentRequest(
eventName: 'toolkit:content:read' | 'toolkit:content:save',
payload: Readonly<{
content?: CreditsContent;
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: CreditsContent,
) {
return emitContentRequest('toolkit:content:save', {
content,
resource,
});
}
export function resetToolkitSocketForTests() {
toolkitSocket?.disconnect();
toolkitSocket = null;
}