From 4e2280b94c1817645f2066245183836bf1a37835 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 20 Apr 2026 12:08:35 -0400 Subject: [PATCH] cron: consolidate split store cache state --- src/cron/store.ts | 51 ++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/cron/store.ts b/src/cron/store.ts index be826580b86..0595496dfee 100644 --- a/src/cron/store.ts +++ b/src/cron/store.ts @@ -6,8 +6,22 @@ import { resolveConfigDir } from "../utils.js"; import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; import type { CronStoreFile } from "./types.js"; -const serializedStoreCache = new Map(); -const storesNeedingSplitMigration = new Set(); +type SerializedStoreCacheEntry = { + configJson?: string; + stateJson?: string; + needsSplitMigration: boolean; +}; + +const serializedStoreCache = new Map(); + +function getSerializedStoreCache(storePath: string): SerializedStoreCacheEntry { + let entry = serializedStoreCache.get(storePath); + if (!entry) { + entry = { needsSplitMigration: false }; + serializedStoreCache.set(storePath, entry); + } + return entry; +} function resolveDefaultCronDir(): string { return path.join(resolveConfigDir(), "cron"); @@ -176,20 +190,16 @@ export async function loadCronStore(storePath: string): Promise { const configJson = JSON.stringify(stripRuntimeOnlyCronFields(store), null, 2); const stateJson = JSON.stringify(extractStateFile(store), null, 2); - serializedStoreCache.set(storePath, configJson); - serializedStoreCache.set(`${storePath}:state`, stateJson); - if (hasLegacyInlineState) { - storesNeedingSplitMigration.add(storePath); - } else { - storesNeedingSplitMigration.delete(storePath); - } + serializedStoreCache.set(storePath, { + configJson, + stateJson, + needsSplitMigration: hasLegacyInlineState, + }); return store; } catch (err) { if ((err as { code?: unknown })?.code === "ENOENT") { serializedStoreCache.delete(storePath); - serializedStoreCache.delete(`${storePath}:state`); - storesNeedingSplitMigration.delete(storePath); return { version: 1, jobs: [] }; } throw err; @@ -224,14 +234,11 @@ export async function saveCronStore( const stateJson = JSON.stringify(stateFile, null, 2); const statePath = resolveStatePath(storePath); - const stateCacheKey = `${storePath}:state`; + const cache = serializedStoreCache.get(storePath); - const cachedConfig = serializedStoreCache.get(storePath); - const cachedState = serializedStoreCache.get(stateCacheKey); - - const configChanged = cachedConfig !== configJson; - const stateChanged = cachedState !== stateJson; - const migrating = storesNeedingSplitMigration.has(storePath); + const configChanged = cache?.configJson !== configJson; + const stateChanged = cache?.stateJson !== stateJson; + const migrating = cache?.needsSplitMigration === true; let stateNeedsWrite = stateChanged; if (!stateNeedsWrite) { @@ -251,10 +258,12 @@ export async function saveCronStore( return; } + const updatedCache = getSerializedStoreCache(storePath); + // Write state first so migration never leaves stripped config without runtime state. if (stateNeedsWrite || migrating) { await atomicWrite(statePath, stateJson); - serializedStoreCache.set(stateCacheKey, stateJson); + updatedCache.stateJson = stateJson; } if (configChanged || migrating) { @@ -270,9 +279,9 @@ export async function saveCronStore( } } await atomicWrite(storePath, configJson); - serializedStoreCache.set(storePath, configJson); + updatedCache.configJson = configJson; } - storesNeedingSplitMigration.delete(storePath); + updatedCache.needsSplitMigration = false; } const RENAME_MAX_RETRIES = 3;