fix: second work-over

This commit is contained in:
2026-02-27 09:06:56 +01:00
parent 00cf30a8f8
commit 467ef10e77
11 changed files with 1040 additions and 8 deletions

View File

@@ -1,6 +1,15 @@
import * as path from 'path';
import { Worker } from 'worker_threads';
export interface BlogmarkWorkerLike {
on(event: string, listener: (...args: unknown[]) => void): void;
postMessage(message: unknown): void;
terminate(): void;
removeAllListeners(): void;
}
export type BlogmarkWorkerFactory = (workerPath: string) => BlogmarkWorkerLike;
interface WorkerRunTransformRequest {
type: 'runTransform';
requestId: string;
@@ -45,7 +54,7 @@ interface ActiveRequest extends QueuedRequest {
}
export class BlogmarkPythonWorkerRuntime {
private worker: Worker | null = null;
private worker: BlogmarkWorkerLike | null = null;
private workerReady = false;
private workerStartPromise: Promise<void> | null = null;
private workerStartResolve: (() => void) | null = null;
@@ -53,6 +62,11 @@ export class BlogmarkPythonWorkerRuntime {
private activeRequest: ActiveRequest | null = null;
private queue: QueuedRequest[] = [];
private requestCounter = 0;
private readonly workerFactory: BlogmarkWorkerFactory;
constructor(workerFactory?: BlogmarkWorkerFactory) {
this.workerFactory = workerFactory ?? ((workerPath: string) => new Worker(workerPath) as unknown as BlogmarkWorkerLike);
}
async executeTransform(params: {
scriptContent: string;
@@ -96,6 +110,12 @@ export class BlogmarkPythonWorkerRuntime {
await this.ensureWorkerStarted();
// Re-check guard after await — another dispatchNext() may have
// activated a request while we were waiting for the worker.
if (this.activeRequest || this.queue.length === 0) {
return;
}
const nextRequest = this.queue.shift();
if (!nextRequest) {
return;
@@ -131,18 +151,20 @@ export class BlogmarkPythonWorkerRuntime {
}
const workerPath = path.join(__dirname, 'blogmarkPython.worker.js');
this.worker = new Worker(workerPath);
this.worker = this.workerFactory(workerPath);
this.workerReady = false;
this.worker.on('message', (message: WorkerResponseMessage) => {
this.handleWorkerMessage(message);
this.worker.on('message', (...args: unknown[]) => {
this.handleWorkerMessage(args[0] as WorkerResponseMessage);
});
this.worker.on('error', (error) => {
this.worker.on('error', (...args: unknown[]) => {
const error = args[0];
this.handleWorkerCrash(error instanceof Error ? error : new Error(String(error)));
});
this.worker.on('exit', (code) => {
this.worker.on('exit', (...args: unknown[]) => {
const code = args[0] as number;
if (code !== 0) {
this.handleWorkerCrash(new Error(`Python worker exited with code ${code}`));
}

View File

@@ -65,7 +65,7 @@ function validateParamValue(methodName: string, param: PythonApiParamContractV1,
type EngineGetter = () => Record<string, (...args: unknown[]) => unknown>;
const ENGINE_MAP: Record<string, EngineGetter> = {
export const ENGINE_MAP: Record<string, EngineGetter> = {
posts: () => {
const { getPostEngine } = require('../engine/PostEngine');
return getPostEngine();

View File

@@ -772,6 +772,9 @@ export function registerIpcHandlers(): void {
return engine.getAllScripts();
});
// Internal: used by the editor macro plugin to detect known Python macros.
// Intentionally excluded from the Python API contract and API.md because
// it is an internal renderer helper, not a user-facing scripting API.
safeHandle('scripts:getEnabledMacroSlugs', async () => {
const engine = getScriptEngine();
const scripts = await engine.getEnabledMacroScripts();

View File

@@ -589,6 +589,7 @@ export interface ElectronAPI {
delete: (id: string) => Promise<boolean>;
get: (id: string) => Promise<ScriptData | null>;
getAll: () => Promise<ScriptData[]>;
/** Internal: editor macro plugin helper. Not exposed via Python API contract. */
getEnabledMacroSlugs: () => Promise<string[]>;
rebuildFromFiles: () => Promise<void>;
};

View File

@@ -15,7 +15,7 @@ import { createDeferredEventGate } from './navigation/deferredEventGate';
import { createAndFocusPost } from './navigation/postCreation';
import { ensureRendererPicoThemeStylesheet, getRendererPicoTheme } from './utils/picoTheme';
import { addWindowEventListener, BDS_EVENT_SCRIPTS_CHANGED } from './utils/windowEvents';
import { refreshPythonMacroSlugs } from './macros';
import { refreshPythonMacroSlugs, wirePythonMacroPreview, invalidatePythonMacroScriptCache } from './macros';
import { useI18n } from './i18n';
import './App.css';
@@ -109,6 +109,9 @@ const App: React.FC = () => {
// Load known Python macro slugs for editor detection
await refreshPythonMacroSlugs();
// Wire Python macro resolver/renderer for editor preview
wirePythonMacroPreview();
} catch (error) {
console.error('Failed to load initial data:', error);
} finally {
@@ -565,6 +568,7 @@ const App: React.FC = () => {
// Refresh Python macro slugs when scripts change
unsubscribers.push(
addWindowEventListener(BDS_EVENT_SCRIPTS_CHANGED, () => {
invalidatePythonMacroScriptCache();
void refreshPythonMacroSlugs();
})
);

View File

@@ -45,3 +45,8 @@ export {
setPythonMacroResolver,
refreshPythonMacroSlugs,
} from './registry';
export {
wirePythonMacroPreview,
invalidatePythonMacroScriptCache,
} from './pythonMacroPreview';

View File

@@ -0,0 +1,72 @@
import type { PythonMacroInfo, PythonMacroResolver, PythonMacroRendererFn } from './types';
import { setPythonMacroResolver } from './registry';
import { getPythonRuntimeManager } from '../python/runtimeManagerInstance';
interface ScriptLike {
id: string;
slug: string;
kind: string;
enabled: boolean;
content: string;
entrypoint: string;
version: number;
}
let cachedMacroScripts: ScriptLike[] | null = null;
export function invalidatePythonMacroScriptCache(): void {
cachedMacroScripts = null;
}
async function loadMacroScripts(): Promise<ScriptLike[]> {
if (cachedMacroScripts) return cachedMacroScripts;
const allScripts = await window.electronAPI?.scripts.getAll();
cachedMacroScripts = (allScripts ?? []).filter(
(s: ScriptLike) => s.kind === 'macro' && s.enabled,
);
return cachedMacroScripts;
}
const resolvePythonMacro: PythonMacroResolver = async (macroName) => {
const scripts = await loadMacroScripts();
const lower = macroName.toLowerCase();
const script = scripts.find((s) => s.slug.toLowerCase() === lower);
if (!script) return null;
return {
scriptId: script.id,
slug: script.slug,
code: script.content,
entrypoint: script.entrypoint,
version: script.version,
};
};
const renderPythonMacro: PythonMacroRendererFn = async (info, params, context) => {
const manager = getPythonRuntimeManager();
const macroContext = {
env: {
isPreview: context.isPreview,
source: { kind: 'script', id: info.scriptId },
},
params: params as Record<string, unknown>,
};
const result = await manager.renderMacroV1(info.code, macroContext, {
entrypoint: info.entrypoint,
cacheKey: `${info.scriptId}:v${info.version}`,
macroSource: { kind: 'script', id: info.scriptId },
});
return result.result.html;
};
/**
* Wire the Python macro resolver and renderer into the macro registry
* so that the editor preview can render Python-backed macros.
* Call once on startup after refreshPythonMacroSlugs().
*/
export function wirePythonMacroPreview(): void {
setPythonMacroResolver(resolvePythonMacro, renderPythonMacro);
}