refactor: debounce writing to disk

This commit is contained in:
Carlos Valente
2026-02-01 18:20:22 +01:00
committed by Carlos Valente
parent e16ec3f388
commit 77779a4648
4 changed files with 65 additions and 21 deletions
@@ -108,11 +108,6 @@ export function createTransaction(options: TransactionOptions): Transaction {
function commit(shouldProcess: boolean = true) { function commit(shouldProcess: boolean = true) {
// if the rundown is mutable we persist the changes // if the rundown is mutable we persist the changes
if (options.mutableRundown) { 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 // update fields which are agnostic of whether the rundown is processed
cachedRundown.revision = cachedRundown.revision + 1; cachedRundown.revision = cachedRundown.revision + 1;
cachedRundown.title = rundown.title; cachedRundown.title = rundown.title;
@@ -133,16 +128,17 @@ export function createTransaction(options: TransactionOptions): Transaction {
cachedRundown.flatOrder = metadata.flatEntryOrder; cachedRundown.flatOrder = metadata.flatEntryOrder;
rundownMetadata = metadata; rundownMetadata = metadata;
} }
// persist after all mutations are applied
getDataProvider().setRundown(cachedRundown.id, cachedRundown);
} }
// if the customFields are mutable we persist the changes // if the customFields are mutable we persist the changes
if (options.mutableCustomFields) { if (options.mutableCustomFields) {
// schedule a database update
setImmediate(async () => {
await getDataProvider().setCustomFields(projectCustomFields);
});
projectCustomFields = customFields; projectCustomFields = customFields;
// persist after reassignment
getDataProvider().setCustomFields(projectCustomFields);
} }
return { return {
@@ -552,10 +548,8 @@ export const rundownMutation = {
/** /**
* Exposes a way to update a rundown which is not active * Exposes a way to update a rundown which is not active
*/ */
export function updateBackgroundRundown(rundownId: string, rundown: Rundown) { export async function updateBackgroundRundown(rundownId: string, rundown: Rundown) {
setImmediate(async () => { await getDataProvider().setRundown(rundownId, rundown);
await getDataProvider().setRundown(rundownId, rundown);
});
} }
/** /**
@@ -652,9 +646,7 @@ export function init(initialRundown: Readonly<Rundown>, initialCustomFields: Rea
rundownMetadata = metadata; rundownMetadata = metadata;
// defer writing to the database // defer writing to the database
setImmediate(async () => { getDataProvider().setRundown(cachedRundown.id, cachedRundown);
await getDataProvider().setRundown(cachedRundown.id, cachedRundown);
});
return { rundown, rundownMetadata, customFields, revision: rundown.revision }; return { rundown, rundownMetadata, customFields, revision: rundown.revision };
} }
@@ -486,7 +486,7 @@ export async function editCustomField(
if (rundownId !== rundown.id) { if (rundownId !== rundown.id) {
const backgroundRundown = structuredClone(projectRundowns[rundownId]); const backgroundRundown = structuredClone(projectRundowns[rundownId]);
customFieldMutation.renameUsages(backgroundRundown, oldKey, newKey); 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) { if (rundownId !== rundown.id) {
const backgroundRundown = structuredClone(projectRundowns[rundownId]); const backgroundRundown = structuredClone(projectRundowns[rundownId]);
customFieldMutation.removeUsages(backgroundRundown, key); customFieldMutation.removeUsages(backgroundRundown, key);
updateBackgroundRundown(rundownId, backgroundRundown); await updateBackgroundRundown(rundownId, backgroundRundown);
} }
} }
+5 -1
View File
@@ -25,7 +25,7 @@ import { integrationRouter } from './api-integration/integration.router.js';
// Import adapters // Import adapters
import { socket } from './adapters/WebsocketAdapter.js'; import { socket } from './adapters/WebsocketAdapter.js';
import { getDataProvider } from './classes/data-provider/DataProvider.js'; import { getDataProvider, flushPendingWrites } from './classes/data-provider/DataProvider.js';
// Services // Services
import { logger } from './classes/Logger.js'; import { logger } from './classes/Logger.js';
@@ -266,6 +266,10 @@ export const startIntegrations = async () => {
export const shutdown = async (exitCode = 0) => { export const shutdown = async (exitCode = 0) => {
consoleHighlight(`Ontime shutting down with code ${exitCode}`); 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 // clear the restore file if it was a normal exit
// 0 means it was a SIGNAL // 0 means it was a SIGNAL
// 1 means crash -> keep the file // 1 means crash -> keep the file
@@ -180,10 +180,58 @@ async function mergeIntoData(newData: Partial<DatabaseModel>): ReadonlyPromise<D
return db.data; return db.data;
} }
// Module-level state for debounced writes
let pendingWrite: NodeJS.Timeout | null = null;
let activeWrite: Promise<void> | 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() { async function persist() {
if (isTest) return; 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(); await db.write();
} }