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) {
// 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<Rundown>, 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 };
}
@@ -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);
}
}
+5 -1
View File
@@ -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
@@ -180,10 +180,58 @@ async function mergeIntoData(newData: Partial<DatabaseModel>): ReadonlyPromise<D
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() {
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();
}