@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
167 lines • 21.5 kB
JavaScript
/**
* MemoryIndexFile - Persistent _index.json manager with debounced writes.
*
* On server restart the MemoryStorageLayer can load cached metadata from
* _index.json instead of re-scanning every memory file on disk. This class
* handles reading, writing (with validation), and debounced scheduling of
* index persistence to avoid excessive I/O during rapid mutations.
*/
import { logger } from '../utils/logger.js';
const DEFAULT_DEBOUNCE_MS = 2000;
export class MemoryIndexFile {
indexPath;
fileOps;
debounceMs;
pendingEntries = null;
debounceTimer = null;
constructor(indexPath, fileOps, options) {
this.indexPath = indexPath;
this.fileOps = fileOps;
this.debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
}
/**
* Read the index from disk.
* Returns null when the file is missing, corrupt, has a version mismatch,
* or the entryCount doesn't match the actual number of entries.
*/
async read() {
try {
const raw = await this.fileOps.readFile(this.indexPath);
const parsed = JSON.parse(raw);
// Schema validation: verify top-level structure
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
return this.handleCorruptIndex('top-level value is not an object');
}
if (parsed.version !== 1) {
logger.warn(`MemoryIndexFile: unsupported version ${parsed.version}, ignoring`);
return null;
}
if (typeof parsed.entries !== 'object' || parsed.entries === null || Array.isArray(parsed.entries)) {
return this.handleCorruptIndex('entries field is not a Record');
}
// Entry-level validation: every entry must have filePath and name
const validEntries = {};
let skippedCount = 0;
for (const [key, entry] of Object.entries(parsed.entries)) {
if (typeof entry === 'object' && entry !== null &&
typeof entry.filePath === 'string' && entry.filePath.length > 0 &&
typeof entry.name === 'string' && entry.name.length > 0) {
validEntries[key] = entry;
}
else {
skippedCount++;
}
}
if (skippedCount > 0) {
logger.warn(`MemoryIndexFile: skipped ${skippedCount} malformed entries during load`);
}
const actualCount = Object.keys(validEntries).length;
if (parsed.entryCount !== actualCount && skippedCount === 0) {
logger.warn(`MemoryIndexFile: entryCount mismatch (header=${parsed.entryCount}, actual=${actualCount}), ignoring`);
return null;
}
// Return with validated entries and corrected count
return { ...parsed, entries: validEntries, entryCount: actualCount };
}
catch (err) {
const code = err.code;
if (code === 'ENOENT') {
return null;
}
if (err instanceof SyntaxError) {
return this.handleCorruptIndex('invalid JSON');
}
// Re-throw unexpected errors
throw err;
}
}
/**
* Handle a corrupt or malformed index file: log a warning and delete it
* so the next scan builds a clean index from disk.
*/
async handleCorruptIndex(reason) {
logger.warn(`MemoryIndexFile: corrupt _index.json (${reason}), deleting for rebuild`);
try {
await this.fileOps.deleteFile(this.indexPath, 'memories', {
source: 'MemoryIndexFile.handleCorruptIndex',
});
}
catch {
// Best-effort deletion — if it fails, the next read() will try again
}
return null;
}
/**
* Write the index to disk immediately.
* Converts the entries array to a Record keyed by `entry.filePath`.
* Catches ENOSPC gracefully (logs warning, does not throw).
*/
async write(entries) {
const entriesRecord = {};
for (const entry of entries) {
entriesRecord[entry.filePath] = entry;
}
const data = {
version: 1,
generatedAt: new Date().toISOString(),
entryCount: entries.length,
entries: entriesRecord,
};
try {
await this.fileOps.writeFile(this.indexPath, JSON.stringify(data, null, 2));
}
catch (err) {
const code = err.code;
if (code === 'ENOSPC') {
logger.warn('MemoryIndexFile: disk full (ENOSPC), skipping index write');
return;
}
throw err;
}
}
/**
* Schedule a debounced write. Multiple calls within the debounce window
* coalesce into a single write using the most recent entries.
*/
scheduleWrite(entries) {
this.pendingEntries = entries;
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.debounceTimer = null;
const toWrite = this.pendingEntries;
this.pendingEntries = null;
if (toWrite) {
this.write(toWrite).catch((err) => {
logger.error('MemoryIndexFile: scheduled write failed', err);
});
}
}, this.debounceMs);
}
/**
* If there are pending entries, write immediately and clear the timer.
*/
async flush() {
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
if (this.pendingEntries !== null) {
const toWrite = this.pendingEntries;
this.pendingEntries = null;
await this.write(toWrite);
}
}
/**
* Cancel any pending timer. Does NOT flush (fire-and-forget cleanup).
*/
dispose() {
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.pendingEntries = null;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWVtb3J5SW5kZXhGaWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3N0b3JhZ2UvTWVtb3J5SW5kZXhGaWxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7O0dBT0c7QUFFSCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFlNUMsTUFBTSxtQkFBbUIsR0FBRyxJQUFJLENBQUM7QUFFakMsTUFBTSxPQUFPLGVBQWU7SUFDVCxTQUFTLENBQVM7SUFDbEIsT0FBTyxDQUF3QjtJQUMvQixVQUFVLENBQVM7SUFFNUIsY0FBYyxHQUErQixJQUFJLENBQUM7SUFDbEQsYUFBYSxHQUF5QyxJQUFJLENBQUM7SUFFbkUsWUFDRSxTQUFpQixFQUNqQixPQUE4QixFQUM5QixPQUFnQztRQUVoQyxJQUFJLENBQUMsU0FBUyxHQUFHLFNBQVMsQ0FBQztRQUMzQixJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztRQUN2QixJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sRUFBRSxVQUFVLElBQUksbUJBQW1CLENBQUM7SUFDL0QsQ0FBQztJQUVEOzs7O09BSUc7SUFDSCxLQUFLLENBQUMsSUFBSTtRQUNSLElBQUksQ0FBQztZQUNILE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQ3hELE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFvQixDQUFDO1lBRWxELGdEQUFnRDtZQUNoRCxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsSUFBSSxNQUFNLEtBQUssSUFBSSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztnQkFDM0UsT0FBTyxJQUFJLENBQUMsa0JBQWtCLENBQUMsa0NBQWtDLENBQUMsQ0FBQztZQUNyRSxDQUFDO1lBRUQsSUFBSSxNQUFNLENB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LEdBQUcsSUFBSSxDQUFDO0lBQzdCLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogTWVtb3J5SW5kZXhGaWxlIC0gUGVyc2lzdGVudCBfaW5kZXguanNvbiBtYW5hZ2VyIHdpdGggZGVib3VuY2VkIHdyaXRlcy5cbiAqXG4gKiBPbiBzZXJ2ZXIgcmVzdGFydCB0aGUgTWVtb3J5U3RvcmFnZUxheWVyIGNhbiBsb2FkIGNhY2hlZCBtZXRhZGF0YSBmcm9tXG4gKiBfaW5kZXguanNvbiBpbnN0ZWFkIG9mIHJlLXNjYW5uaW5nIGV2ZXJ5IG1lbW9yeSBmaWxlIG9uIGRpc2suICBUaGlzIGNsYXNzXG4gKiBoYW5kbGVzIHJlYWRpbmcsIHdyaXRpbmcgKHdpdGggdmFsaWRhdGlvbiksIGFuZCBkZWJvdW5jZWQgc2NoZWR1bGluZyBvZlxuICogaW5kZXggcGVyc2lzdGVuY2UgdG8gYXZvaWQgZXhjZXNzaXZlIEkvTyBkdXJpbmcgcmFwaWQgbXV0YXRpb25zLlxuICovXG5cbmltcG9ydCB7IGxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5pbXBvcnQgdHlwZSB7IEZpbGVPcGVyYXRpb25zU2VydmljZSB9IGZyb20gJy4uL3NlcnZpY2VzL0ZpbGVPcGVyYXRpb25zU2VydmljZS5qcyc7XG5pbXBvcnQgdHlwZSB7IEVsZW1lbnRJbmRleEVudHJ5IH0gZnJvbSAnLi90eXBlcy5qcyc7XG5cbmV4cG9ydCBpbnRlcmZhY2UgTWVtb3J5SW5kZXhEYXRhIHtcbiAgdmVyc2lvbjogMTtcbiAgZ2VuZXJhdGVkQXQ6IHN0cmluZztcbiAgZW50cnlDb3VudDogbnVtYmVyO1xuICBlbnRyaWVzOiBSZWNvcmQ8c3RyaW5nLCBFbGVtZW50SW5kZXhFbnRyeT47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgTWVtb3J5SW5kZXhGaWxlT3B0aW9ucyB7XG4gIGRlYm91bmNlTXM/OiBudW1iZXI7XG59XG5cbmNvbnN0IERFRkFVTFRfREVCT1VOQ0VfTVMgPSAyMDAwO1xuXG5leHBvcnQgY2xhc3MgTWVtb3J5SW5kZXhGaWxlIHtcbiAgcHJpdmF0ZSByZWFkb25seSBpbmRleFBhdGg6IHN0cmluZztcbiAgcHJpdmF0ZSByZWFkb25seSBmaWxlT3BzOiBGaWxlT3BlcmF0aW9uc1NlcnZpY2U7XG4gIHByaXZhdGUgcmVhZG9ubHkgZGVib3VuY2VNczogbnVtYmVyO1xuXG4gIHByaXZhdGUgcGVuZGluZ0VudHJpZXM6IEVsZW1lbnRJbmRleEVudHJ5W10gfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBkZWJvdW5jZVRpbWVyOiBSZXR1cm5UeXBlPHR5cGVvZiBzZXRUaW1lb3V0PiB8IG51bGwgPSBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIGluZGV4UGF0aDogc3RyaW5nLFxuICAgIGZpbGVPcHM6IEZpbGVPcGVyYXRpb25zU2VydmljZSxcbiAgICBvcHRpb25zPzogTWVtb3J5SW5kZXhGaWxlT3B0aW9ucyxcbiAgKSB7XG4gICAgdGhpcy5pbmRleFBhdGggPSBpbmRleFBhdGg7XG4gICAgdGhpcy5maWxlT3BzID0gZmlsZU9wcztcbiAgICB0aGlzLmRlYm91bmNlTXMgPSBvcHRpb25zPy5kZWJvdW5jZU1zID8/IERFRkFVTFRfREVCT1VOQ0VfTVM7XG4gIH1cblxuICAvKipcbiAgICogUmVhZCB0aGUgaW5kZXggZnJvbSBkaXNrLlxuICAgKiBSZXR1cm5zIG51bGwgd2hlbiB0aGUgZmlsZSBpcyBtaXNzaW5nLCBjb3JydXB0LCBoYXMgYSB2ZXJzaW9uIG1pc21hdGNoLFxuICAgKiBvciB0aGUgZW50cnlDb3VudCBkb2Vzbid0IG1hdGNoIHRoZSBhY3R1YWwgbnVtYmVyIG9mIGVudHJpZXMuXG4gICAqL1xuICBhc3luYyByZWFkKCk6IFByb21pc2U8TWVtb3J5SW5kZXhEYXRhIHwgbnVsbD4ge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCByYXcgPSBhd2FpdCB0aGlzLmZpbGVPcHMucmVhZEZpbGUodGhpcy5pbmRleFBhdGgpO1xuICAgICAgY29uc3QgcGFyc2VkID0gSlNPTi5wYXJzZShyYXcpIGFzIE1lbW9yeUluZGV4RGF0YTtcblxuICAgICAgLy8gU2NoZW1hIHZhbGlkYXRpb246IHZlcmlmeSB0b3AtbGV2ZWwgc3RydWN0dXJlXG4gICAgICBpZiAodHlwZW9mIHBhcnNlZCAhPT0gJ29iamVjdCcgfHwgcGFyc2VkID09PSBudWxsIHx8IEFycmF5LmlzQXJyYXkocGFyc2VkKSkge1xuICAgICAgICByZXR1cm4gdGhpcy5oYW5kbGVDb3JydXB0SW5kZXgoJ3RvcC1sZXZlbCB2YWx1ZSBpcyBub3QgYW4gb2JqZWN0Jyk7XG4gICAgICB9XG5cbiAgICAgIGlmIChwYXJzZWQudmVyc2lvbiAhPT0gMSkge1xuICAgICAgICBsb2dnZXIud2FybihgTWVtb3J5SW5kZXhGaWxlOiB1bnN1cHBvcnRlZCB2ZXJzaW9uICR7KHBhcnNlZCBhcyBhbnkpLnZlcnNpb259LCBpZ25vcmluZ2ApO1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICAgIH1cblxuICAgICAgaWYgKHR5cGVvZiBwYXJzZWQuZW50cmllcyAhPT0gJ29iamVjdCcgfHwgcGFyc2VkLmVudHJpZXMgPT09IG51bGwgfHwgQXJyYXkuaXNBcnJheShwYXJzZWQuZW50cmllcykpIHtcbiAgICAgICAgcmV0dXJuIHRoaXMuaGFuZGxlQ29ycnVwdEluZGV4KCdlbnRyaWVzIGZpZWxkIGlzIG5vdCBhIFJlY29yZCcpO1xuICAgICAgfVxuXG4gICAgICAvLyBFbnRyeS1sZXZlbCB2YWxpZGF0aW9uOiBldmVyeSBlbnRyeSBtdXN0IGhhdmUgZmlsZVBhdGggYW5kIG5hbWVcbiAgICAgIGNvbnN0IHZhbGlkRW50cmllczogUmVjb3JkPHN0cmluZywgRWxlbWVudEluZGV4RW50cnk+ID0ge307XG4gICAgICBsZXQgc2tpcHBlZENvdW50ID0gMDtcbiAgICAgIGZvciAoY29uc3QgW2tleSwgZW50cnldIG9mIE9iamVjdC5lbnRyaWVzKHBhcnNlZC5lbnRyaWVzKSkge1xuICAgICAgICBpZiAoXG4gICAgICAgICAgdHlwZW9mIGVudHJ5ID09PSAnb2JqZWN0JyAmJiBlbnRyeSAhPT0gbnVsbCAmJlxuICAgICAgICAgIHR5cGVvZiBlbnRyeS5maWxlUGF0aCA9PT0gJ3N0cmluZycgJiYgZW50cnkuZmlsZVBhdGgubGVuZ3RoID4gMCAmJlxuICAgICAgICAgIHR5cGVvZiBlbnRyeS5uYW1lID09PSAnc3RyaW5nJyAmJiBlbnRyeS5uYW1lLmxlbmd0aCA+IDBcbiAgICAgICAgKSB7XG4gICAgICAgICAgdmFsaWRFbnRyaWVzW2tleV0gPSBlbnRyeTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBza2lwcGVkQ291bnQrKztcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAoc2tpcHBlZENvdW50ID4gMCkge1xuICAgICAgICBsb2dnZXIud2FybihcbiAgICAgICAgICBgTWVtb3J5SW5kZXhGaWxlOiBza2lwcGVkICR7c2tpcHBlZENvdW50fSBtYWxmb3JtZWQgZW50cmllcyBkdXJpbmcgbG9hZGAsXG4gICAgICAgICk7XG4gICAgICB9XG5cbiAgICAgIGNvbnN0IGFjdHVhbENvdW50ID0gT2JqZWN0LmtleXModmFsaWRFbnRyaWVzKS5sZW5ndGg7XG4gICAgICBpZiAocGFyc2VkLmVudHJ5Q291bnQgIT09IGFjdHVhbENvdW50ICYmIHNraXBwZWRDb3VudCA9PT0gMCkge1xuICAgICAgICBsb2dnZXIud2FybihcbiAgICAgICAgICBgTWVtb3J5SW5kZXhGaWxlOiBlbnRyeUNvdW50IG1pc21hdGNoIChoZWFkZXI9JHtwYXJzZWQuZW50cnlDb3VudH0sIGFjdHVhbD0ke2FjdHVhbENvdW50fSksIGlnbm9yaW5nYCxcbiAgICAgICAgKTtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG5cbiAgICAgIC8vIFJldHVybiB3aXRoIHZhbGlkYXRlZCBlbnRyaWVzIGFuZCBjb3JyZWN0ZWQgY291bnRcbiAgICAgIHJldHVybiB7IC4uLnBhcnNlZCwgZW50cmllczogdmFsaWRFbnRyaWVzLCBlbnRyeUNvdW50OiBhY3R1YWxDb3VudCB9O1xuICAgIH0gY2F0Y2ggKGVycjogdW5rbm93bikge1xuICAgICAgY29uc3QgY29kZSA9IChlcnIgYXMgTm9kZUpTLkVycm5vRXhjZXB0aW9uKS5jb2RlO1xuICAgICAgaWYgKGNvZGUgPT09ICdFTk9FTlQnKSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgICAgfVxuICAgICAgaWYgKGVyciBpbnN0YW5jZW9mIFN5bnRheEVycm9yKSB7XG4gICAgICAgIHJldHVybiB0aGlzLmhhbmRsZUNvcnJ1cHRJbmRleCgnaW52YWxpZCBKU09OJyk7XG4gICAgICB9XG4gICAgICAvLyBSZS10aHJvdyB1bmV4cGVjdGVkIGVycm9yc1xuICAgICAgdGhyb3cgZXJyO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBIYW5kbGUgYSBjb3JydXB0IG9yIG1hbGZvcm1lZCBpbmRleCBmaWxlOiBsb2cgYSB3YXJuaW5nIGFuZCBkZWxldGUgaXRcbiAgICogc28gdGhlIG5leHQgc2NhbiBidWlsZHMgYSBjbGVhbiBpbmRleCBmcm9tIGRpc2suXG4gICAqL1xuICBwcml2YXRlIGFzeW5jIGhhbmRsZUNvcnJ1cHRJbmRleChyZWFzb246IHN0cmluZyk6IFByb21pc2U8bnVsbD4ge1xuICAgIGxvZ2dlci53YXJuKGBNZW1vcnlJbmRleEZpbGU6IGNvcnJ1cHQgX2luZGV4Lmpzb24gKCR7cmVhc29ufSksIGRlbGV0aW5nIGZvciByZWJ1aWxkYCk7XG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IHRoaXMuZmlsZU9wcy5kZWxldGVGaWxlKHRoaXMuaW5kZXhQYXRoLCAnbWVtb3JpZXMnIGFzIGFueSwge1xuICAgICAgICBzb3VyY2U6ICdNZW1vcnlJbmRleEZpbGUuaGFuZGxlQ29ycnVwdEluZGV4JyxcbiAgICAgIH0pO1xuICAgIH0gY2F0Y2gge1xuICAgICAgLy8gQmVzdC1lZmZvcnQgZGVsZXRpb24g4oCUIGlmIGl0IGZhaWxzLCB0aGUgbmV4dCByZWFkKCkgd2lsbCB0cnkgYWdhaW5cbiAgICB9XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cblxuICAvKipcbiAgICogV3JpdGUgdGhlIGluZGV4IHRvIGRpc2sgaW1tZWRpYXRlbHkuXG4gICAqIENvbnZlcnRzIHRoZSBlbnRyaWVzIGFycmF5IHRvIGEgUmVjb3JkIGtleWVkIGJ5IGBlbnRyeS5maWxlUGF0aGAuXG4gICAqIENhdGNoZXMgRU5PU1BDIGdyYWNlZnVsbHkgKGxvZ3Mgd2FybmluZywgZG9lcyBub3QgdGhyb3cpLlxuICAgKi9cbiAgYXN5bmMgd3JpdGUoZW50cmllczogRWxlbWVudEluZGV4RW50cnlbXSk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IGVudHJpZXNSZWNvcmQ6IFJlY29yZDxzdHJpbmcsIEVsZW1lbnRJbmRleEVudHJ5PiA9IHt9O1xuICAgIGZvciAoY29uc3QgZW50cnkgb2YgZW50cmllcykge1xuICAgICAgZW50cmllc1JlY29yZFtlbnRyeS5maWxlUGF0aF0gPSBlbnRyeTtcbiAgICB9XG5cbiAgICBjb25zdCBkYXRhOiBNZW1vcnlJbmRleERhdGEgPSB7XG4gICAgICB2ZXJzaW9uOiAxLFxuICAgICAgZ2VuZXJhdGVkQXQ6IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSxcbiAgICAgIGVudHJ5Q291bnQ6IGVudHJpZXMubGVuZ3RoLFxuICAgICAgZW50cmllczogZW50cmllc1JlY29yZCxcbiAgICB9O1xuXG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IHRoaXMuZmlsZU9wcy53cml0ZUZpbGUodGhpcy5pbmRleFBhdGgsIEpTT04uc3RyaW5naWZ5KGRhdGEsIG51bGwsIDIpKTtcbiAgICB9IGNhdGNoIChlcnI6IHVua25vd24pIHtcbiAgICAgIGNvbnN0IGNvZGUgPSAoZXJyIGFzIE5vZGVKUy5FcnJub0V4Y2VwdGlvbikuY29kZTtcbiAgICAgIGlmIChjb2RlID09PSAnRU5PU1BDJykge1xuICAgICAgICBsb2dnZXIud2FybignTWVtb3J5SW5kZXhGaWxlOiBkaXNrIGZ1bGwgKEVOT1NQQyksIHNraXBwaW5nIGluZGV4IHdyaXRlJyk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIHRocm93IGVycjtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogU2NoZWR1bGUgYSBkZWJvdW5jZWQgd3JpdGUuICBNdWx0aXBsZSBjYWxscyB3aXRoaW4gdGhlIGRlYm91bmNlIHdpbmRvd1xuICAgKiBjb2FsZXNjZSBpbnRvIGEgc2luZ2xlIHdyaXRlIHVzaW5nIHRoZSBtb3N0IHJlY2VudCBlbnRyaWVzLlxuICAgKi9cbiAgc2NoZWR1bGVXcml0ZShlbnRyaWVzOiBFbGVtZW50SW5kZXhFbnRyeVtdKTogdm9pZCB7XG4gICAgdGhpcy5wZW5kaW5nRW50cmllcyA9IGVudHJpZXM7XG5cbiAgICBpZiAodGhpcy5kZWJvdW5jZVRpbWVyICE9PSBudWxsKSB7XG4gICAgICBjbGVhclRpbWVvdXQodGhpcy5kZWJvdW5jZVRpbWVyKTtcbiAgICB9XG5cbiAgICB0aGlzLmRlYm91bmNlVGltZXIgPSBzZXRUaW1lb3V0KCgpID0+IHtcbiAgICAgIHRoaXMuZGVib3VuY2VUaW1lciA9IG51bGw7XG4gICAgICBjb25zdCB0b1dyaXRlID0gdGhpcy5wZW5kaW5nRW50cmllcztcbiAgICAgIHRoaXMucGVuZGluZ0VudHJpZXMgPSBudWxsO1xuICAgICAgaWYgKHRvV3JpdGUpIHtcbiAgICAgICAgdGhpcy53cml0ZSh0b1dyaXRlKS5jYXRjaCgoZXJyKSA9PiB7XG4gICAgICAgICAgbG9nZ2VyLmVycm9yKCdNZW1vcnlJbmRleEZpbGU6IHNjaGVkdWxlZCB3cml0ZSBmYWlsZWQnLCBlcnIpO1xuICAgICAgICB9KTtcbiAgICAgIH1cbiAgICB9LCB0aGlzLmRlYm91bmNlTXMpO1xuICB9XG5cbiAgLyoqXG4gICAqIElmIHRoZXJlIGFyZSBwZW5kaW5nIGVudHJpZXMsIHdyaXRlIGltbWVkaWF0ZWx5IGFuZCBjbGVhciB0aGUgdGltZXIuXG4gICAqL1xuICBhc3luYyBmbHVzaCgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICBpZiAodGhpcy5kZWJvdW5jZVRpbWVyICE9PSBudWxsKSB7XG4gICAgICBjbGVhclRpbWVvdXQodGhpcy5kZWJvdW5jZVRpbWVyKTtcbiAgICAgIHRoaXMuZGVib3VuY2VUaW1lciA9IG51bGw7XG4gICAgfVxuXG4gICAgaWYgKHRoaXMucGVuZGluZ0VudHJpZXMgIT09IG51bGwpIHtcbiAgICAgIGNvbnN0IHRvV3JpdGUgPSB0aGlzLnBlbmRpbmdFbnRyaWVzO1xuICAgICAgdGhpcy5wZW5kaW5nRW50cmllcyA9IG51bGw7XG4gICAgICBhd2FpdCB0aGlzLndyaXRlKHRvV3JpdGUpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBDYW5jZWwgYW55IHBlbmRpbmcgdGltZXIuICBEb2VzIE5PVCBmbHVzaCAoZmlyZS1hbmQtZm9yZ2V0IGNsZWFudXApLlxuICAgKi9cbiAgZGlzcG9zZSgpOiB2b2lkIHtcbiAgICBpZiAodGhpcy5kZWJvdW5jZVRpbWVyICE9PSBudWxsKSB7XG4gICAgICBjbGVhclRpbWVvdXQodGhpcy5kZWJvdW5jZVRpbWVyKTtcbiAgICAgIHRoaXMuZGVib3VuY2VUaW1lciA9IG51bGw7XG4gICAgfVxuICAgIHRoaXMucGVuZGluZ0VudHJpZXMgPSBudWxsO1xuICB9XG59XG4iXX0=