diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index 91040af4c..775e4d247 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -108,11 +108,6 @@ export function createTransaction(options: TransactionOptions): Transaction { function commit(shouldProcess: boolean = true) { // if the rundown is mutable we persist the changes if (options.mutableRundown) { - // schedule a database update - setImmediate(async () => { - await getDataProvider().setRundown(cachedRundown.id, cachedRundown); - }); - // update fields which are agnostic of whether the rundown is processed cachedRundown.revision = cachedRundown.revision + 1; cachedRundown.title = rundown.title; @@ -133,16 +128,17 @@ export function createTransaction(options: TransactionOptions): Transaction { cachedRundown.flatOrder = metadata.flatEntryOrder; rundownMetadata = metadata; } + + // persist after all mutations are applied + getDataProvider().setRundown(cachedRundown.id, cachedRundown); } // if the customFields are mutable we persist the changes if (options.mutableCustomFields) { - // schedule a database update - setImmediate(async () => { - await getDataProvider().setCustomFields(projectCustomFields); - }); - projectCustomFields = customFields; + + // persist after reassignment + getDataProvider().setCustomFields(projectCustomFields); } return { @@ -552,10 +548,8 @@ export const rundownMutation = { /** * Exposes a way to update a rundown which is not active */ -export function updateBackgroundRundown(rundownId: string, rundown: Rundown) { - setImmediate(async () => { - await getDataProvider().setRundown(rundownId, rundown); - }); +export async function updateBackgroundRundown(rundownId: string, rundown: Rundown) { + await getDataProvider().setRundown(rundownId, rundown); } /** @@ -652,9 +646,7 @@ export function init(initialRundown: Readonly, initialCustomFields: Rea rundownMetadata = metadata; // defer writing to the database - setImmediate(async () => { - await getDataProvider().setRundown(cachedRundown.id, cachedRundown); - }); + getDataProvider().setRundown(cachedRundown.id, cachedRundown); return { rundown, rundownMetadata, customFields, revision: rundown.revision }; } diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index f4d37c24a..a5f61bee7 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -486,7 +486,7 @@ export async function editCustomField( if (rundownId !== rundown.id) { const backgroundRundown = structuredClone(projectRundowns[rundownId]); customFieldMutation.renameUsages(backgroundRundown, oldKey, newKey); - updateBackgroundRundown(rundownId, backgroundRundown); + await updateBackgroundRundown(rundownId, backgroundRundown); } } @@ -526,7 +526,7 @@ export async function deleteCustomField(key: CustomFieldKey, projectRundowns: Pr if (rundownId !== rundown.id) { const backgroundRundown = structuredClone(projectRundowns[rundownId]); customFieldMutation.removeUsages(backgroundRundown, key); - updateBackgroundRundown(rundownId, backgroundRundown); + await updateBackgroundRundown(rundownId, backgroundRundown); } } diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index efa4028f8..347774a6a 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -25,7 +25,7 @@ import { integrationRouter } from './api-integration/integration.router.js'; // Import adapters import { socket } from './adapters/WebsocketAdapter.js'; -import { getDataProvider } from './classes/data-provider/DataProvider.js'; +import { getDataProvider, flushPendingWrites } from './classes/data-provider/DataProvider.js'; // Services import { logger } from './classes/Logger.js'; @@ -266,6 +266,10 @@ export const startIntegrations = async () => { export const shutdown = async (exitCode = 0) => { consoleHighlight(`Ontime shutting down with code ${exitCode}`); + await flushPendingWrites().catch((_error) => { + /** nothing do to here */ + }); + // clear the restore file if it was a normal exit // 0 means it was a SIGNAL // 1 means crash -> keep the file diff --git a/apps/server/src/classes/data-provider/DataProvider.ts b/apps/server/src/classes/data-provider/DataProvider.ts index 915b10bf2..4df2d6b79 100644 --- a/apps/server/src/classes/data-provider/DataProvider.ts +++ b/apps/server/src/classes/data-provider/DataProvider.ts @@ -180,10 +180,58 @@ async function mergeIntoData(newData: Partial): ReadonlyPromise | null = null; +const writeDelayMs = 3000; // 3 seconds + /** - * Handles persisting data to file + * Handles persisting data to file with trailing-edge debounce + * Multiple rapid calls will be coalesced into a single write */ async function persist() { if (isTest) return; + + // Cancel any pending write and reschedule + if (pendingWrite) { + clearTimeout(pendingWrite); + } + + // Schedule new write after quiet period + pendingWrite = setTimeout(async () => { + pendingWrite = null; + + // Wait for any in-progress write to finish first + if (activeWrite) { + await activeWrite; + } + + try { + activeWrite = db.write(); + await activeWrite; + } catch (error) { + console.error('Failed to persist database:', error); + } finally { + activeWrite = null; + } + }, writeDelayMs); +} + +/** + * Force immediate write of any pending changes + */ +export async function flushPendingWrites() { + if (isTest) return; + + if (pendingWrite) { + clearTimeout(pendingWrite); + pendingWrite = null; + } + + // Wait for any in-progress write to finish + if (activeWrite) { + await activeWrite; + } + await db.write(); }