Repository: withastro/astro
File: packages/astro/src/content/mutable-data-store.ts (TypeScript)

Code before:
import { promises as fs, type PathLike, existsSync } from 'node:fs';
import * as devalue from 'devalue';
import { Traverse } from 'neotraverse/modern';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, ImmutableDataStore } from './data-store.js';
import { contentModuleToId } from './utils.js';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const SAVE_DEBOUNCE_MS = 500;

// Write a file atomically, without triggering the file watcher
async function writeFileAtomic(filePath: PathLike, data: string) {
	const dir = await fs.mkdtemp(tmpdir() + '/astro-');
	const tempFile = join(dir, 'temp');
	await fs.writeFile(tempFile, data);
	try {
		await fs.rename(tempFile, filePath);
	} finally {
		await fs.rmdir(dir).catch(() => {});
	}
}

/**
 * Extends the DataStore with the ability to change entries and write them to disk.
 * This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed.
 */
export class MutableDataStore extends ImmutableDataStore {
	#file?: PathLike;

	#assetsFile?: PathLike;
	#modulesFile?: PathLike;

	#saveTimeout: NodeJS.Timeout | undefined;
	#assetsSaveTimeout: NodeJS.Timeout | undefined;
	#modulesSaveTimeout: NodeJS.Timeout | undefined;

	#dirty = false;
	#assetsDirty = false;
	#modulesDirty = false;

	#assetImports = new Set<string>();
	#moduleImports = new Map<string, string>();

	set(collectionName: string, key: string, value: unknown) {
		const collection = this._collections.get(collectionName) ?? new Map();

Review comment (performance):
I'm not too fond of this solution because now ALL projects, without discrimination, will incur in **four** (create temp dir, write file, rename file, remove directory) writing I/O operations instead of one. And writing operations are slow.

Maybe we could have an internal heuristic, where we enable this atomic rewriting only when we exceed a certain number of collections. 

As you said, this is hard to test and incur in a project.

Code after:
import { promises as fs, type PathLike, existsSync } from 'node:fs';
import * as devalue from 'devalue';
import { Traverse } from 'neotraverse/modern';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, ImmutableDataStore } from './data-store.js';
import { contentModuleToId } from './utils.js';

const SAVE_DEBOUNCE_MS = 500;

const MAX_DEPTH = 10;

/**
 * Extends the DataStore with the ability to change entries and write them to disk.
 * This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed.
 */
export class MutableDataStore extends ImmutableDataStore {
	#file?: PathLike;

	#assetsFile?: PathLike;
	#modulesFile?: PathLike;

	#saveTimeout: NodeJS.Timeout | undefined;
	#assetsSaveTimeout: NodeJS.Timeout | undefined;
	#modulesSaveTimeout: NodeJS.Timeout | undefined;

	#dirty = false;
	#assetsDirty = false;
	#modulesDirty = false;

	#assetImports = new Set<string>();
	#moduleImports = new Map<string, string>();

	set(collectionName: string, key: string, value: unknown) {
		const collection = this._collections.get(collectionName) ?? new Map();
		collection.set(String(key), value);
		this._collections.set(collectionName, collection);
		this.#saveToDiskDebounced();
	}

	delete(collectionName: string, key: string) {
		const collection = this._collections.get(collectionName);
		if (collection) {
			collection.delete(String(key));
			this.#saveToDiskDebounced();
		}
	}