321 lines
7.6 KiB
TypeScript
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;
|
|
}
|