UNPKG

@aurios/jason

Version:

A simple, lightweight, and embeddable JSON document database built on Bun.

124 lines (123 loc) 4.42 kB
import { readFile, stat } from "node:fs/promises"; import { basename, join } from "node:path"; import { parse, stringify } from "devalue"; import { MetadataPersistenceError } from "../core/errors.js"; import Writer from "../io/writer.js"; const MAX_METADATA_SIZE = 1024 * 10; const METADATA_PARSE_TIMEOUT = 500; export default class Metadata { #metadataPath; #metadata; #writer; constructor(path) { this.#metadataPath = join(path, "_metadata.json"); this.#writer = new Writer(path); this.#metadata = this.#initializeMetadata(path); } get documentCount() { return this.#metadata.documentCount; } get indexes() { return this.#metadata.indexes; } async incrementDocumentCount(amount = 1) { this.#metadata.documentCount += amount; await this.#persist({ documentCount: this.#metadata.documentCount }); } async decrementDocumentCount(amount = 1) { this.#metadata.documentCount -= amount; await this.#persist({ documentCount: this.#metadata.documentCount }); } async addIndex(indexName) { if (!this.#metadata.indexes.includes(indexName)) { this.#metadata.indexes.push(indexName); await this.#persist({ indexes: this.#metadata.indexes }); } } async updateLastModified() { await this.#persist({ lastModified: Date.now() }); } async #validateMetadata(data) { if (!data || typeof data !== "object") { throw new Error("Invalid metadata structure"); } const metadata = data; if (typeof metadata.documentCount !== "number" || metadata.documentCount < 0) { throw new Error("Invalid document count"); } if (!Array.isArray(metadata.indexes)) { throw new Error("Invalid indexes format"); } return metadata; } async #persist(update) { try { const newMetadata = { ...this.#metadata, ...update, lastModified: Date.now(), }; await this.#writer.write("_metadata", stringify(newMetadata)); } catch (error) { throw new MetadataPersistenceError(`Failed to save metadata: ${error instanceof Error ? error.message : "Unknown error"}`, error); } } /** * Initializes the collection metadata with the given name and options. * * If the options include generateMetadata, the collection metadata will be * initialized with an empty object. Otherwise, it will be initialized with * the default collection metadata. * * @param name - The name of the collection. * @param generateMetadata - Whether to initialize the collection metadata * with an empty object. Defaults to false. * @returns The initialized collection metadata. */ #initializeMetadata(name) { return { name: basename(name, ".json"), documentCount: 0, indexes: [], lastModified: Date.now(), }; } /** * Loads the collection metadata from the file system. * * Reads the metadata file and parses its JSON content to update the * metadata property. If the file cannot be read or does not exist, * it initializes the metadata by saving the default metadata to * the file system. * * @returns A promise that resolves when the metadata is loaded. */ async load() { try { const [stats, data] = await Promise.all([ stat(this.#metadataPath), readFile(this.#metadataPath, "utf-8"), // new Promise((_, reject) => // setTimeout( // () => reject(new Error("Metadata load timeout")), // METADATA_PARSE_TIMEOUT // ) // ), ]); if (stats.size > MAX_METADATA_SIZE) { throw new Error("Metadata file is too large"); } const parsed = parse(data); const validated = await this.#validateMetadata(parsed); this.#metadata = { ...this.#initializeMetadata(this.#metadata.name), ...validated, }; } catch (error) { await this.#persist(this.#metadata); } } }