diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 917ea608..17ca6aab 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -163,8 +163,4 @@ else echo "Warning: package.json not found" fi -node dist/backend/backend/starter.js - -echo "All services started" - -tail -f /dev/null +exec node dist/backend/backend/starter.js diff --git a/src/backend/tests/utils/database-save-trigger.test.ts b/src/backend/tests/utils/database-save-trigger.test.ts index 82b38e13..c33d33a9 100644 --- a/src/backend/tests/utils/database-save-trigger.test.ts +++ b/src/backend/tests/utils/database-save-trigger.test.ts @@ -38,4 +38,28 @@ describe("DatabaseSaveTrigger", () => { expect(DatabaseSaveTrigger.isDirty).toBe(false); expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false); }); + + it("queues a force save behind an in-flight save", async () => { + let finishFirstSave: (() => void) | undefined; + const firstSave = new Promise((resolve) => { + finishFirstSave = resolve; + }); + const save = vi + .fn<() => Promise>() + .mockReturnValueOnce(firstSave) + .mockResolvedValueOnce(undefined); + DatabaseSaveTrigger.initialize(save); + + const first = DatabaseSaveTrigger.forceSave("first_write"); + await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1)); + + const second = DatabaseSaveTrigger.forceSave("sso_provider_write"); + expect(save).toHaveBeenCalledTimes(1); + + finishFirstSave?.(); + await Promise.all([first, second]); + + expect(save).toHaveBeenCalledTimes(2); + expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false); + }); }); diff --git a/src/backend/utils/database-save-trigger.ts b/src/backend/utils/database-save-trigger.ts index 08d07e43..45788b44 100644 --- a/src/backend/utils/database-save-trigger.ts +++ b/src/backend/utils/database-save-trigger.ts @@ -4,6 +4,7 @@ export class DatabaseSaveTrigger { private static saveFunction: (() => Promise) | null = null; private static isInitialized = false; private static pendingSave = false; + private static activeSave: Promise | null = null; private static saveTimeout: NodeJS.Timeout | null = null; private static _dirty = false; @@ -38,14 +39,10 @@ export class DatabaseSaveTrigger { } this.saveTimeout = setTimeout(async () => { - if (this.pendingSave) { - return; - } - - this.pendingSave = true; + this.saveTimeout = null; try { - await this.saveFunction!(); + await this.runSave(); this._dirty = false; } catch (error) { databaseLogger.error("Database save failed", error, { @@ -53,8 +50,6 @@ export class DatabaseSaveTrigger { reason, error: error instanceof Error ? error.message : "Unknown error", }); - } finally { - this.pendingSave = false; } }, 2000); } @@ -76,14 +71,9 @@ export class DatabaseSaveTrigger { this.saveTimeout = null; } - if (this.pendingSave) { - return; - } - - this.pendingSave = true; - try { - await this.saveFunction(); + await this.runSave(); + this._dirty = false; } catch (error) { databaseLogger.error("Database force save failed", error, { operation: "db_save_trigger_force_failed", @@ -91,8 +81,29 @@ export class DatabaseSaveTrigger { error: error instanceof Error ? error.message : "Unknown error", }); throw error; + } + } + + private static async runSave(): Promise { + while (this.activeSave) { + try { + await this.activeSave; + } catch { + // The queued save must still run after an earlier save failed. + } + } + + const save = Promise.resolve().then(() => this.saveFunction!()); + this.activeSave = save; + this.pendingSave = true; + + try { + await save; } finally { - this.pendingSave = false; + if (this.activeSave === save) { + this.activeSave = null; + this.pendingSave = false; + } } } @@ -115,6 +126,7 @@ export class DatabaseSaveTrigger { } this.pendingSave = false; + this.activeSave = null; this.isInitialized = false; this.saveFunction = null; }