fix(context): trigger engine start as fire-and-forget in getProjectContext

Previously, getProjectContext awaited ensureEngine() which could block the
HTTP request handler for several seconds while a project engine initialised
(e.g. for newly-registered projects). This caused curl --max-time timeouts
and empty responses on the first request to a new project.

Change: replace await ensureEngine() with onProjectAccessed() (fire-and-forget).
- First request to an unstarted project uses getOrCreateProjectStore directly
  (fast, reads from SQLite) — no blocking.
- Engine starts in background; subsequent requests use the running engine.
- Eliminates timing-dependent restart workarounds in init scripts.

Fixes: fn settings import followed by immediate API calls returning empty.
This commit is contained in:
Josemi Liebana
2026-05-25 03:13:16 +02:00
parent fbf7e2cb4d
commit 435963d231
2 changed files with 10 additions and 8 deletions

View File

@@ -703,16 +703,17 @@ describe("routes/context project scoping helpers", () => {
expect(getOrCreateSpy).not.toHaveBeenCalled();
});
it("getProjectContext falls back to scoped store when ensureEngine throws", async () => {
it("getProjectContext triggers background engine start and falls back to scoped store for first request", async () => {
const store = createMockStore();
const fallbackStore = createMockStore();
const getOrCreateSpy = vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValueOnce(fallbackStore);
const req = { query: { projectId: "proj-123" }, body: {} } as unknown as express.Request;
const onProjectAccessedMock = vi.fn();
const options = {
engineManager: {
getEngine: vi.fn().mockReturnValue(undefined),
ensureEngine: vi.fn().mockRejectedValue(new Error("startup failed")),
onProjectAccessed: onProjectAccessedMock,
},
} as any;
@@ -721,7 +722,8 @@ describe("routes/context project scoping helpers", () => {
expect(context.projectId).toBe("proj-123");
expect(context.engine).toBeUndefined();
expect(context.store).toBe(fallbackStore);
expect(options.engineManager.ensureEngine).toHaveBeenCalledWith("proj-123");
// Engine start is triggered as fire-and-forget — not blocking the request
expect(onProjectAccessedMock).toHaveBeenCalledWith("proj-123");
expect(getOrCreateSpy).toHaveBeenCalledWith("proj-123");
});
});

View File

@@ -83,11 +83,11 @@ export async function getProjectContext(
if (projectId && engineManager) {
let engine = engineManager.getEngine(projectId);
if (!engine) {
try {
engine = await engineManager.ensureEngine(projectId);
} catch {
// fall through
}
// Trigger lazy engine start as fire-and-forget so this request is not
// blocked while the engine initialises (engine.start() may take several
// seconds for a newly-registered project).
// The engine will be available for subsequent requests once it starts.
engineManager.onProjectAccessed(projectId);
}
if (engine) {
return { store: engine.getTaskStore(), engine, projectId };