Files
memind/mindspace-remote-server-adapter.mjs
T

174 lines
5.0 KiB
JavaScript

import {
MINDSPACE_SERVER_ADAPTER_BINDINGS,
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
} from './mindspace-server-adapter-contract.mjs';
function trimTrailingSlash(value) {
return String(value ?? '').trim().replace(/\/+$/, '');
}
function resolveRemoteAdapterEndpoint(endpoint) {
const base = trimTrailingSlash(endpoint);
return base || '';
}
function normalizeOperationBasePath(value) {
const raw = String(value ?? '').trim();
if (!raw) return '/mindspace/v1/adapter';
return `/${raw.replace(/^\/+/, '').replace(/\/+$/, '')}`;
}
function buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath = '/mindspace/v1/adapter') {
const base = resolveRemoteAdapterEndpoint(endpoint);
if (!base) {
throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL');
}
const normalizedBasePath = normalizeOperationBasePath(operationBasePath);
return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`;
}
async function parseRemoteOperationResponse(response, bindingKey, method) {
if (!response.ok) {
const bodyText = await response.text().catch(() => '');
throw new Error(
`MindSpace remote adapter ${bindingKey}.${method}() failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`,
);
}
if (response.status === 204) return null;
const text = await response.text();
if (!text) return null;
return JSON.parse(text);
}
async function invokeRemoteOperation({
endpoint,
operationBasePath,
authToken,
timeoutMs,
bindingKey,
method,
args,
fetchFn,
}) {
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId =
controller && Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => controller.abort(new Error('MindSpace remote adapter request timed out')), timeoutMs)
: null;
try {
const response = await fetchFn(buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
},
body: JSON.stringify({
args,
}),
signal: controller?.signal,
});
return parseRemoteOperationResponse(response, bindingKey, method);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
function createRemoteBindingProxy({
endpoint,
bindingKey,
fetchFn,
authToken,
timeoutMs,
operationBasePath,
}) {
const methods = MINDSPACE_SERVER_ADAPTER_BINDINGS[bindingKey] ?? [];
const methodSet = new Set(methods);
const proxyTarget = {};
for (const method of methods) {
proxyTarget[method] = async (...args) =>
invokeRemoteOperation({
endpoint,
operationBasePath,
authToken,
timeoutMs,
bindingKey,
method,
args,
fetchFn,
});
}
return new Proxy(proxyTarget, {
get(target, prop) {
if (prop === Symbol.toStringTag) return 'MindSpaceRemoteBindingProxy';
if (typeof prop === 'string' && !methodSet.has(prop)) {
throw new Error(`MindSpace remote adapter binding "${bindingKey}" does not expose ${bindingKey}.${prop}()`);
}
return target[prop];
},
});
}
export function createMindSpaceRemoteServerAdapter({
logger = console,
endpoint = '',
authToken = '',
operationBasePath = '/mindspace/v1/adapter',
timeoutMs = 15_000,
fetchFn = globalThis.fetch?.bind(globalThis),
} = {}) {
const endpointUrl = resolveRemoteAdapterEndpoint(endpoint);
const normalizedOperationBasePath = normalizeOperationBasePath(operationBasePath);
const invoke = fetchFn
? fetchFn
: async () => {
throw new Error('MindSpace remote server adapter requires a fetch implementation');
};
const bindings = Object.fromEntries(
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS.map((bindingKey) => [
bindingKey,
createRemoteBindingProxy({
endpoint: endpointUrl,
bindingKey,
fetchFn: invoke,
authToken,
timeoutMs,
operationBasePath: normalizedOperationBasePath,
}),
]),
);
return {
kind: 'remote',
implementationStatus: 'scaffold',
endpoint: endpointUrl,
operationBasePath: normalizedOperationBasePath,
timeoutMs,
storageRoot: null,
publicBaseUrl: null,
storageAdapter: null,
...bindings,
assertReady() {
if (!endpointUrl) {
throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL');
}
return true;
},
startBackgroundJobs() {
logger.log?.(
`MindSpace remote server adapter enabled${endpointUrl ? ` (${endpointUrl})` : ''}; background jobs stay on the remote runtime`,
);
return {
publicationCleanup: null,
agentWorker: null,
workspaceMaintenance: null,
};
},
};
}
export const mindspaceRemoteServerAdapterInternals = {
buildRemoteOperationUrl,
invokeRemoteOperation,
normalizeOperationBasePath,
};