Files
blb-throne-of-the-bone-king/src/socket/toolkitSocketClient.ts
T
jacob-mathison 1e9d3f0917
ci/woodpecker/push/woodpecker Pipeline was successful
Lint and Build Checks / Lint, Test, and Build (push) Failing after 345h51m17s
feat(styles): add browser scss builder system
2026-06-25 09:46:09 -05:00

321 lines
7.6 KiB
TypeScript

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<Record<string, string>>;
};
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<ToolkitContentPayload>;
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<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.');
}
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<ToolkitAssetUploadPayload>;
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<ToolkitAssetUploadResponse>;
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;
}