feat: finalize mindspace service extraction phase a
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user