UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

359 lines (358 loc) 13.8 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * NodeStorage.ts * * ARCHITECTURE DOCUMENTATION * ========================== * * NodeStorage is the local file system implementation of IStorage for Node.js environments. * It provides real-time file system watching using Node.js fs.watch() API. * * FILE SYSTEM WATCHING: * --------------------- * - startWatching(): Begins recursive watching of the storage root folder * - stopWatching(): Stops a specific watcher by ID * - stopAllWatching(): Stops all watchers on this storage * * When file changes are detected: * 1. fs.watch() callback fires with filename and event type * 2. NodeStorage determines if it's a file or folder change * 3. Appropriate notify*() method is called on StorageBase * 4. Events propagate to listeners (HttpServer, etc.) * * DEBOUNCING: * ----------- * File system events can fire multiple times for a single logical change. * We debounce events by path, waiting 100ms after the last event before * processing to coalesce rapid-fire events. * * PIPELINE INTEGRATION: * --------------------- * NodeStorage (this) -> HttpServer (traps events) -> WebSocket -> HttpStorage -> MCWorld -> WorldView * * RELATED FILES: * -------------- * - IStorageWatcher.ts: Watcher interface definition * - NodeFolder.ts: Folder implementation (also supports watching) * - NodeFile.ts: File implementation * - HttpServer.ts: Traps events and broadcasts via WebSocket */ const NodeFolder_1 = __importDefault(require("./NodeFolder")); const StorageBase_1 = __importDefault(require("../storage/StorageBase")); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const NodeFile_1 = __importDefault(require("./NodeFile")); const ZipStorage_1 = __importDefault(require("../storage/ZipStorage")); const Log_1 = __importDefault(require("../core/Log")); const ste_events_1 = require("ste-events"); /** Default debounce delay for file system events in milliseconds */ const WATCHER_DEBOUNCE_MS = 100; class NodeStorage extends StorageBase_1.default { rootPath; name; rootFolder; static platformFolderDelimiter = path.sep; /** Active file system watchers, keyed by watcher ID */ _watchers = new Map(); /** Debounce timers for coalescing rapid file system events */ _debounceTimers = new Map(); /** Counter for generating unique watcher IDs */ _watcherIdCounter = 0; /** Event dispatcher for storage change events */ #onStorageChange = new ste_events_1.EventDispatcher(); get folderDelimiter() { return path.sep; } get isWatching() { return this._watchers.size > 0; } get onStorageChange() { return this.#onStorageChange.asEvent(); } constructor(incomingPath, name) { super(); if (NodeStorage.platformFolderDelimiter === "\\") { incomingPath = incomingPath.replace(/\//gi, NodeStorage.platformFolderDelimiter); incomingPath = incomingPath.replace(/\\\\/gi, "\\"); } else if (NodeStorage.platformFolderDelimiter === "/") { incomingPath = incomingPath.replace(/\\/gi, NodeStorage.platformFolderDelimiter); incomingPath = incomingPath.replace(/\/\//gi, NodeStorage.platformFolderDelimiter); } this.rootPath = incomingPath; this.name = name; this.rootFolder = new NodeFolder_1.default(this, null, incomingPath, name); } joinPath(pathA, pathB) { let fullPath = pathA; if (!fullPath.endsWith(NodeStorage.platformFolderDelimiter)) { fullPath += NodeStorage.platformFolderDelimiter; } fullPath += pathB; return fullPath; } static async createFromPath(path) { const lastDot = path.lastIndexOf("."); let lastSlash = path.lastIndexOf("/"); let lastBackslash = path.lastIndexOf("\\"); if (lastBackslash > lastSlash) { lastSlash = lastBackslash; } if (lastDot > lastSlash) { const ns = new NodeStorage(path.substring(0, lastSlash), ""); return (await ns.rootFolder.ensureFileFromRelativePath(path.substring(lastSlash))); } else { const ns = new NodeStorage(path, ""); return ns.rootFolder; } } static async createFromPathIncludingZip(path) { const content = await NodeStorage.createFromPath(path); if (content instanceof NodeFile_1.default && (path.endsWith(".mcpack") || path.endsWith(".mcaddon") || path.endsWith(".mcworld") || path.endsWith(".zip") || path.endsWith(".mcproject"))) { const zs = await ZipStorage_1.default.loadFromFile(content); return zs?.rootFolder; } if (content instanceof NodeFolder_1.default) { return content; } return undefined; } static getParentFolderPath(parentPath) { const lastDelim = parentPath.lastIndexOf(this.platformFolderDelimiter); if (lastDelim < 0) { return parentPath; } return parentPath.substring(0, lastDelim); } static ensureEndsWithDelimiter(pth) { if (!pth.endsWith(NodeStorage.platformFolderDelimiter)) { pth = pth + NodeStorage.platformFolderDelimiter; } return pth; } static ensureStartsWithDelimiter(pth) { if (!pth.startsWith(NodeStorage.platformFolderDelimiter)) { pth = NodeStorage.platformFolderDelimiter + pth; } return pth; } async getAvailable() { this.available = true; return this.available; } /** * Start watching the storage for file system changes. * Uses Node.js fs.watch() with recursive option where supported. * * @returns A unique watcher ID that can be used to stop this watcher */ startWatching() { const watcherId = `watcher-${++this._watcherIdCounter}`; const watchers = []; try { // Create a recursive watcher on the root path const watcher = fs.watch(this.rootPath, { recursive: true, persistent: false }, (eventType, filename) => { if (filename) { this._handleWatchEvent(watcherId, eventType, filename); } }); watcher.on("error", (err) => { Log_1.default.debug(`Watcher error for ${this.rootPath}: ${err.message}`); }); watchers.push(watcher); this._watchers.set(watcherId, watchers); Log_1.default.verbose(`Started watching ${this.rootPath} with ID ${watcherId}`); } catch (err) { Log_1.default.debug(`Failed to start watcher for ${this.rootPath}: ${err.message}`); } return watcherId; } /** * Stop a specific watcher by ID. */ stopWatching(watcherId) { const watchers = this._watchers.get(watcherId); if (watchers) { for (const watcher of watchers) { try { watcher.close(); } catch (e) { // Ignore close errors } } this._watchers.delete(watcherId); Log_1.default.verbose(`Stopped watcher ${watcherId} for ${this.rootPath}`); } } /** * Stop all watchers on this storage. */ stopAllWatching() { for (const watchers of this._watchers.values()) { for (const watcher of watchers) { try { watcher.close(); } catch (e) { // Ignore close errors } } } this._watchers.clear(); // Clear all debounce timers for (const timer of this._debounceTimers.values()) { clearTimeout(timer); } this._debounceTimers.clear(); Log_1.default.verbose(`Stopped all watchers for ${this.rootPath}`); } /** * Handle a file system watch event with debouncing. */ _handleWatchEvent(watcherId, eventType, filename) { // Create a debounce key based on the filename const debounceKey = `${watcherId}:${filename}`; // Clear any existing debounce timer for this path const existingTimer = this._debounceTimers.get(debounceKey); if (existingTimer) { clearTimeout(existingTimer); } // Set a new debounce timer const timer = setTimeout(() => { this._debounceTimers.delete(debounceKey); this._processWatchEvent(eventType, filename); }, WATCHER_DEBOUNCE_MS); this._debounceTimers.set(debounceKey, timer); } /** * Process a debounced file system event. */ async _processWatchEvent(eventType, filename) { try { const fullPath = path.join(this.rootPath, filename); const relativePath = "/" + filename.replace(/\\/g, "/"); // Check if the path exists const exists = fs.existsSync(fullPath); // Determine if it's a file or folder let isFile = true; let isDirectory = false; if (exists) { try { const stat = fs.statSync(fullPath); isFile = stat.isFile(); isDirectory = stat.isDirectory(); } catch (e) { // If we can't stat it, assume it was removed isFile = true; } } // Determine change type let changeType; if (!exists) { changeType = "removed"; } else if (eventType === "rename") { // "rename" can mean added or removed - we check existence above changeType = "added"; } else { changeType = "modified"; } // Create and dispatch the storage change event const changeEvent = { changeType, path: relativePath, isFile, timestamp: new Date(), source: "fswatch", }; this.#onStorageChange.dispatch(this, changeEvent); // Also dispatch the appropriate specific event if (isFile) { if (changeType === "removed") { this.notifyFileRemoved(relativePath); } else if (changeType === "added") { // Get or create the file and notify const file = await this.rootFolder.getFileFromRelativePath(relativePath); if (file) { this.notifyFileAdded(file); } } else { // Modified - reload and notify const file = await this.rootFolder.getFileFromRelativePath(relativePath); if (file) { await file.scanForChanges(); } } } else if (isDirectory) { if (changeType === "removed") { // Folder removed - find and notify this.notifyFolderRemoved({ storageRelativePath: relativePath, name: path.basename(filename) }); } else if (changeType === "added") { // Folder added const folder = await this.rootFolder.getFolderFromRelativePath(relativePath); if (folder) { this.notifyFolderAdded(folder); } } } Log_1.default.verbose(`Storage change: ${changeType} ${isFile ? "file" : "folder"} ${relativePath}`); } catch (err) { Log_1.default.debug(`Error processing watch event for ${filename}: ${err.message}`); } } } exports.default = NodeStorage;