mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
Fix plugin reload lifecycle wiring
This commit is contained in:
parent
89cc5c4e20
commit
34a00a7d32
3 changed files with 63 additions and 18 deletions
|
|
@ -126,4 +126,35 @@ describe("pluginLifecycleManager.restartWorker", () => {
|
||||||
expect(started).toHaveBeenCalledTimes(1);
|
expect(started).toHaveBeenCalledTimes(1);
|
||||||
expect(started).toHaveBeenCalledWith({ pluginId: "plugin-1", pluginKey: "example.plugin" });
|
expect(started).toHaveBeenCalledWith({ pluginId: "plugin-1", pluginKey: "example.plugin" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses a late-bound runtime loader after bootstrap wiring completes", async () => {
|
||||||
|
mockRegistry.getById.mockResolvedValue(pluginRecord);
|
||||||
|
mockRegistry.updateStatus.mockResolvedValue(pluginRecord);
|
||||||
|
|
||||||
|
const { handle, workerManager } = makeWorkerManagerStub();
|
||||||
|
let runtimeLoader: Partial<PluginLoader> | undefined;
|
||||||
|
const lifecycle = pluginLifecycleManager(
|
||||||
|
{} as never,
|
||||||
|
{
|
||||||
|
resolveLoader: () => runtimeLoader as PluginLoader | undefined,
|
||||||
|
workerManager,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
runtimeLoader = {
|
||||||
|
hasRuntimeServices: vi.fn().mockReturnValue(true) as PluginLoader["hasRuntimeServices"],
|
||||||
|
loadSingle: vi.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
plugin: pluginRecord,
|
||||||
|
registered: { worker: true, eventSubscriptions: 0, jobs: 0, webhooks: 0, tools: 0 },
|
||||||
|
}) as PluginLoader["loadSingle"],
|
||||||
|
unloadSingle: vi.fn().mockResolvedValue(undefined) as PluginLoader["unloadSingle"],
|
||||||
|
};
|
||||||
|
|
||||||
|
await lifecycle.restartWorker("plugin-1");
|
||||||
|
|
||||||
|
expect(runtimeLoader.unloadSingle).toHaveBeenCalledWith("plugin-1", "example.plugin");
|
||||||
|
expect(runtimeLoader.loadSingle).toHaveBeenCalledWith("plugin-1");
|
||||||
|
expect(handle.restart).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,11 @@ export async function createApp(
|
||||||
const eventBus = createPluginEventBus();
|
const eventBus = createPluginEventBus();
|
||||||
setPluginEventBus(eventBus);
|
setPluginEventBus(eventBus);
|
||||||
const jobStore = pluginJobStore(db);
|
const jobStore = pluginJobStore(db);
|
||||||
const lifecycle = pluginLifecycleManager(db, { workerManager });
|
let loader: ReturnType<typeof pluginLoader>;
|
||||||
|
const lifecycle = pluginLifecycleManager(db, {
|
||||||
|
workerManager,
|
||||||
|
resolveLoader: () => loader,
|
||||||
|
});
|
||||||
const scheduler = createPluginJobScheduler({
|
const scheduler = createPluginJobScheduler({
|
||||||
db,
|
db,
|
||||||
jobStore,
|
jobStore,
|
||||||
|
|
@ -261,7 +265,7 @@ export async function createApp(
|
||||||
});
|
});
|
||||||
const hostServiceCleanup = createPluginHostServiceCleanup(lifecycle, hostServicesDisposers);
|
const hostServiceCleanup = createPluginHostServiceCleanup(lifecycle, hostServicesDisposers);
|
||||||
let viteHtmlRenderer: ReturnType<typeof createCachedViteHtmlRenderer> | null = null;
|
let viteHtmlRenderer: ReturnType<typeof createCachedViteHtmlRenderer> | null = null;
|
||||||
const loader = pluginLoader(
|
loader = pluginLoader(
|
||||||
db,
|
db,
|
||||||
{
|
{
|
||||||
localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
|
localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,8 @@ export interface PluginLifecycleManager {
|
||||||
export interface PluginLifecycleManagerOptions {
|
export interface PluginLifecycleManagerOptions {
|
||||||
/** Plugin loader instance. Falls back to the default if omitted. */
|
/** Plugin loader instance. Falls back to the default if omitted. */
|
||||||
loader?: PluginLoader;
|
loader?: PluginLoader;
|
||||||
|
/** Resolve the loader lazily for bootstrap paths with circular wiring. */
|
||||||
|
resolveLoader?: () => PluginLoader | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker process manager. When provided, lifecycle transitions that bring
|
* Worker process manager. When provided, lifecycle transitions that bring
|
||||||
|
|
@ -308,6 +310,7 @@ export function pluginLifecycleManager(
|
||||||
// Support the legacy signature: pluginLifecycleManager(db, loader)
|
// Support the legacy signature: pluginLifecycleManager(db, loader)
|
||||||
// as well as the new options object form.
|
// as well as the new options object form.
|
||||||
let loaderArg: PluginLoader | undefined;
|
let loaderArg: PluginLoader | undefined;
|
||||||
|
let resolveLoader: (() => PluginLoader | undefined) | undefined;
|
||||||
let workerManager: PluginWorkerManager | undefined;
|
let workerManager: PluginWorkerManager | undefined;
|
||||||
|
|
||||||
if (options && typeof options === "object" && "discoverAll" in options) {
|
if (options && typeof options === "object" && "discoverAll" in options) {
|
||||||
|
|
@ -316,11 +319,12 @@ export function pluginLifecycleManager(
|
||||||
} else if (options && typeof options === "object") {
|
} else if (options && typeof options === "object") {
|
||||||
const opts = options as PluginLifecycleManagerOptions;
|
const opts = options as PluginLifecycleManagerOptions;
|
||||||
loaderArg = opts.loader;
|
loaderArg = opts.loader;
|
||||||
|
resolveLoader = opts.resolveLoader;
|
||||||
workerManager = opts.workerManager;
|
workerManager = opts.workerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
const registry = pluginRegistryService(db);
|
const registry = pluginRegistryService(db);
|
||||||
const pluginLoaderInstance = loaderArg ?? pluginLoader(db);
|
const fallbackLoader = loaderArg ?? pluginLoader(db);
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
emitter.setMaxListeners(100); // plugins may have many listeners; 100 is a safe upper bound
|
emitter.setMaxListeners(100); // plugins may have many listeners; 100 is a safe upper bound
|
||||||
|
|
||||||
|
|
@ -386,6 +390,10 @@ export function pluginLifecycleManager(
|
||||||
emitter.emit(event, payload);
|
emitter.emit(event, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentLoader(): PluginLoader {
|
||||||
|
return resolveLoader?.() ?? loaderArg ?? fallbackLoader;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Worker management helpers
|
// Worker management helpers
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -415,14 +423,15 @@ export function pluginLifecycleManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activateReadyPlugin(pluginId: string): Promise<void> {
|
async function activateReadyPlugin(pluginId: string): Promise<void> {
|
||||||
|
const loader = currentLoader();
|
||||||
const supportsRuntimeActivation =
|
const supportsRuntimeActivation =
|
||||||
typeof pluginLoaderInstance.hasRuntimeServices === "function"
|
typeof loader.hasRuntimeServices === "function"
|
||||||
&& typeof pluginLoaderInstance.loadSingle === "function";
|
&& typeof loader.loadSingle === "function";
|
||||||
if (!supportsRuntimeActivation || !pluginLoaderInstance.hasRuntimeServices()) {
|
if (!supportsRuntimeActivation || !loader.hasRuntimeServices()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadResult = await pluginLoaderInstance.loadSingle(pluginId);
|
const loadResult = await loader.loadSingle(pluginId);
|
||||||
if (!loadResult.success) {
|
if (!loadResult.success) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
loadResult.error
|
loadResult.error
|
||||||
|
|
@ -435,12 +444,13 @@ export function pluginLifecycleManager(
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
pluginKey: string,
|
pluginKey: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const loader = currentLoader();
|
||||||
const supportsRuntimeDeactivation =
|
const supportsRuntimeDeactivation =
|
||||||
typeof pluginLoaderInstance.hasRuntimeServices === "function"
|
typeof loader.hasRuntimeServices === "function"
|
||||||
&& typeof pluginLoaderInstance.unloadSingle === "function";
|
&& typeof loader.unloadSingle === "function";
|
||||||
|
|
||||||
if (supportsRuntimeDeactivation && pluginLoaderInstance.hasRuntimeServices()) {
|
if (supportsRuntimeDeactivation && loader.hasRuntimeServices()) {
|
||||||
await pluginLoaderInstance.unloadSingle(pluginId, pluginKey);
|
await loader.unloadSingle(pluginId, pluginKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,7 +551,7 @@ export function pluginLifecycleManager(
|
||||||
// If already uninstalled and removeData, hard-delete
|
// If already uninstalled and removeData, hard-delete
|
||||||
if (plugin.status === "uninstalled") {
|
if (plugin.status === "uninstalled") {
|
||||||
if (removeData) {
|
if (removeData) {
|
||||||
await pluginLoaderInstance.cleanupInstallArtifacts(plugin);
|
await currentLoader().cleanupInstallArtifacts(plugin);
|
||||||
const deleted = await registry.uninstall(pluginId, true);
|
const deleted = await registry.uninstall(pluginId, true);
|
||||||
log.info(
|
log.info(
|
||||||
{ pluginId, pluginKey: plugin.pluginKey },
|
{ pluginId, pluginKey: plugin.pluginKey },
|
||||||
|
|
@ -561,7 +571,7 @@ export function pluginLifecycleManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
await deactivatePluginRuntime(pluginId, plugin.pluginKey);
|
await deactivatePluginRuntime(pluginId, plugin.pluginKey);
|
||||||
await pluginLoaderInstance.cleanupInstallArtifacts(plugin);
|
await currentLoader().cleanupInstallArtifacts(plugin);
|
||||||
|
|
||||||
// Perform the uninstall via registry (handles soft/hard delete)
|
// Perform the uninstall via registry (handles soft/hard delete)
|
||||||
const result = await registry.uninstall(pluginId, removeData);
|
const result = await registry.uninstall(pluginId, removeData);
|
||||||
|
|
@ -656,7 +666,7 @@ export function pluginLifecycleManager(
|
||||||
|
|
||||||
// 1. Download and validate new package via loader
|
// 1. Download and validate new package via loader
|
||||||
const { oldManifest, newManifest, discovered } =
|
const { oldManifest, newManifest, discovered } =
|
||||||
await pluginLoaderInstance.upgradePlugin(pluginId, { version });
|
await currentLoader().upgradePlugin(pluginId, { version });
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
{
|
{
|
||||||
|
|
@ -777,10 +787,10 @@ export function pluginLifecycleManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportsRuntimeActivation =
|
const supportsRuntimeActivation =
|
||||||
typeof pluginLoaderInstance.hasRuntimeServices === "function"
|
typeof currentLoader().hasRuntimeServices === "function"
|
||||||
&& typeof pluginLoaderInstance.loadSingle === "function"
|
&& typeof currentLoader().loadSingle === "function"
|
||||||
&& typeof pluginLoaderInstance.unloadSingle === "function"
|
&& typeof currentLoader().unloadSingle === "function"
|
||||||
&& pluginLoaderInstance.hasRuntimeServices();
|
&& currentLoader().hasRuntimeServices();
|
||||||
|
|
||||||
if (supportsRuntimeActivation) {
|
if (supportsRuntimeActivation) {
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue