@componentor/fs 3.0.49 → 3.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +90 -14
- package/dist/index.js.map +1 -1
- package/dist/workers/opfs-sync.worker.js +74 -2
- package/dist/workers/opfs-sync.worker.js.map +1 -1
- package/dist/workers/repair.worker.js +90 -14
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +90 -14
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +90 -14
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +12 -0
|
@@ -160,9 +160,24 @@ var RENAME_CHUNK = 2 * 1024 * 1024;
|
|
|
160
160
|
async function renameInOPFS(oldPath, newPath) {
|
|
161
161
|
let srcAccess = null;
|
|
162
162
|
let dstAccess = null;
|
|
163
|
+
let oldDir;
|
|
164
|
+
let oldHandle;
|
|
165
|
+
try {
|
|
166
|
+
oldDir = await navigateToParent(oldPath);
|
|
167
|
+
oldHandle = await oldDir.getFileHandle(basename(oldPath));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (err?.name === "TypeMismatchError" || err?.name === "NotFoundError" || err?.message?.includes("TypeMismatch") || err?.message?.includes("not a file") || err?.message?.includes("not an entry of requested type")) {
|
|
170
|
+
try {
|
|
171
|
+
await renameDirInOPFS(oldPath, newPath);
|
|
172
|
+
} catch (dirErr) {
|
|
173
|
+
console.warn("[opfs-sync] rename (dir) failed:", oldPath, "\u2192", newPath, dirErr);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.warn("[opfs-sync] rename failed:", oldPath, "\u2192", newPath, err);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
163
180
|
try {
|
|
164
|
-
const oldDir = await navigateToParent(oldPath);
|
|
165
|
-
const oldHandle = await oldDir.getFileHandle(basename(oldPath));
|
|
166
181
|
srcAccess = await oldHandle.createSyncAccessHandle();
|
|
167
182
|
const size = srcAccess.getSize();
|
|
168
183
|
const newDir = await ensureParentDirs(newPath);
|
|
@@ -209,6 +224,63 @@ async function renameInOPFS(oldPath, newPath) {
|
|
|
209
224
|
}
|
|
210
225
|
}
|
|
211
226
|
}
|
|
227
|
+
async function renameDirInOPFS(oldPath, newPath) {
|
|
228
|
+
const oldParent = await navigateToParent(oldPath);
|
|
229
|
+
const srcDir = await oldParent.getDirectoryHandle(basename(oldPath));
|
|
230
|
+
const newParent = await ensureParentDirs(newPath);
|
|
231
|
+
try {
|
|
232
|
+
await newParent.removeEntry(basename(newPath), { recursive: true });
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (e?.name !== "NotFoundError") throw e;
|
|
235
|
+
}
|
|
236
|
+
const dstDir = await newParent.getDirectoryHandle(basename(newPath), { create: true });
|
|
237
|
+
await copyDirContents(srcDir, dstDir);
|
|
238
|
+
await oldParent.removeEntry(basename(oldPath), { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
async function copyDirContents(src, dst) {
|
|
241
|
+
for await (const [name, handle] of src.entries()) {
|
|
242
|
+
if (handle.kind === "directory") {
|
|
243
|
+
const childDst = await dst.getDirectoryHandle(name, { create: true });
|
|
244
|
+
await copyDirContents(handle, childDst);
|
|
245
|
+
} else {
|
|
246
|
+
const fileHandle = handle;
|
|
247
|
+
const dstFile = await dst.getFileHandle(name, { create: true });
|
|
248
|
+
let srcAccess = null;
|
|
249
|
+
let dstAccess = null;
|
|
250
|
+
try {
|
|
251
|
+
srcAccess = await fileHandle.createSyncAccessHandle();
|
|
252
|
+
dstAccess = await dstFile.createSyncAccessHandle();
|
|
253
|
+
const size = srcAccess.getSize();
|
|
254
|
+
dstAccess.truncate(0);
|
|
255
|
+
if (size > 0) {
|
|
256
|
+
const chunk = new Uint8Array(Math.min(size, RENAME_CHUNK));
|
|
257
|
+
let offset = 0;
|
|
258
|
+
while (offset < size) {
|
|
259
|
+
const len = Math.min(chunk.length, size - offset);
|
|
260
|
+
const view = len === chunk.length ? chunk : chunk.subarray(0, len);
|
|
261
|
+
srcAccess.read(view, { at: offset });
|
|
262
|
+
dstAccess.write(view, { at: offset });
|
|
263
|
+
offset += len;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
dstAccess.flush();
|
|
267
|
+
} finally {
|
|
268
|
+
if (dstAccess) {
|
|
269
|
+
try {
|
|
270
|
+
dstAccess.close();
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (srcAccess) {
|
|
275
|
+
try {
|
|
276
|
+
srcAccess.close();
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
212
284
|
async function navigateToParent(path) {
|
|
213
285
|
const parts = pathSegments(path);
|
|
214
286
|
parts.pop();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/src/workers/opfs-sync.worker.ts"],"sourcesContent":["/**\n * OPFS Sync Worker — optional bidirectional mirror between VFS and real OPFS.\n *\n * Spawned by the server worker when opfsSync is enabled.\n * Receives mutation events from the server, writes them to real OPFS files.\n * Uses FileSystemObserver to detect external OPFS changes and syncs them back.\n */\n\ninterface SyncEvent {\n op: 'write' | 'delete' | 'mkdir' | 'rename';\n path: string;\n newPath?: string;\n data?: ArrayBuffer;\n ts: number;\n}\n\nlet serverPort: MessagePort;\nlet mirrorRoot: FileSystemDirectoryHandle;\n\n// Normalize path: resolve \".\" and \"..\", ensure leading /, collapse //, strip trailing /\nfunction normalizePath(p: string): string {\n if (p.charCodeAt(0) !== 47) p = '/' + p;\n if (p.indexOf('//') !== -1) p = p.replace(/\\/\\/+/g, '/');\n // Resolve \".\" and \"..\" segments\n if (p.indexOf('/.') !== -1) {\n const parts = p.split('/');\n const resolved: string[] = [];\n for (const part of parts) {\n if (part === '.' || part === '') continue;\n if (part === '..') { resolved.pop(); continue; }\n resolved.push(part);\n }\n p = '/' + resolved.join('/');\n }\n if (p.length > 1 && p.charCodeAt(p.length - 1) === 47) p = p.slice(0, -1);\n return p || '/';\n}\n\n// Split a normalized path into segments safe for OPFS (no empty, \".\", or \"..\" entries)\nfunction pathSegments(p: string): string[] {\n return normalizePath(p).split('/').filter(Boolean);\n}\n\n// Echo suppression — two structures:\n//\n// pendingPaths (Set): paths currently in the queue or being processed.\n// Added on enqueue, removed after OPFS operation completes.\n// No timeout — stays as long as the item is in the queue.\n// Prevents false externals when queue takes >1s (e.g., 500-file batches).\n//\n// completedPaths (Map<path, timestamp>): paths recently written by us.\n// Added when processing completes, removed ONLY by periodic cleanup.\n// Grace window catches delayed/batched observer events after processing.\n//\n// Parent path check (opt-in): if /dir was deleted by us, /dir/file disappearing\n// is also our echo (recursive removeEntry fires per-child events).\n// ONLY used for 'disappeared' events — NOT for 'appeared'/'modified', since\n// creating /dir doesn't mean /dir/new-file appearing is our echo.\n\nconst pendingPaths = new Set<string>();\nconst completedPaths = new Map<string, number>();\nconst GRACE_MS = 3000;\n\nfunction trackPending(path: string): void {\n pendingPaths.add(normalizePath(path));\n}\n\nfunction untrackPending(path: string): void {\n pendingPaths.delete(normalizePath(path));\n}\n\nfunction trackCompleted(path: string): void {\n completedPaths.set(normalizePath(path), Date.now());\n}\n\nfunction isOurEcho(path: string, checkParents = false): boolean {\n path = normalizePath(path);\n const now = Date.now();\n\n // Check exact path\n if (pendingPaths.has(path)) return true;\n const ts = completedPaths.get(path);\n if (ts && now - ts < GRACE_MS) return true;\n\n // Walk up parent paths — ONLY for 'disappeared' events.\n // Handles recursive delete cascading: removeEntry(dir, {recursive:true})\n // fires individual 'disappeared' for every child file.\n // NOT used for 'appeared'/'modified' — a parent being tracked doesn't mean\n // a new file appearing inside it is our echo (could be genuinely external).\n if (checkParents) {\n let parent = path;\n while (true) {\n const slash = parent.lastIndexOf('/');\n if (slash <= 0) break;\n parent = parent.substring(0, slash);\n if (pendingPaths.has(parent)) return true;\n const pts = completedPaths.get(parent);\n if (pts && now - pts < GRACE_MS) return true;\n }\n }\n\n return false;\n}\n\n// Periodic cleanup — the ONLY way completedPaths entries get removed\nsetInterval(() => {\n const cutoff = Date.now() - GRACE_MS;\n for (const [p, ts] of completedPaths) {\n if (ts < cutoff) completedPaths.delete(p);\n }\n}, 5000);\n\n// Event queue — process one at a time, in order\nconst queue: SyncEvent[] = [];\nlet processing = false;\n\nfunction enqueue(event: SyncEvent): void {\n trackPending(event.path);\n if (event.op === 'rename' && event.newPath) {\n trackPending(event.newPath);\n }\n\n // Coalesce bursts of 'write' events for the same path that are still\n // waiting in the queue. Each write event carries a full-file\n // ArrayBuffer — under backpressure (slow OPFS, big files) the queue\n // can otherwise hold many superseded buffers at once. Replacing the\n // pending event's payload with the newest drops the stale buffer for\n // GC while preserving queue ordering for non-write ops.\n //\n // Note: the event at index 0 may already be mid-flight inside\n // processNext (after its `queue.shift()`), so we only scan what's\n // still queued.\n if (event.op === 'write') {\n for (let i = queue.length - 1; i >= 0; i--) {\n const pending = queue[i];\n if (pending.op === 'write' && normalizePath(pending.path) === normalizePath(event.path)) {\n pending.data = event.data;\n pending.ts = event.ts;\n return;\n }\n }\n }\n\n queue.push(event);\n if (!processing) processNext();\n}\n\nasync function processNext(): Promise<void> {\n if (queue.length === 0) {\n processing = false;\n return;\n }\n processing = true;\n\n const event = queue.shift()!;\n\n try {\n switch (event.op) {\n case 'write':\n if (event.data) {\n await writeToOPFS(event.path, event.data);\n } else {\n // No data but file should still exist (empty file like .gitkeep)\n await writeToOPFS(event.path, new ArrayBuffer(0));\n }\n break;\n case 'delete':\n await deleteFromOPFS(event.path);\n break;\n case 'mkdir':\n await mkdirInOPFS(event.path);\n break;\n case 'rename':\n await renameInOPFS(event.path, event.newPath!);\n break;\n }\n } catch (err) {\n console.warn('[opfs-sync] mirror failed:', event.op, event.path, err);\n }\n\n // Move from pending → completed (starts grace window for delayed observer events)\n untrackPending(event.path);\n trackCompleted(event.path);\n if (event.op === 'rename' && event.newPath) {\n untrackPending(event.newPath);\n trackCompleted(event.newPath);\n }\n\n processNext();\n}\n\nasync function ensureParentDirs(path: string): Promise<FileSystemDirectoryHandle> {\n const parts = pathSegments(path);\n parts.pop(); // Remove filename\n\n let dir = mirrorRoot;\n for (const part of parts) {\n dir = await dir.getDirectoryHandle(part, { create: true });\n }\n return dir;\n}\n\nfunction basename(path: string): string {\n const parts = pathSegments(path);\n return parts[parts.length - 1] || '';\n}\n\nasync function writeToOPFS(path: string, data: ArrayBuffer): Promise<void> {\n const dir = await ensureParentDirs(path);\n const name = basename(path);\n const fileHandle = await dir.getFileHandle(name, { create: true });\n // Use createSyncAccessHandle for reliable writes in Worker context\n // (createWritable can silently fail in nested workers for OPFS files)\n const accessHandle = await fileHandle.createSyncAccessHandle();\n try {\n accessHandle.truncate(0);\n accessHandle.write(new Uint8Array(data), { at: 0 });\n accessHandle.flush();\n } finally {\n accessHandle.close();\n }\n}\n\nasync function deleteFromOPFS(path: string): Promise<void> {\n try {\n const dir = await navigateToParent(path);\n await dir.removeEntry(basename(path), { recursive: true });\n } catch {\n // File may not exist in OPFS — that's fine\n }\n}\n\nasync function mkdirInOPFS(path: string): Promise<void> {\n let dir = mirrorRoot;\n for (const part of pathSegments(path)) {\n dir = await dir.getDirectoryHandle(part, { create: true });\n }\n}\n\n// Chunk size for the chunked rename copy. Caps peak memory during a\n// rename at this size rather than the full file size.\nconst RENAME_CHUNK = 2 * 1024 * 1024;\n\nasync function renameInOPFS(oldPath: string, newPath: string): Promise<void> {\n // OPFS has no native rename — copy + delete. Copy through two sync\n // access handles in fixed-size chunks so a rename of a large file\n // doesn't require materializing the whole file in memory.\n let srcAccess: FileSystemSyncAccessHandle | null = null;\n let dstAccess: FileSystemSyncAccessHandle | null = null;\n try {\n const oldDir = await navigateToParent(oldPath);\n const oldHandle = await oldDir.getFileHandle(basename(oldPath));\n srcAccess = await oldHandle.createSyncAccessHandle();\n const size = srcAccess.getSize();\n\n const newDir = await ensureParentDirs(newPath);\n const newHandle = await newDir.getFileHandle(basename(newPath), { create: true });\n dstAccess = await newHandle.createSyncAccessHandle();\n dstAccess.truncate(0);\n\n if (size > 0) {\n const chunk = new Uint8Array(Math.min(size, RENAME_CHUNK));\n let offset = 0;\n while (offset < size) {\n const len = Math.min(chunk.length, size - offset);\n const view = len === chunk.length ? chunk : chunk.subarray(0, len);\n srcAccess.read(view, { at: offset });\n dstAccess.write(view, { at: offset });\n offset += len;\n }\n }\n dstAccess.flush();\n\n // Release handles before removeEntry — can't unlink a file that\n // still has an open sync access handle.\n try { dstAccess.close(); } catch { /* ignore */ }\n dstAccess = null;\n try { srcAccess.close(); } catch { /* ignore */ }\n srcAccess = null;\n\n await oldDir.removeEntry(basename(oldPath));\n } catch (err) {\n console.warn('[opfs-sync] rename failed:', oldPath, '→', newPath, err);\n } finally {\n if (dstAccess) { try { dstAccess.close(); } catch { /* ignore */ } }\n if (srcAccess) { try { srcAccess.close(); } catch { /* ignore */ } }\n }\n}\n\nasync function navigateToParent(path: string): Promise<FileSystemDirectoryHandle> {\n const parts = pathSegments(path);\n parts.pop();\n\n let dir = mirrorRoot;\n for (const part of parts) {\n dir = await dir.getDirectoryHandle(part);\n }\n return dir;\n}\n\n// ========== FileSystemObserver for external changes ==========\n\nfunction setupObserver(): void {\n if (typeof FileSystemObserver === 'undefined') {\n console.warn('[opfs-sync] FileSystemObserver not available — external changes will not be detected');\n return;\n }\n\n console.log('[opfs-sync] Setting up FileSystemObserver on mirrorRoot:', mirrorRoot.name || '(opfs-root)');\n\n const observer = new FileSystemObserver((records) => {\n //console.log(`[opfs-sync] observer fired: ${records.length} record(s), pending=${pendingPaths.size}, completed=${completedPaths.size}`);\n for (const record of records) {\n const path = normalizePath('/' + record.relativePathComponents.join('/'));\n\n // Skip VFS binary file and internal files\n if (path === '/.vfs.bin' || path === '/.vfs' || path.startsWith('/.vfs')) continue;\n\n // Echo suppression — check parents only for 'disappeared' (recursive delete cascading)\n const isDelete = record.type === 'disappeared';\n if (isOurEcho(path, isDelete)) {\n //console.log('[opfs-sync] suppressed (echo):', record.type, path);\n continue;\n }\n\n //console.log('[opfs-sync] external:', record.type, path);\n switch (record.type) {\n case 'appeared':\n case 'modified':\n syncExternalChange(path, record.changedHandle);\n break;\n case 'disappeared':\n syncExternalDelete(path);\n break;\n case 'moved': {\n const from = normalizePath('/' + record.relativePathMovedFrom!.join('/'));\n //console.log('[opfs-sync] external: moved from', from, '→', path);\n syncExternalRename(from, path);\n break;\n }\n }\n }\n });\n\n observer.observe(mirrorRoot, { recursive: true });\n}\n\nasync function syncExternalChange(path: string, handle: FileSystemHandle | null): Promise<void> {\n try {\n if (!handle || handle.kind !== 'file') return;\n\n const fileHandle = handle as FileSystemFileHandle;\n const file = await fileHandle.getFile();\n const data = await file.arrayBuffer();\n\n serverPort.postMessage({\n op: 'external-write',\n path,\n data,\n ts: Date.now(),\n }, [data]);\n } catch (err) {\n // File may have been deleted between observer event and our read, or\n // a sync access handle may be holding the lock — either is fine to skip\n console.warn('[opfs-sync] external change read failed:', path, err);\n }\n}\n\nfunction syncExternalDelete(path: string): void {\n serverPort.postMessage({\n op: 'external-delete',\n path,\n ts: Date.now(),\n });\n}\n\nfunction syncExternalRename(oldPath: string, newPath: string): void {\n serverPort.postMessage({\n op: 'external-rename',\n path: oldPath,\n newPath,\n ts: Date.now(),\n });\n}\n\n// ========== Initialization ==========\n\nself.onmessage = async (e: MessageEvent) => {\n const msg = e.data;\n\n if (msg.type === 'init') {\n serverPort = e.ports[0];\n mirrorRoot = await navigator.storage.getDirectory();\n\n // Navigate to mirror root if specified\n if (msg.root && msg.root !== '/') {\n const segments = msg.root.split('/').filter(Boolean);\n for (const segment of segments) {\n mirrorRoot = await mirrorRoot.getDirectoryHandle(segment, { create: true });\n }\n }\n\n console.log('[opfs-sync] initialized with root:', msg.root || '/', 'mirrorRoot.name:', mirrorRoot.name || '(opfs-root)');\n\n // Set up FileSystemObserver\n setupObserver();\n\n // Listen for events from server\n serverPort.onmessage = (ev: MessageEvent) => {\n const event = ev.data as SyncEvent;\n enqueue(event);\n };\n serverPort.start();\n\n (self as unknown as Worker).postMessage({ type: 'ready' });\n return;\n }\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/src/workers/opfs-sync.worker.ts"],"sourcesContent":["/**\n * OPFS Sync Worker — optional bidirectional mirror between VFS and real OPFS.\n *\n * Spawned by the server worker when opfsSync is enabled.\n * Receives mutation events from the server, writes them to real OPFS files.\n * Uses FileSystemObserver to detect external OPFS changes and syncs them back.\n */\n\ninterface SyncEvent {\n op: 'write' | 'delete' | 'mkdir' | 'rename';\n path: string;\n newPath?: string;\n data?: ArrayBuffer;\n ts: number;\n}\n\nlet serverPort: MessagePort;\nlet mirrorRoot: FileSystemDirectoryHandle;\n\n// Normalize path: resolve \".\" and \"..\", ensure leading /, collapse //, strip trailing /\nfunction normalizePath(p: string): string {\n if (p.charCodeAt(0) !== 47) p = '/' + p;\n if (p.indexOf('//') !== -1) p = p.replace(/\\/\\/+/g, '/');\n // Resolve \".\" and \"..\" segments\n if (p.indexOf('/.') !== -1) {\n const parts = p.split('/');\n const resolved: string[] = [];\n for (const part of parts) {\n if (part === '.' || part === '') continue;\n if (part === '..') { resolved.pop(); continue; }\n resolved.push(part);\n }\n p = '/' + resolved.join('/');\n }\n if (p.length > 1 && p.charCodeAt(p.length - 1) === 47) p = p.slice(0, -1);\n return p || '/';\n}\n\n// Split a normalized path into segments safe for OPFS (no empty, \".\", or \"..\" entries)\nfunction pathSegments(p: string): string[] {\n return normalizePath(p).split('/').filter(Boolean);\n}\n\n// Echo suppression — two structures:\n//\n// pendingPaths (Set): paths currently in the queue or being processed.\n// Added on enqueue, removed after OPFS operation completes.\n// No timeout — stays as long as the item is in the queue.\n// Prevents false externals when queue takes >1s (e.g., 500-file batches).\n//\n// completedPaths (Map<path, timestamp>): paths recently written by us.\n// Added when processing completes, removed ONLY by periodic cleanup.\n// Grace window catches delayed/batched observer events after processing.\n//\n// Parent path check (opt-in): if /dir was deleted by us, /dir/file disappearing\n// is also our echo (recursive removeEntry fires per-child events).\n// ONLY used for 'disappeared' events — NOT for 'appeared'/'modified', since\n// creating /dir doesn't mean /dir/new-file appearing is our echo.\n\nconst pendingPaths = new Set<string>();\nconst completedPaths = new Map<string, number>();\nconst GRACE_MS = 3000;\n\nfunction trackPending(path: string): void {\n pendingPaths.add(normalizePath(path));\n}\n\nfunction untrackPending(path: string): void {\n pendingPaths.delete(normalizePath(path));\n}\n\nfunction trackCompleted(path: string): void {\n completedPaths.set(normalizePath(path), Date.now());\n}\n\nfunction isOurEcho(path: string, checkParents = false): boolean {\n path = normalizePath(path);\n const now = Date.now();\n\n // Check exact path\n if (pendingPaths.has(path)) return true;\n const ts = completedPaths.get(path);\n if (ts && now - ts < GRACE_MS) return true;\n\n // Walk up parent paths — ONLY for 'disappeared' events.\n // Handles recursive delete cascading: removeEntry(dir, {recursive:true})\n // fires individual 'disappeared' for every child file.\n // NOT used for 'appeared'/'modified' — a parent being tracked doesn't mean\n // a new file appearing inside it is our echo (could be genuinely external).\n if (checkParents) {\n let parent = path;\n while (true) {\n const slash = parent.lastIndexOf('/');\n if (slash <= 0) break;\n parent = parent.substring(0, slash);\n if (pendingPaths.has(parent)) return true;\n const pts = completedPaths.get(parent);\n if (pts && now - pts < GRACE_MS) return true;\n }\n }\n\n return false;\n}\n\n// Periodic cleanup — the ONLY way completedPaths entries get removed\nsetInterval(() => {\n const cutoff = Date.now() - GRACE_MS;\n for (const [p, ts] of completedPaths) {\n if (ts < cutoff) completedPaths.delete(p);\n }\n}, 5000);\n\n// Event queue — process one at a time, in order\nconst queue: SyncEvent[] = [];\nlet processing = false;\n\nfunction enqueue(event: SyncEvent): void {\n trackPending(event.path);\n if (event.op === 'rename' && event.newPath) {\n trackPending(event.newPath);\n }\n\n // Coalesce bursts of 'write' events for the same path that are still\n // waiting in the queue. Each write event carries a full-file\n // ArrayBuffer — under backpressure (slow OPFS, big files) the queue\n // can otherwise hold many superseded buffers at once. Replacing the\n // pending event's payload with the newest drops the stale buffer for\n // GC while preserving queue ordering for non-write ops.\n //\n // Note: the event at index 0 may already be mid-flight inside\n // processNext (after its `queue.shift()`), so we only scan what's\n // still queued.\n if (event.op === 'write') {\n for (let i = queue.length - 1; i >= 0; i--) {\n const pending = queue[i];\n if (pending.op === 'write' && normalizePath(pending.path) === normalizePath(event.path)) {\n pending.data = event.data;\n pending.ts = event.ts;\n return;\n }\n }\n }\n\n queue.push(event);\n if (!processing) processNext();\n}\n\nasync function processNext(): Promise<void> {\n if (queue.length === 0) {\n processing = false;\n return;\n }\n processing = true;\n\n const event = queue.shift()!;\n\n try {\n switch (event.op) {\n case 'write':\n if (event.data) {\n await writeToOPFS(event.path, event.data);\n } else {\n // No data but file should still exist (empty file like .gitkeep)\n await writeToOPFS(event.path, new ArrayBuffer(0));\n }\n break;\n case 'delete':\n await deleteFromOPFS(event.path);\n break;\n case 'mkdir':\n await mkdirInOPFS(event.path);\n break;\n case 'rename':\n await renameInOPFS(event.path, event.newPath!);\n break;\n }\n } catch (err) {\n console.warn('[opfs-sync] mirror failed:', event.op, event.path, err);\n }\n\n // Move from pending → completed (starts grace window for delayed observer events)\n untrackPending(event.path);\n trackCompleted(event.path);\n if (event.op === 'rename' && event.newPath) {\n untrackPending(event.newPath);\n trackCompleted(event.newPath);\n }\n\n processNext();\n}\n\nasync function ensureParentDirs(path: string): Promise<FileSystemDirectoryHandle> {\n const parts = pathSegments(path);\n parts.pop(); // Remove filename\n\n let dir = mirrorRoot;\n for (const part of parts) {\n dir = await dir.getDirectoryHandle(part, { create: true });\n }\n return dir;\n}\n\nfunction basename(path: string): string {\n const parts = pathSegments(path);\n return parts[parts.length - 1] || '';\n}\n\nasync function writeToOPFS(path: string, data: ArrayBuffer): Promise<void> {\n const dir = await ensureParentDirs(path);\n const name = basename(path);\n const fileHandle = await dir.getFileHandle(name, { create: true });\n // Use createSyncAccessHandle for reliable writes in Worker context\n // (createWritable can silently fail in nested workers for OPFS files)\n const accessHandle = await fileHandle.createSyncAccessHandle();\n try {\n accessHandle.truncate(0);\n accessHandle.write(new Uint8Array(data), { at: 0 });\n accessHandle.flush();\n } finally {\n accessHandle.close();\n }\n}\n\nasync function deleteFromOPFS(path: string): Promise<void> {\n try {\n const dir = await navigateToParent(path);\n await dir.removeEntry(basename(path), { recursive: true });\n } catch {\n // File may not exist in OPFS — that's fine\n }\n}\n\nasync function mkdirInOPFS(path: string): Promise<void> {\n let dir = mirrorRoot;\n for (const part of pathSegments(path)) {\n dir = await dir.getDirectoryHandle(part, { create: true });\n }\n}\n\n// Chunk size for the chunked rename copy. Caps peak memory during a\n// rename at this size rather than the full file size.\nconst RENAME_CHUNK = 2 * 1024 * 1024;\n\nasync function renameInOPFS(oldPath: string, newPath: string): Promise<void> {\n // OPFS has no native rename — copy + delete. Try the file path first\n // (which is the common case: write/copy/etc events). If `oldPath` is\n // actually a directory (e.g. Vite committing `.vite/deps_temp_X` →\n // `.vite/deps`), `getFileHandle` rejects with TypeMismatchError —\n // fall through to the directory-aware branch which recursively walks\n // the source tree.\n let srcAccess: FileSystemSyncAccessHandle | null = null;\n let dstAccess: FileSystemSyncAccessHandle | null = null;\n let oldDir: FileSystemDirectoryHandle;\n let oldHandle: FileSystemFileHandle;\n try {\n oldDir = await navigateToParent(oldPath);\n oldHandle = await oldDir.getFileHandle(basename(oldPath));\n } catch (err: any) {\n // Not a file — try directory. (TypeMismatchError on Safari, plus\n // generic \"not a file\" / \"wrong handle type\" messages on other\n // engines.)\n if (err?.name === 'TypeMismatchError' ||\n err?.name === 'NotFoundError' ||\n err?.message?.includes('TypeMismatch') ||\n err?.message?.includes('not a file') ||\n err?.message?.includes('not an entry of requested type')) {\n try {\n await renameDirInOPFS(oldPath, newPath);\n } catch (dirErr) {\n console.warn('[opfs-sync] rename (dir) failed:', oldPath, '→', newPath, dirErr);\n }\n return;\n }\n console.warn('[opfs-sync] rename failed:', oldPath, '→', newPath, err);\n return;\n }\n // File rename — copy through two sync access handles in fixed-size\n // chunks so a rename of a large file doesn't require materializing\n // the whole file in memory.\n try {\n srcAccess = await oldHandle.createSyncAccessHandle();\n const size = srcAccess.getSize();\n\n const newDir = await ensureParentDirs(newPath);\n const newHandle = await newDir.getFileHandle(basename(newPath), { create: true });\n dstAccess = await newHandle.createSyncAccessHandle();\n dstAccess.truncate(0);\n\n if (size > 0) {\n const chunk = new Uint8Array(Math.min(size, RENAME_CHUNK));\n let offset = 0;\n while (offset < size) {\n const len = Math.min(chunk.length, size - offset);\n const view = len === chunk.length ? chunk : chunk.subarray(0, len);\n srcAccess.read(view, { at: offset });\n dstAccess.write(view, { at: offset });\n offset += len;\n }\n }\n dstAccess.flush();\n\n // Release handles before removeEntry — can't unlink a file that\n // still has an open sync access handle.\n try { dstAccess.close(); } catch { /* ignore */ }\n dstAccess = null;\n try { srcAccess.close(); } catch { /* ignore */ }\n srcAccess = null;\n\n await oldDir.removeEntry(basename(oldPath));\n } catch (err) {\n console.warn('[opfs-sync] rename failed:', oldPath, '→', newPath, err);\n } finally {\n if (dstAccess) { try { dstAccess.close(); } catch { /* ignore */ } }\n if (srcAccess) { try { srcAccess.close(); } catch { /* ignore */ } }\n }\n}\n\n/**\n * Directory-aware rename in OPFS — walks the source tree and copies\n * each file via sync access handles, then deletes the source. Used\n * when `renameInOPFS` discovers oldPath is a directory rather than\n * a file.\n *\n * Replaces the destination directory entirely if it already exists,\n * matching Node.js rename semantics for directories.\n */\nasync function renameDirInOPFS(oldPath: string, newPath: string): Promise<void> {\n // Resolve the source directory handle.\n const oldParent = await navigateToParent(oldPath);\n const srcDir = await oldParent.getDirectoryHandle(basename(oldPath));\n\n // Wipe the target if it exists (file or directory) so the rename\n // is a clean replace, then recreate it as a fresh directory.\n const newParent = await ensureParentDirs(newPath);\n try {\n await newParent.removeEntry(basename(newPath), { recursive: true });\n } catch (e: any) {\n // NotFoundError is expected when target doesn't exist; everything\n // else we let bubble (caller logs a single warning).\n if (e?.name !== 'NotFoundError') throw e;\n }\n const dstDir = await newParent.getDirectoryHandle(basename(newPath), { create: true });\n\n await copyDirContents(srcDir, dstDir);\n\n // Remove the now-copied source.\n await oldParent.removeEntry(basename(oldPath), { recursive: true });\n}\n\n/**\n * Recursively copy every entry under `src` into `dst`.\n * Uses sync access handles for files (chunked) so memory peaks at\n * RENAME_CHUNK regardless of file size.\n */\nasync function copyDirContents(\n src: FileSystemDirectoryHandle,\n dst: FileSystemDirectoryHandle,\n): Promise<void> {\n // entries() is async-iterable: [name, FileSystemHandle]\n for await (const [name, handle] of (src as any).entries()) {\n if (handle.kind === 'directory') {\n const childDst = await dst.getDirectoryHandle(name, { create: true });\n await copyDirContents(handle as FileSystemDirectoryHandle, childDst);\n } else {\n const fileHandle = handle as FileSystemFileHandle;\n const dstFile = await dst.getFileHandle(name, { create: true });\n let srcAccess: FileSystemSyncAccessHandle | null = null;\n let dstAccess: FileSystemSyncAccessHandle | null = null;\n try {\n srcAccess = await fileHandle.createSyncAccessHandle();\n dstAccess = await dstFile.createSyncAccessHandle();\n const size = srcAccess.getSize();\n dstAccess.truncate(0);\n if (size > 0) {\n const chunk = new Uint8Array(Math.min(size, RENAME_CHUNK));\n let offset = 0;\n while (offset < size) {\n const len = Math.min(chunk.length, size - offset);\n const view = len === chunk.length ? chunk : chunk.subarray(0, len);\n srcAccess.read(view, { at: offset });\n dstAccess.write(view, { at: offset });\n offset += len;\n }\n }\n dstAccess.flush();\n } finally {\n if (dstAccess) { try { dstAccess.close(); } catch { /* ignore */ } }\n if (srcAccess) { try { srcAccess.close(); } catch { /* ignore */ } }\n }\n }\n }\n}\n\nasync function navigateToParent(path: string): Promise<FileSystemDirectoryHandle> {\n const parts = pathSegments(path);\n parts.pop();\n\n let dir = mirrorRoot;\n for (const part of parts) {\n dir = await dir.getDirectoryHandle(part);\n }\n return dir;\n}\n\n// ========== FileSystemObserver for external changes ==========\n\nfunction setupObserver(): void {\n if (typeof FileSystemObserver === 'undefined') {\n console.warn('[opfs-sync] FileSystemObserver not available — external changes will not be detected');\n return;\n }\n\n console.log('[opfs-sync] Setting up FileSystemObserver on mirrorRoot:', mirrorRoot.name || '(opfs-root)');\n\n const observer = new FileSystemObserver((records) => {\n //console.log(`[opfs-sync] observer fired: ${records.length} record(s), pending=${pendingPaths.size}, completed=${completedPaths.size}`);\n for (const record of records) {\n const path = normalizePath('/' + record.relativePathComponents.join('/'));\n\n // Skip VFS binary file and internal files\n if (path === '/.vfs.bin' || path === '/.vfs' || path.startsWith('/.vfs')) continue;\n\n // Echo suppression — check parents only for 'disappeared' (recursive delete cascading)\n const isDelete = record.type === 'disappeared';\n if (isOurEcho(path, isDelete)) {\n //console.log('[opfs-sync] suppressed (echo):', record.type, path);\n continue;\n }\n\n //console.log('[opfs-sync] external:', record.type, path);\n switch (record.type) {\n case 'appeared':\n case 'modified':\n syncExternalChange(path, record.changedHandle);\n break;\n case 'disappeared':\n syncExternalDelete(path);\n break;\n case 'moved': {\n const from = normalizePath('/' + record.relativePathMovedFrom!.join('/'));\n //console.log('[opfs-sync] external: moved from', from, '→', path);\n syncExternalRename(from, path);\n break;\n }\n }\n }\n });\n\n observer.observe(mirrorRoot, { recursive: true });\n}\n\nasync function syncExternalChange(path: string, handle: FileSystemHandle | null): Promise<void> {\n try {\n if (!handle || handle.kind !== 'file') return;\n\n const fileHandle = handle as FileSystemFileHandle;\n const file = await fileHandle.getFile();\n const data = await file.arrayBuffer();\n\n serverPort.postMessage({\n op: 'external-write',\n path,\n data,\n ts: Date.now(),\n }, [data]);\n } catch (err) {\n // File may have been deleted between observer event and our read, or\n // a sync access handle may be holding the lock — either is fine to skip\n console.warn('[opfs-sync] external change read failed:', path, err);\n }\n}\n\nfunction syncExternalDelete(path: string): void {\n serverPort.postMessage({\n op: 'external-delete',\n path,\n ts: Date.now(),\n });\n}\n\nfunction syncExternalRename(oldPath: string, newPath: string): void {\n serverPort.postMessage({\n op: 'external-rename',\n path: oldPath,\n newPath,\n ts: Date.now(),\n });\n}\n\n// ========== Initialization ==========\n\nself.onmessage = async (e: MessageEvent) => {\n const msg = e.data;\n\n if (msg.type === 'init') {\n serverPort = e.ports[0];\n mirrorRoot = await navigator.storage.getDirectory();\n\n // Navigate to mirror root if specified\n if (msg.root && msg.root !== '/') {\n const segments = msg.root.split('/').filter(Boolean);\n for (const segment of segments) {\n mirrorRoot = await mirrorRoot.getDirectoryHandle(segment, { create: true });\n }\n }\n\n console.log('[opfs-sync] initialized with root:', msg.root || '/', 'mirrorRoot.name:', mirrorRoot.name || '(opfs-root)');\n\n // Set up FileSystemObserver\n setupObserver();\n\n // Listen for events from server\n serverPort.onmessage = (ev: MessageEvent) => {\n const event = ev.data as SyncEvent;\n enqueue(event);\n };\n serverPort.start();\n\n (self as unknown as Worker).postMessage({ type: 'ready' });\n return;\n }\n};\n"],"mappings":"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","names":[]}
|
|
@@ -164,6 +164,22 @@ var VFSEngine = class {
|
|
|
164
164
|
// generation when implicitDirs was last rebuilt
|
|
165
165
|
pathIndexGen = 0;
|
|
166
166
|
// bumped on every pathIndex mutation
|
|
167
|
+
// Incrementally maintained "number of pathIndex entries that have this
|
|
168
|
+
// path as a strict ancestor" map. Lets `isImplicitDirectory` answer in
|
|
169
|
+
// O(1) — an implicit dir P is exactly !pathIndex.has(P) && descCount[P] > 0.
|
|
170
|
+
// Without this, every `isImplicitDirectory` call triggered an O(N×depth)
|
|
171
|
+
// rebuild of `implicitDirs`, and the 3.0.49 fix put one of those calls on
|
|
172
|
+
// the hot path of every fresh write/symlink/link/copy — making batch
|
|
173
|
+
// writes O(N²) on total path count.
|
|
174
|
+
descCount = /* @__PURE__ */ new Map();
|
|
175
|
+
// descCount is in sync with pathIndex iff descCountGen >= pathIndexGen.
|
|
176
|
+
// Helpers `setPathIndex`/`deletePathIndex` keep them in sync. Code that
|
|
177
|
+
// mutates `pathIndex` directly (only test scaffolding does this in
|
|
178
|
+
// practice — see the implicit-directory tests in vfs-engine.test.ts)
|
|
179
|
+
// bumps `pathIndexGen` without going through the helpers, which leaves
|
|
180
|
+
// descCount stale; `isImplicitDirectory` notices the mismatch and
|
|
181
|
+
// recomputes descCount on demand.
|
|
182
|
+
descCountGen = 0;
|
|
167
183
|
// Configurable upper bounds
|
|
168
184
|
maxInodes = 4e6;
|
|
169
185
|
maxBlocks = 4e6;
|
|
@@ -446,7 +462,7 @@ var VFSEngine = class {
|
|
|
446
462
|
if (!path.startsWith("/") || path.includes("\0")) {
|
|
447
463
|
throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
|
|
448
464
|
}
|
|
449
|
-
this.
|
|
465
|
+
this.setPathIndex(path, i);
|
|
450
466
|
}
|
|
451
467
|
this.pathIndexGen++;
|
|
452
468
|
}
|
|
@@ -768,7 +784,7 @@ var VFSEngine = class {
|
|
|
768
784
|
gid: this.processGid
|
|
769
785
|
};
|
|
770
786
|
this.writeInode(idx, inode);
|
|
771
|
-
this.
|
|
787
|
+
this.setPathIndex(path, idx);
|
|
772
788
|
this.pathIndexGen++;
|
|
773
789
|
return idx;
|
|
774
790
|
}
|
|
@@ -926,7 +942,7 @@ var VFSEngine = class {
|
|
|
926
942
|
this.freeBlockRange(inode.firstBlock, inode.blockCount);
|
|
927
943
|
inode.type = INODE_TYPE.FREE;
|
|
928
944
|
this.writeInode(idx, inode);
|
|
929
|
-
this.
|
|
945
|
+
this.deletePathIndex(path);
|
|
930
946
|
this.pathIndexGen++;
|
|
931
947
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
932
948
|
this.commitPending();
|
|
@@ -1048,7 +1064,7 @@ var VFSEngine = class {
|
|
|
1048
1064
|
this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
|
|
1049
1065
|
descInode.type = INODE_TYPE.FREE;
|
|
1050
1066
|
this.writeInode(descIdx, descInode);
|
|
1051
|
-
this.
|
|
1067
|
+
this.deletePathIndex(desc);
|
|
1052
1068
|
}
|
|
1053
1069
|
this.pathIndexGen++;
|
|
1054
1070
|
this.commitPending();
|
|
@@ -1068,12 +1084,12 @@ var VFSEngine = class {
|
|
|
1068
1084
|
this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
|
|
1069
1085
|
childInode.type = INODE_TYPE.FREE;
|
|
1070
1086
|
this.writeInode(childIdx, childInode);
|
|
1071
|
-
this.
|
|
1087
|
+
this.deletePathIndex(child);
|
|
1072
1088
|
}
|
|
1073
1089
|
}
|
|
1074
1090
|
inode.type = INODE_TYPE.FREE;
|
|
1075
1091
|
this.writeInode(idx, inode);
|
|
1076
|
-
this.
|
|
1092
|
+
this.deletePathIndex(path);
|
|
1077
1093
|
this.pathIndexGen++;
|
|
1078
1094
|
if (idx < this.freeInodeHint) this.freeInodeHint = idx;
|
|
1079
1095
|
this.commitPending();
|
|
@@ -1164,7 +1180,7 @@ var VFSEngine = class {
|
|
|
1164
1180
|
this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
|
|
1165
1181
|
existingInode.type = INODE_TYPE.FREE;
|
|
1166
1182
|
this.writeInode(existingIdx, existingInode);
|
|
1167
|
-
this.
|
|
1183
|
+
this.deletePathIndex(newPath);
|
|
1168
1184
|
if (existingIdx < this.freeInodeHint) this.freeInodeHint = existingIdx;
|
|
1169
1185
|
}
|
|
1170
1186
|
if (cleanDescendants) {
|
|
@@ -1174,7 +1190,7 @@ var VFSEngine = class {
|
|
|
1174
1190
|
this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
|
|
1175
1191
|
descInode.type = INODE_TYPE.FREE;
|
|
1176
1192
|
this.writeInode(descIdx, descInode);
|
|
1177
|
-
this.
|
|
1193
|
+
this.deletePathIndex(desc);
|
|
1178
1194
|
if (descIdx < this.freeInodeHint) this.freeInodeHint = descIdx;
|
|
1179
1195
|
}
|
|
1180
1196
|
}
|
|
@@ -1185,8 +1201,8 @@ var VFSEngine = class {
|
|
|
1185
1201
|
inode.pathLength = pathLen;
|
|
1186
1202
|
inode.mtime = Date.now();
|
|
1187
1203
|
this.writeInode(idx, inode);
|
|
1188
|
-
this.
|
|
1189
|
-
this.
|
|
1204
|
+
this.deletePathIndex(oldPath);
|
|
1205
|
+
this.setPathIndex(newPath, idx);
|
|
1190
1206
|
this.pathIndexGen++;
|
|
1191
1207
|
if (inode.type === INODE_TYPE.DIRECTORY) {
|
|
1192
1208
|
const prefix = oldPath === "/" ? "/" : oldPath + "/";
|
|
@@ -1204,8 +1220,8 @@ var VFSEngine = class {
|
|
|
1204
1220
|
childInode.pathOffset = cpo;
|
|
1205
1221
|
childInode.pathLength = cpl;
|
|
1206
1222
|
this.writeInode(i, childInode);
|
|
1207
|
-
this.
|
|
1208
|
-
this.
|
|
1223
|
+
this.deletePathIndex(p);
|
|
1224
|
+
this.setPathIndex(childNewPath, i);
|
|
1209
1225
|
}
|
|
1210
1226
|
}
|
|
1211
1227
|
this.commitPending();
|
|
@@ -1674,11 +1690,71 @@ var VFSEngine = class {
|
|
|
1674
1690
|
/**
|
|
1675
1691
|
* Check if a path is an implicit directory (exists because files exist under it,
|
|
1676
1692
|
* but no explicit directory inode was created for it).
|
|
1693
|
+
*
|
|
1694
|
+
* O(1) via the incrementally maintained `descCount` map (an implicit dir
|
|
1695
|
+
* is exactly !pathIndex.has(P) && descCount[P] > 0). If `pathIndex` was
|
|
1696
|
+
* mutated directly without going through the helpers (test scaffolding),
|
|
1697
|
+
* descCount is stale and we rebuild it from scratch — once — to resync.
|
|
1677
1698
|
*/
|
|
1678
1699
|
isImplicitDirectory(path) {
|
|
1679
1700
|
if (path === "/") return false;
|
|
1680
|
-
this.
|
|
1681
|
-
|
|
1701
|
+
if (this.pathIndex.has(path)) return false;
|
|
1702
|
+
if (this.descCountGen < this.pathIndexGen) this.rebuildDescCount();
|
|
1703
|
+
return (this.descCount.get(path) ?? 0) > 0;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Recompute `descCount` from scratch by walking every pathIndex entry's
|
|
1707
|
+
* ancestor chain. O(N×depth). Only triggered when something bypassed the
|
|
1708
|
+
* setPathIndex/deletePathIndex helpers — in production code that's
|
|
1709
|
+
* never; the tests exercise this path.
|
|
1710
|
+
*/
|
|
1711
|
+
rebuildDescCount() {
|
|
1712
|
+
this.descCount.clear();
|
|
1713
|
+
for (const path of this.pathIndex.keys()) {
|
|
1714
|
+
this.bumpDescCount(path);
|
|
1715
|
+
}
|
|
1716
|
+
this.descCountGen = this.pathIndexGen;
|
|
1717
|
+
}
|
|
1718
|
+
// ---- pathIndex helpers — keep `descCount` in sync ----
|
|
1719
|
+
// Every pathIndex.set/delete in the engine MUST go through these so the
|
|
1720
|
+
// `descCount` map (used by `isImplicitDirectory`) stays correct. We
|
|
1721
|
+
// anticipate the caller's `pathIndexGen++` by setting `descCountGen` to
|
|
1722
|
+
// `pathIndexGen + 1`; idempotent across multiple helper calls within a
|
|
1723
|
+
// single logical op (e.g. rmdir doing N deletes then one bump). Test
|
|
1724
|
+
// code that mutates `pathIndex` directly leaves descCountGen behind,
|
|
1725
|
+
// which is what triggers the rebuild path in `isImplicitDirectory`.
|
|
1726
|
+
setPathIndex(path, idx) {
|
|
1727
|
+
const had = this.pathIndex.has(path);
|
|
1728
|
+
this.pathIndex.set(path, idx);
|
|
1729
|
+
if (!had) this.bumpDescCount(path);
|
|
1730
|
+
this.descCountGen = this.pathIndexGen + 1;
|
|
1731
|
+
}
|
|
1732
|
+
deletePathIndex(path) {
|
|
1733
|
+
const had = this.pathIndex.delete(path);
|
|
1734
|
+
if (had) this.decDescCount(path);
|
|
1735
|
+
this.descCountGen = this.pathIndexGen + 1;
|
|
1736
|
+
return had;
|
|
1737
|
+
}
|
|
1738
|
+
bumpDescCount(path) {
|
|
1739
|
+
let pos = path.length;
|
|
1740
|
+
while (true) {
|
|
1741
|
+
pos = path.lastIndexOf("/", pos - 1);
|
|
1742
|
+
if (pos <= 0) break;
|
|
1743
|
+
const ancestor = path.substring(0, pos);
|
|
1744
|
+
this.descCount.set(ancestor, (this.descCount.get(ancestor) ?? 0) + 1);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
decDescCount(path) {
|
|
1748
|
+
let pos = path.length;
|
|
1749
|
+
while (true) {
|
|
1750
|
+
pos = path.lastIndexOf("/", pos - 1);
|
|
1751
|
+
if (pos <= 0) break;
|
|
1752
|
+
const ancestor = path.substring(0, pos);
|
|
1753
|
+
const cur = this.descCount.get(ancestor);
|
|
1754
|
+
if (cur === void 0) break;
|
|
1755
|
+
if (cur <= 1) this.descCount.delete(ancestor);
|
|
1756
|
+
else this.descCount.set(ancestor, cur - 1);
|
|
1757
|
+
}
|
|
1682
1758
|
}
|
|
1683
1759
|
/**
|
|
1684
1760
|
* Get direct children of a directory path, including implicit subdirectories.
|