@componentor/fs 3.0.43 → 3.0.45

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.
@@ -67,6 +67,16 @@ function enqueue(event) {
67
67
  if (event.op === "rename" && event.newPath) {
68
68
  trackPending(event.newPath);
69
69
  }
70
+ if (event.op === "write") {
71
+ for (let i = queue.length - 1; i >= 0; i--) {
72
+ const pending = queue[i];
73
+ if (pending.op === "write" && normalizePath(pending.path) === normalizePath(event.path)) {
74
+ pending.data = event.data;
75
+ pending.ts = event.ts;
76
+ return;
77
+ }
78
+ }
79
+ }
70
80
  queue.push(event);
71
81
  if (!processing) processNext();
72
82
  }
@@ -146,25 +156,57 @@ async function mkdirInOPFS(path) {
146
156
  dir = await dir.getDirectoryHandle(part, { create: true });
147
157
  }
148
158
  }
159
+ var RENAME_CHUNK = 2 * 1024 * 1024;
149
160
  async function renameInOPFS(oldPath, newPath) {
161
+ let srcAccess = null;
162
+ let dstAccess = null;
150
163
  try {
151
164
  const oldDir = await navigateToParent(oldPath);
152
165
  const oldHandle = await oldDir.getFileHandle(basename(oldPath));
153
- const file = await oldHandle.getFile();
154
- const data = await file.arrayBuffer();
166
+ srcAccess = await oldHandle.createSyncAccessHandle();
167
+ const size = srcAccess.getSize();
155
168
  const newDir = await ensureParentDirs(newPath);
156
169
  const newHandle = await newDir.getFileHandle(basename(newPath), { create: true });
157
- const accessHandle = await newHandle.createSyncAccessHandle();
170
+ dstAccess = await newHandle.createSyncAccessHandle();
171
+ dstAccess.truncate(0);
172
+ if (size > 0) {
173
+ const chunk = new Uint8Array(Math.min(size, RENAME_CHUNK));
174
+ let offset = 0;
175
+ while (offset < size) {
176
+ const len = Math.min(chunk.length, size - offset);
177
+ const view = len === chunk.length ? chunk : chunk.subarray(0, len);
178
+ srcAccess.read(view, { at: offset });
179
+ dstAccess.write(view, { at: offset });
180
+ offset += len;
181
+ }
182
+ }
183
+ dstAccess.flush();
158
184
  try {
159
- accessHandle.truncate(0);
160
- accessHandle.write(new Uint8Array(data), { at: 0 });
161
- accessHandle.flush();
162
- } finally {
163
- accessHandle.close();
185
+ dstAccess.close();
186
+ } catch {
164
187
  }
188
+ dstAccess = null;
189
+ try {
190
+ srcAccess.close();
191
+ } catch {
192
+ }
193
+ srcAccess = null;
165
194
  await oldDir.removeEntry(basename(oldPath));
166
195
  } catch (err) {
167
196
  console.warn("[opfs-sync] rename failed:", oldPath, "\u2192", newPath, err);
197
+ } finally {
198
+ if (dstAccess) {
199
+ try {
200
+ dstAccess.close();
201
+ } catch {
202
+ }
203
+ }
204
+ if (srcAccess) {
205
+ try {
206
+ srcAccess.close();
207
+ } catch {
208
+ }
209
+ }
168
210
  }
169
211
  }
