jjj 0.2.1

A modal interface for Jujutsu.
import { promises as fs, 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 { ImmutableDataStore } from "./data-store.js";
import { contentModuleToId } from "./utils.js";
const SAVE_DEBOUNCE_MS = 500;
const MAX_DEPTH = 10;
class MutableDataStore extends ImmutableDataStore {
  #file;
  #assetsFile;
  #modulesFile;
  #saveTimeout;
  #assetsSaveTimeout;
  #modulesSaveTimeout;
  #dirty = false;
  #assetsDirty = false;
  #modulesDirty = false;
  #assetImports = /* @__PURE__ */ new Set();
  #moduleImports = /* @__PURE__ */ new Map();
  set(collectionName, key, value) {
    const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
    collection.set(String(key), value);
    this._collections.set(collectionName, collection);
    this.#saveToDiskDebounced();
  }
  delete(collectionName, key) {
    const collection = this._collections.get(collectionName);
    if (collection) {
      collection.delete(String(key));
      this.#saveToDiskDebounced();
    }
  }
  clear(collectionName) {
    this._collections.delete(collectionName);
    this.#saveToDiskDebounced();
  }
  clearAll() {
    this._collections.clear();
    this.#saveToDiskDebounced();
  }
  addAssetImport(assetImport, filePath) {
    const id = imageSrcToImportId(assetImport, filePath);
    if (id) {
      this.#assetImports.add(id);
      this.#writeAssetsImportsDebounced();
    }
  }
  addAssetImports(assets, filePath) {
    assets.forEach((asset) => this.addAssetImport(asset, filePath));
  }
  addModuleImport(fileName) {
    const id = contentModuleToId(fileName);
    if (id) {
      this.#moduleImports.set(fileName, id);
      this.#writeModulesImportsDebounced();
    }
  }
  async writeAssetImports(filePath) {
    this.#assetsFile = filePath;
    if (this.#assetImports.size === 0) {
      try {
        await this.#writeFileAtomic(filePath, "export default new Map();");
      } catch (err) {
        throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
      }
    }
    if (!this.#assetsDirty && existsSync(filePath)) {
      return;
    }
    const imports = [];
    const exports = [];
    this.#assetImports.forEach((id) => {
      const symbol = importIdToSymbolName(id);
      imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
      exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
    });
    const code = (
      /* js */
      `
${imports.join("\n")}
export default new Map([${exports.join(", ")}]);
		`
    );
    try {
      await this.#writeFileAtomic(filePath, code);
    } catch (err) {
      throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
    }
    this.#assetsDirty = false;
  }
  async writeModuleImports(filePath) {
    this.#modulesFile = filePath;
    if (this.#moduleImports.size === 0) {
      try {
        await this.#writeFileAtomic(filePath, "export default new Map();");
      } catch (err) {
        throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
      }
    }
    if (!this.#modulesDirty && existsSync(filePath)) {
      return;
    }
    const lines = [];
    for (const [fileName, specifier] of this.#moduleImports) {
      lines.push(`[${JSON.stringify(fileName)}, () => import(${JSON.stringify(specifier)})]`);
    }
    const code = `
export default new Map([
${lines.join(",\n")}]);
		`;
    try {
      await this.#writeFileAtomic(filePath, code);
    } catch (err) {
      throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
    }
    this.#modulesDirty = false;
  }
  #writeAssetsImportsDebounced() {
    this.#assetsDirty = true;
    if (this.#assetsFile) {
      if (this.#assetsSaveTimeout) {
        clearTimeout(this.#assetsSaveTimeout);
      }
      this.#assetsSaveTimeout = setTimeout(() => {
        this.#assetsSaveTimeout = void 0;
        this.writeAssetImports(this.#assetsFile);
      }, SAVE_DEBOUNCE_MS);
    }
  }
  #writeModulesImportsDebounced() {
    this.#modulesDirty = true;
    if (this.#modulesFile) {
      if (this.#modulesSaveTimeout) {
        clearTimeout(this.#modulesSaveTimeout);
      }
      this.#modulesSaveTimeout = setTimeout(() => {
        this.#modulesSaveTimeout = void 0;
        this.writeModuleImports(this.#modulesFile);
      }, SAVE_DEBOUNCE_MS);
    }
  }
  #saveToDiskDebounced() {
    this.#dirty = true;
    if (this.#saveTimeout) {
      clearTimeout(this.#saveTimeout);
    }
    this.#saveTimeout = setTimeout(() => {
      this.#saveTimeout = void 0;
      if (this.#file) {
        this.writeToDisk();
      }
    }, SAVE_DEBOUNCE_MS);
  }
  #writing = /* @__PURE__ */ new Set();
  #pending = /* @__PURE__ */ new Set();
  async #writeFileAtomic(filePath, data, depth = 0) {
    if (depth > MAX_DEPTH) {
      return;
    }
    const fileKey = filePath.toString();
    if (this.#writing.has(fileKey)) {
      this.#pending.add(fileKey);
      return;
    }
    this.#writing.add(fileKey);
    const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`;
    try {
      const oldData = await fs.readFile(filePath, "utf-8").catch(() => "");
      if (oldData === data) {
        return;
      }
      await fs.writeFile(tempFile, data);
      await fs.rename(tempFile, filePath);
    } finally {
      this.#writing.delete(fileKey);
      if (this.#pending.has(fileKey)) {
        this.#pending.delete(fileKey);
        await this.#writeFileAtomic(filePath, data, depth + 1);
      }
    }
  }
  scopedStore(collectionName) {
    return {
      get: (key) => this.get(collectionName, key),
      entries: () => this.entries(collectionName),
      values: () => this.values(collectionName),
      keys: () => this.keys(collectionName),
      set: ({
        id: key,
        data,
        body,
        filePath,
        deferredRender,
        digest,
        rendered,
        assetImports,
        legacyId
      }) => {
        if (!key) {
          throw new Error(`ID must be a non-empty string`);
        }
        const id = String(key);
        if (digest) {
          const existing = this.get(collectionName, id);
          if (existing && existing.digest === digest) {
            return false;
          }
        }
        const foundAssets = new Set(assetImports);
        new Traverse(data).forEach((_, val) => {
          if (typeof val === "string" && val.startsWith(IMAGE_IMPORT_PREFIX)) {
            const src = val.replace(IMAGE_IMPORT_PREFIX, "");
            foundAssets.add(src);
          }
        });
        const entry = {
          id,
          data
        };
        if (body) {
          entry.body = body;
        }
        if (filePath) {
          if (filePath.startsWith("/")) {
            throw new Error(`File path must be relative to the site root. Got: ${filePath}`);
          }
          entry.filePath = filePath;
        }
        if (foundAssets.size) {
          entry.assetImports = Array.from(foundAssets);
          this.addAssetImports(entry.assetImports, filePath);
        }
        if (digest) {
          entry.digest = digest;
        }
        if (rendered) {
          entry.rendered = rendered;
        }
        if (legacyId) {
          entry.legacyId = legacyId;
        }
        if (deferredRender) {
          entry.deferredRender = deferredRender;
          if (filePath) {
            this.addModuleImport(filePath);
          }
        }
        this.set(collectionName, id, entry);
        return true;
      },
      delete: (key) => this.delete(collectionName, key),
      clear: () => this.clear(collectionName),
      has: (key) => this.has(collectionName, key),
      addAssetImport: (assetImport, fileName) => this.addAssetImport(assetImport, fileName),
      addAssetImports: (assets, fileName) => this.addAssetImports(assets, fileName),
      addModuleImport: (fileName) => this.addModuleImport(fileName)
    };
  }
  /**
   * Returns a MetaStore for a given collection, or if no collection is provided, the default meta collection.
   */
  metaStore(collectionName = ":meta") {
    const collectionKey = `meta:${collectionName}`;
    return {
      get: (key) => this.get(collectionKey, key),
      set: (key, data) => this.set(collectionKey, key, data),
      delete: (key) => this.delete(collectionKey, key),
      has: (key) => this.has(collectionKey, key)
    };
  }
  toString() {
    return devalue.stringify(this._collections);
  }
  async writeToDisk() {
    if (!this.#dirty) {
      return;
    }
    if (!this.#file) {
      throw new AstroError(AstroErrorData.UnknownFilesystemError);
    }
    try {
      await this.#writeFileAtomic(this.#file, this.toString());
      this.#dirty = false;
    } catch (err) {
      throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
    }
  }
  /**
   * Attempts to load a MutableDataStore from the virtual module.
   * This only works in Vite.
   */
  static async fromModule() {
    try {
      const data = await import("astro:data-layer-content");
      const map = devalue.unflatten(data.default);
      return MutableDataStore.fromMap(map);
    } catch {
    }
    return new MutableDataStore();
  }
  static async fromMap(data) {
    const store = new MutableDataStore();
    store._collections = data;
    return store;
  }
  static async fromString(data) {
    const map = devalue.parse(data);
    return MutableDataStore.fromMap(map);
  }
  static async fromFile(filePath) {
    try {
      if (existsSync(filePath)) {
        const data = await fs.readFile(filePath, "utf-8");
        const store2 = await MutableDataStore.fromString(data);
        store2.#file = filePath;
        return store2;
      } else {
        await fs.mkdir(new URL("./", filePath), { recursive: true });
      }
    } catch {
    }
    const store = new MutableDataStore();
    store.#file = filePath;
    return store;
  }
}
export {
  MutableDataStore
};