fix: lots of missing pieces for python macro handling

This commit is contained in:
2026-02-27 08:33:12 +01:00
parent 916d9459ef
commit 00cf30a8f8
31 changed files with 1715 additions and 431 deletions

View File

@@ -6,9 +6,20 @@ interface WorkerRenderMacroRequest {
scriptContent: string;
entrypoint: string;
contextJson: string;
postDataJson?: string | null;
cacheKey?: string;
}
interface WorkerApiResultMessage {
type: 'apiResult';
callId: string;
ok: boolean;
result?: unknown;
error?: string;
}
type WorkerIncomingMessage = WorkerRenderMacroRequest | WorkerApiResultMessage;
interface WorkerReadyMessage {
type: 'ready';
}
@@ -32,27 +43,131 @@ interface WorkerFatalErrorMessage {
error: string;
}
type WorkerResponseMessage = WorkerReadyMessage | WorkerMacroResultMessage | WorkerMacroErrorMessage | WorkerFatalErrorMessage;
interface WorkerApiCallMessage {
type: 'apiCall';
requestId: string;
callId: string;
method: string;
args: Record<string, unknown>;
}
type WorkerResponseMessage = WorkerReadyMessage | WorkerMacroResultMessage | WorkerMacroErrorMessage | WorkerFatalErrorMessage | WorkerApiCallMessage;
type PyodideRuntime = {
globals: {
set: (name: string, value: unknown) => void;
} | any;
runPythonAsync: (code: string) => Promise<unknown>;
registerJsModule: (name: string, module: Record<string, unknown>) => void;
};
let runtimePromise: Promise<PyodideRuntime> | null = null;
let lastCacheKey: string | null = null;
let activeRequestId: string | null = null;
let apiCallCounter = 0;
function postMessage(message: WorkerResponseMessage): void {
interface PendingApiCall {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}
const pendingApiCalls = new Map<string, PendingApiCall>();
function postWorkerMessage(message: WorkerResponseMessage): void {
parentPort?.postMessage(message);
}
function toRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
}
function requestHostApi(requestId: string, method: string, args: Record<string, unknown>): Promise<unknown> {
apiCallCounter += 1;
const callId = `api-${apiCallCounter}`;
return new Promise((resolve, reject) => {
pendingApiCalls.set(callId, { resolve, reject });
postWorkerMessage({
type: 'apiCall',
requestId,
callId,
method,
args,
});
});
}
function handleApiResultMessage(message: WorkerApiResultMessage): void {
const pendingCall = pendingApiCalls.get(message.callId);
if (!pendingCall) {
return;
}
pendingApiCalls.delete(message.callId);
if (message.ok) {
pendingCall.resolve(message.result);
return;
}
pendingCall.reject(new Error(message.error ?? 'Host API call failed'));
}
function rejectPendingApiCalls(message: string): void {
for (const [callId, pendingCall] of pendingApiCalls.entries()) {
pendingApiCalls.delete(callId);
pendingCall.reject(new Error(message));
}
}
async function getRuntime(): Promise<PyodideRuntime> {
if (!runtimePromise) {
runtimePromise = (async () => {
const pyodideModule = await import('pyodide');
return (await pyodideModule.loadPyodide()) as unknown as PyodideRuntime;
const runtime = (await pyodideModule.loadPyodide()) as unknown as PyodideRuntime;
// Register bds_api transport bridge
runtime.registerJsModule('__bds_transport', {
call_host_api: async (method: unknown, argsJson: unknown) => {
if (!activeRequestId) {
throw new Error('No active Python request for host API bridge');
}
if (typeof method !== 'string' || method.length === 0) {
throw new Error('Host API method must be a non-empty string');
}
let parsedArgs: Record<string, unknown> = {};
if (typeof argsJson === 'string' && argsJson.length > 0) {
const decoded = JSON.parse(argsJson);
parsedArgs = toRecord(decoded);
}
const result = await requestHostApi(activeRequestId, method, parsedArgs);
return JSON.stringify(result ?? null);
},
});
// Install bds_api module
const { generatePythonApiModuleV1 } = await import('../shared/generatePythonApiModuleV1');
runtime.globals.set('__bds_api_module_source', generatePythonApiModuleV1());
await runtime.runPythonAsync(`
import sys
import types
__bds_api_module = types.ModuleType("bds_api")
exec(__bds_api_module_source, __bds_api_module.__dict__)
from __bds_transport import call_host_api as __bds_call_host_api
__bds_api_module.bds = __bds_api_module.install_bds_api(__bds_call_host_api)
sys.modules["bds_api"] = __bds_api_module
`);
return runtime;
})();
}
@@ -60,6 +175,7 @@ async function getRuntime(): Promise<PyodideRuntime> {
}
async function renderMacro(request: WorkerRenderMacroRequest): Promise<void> {
activeRequestId = request.requestId;
try {
const runtime = await getRuntime();
@@ -72,6 +188,7 @@ async function renderMacro(request: WorkerRenderMacroRequest): Promise<void> {
runtime.globals.set('__bds_macro_context_json', request.contextJson);
runtime.globals.set('__bds_macro_entrypoint', request.entrypoint);
runtime.globals.set('__bds_macro_post_data_json', request.postDataJson ?? '');
const rawResult = await runtime.runPythonAsync(`
import json as _json
@@ -81,7 +198,9 @@ _macro_ep = __bds_macro_entrypoint
_macro_fn = globals().get(_macro_ep)
if _macro_fn is None or not callable(_macro_fn):
raise RuntimeError(f"Macro entrypoint '{_macro_ep}' is not callable")
_macro_result = _macro_fn(_macro_ctx)
_macro_post_json = __bds_macro_post_data_json
_macro_post = _json.loads(_macro_post_json) if _macro_post_json else None
_macro_result = _macro_fn(_macro_ctx, _macro_post)
if _macro_result is None:
raise RuntimeError("Macro function returned None")
if not isinstance(_macro_result, dict):
@@ -93,7 +212,7 @@ _json.dumps(_macro_result)
const parsed = JSON.parse(String(rawResult));
postMessage({
postWorkerMessage({
type: 'macroResult',
requestId: request.requestId,
html: typeof parsed.html === 'string' ? parsed.html : '',
@@ -101,12 +220,21 @@ _json.dumps(_macro_result)
warnings: Array.isArray(parsed.warnings) ? parsed.warnings : undefined,
});
} catch (error) {
rejectPendingApiCalls('Python macro execution failed');
const message = error instanceof Error ? error.message : String(error);
postMessage({ type: 'macroError', requestId: request.requestId, error: message });
postWorkerMessage({ type: 'macroError', requestId: request.requestId, error: message });
} finally {
rejectPendingApiCalls('Python macro execution finished');
activeRequestId = null;
}
}
parentPort?.on('message', (message: WorkerRenderMacroRequest) => {
parentPort?.on('message', (message: WorkerIncomingMessage) => {
if (message.type === 'apiResult') {
handleApiResultMessage(message);
return;
}
if (message.type !== 'renderMacro') {
return;
}
@@ -116,9 +244,9 @@ parentPort?.on('message', (message: WorkerRenderMacroRequest) => {
void getRuntime()
.then(() => {
postMessage({ type: 'ready' });
postWorkerMessage({ type: 'ready' });
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
postMessage({ type: 'error', error: message });
postWorkerMessage({ type: 'error', error: message });
});