170
212
  async function navigateToParent(path) {
@@ -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 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\nasync function renameInOPFS(oldPath: string, newPath: string): Promise<void> {\n // OPFS doesn't have a native rename — copy + delete\n try {\n const oldDir = await navigateToParent(oldPath);\n const oldHandle = await oldDir.getFileHandle(basename(oldPath));\n const file = await oldHandle.getFile();\n const data = await file.arrayBuffer();\n\n const newDir = await ensureParentDirs(newPath);\n const newHandle = await newDir.getFileHandle(basename(newPath), { create: true });\n const accessHandle = await newHandle.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 await oldDir.removeEntry(basename(oldPath));\n } catch (err) {\n console.warn('[opfs-sync] rename failed:', oldPath, '→', newPath, err);\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. 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":[]}
@@ -510,14 +510,23 @@ var VFSEngine = class {
510
510
  growPathTable(needed) {
511
511
  const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
512
512
  const growth = newSize - this.pathTableSize;
513
- const dataSize = this.totalBlocks * this.blockSize;
514
- const dataBuf = new Uint8Array(dataSize);
515
- this.handle.read(dataBuf, { at: this.dataOffset });
516
513
  const newTotalSize = this.handle.getSize() + growth;
517
514
  this.handle.truncate(newTotalSize);
515
+ const dataSize = this.totalBlocks * this.blockSize;
516
+ const CHUNK = 4 * 1024 * 1024;
517
+ const scratch = new Uint8Array(Math.min(CHUNK, Math.max(dataSize, 1)));
518
+ let remaining = dataSize;
519
+ while (remaining > 0) {
520
+ const chunk = Math.min(remaining, CHUNK);
521
+ const srcAt = this.dataOffset + (remaining - chunk);
522
+ const dstAt = this.dataOffset + growth + (remaining - chunk);
523
+ const slice = chunk < scratch.length ? scratch.subarray(0, chunk) : scratch;
524
+ this.handle.read(slice, { at: srcAt });
525
+ this.handle.write(slice, { at: dstAt });
526
+ remaining -= chunk;
527
+ }
518
528
  const newBitmapOffset = this.bitmapOffset + growth;
519
529
  const newDataOffset = this.dataOffset + growth;
520
- this.handle.write(dataBuf, { at: newDataOffset });
521
530
  this.handle.write(this.bitmap, { at: newBitmapOffset });
522
531
  this.pathTableSize = newSize;
523
532
  this.bitmapOffset = newBitmapOffset;
@@ -525,6 +534,23 @@ var VFSEngine = class {
525
534
  this.superblockDirty = true;
526
535
  }
527
536
  // ========== Bitmap I/O ==========
537
+ // Write `length` zero bytes at absolute file offset `at` via a small
538
+ // reusable scratch buffer. Used to materialize POSIX "holes" when a
539
+ // write starts past the current file size — those bytes must read as
540
+ // zeros rather than whatever stale data happened to live in the
541
+ // underlying storage blocks.
542
+ zeroFileRange(at, length) {
543
+ if (length <= 0) return;
544
+ const CHUNK = 4 * 1024 * 1024;
545
+ const zeros = new Uint8Array(Math.min(length, CHUNK));
546
+ let written = 0;
547
+ while (written < length) {
548
+ const n = Math.min(CHUNK, length - written);
549
+ const slice = n < zeros.length ? zeros.subarray(0, n) : zeros;
550
+ this.handle.write(slice, { at: at + written });
551
+ written += n;
552
+ }
553
+ }
528
554
  allocateBlocks(count) {
529
555
  if (count === 0) return 0;
530
556
  const bitmap = this.bitmap;
@@ -849,17 +875,28 @@ var VFSEngine = class {
849
875
  }
850
876
  const inode = this.readInode(existingIdx);
851
877
  if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
852
- const existing = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
853
- const combined = new Uint8Array(existing.byteLength + data.byteLength);
854
- combined.set(existing);
855
- combined.set(data, existing.byteLength);
856
- const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
857
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
878
+ const combinedSize = inode.size + data.byteLength;
879
+ const neededBlocks = Math.ceil(combinedSize / this.blockSize);
858
880
  const newFirst = this.allocateBlocks(neededBlocks);
859
- this.writeData(newFirst, combined);
881
+ const newBase = this.dataOffset + newFirst * this.blockSize;
882
+ if (inode.size > 0) {
883
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
884
+ const CHUNK = 4 * 1024 * 1024;
885
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
886
+ let copied = 0;
887
+ while (copied < inode.size) {
888
+ const n = Math.min(CHUNK, inode.size - copied);
889
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
890
+ this.handle.read(slice, { at: oldBase + copied });
891
+ this.handle.write(slice, { at: newBase + copied });
892
+ copied += n;
893
+ }
894
+ }
895
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
896
+ this.handle.write(data, { at: newBase + inode.size });
860
897
  inode.firstBlock = newFirst;
861
898
  inode.blockCount = neededBlocks;
862
- inode.size = combined.byteLength;
899
+ inode.size = combinedSize;
863
900
  inode.mtime = Date.now();
864
901
  this.writeInode(existingIdx, inode);
865
902
  this.commitPending();
@@ -1122,13 +1159,29 @@ var VFSEngine = class {
1122
1159
  } else if (len > inode.size) {
1123
1160
  const neededBlocks = Math.ceil(len / this.blockSize);
1124
1161
  if (neededBlocks > inode.blockCount) {
1125
- const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
1126
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
1127
1162
  const newFirst = this.allocateBlocks(neededBlocks);
1128
- const newData = new Uint8Array(len);
1129
- newData.set(oldData);
1130
- this.writeData(newFirst, newData);
1163
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1164
+ if (inode.size > 0) {
1165
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
1166
+ const CHUNK = 4 * 1024 * 1024;
1167
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
1168
+ let copied = 0;
1169
+ while (copied < inode.size) {
1170
+ const n = Math.min(CHUNK, inode.size - copied);
1171
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1172
+ this.handle.read(slice, { at: oldBase + copied });
1173
+ this.handle.write(slice, { at: newBase + copied });
1174
+ copied += n;
1175
+ }
1176
+ }
1177
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
1178
+ this.zeroFileRange(newBase + inode.size, len - inode.size);
1131
1179
  inode.firstBlock = newFirst;
1180
+ } else {
1181
+ this.zeroFileRange(
1182
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
1183
+ len - inode.size
1184
+ );
1132
1185
  }
1133
1186
  inode.blockCount = neededBlocks;
1134
1187
  inode.size = len;
@@ -1149,8 +1202,36 @@ var VFSEngine = class {
1149
1202
  if (flags & 1 && this.pathIndex.has(destPath)) {
1150
1203
  return { status: CODE_TO_STATUS.EEXIST };
1151
1204
  }
1152
- const data = srcInode.size > 0 ? this.readData(srcInode.firstBlock, srcInode.blockCount, srcInode.size) : new Uint8Array(0);
1153
- return this.write(destPath, data);
1205
+ if (srcPath === destPath) return { status: 0 };
1206
+ const srcSize = srcInode.size;
1207
+ const srcFirstBlock = srcInode.firstBlock;
1208
+ const emptyStatus = this.write(destPath, new Uint8Array(0));
1209
+ if (emptyStatus.status !== 0) return emptyStatus;
1210
+ if (srcSize === 0) return { status: 0 };
1211
+ const destIdx = this.resolvePathComponents(destPath, true);
1212
+ if (destIdx === void 0) return { status: CODE_TO_STATUS.EIO };
1213
+ const destInode = this.readInode(destIdx);
1214
+ const neededBlocks = Math.ceil(srcSize / this.blockSize);
1215
+ const newFirst = this.allocateBlocks(neededBlocks);
1216
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1217
+ const srcBase = this.dataOffset + srcFirstBlock * this.blockSize;
1218
+ const CHUNK = 4 * 1024 * 1024;
1219
+ const scratch = new Uint8Array(Math.min(CHUNK, srcSize));
1220
+ let copied = 0;
1221
+ while (copied < srcSize) {
1222
+ const n = Math.min(CHUNK, srcSize - copied);
1223
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1224
+ this.handle.read(slice, { at: srcBase + copied });
1225
+ this.handle.write(slice, { at: newBase + copied });
1226
+ copied += n;
1227
+ }
1228
+ destInode.firstBlock = newFirst;
1229
+ destInode.blockCount = neededBlocks;
1230
+ destInode.size = srcSize;
1231
+ destInode.mtime = Date.now();
1232
+ this.writeInode(destIdx, destInode);
1233
+ this.commitPending();
1234
+ return { status: 0 };
1154
1235
  }
1155
1236
  // ---- ACCESS ----
1156
1237
  access(path, mode = 0) {
@@ -1314,16 +1395,35 @@ var VFSEngine = class {
1314
1395
  if (endPos > inode.size) {
1315
1396
  const neededBlocks = Math.ceil(endPos / this.blockSize);
1316
1397
  if (neededBlocks > inode.blockCount) {
1317
- const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1318
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
1319
1398
  const newFirst = this.allocateBlocks(neededBlocks);
1320
- const newBuf = new Uint8Array(endPos);
1321
- newBuf.set(oldData);
1322
- newBuf.set(data, pos);
1323
- this.writeData(newFirst, newBuf);
1399
+ const newBase = this.dataOffset + newFirst * this.blockSize;
1400
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
1401
+ if (inode.size > 0) {
1402
+ const CHUNK = 4 * 1024 * 1024;
1403
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
1404
+ let copied = 0;
1405
+ while (copied < inode.size) {
1406
+ const n = Math.min(CHUNK, inode.size - copied);
1407
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
1408
+ this.handle.read(slice, { at: oldBase + copied });
1409
+ this.handle.write(slice, { at: newBase + copied });
1410
+ copied += n;
1411
+ }
1412
+ }
1413
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
1414
+ if (pos > inode.size) {
1415
+ this.zeroFileRange(newBase + inode.size, pos - inode.size);
1416
+ }
1417
+ this.handle.write(data, { at: newBase + pos });
1324
1418
  inode.firstBlock = newFirst;
1325
1419
  inode.blockCount = neededBlocks;
1326
1420
  } else {
1421
+ if (pos > inode.size) {
1422
+ this.zeroFileRange(
1423
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
1424
+ pos - inode.size
1425
+ );
1426
+ }
1327
1427
  const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
1328
1428
  this.handle.write(data, { at: dataOffset });
1329
1429
  }