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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user