@powerhousedao/pglite-fs 6.0.0-dev.253 → 6.0.2-staging.9
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.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["fs"],"sources":["../src/atomic-node-fs.ts"],"sourcesContent":["import { MemoryFS } from \"@electric-sql/pglite\";\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n// PGDATA in PGLite 0.3.x's MEMFS layout. The compiled bundle uses\n// `/tmp/pglite/base` (PG install prefix `/tmp/pglite`, data dir `base/` inside\n// it). Not exported as a runtime constant, so we hardcode.\nconst PGDATA = \"/tmp/pglite/base\";\nconst SNAPSHOT_NAME = \"snapshot.bin\";\nconst SNAPSHOT_TMP = \"snapshot.bin.tmp\";\nconst MAGIC = new Uint8Array([0x50, 0x47, 0x4c, 0x41]); // \"PGLA\"\nconst FORMAT_VERSION = 1;\n\ntype EntryType = 0 | 1; // 0=dir, 1=file\n\ninterface MemFs {\n readdir(path: string): string[];\n stat(path: string): { mode: number; size: number };\n readFile(path: string, opts?: { encoding: \"binary\" }): Uint8Array;\n writeFile(path: string, data: Uint8Array): void;\n mkdir(path: string, mode?: number): void;\n chmod(path: string, mode: number): void;\n analyzePath(path: string): { exists: boolean };\n isDir(mode: number): boolean;\n isFile(mode: number): boolean;\n}\n\nexport interface AtomicNodeFsLogger {\n warn(message: string): void;\n}\n\nexport interface AtomicNodeFsOptions {\n logger?: AtomicNodeFsLogger;\n /**\n * Coalesce `syncToFs` calls into a trailing-edge disk write at most every\n * `flushIntervalMs` milliseconds. PGLite calls `syncToFs` after every\n * non-transactional query (including each BEGIN/INSERT/COMMIT emitted by\n * Kysely), so a synchronous full-tree snapshot per call caps write\n * throughput at one query per snapshot duration. Deferred mode trades\n * worst-case crash loss of up to `flushIntervalMs` of writes for sustained\n * throughput. `closeFs` always drains pending writes durably before\n * returning.\n *\n * Default `0` preserves the original per-call synchronous behavior.\n */\n flushIntervalMs?: number;\n}\n\n/**\n * PGLite Filesystem that holds the working data dir in Emscripten MEMFS and\n * atomically swaps a single-file on-disk snapshot on `syncToFs`. A SIGKILL\n * mid-write leaves the previous snapshot intact, so the next startup loads\n * cleanly rather than aborting on torn WAL.\n *\n * Intended for local dev use — full-tree snapshots are fine at dev volume but\n * won't scale to production write rates. For write-heavy workloads, pass\n * `flushIntervalMs` to coalesce multiple PGLite syncs into one disk write.\n */\nexport class AtomicNodeFs extends MemoryFS {\n private readonly hostDir: string;\n private readonly logger?: AtomicNodeFsLogger;\n private readonly flushIntervalMs: number;\n\n private dirty = false;\n private flushTimer?: ReturnType<typeof setTimeout>;\n private flushInFlight?: Promise<void>;\n\n constructor(\n hostDir: string,\n optionsOrLogger?: AtomicNodeFsOptions | AtomicNodeFsLogger,\n ) {\n super();\n this.hostDir = path.resolve(hostDir);\n const options = normalizeOptions(optionsOrLogger);\n this.logger = options.logger;\n this.flushIntervalMs = Math.max(0, options.flushIntervalMs ?? 0);\n }\n\n async initialSyncFs(): Promise<void> {\n await fs.mkdir(this.hostDir, { recursive: true });\n const snapPath = path.join(this.hostDir, SNAPSHOT_NAME);\n const tmpPath = path.join(this.hostDir, SNAPSHOT_TMP);\n\n // Drop any leftover staging file from a prior crashed write.\n await fs.rm(tmpPath, { force: true });\n\n const memFs = this.pg!.Module.FS as MemFs;\n\n if (await fileExists(snapPath)) {\n const bytes = await fs.readFile(snapPath);\n restoreMemfs(memFs, PGDATA, bytes);\n return;\n }\n\n const legacyMarker = path.join(this.hostDir, \"PG_VERSION\");\n if (await fileExists(legacyMarker)) {\n this.logger?.warn(\n `Migrating legacy PGLite data dir at ${this.hostDir} to atomic snapshot. Original files retained alongside snapshot.bin as a backup; remove them once the new snapshot is verified.`,\n );\n await loadLegacyIntoMemfs(memFs, PGDATA, this.hostDir);\n return;\n }\n }\n\n async syncToFs(relaxedDurability?: boolean): Promise<void> {\n if (this.flushIntervalMs === 0) {\n await this.writeSnapshot(relaxedDurability ?? false);\n return;\n }\n this.dirty = true;\n this.scheduleDeferredFlush(relaxedDurability ?? false);\n }\n\n async closeFs(): Promise<void> {\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n while (this.flushInFlight) {\n await this.flushInFlight;\n }\n this.dirty = false;\n await this.writeSnapshot(false);\n await super.closeFs();\n }\n\n private scheduleDeferredFlush(relaxedDurability: boolean): void {\n if (this.flushTimer || this.flushInFlight) return;\n this.flushTimer = setTimeout(() => {\n this.flushTimer = undefined;\n this.flushInFlight = this.drainDirty(relaxedDurability)\n .catch((err) => {\n this.logger?.warn(\n `AtomicNodeFs deferred flush failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n })\n .finally(() => {\n this.flushInFlight = undefined;\n });\n }, this.flushIntervalMs);\n // Don't keep the event loop alive solely for a pending flush; closeFs is\n // responsible for draining before shutdown.\n this.flushTimer.unref?.();\n }\n\n private async drainDirty(relaxedDurability: boolean): Promise<void> {\n while (this.dirty) {\n this.dirty = false;\n await this.writeSnapshot(relaxedDurability);\n }\n }\n\n private async writeSnapshot(relaxedDurability: boolean): Promise<void> {\n const memFs = this.pg!.Module.FS as MemFs;\n const bytes = serializeMemfs(memFs, PGDATA);\n const snapPath = path.join(this.hostDir, SNAPSHOT_NAME);\n const tmpPath = path.join(this.hostDir, SNAPSHOT_TMP);\n\n const fh = await fs.open(tmpPath, \"w\");\n try {\n await fh.write(bytes);\n if (!relaxedDurability) await fh.sync();\n } finally {\n await fh.close();\n }\n\n await fs.rename(tmpPath, snapPath);\n\n if (!relaxedDurability) {\n try {\n const dirFh = await fs.open(this.hostDir, \"r\");\n try {\n await dirFh.sync();\n } finally {\n await dirFh.close();\n }\n } catch {\n // Some platforms reject fsync on a directory fd. The rename itself is\n // still atomic at the inode level; durability of the directory entry\n // is best-effort.\n }\n }\n }\n}\n\nfunction normalizeOptions(\n optionsOrLogger: AtomicNodeFsOptions | AtomicNodeFsLogger | undefined,\n): AtomicNodeFsOptions {\n if (!optionsOrLogger) return {};\n if (isLogger(optionsOrLogger)) {\n return { logger: optionsOrLogger };\n }\n return optionsOrLogger;\n}\n\nfunction isLogger(\n value: AtomicNodeFsOptions | AtomicNodeFsLogger,\n): value is AtomicNodeFsLogger {\n return (\n \"warn\" in value &&\n typeof (value as AtomicNodeFsLogger).warn === \"function\"\n );\n}\n\nasync function fileExists(p: string): Promise<boolean> {\n try {\n await fs.stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction ensureDir(FS: MemFs, dir: string): void {\n const parts = dir.split(\"/\").filter(Boolean);\n let cur = \"\";\n for (const p of parts) {\n cur += \"/\" + p;\n if (!FS.analyzePath(cur).exists) FS.mkdir(cur);\n }\n}\n\ninterface Entry {\n type: EntryType;\n mode: number;\n relPath: string;\n data?: Uint8Array;\n}\n\nfunction serializeMemfs(FS: MemFs, root: string): Uint8Array {\n const entries: Entry[] = [];\n\n const walk = (dir: string, rel: string) => {\n const names = FS.readdir(dir);\n for (const name of names) {\n if (name === \".\" || name === \"..\") continue;\n const full = dir + \"/\" + name;\n const r = rel === \"\" ? name : rel + \"/\" + name;\n const stat = FS.stat(full);\n if (FS.isDir(stat.mode)) {\n entries.push({ type: 0, mode: stat.mode & 0o7777, relPath: r });\n walk(full, r);\n } else if (FS.isFile(stat.mode)) {\n const data = FS.readFile(full, { encoding: \"binary\" });\n entries.push({\n type: 1,\n mode: stat.mode & 0o7777,\n relPath: r,\n data,\n });\n }\n // skip symlinks, sockets, etc. — PGLite doesn't create them in PGDATA.\n }\n };\n walk(root, \"\");\n\n const encoder = new TextEncoder();\n const encodedPaths = entries.map((e) => encoder.encode(e.relPath));\n\n let size = 4 + 4 + 4; // magic + version + count\n for (let i = 0; i < entries.length; i++) {\n size += 1 + 4 + 4 + encodedPaths[i].byteLength + 4;\n size += entries[i].data?.byteLength ?? 0;\n }\n\n const out = new Uint8Array(size);\n const view = new DataView(out.buffer);\n let off = 0;\n\n out.set(MAGIC, off);\n off += 4;\n view.setUint32(off, FORMAT_VERSION, true);\n off += 4;\n view.setUint32(off, entries.length, true);\n off += 4;\n\n for (let i = 0; i < entries.length; i++) {\n const e = entries[i];\n const pathBytes = encodedPaths[i];\n view.setUint8(off, e.type);\n off += 1;\n view.setUint32(off, e.mode, true);\n off += 4;\n view.setUint32(off, pathBytes.byteLength, true);\n off += 4;\n out.set(pathBytes, off);\n off += pathBytes.byteLength;\n const dataLen = e.data?.byteLength ?? 0;\n view.setUint32(off, dataLen, true);\n off += 4;\n if (dataLen > 0 && e.data) {\n out.set(e.data, off);\n off += dataLen;\n }\n }\n\n return out;\n}\n\nfunction restoreMemfs(FS: MemFs, root: string, bytes: Uint8Array): void {\n ensureDir(FS, root);\n\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n let off = 0;\n\n for (let i = 0; i < 4; i++) {\n if (bytes[off + i] !== MAGIC[i]) {\n throw new Error(\"AtomicNodeFs: invalid snapshot magic\");\n }\n }\n off += 4;\n\n const version = view.getUint32(off, true);\n off += 4;\n if (version !== FORMAT_VERSION) {\n throw new Error(`AtomicNodeFs: unsupported snapshot version ${version}`);\n }\n const count = view.getUint32(off, true);\n off += 4;\n\n const decoder = new TextDecoder();\n\n for (let i = 0; i < count; i++) {\n const type = view.getUint8(off);\n off += 1;\n const mode = view.getUint32(off, true);\n off += 4;\n const pathLen = view.getUint32(off, true);\n off += 4;\n const relPath = decoder.decode(bytes.subarray(off, off + pathLen));\n off += pathLen;\n const dataLen = view.getUint32(off, true);\n off += 4;\n const full = root + \"/\" + relPath;\n\n if (type === 0) {\n if (!FS.analyzePath(full).exists) FS.mkdir(full, mode);\n else FS.chmod(full, mode);\n } else {\n const data = bytes.subarray(off, off + dataLen);\n FS.writeFile(full, data);\n FS.chmod(full, mode);\n }\n off += dataLen;\n }\n}\n\nasync function loadLegacyIntoMemfs(\n FS: MemFs,\n root: string,\n hostDir: string,\n): Promise<void> {\n ensureDir(FS, root);\n\n const skip = new Set([SNAPSHOT_NAME, SNAPSHOT_TMP, \"postmaster.pid\"]);\n\n const walk = async (diskPath: string, memPath: string) => {\n const ents = await fs.readdir(diskPath, { withFileTypes: true });\n for (const ent of ents) {\n if (skip.has(ent.name)) continue;\n const diskFull = path.join(diskPath, ent.name);\n const memFull = memPath + \"/\" + ent.name;\n const stat = await fs.stat(diskFull);\n if (ent.isDirectory()) {\n if (!FS.analyzePath(memFull).exists) {\n FS.mkdir(memFull, stat.mode & 0o7777);\n }\n await walk(diskFull, memFull);\n } else if (ent.isFile()) {\n const data = await fs.readFile(diskFull);\n FS.writeFile(memFull, data);\n FS.chmod(memFull, stat.mode & 0o7777);\n }\n }\n };\n\n await walk(hostDir, root);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["fs"],"sources":["../src/atomic-node-fs.ts"],"sourcesContent":["import { MemoryFS } from \"@electric-sql/pglite\";\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n// PGDATA in PGLite 0.3.x's MEMFS layout. The compiled bundle uses\n// `/tmp/pglite/base` (PG install prefix `/tmp/pglite`, data dir `base/` inside\n// it). Not exported as a runtime constant, so we hardcode.\nconst PGDATA = \"/tmp/pglite/base\";\nconst SNAPSHOT_NAME = \"snapshot.bin\";\nconst SNAPSHOT_TMP = \"snapshot.bin.tmp\";\nconst MAGIC = new Uint8Array([0x50, 0x47, 0x4c, 0x41]); // \"PGLA\"\nconst FORMAT_VERSION = 1;\n\ntype EntryType = 0 | 1; // 0=dir, 1=file\n\ninterface MemFs {\n readdir(path: string): string[];\n stat(path: string): { mode: number; size: number };\n readFile(path: string, opts?: { encoding: \"binary\" }): Uint8Array;\n writeFile(path: string, data: Uint8Array): void;\n mkdir(path: string, mode?: number): void;\n chmod(path: string, mode: number): void;\n analyzePath(path: string): { exists: boolean };\n isDir(mode: number): boolean;\n isFile(mode: number): boolean;\n}\n\nexport interface AtomicNodeFsLogger {\n warn(message: string): void;\n}\n\nexport interface AtomicNodeFsOptions {\n logger?: AtomicNodeFsLogger;\n /**\n * Coalesce `syncToFs` calls into a trailing-edge disk write at most every\n * `flushIntervalMs` milliseconds. PGLite calls `syncToFs` after every\n * non-transactional query (including each BEGIN/INSERT/COMMIT emitted by\n * Kysely), so a synchronous full-tree snapshot per call caps write\n * throughput at one query per snapshot duration. Deferred mode trades\n * worst-case crash loss of up to `flushIntervalMs` of writes for sustained\n * throughput. `closeFs` always drains pending writes durably before\n * returning.\n *\n * Default `0` preserves the original per-call synchronous behavior.\n */\n flushIntervalMs?: number;\n}\n\n/**\n * PGLite Filesystem that holds the working data dir in Emscripten MEMFS and\n * atomically swaps a single-file on-disk snapshot on `syncToFs`. A SIGKILL\n * mid-write leaves the previous snapshot intact, so the next startup loads\n * cleanly rather than aborting on torn WAL.\n *\n * Intended for local dev use — full-tree snapshots are fine at dev volume but\n * won't scale to production write rates. For write-heavy workloads, pass\n * `flushIntervalMs` to coalesce multiple PGLite syncs into one disk write.\n */\nexport class AtomicNodeFs extends MemoryFS {\n private readonly hostDir: string;\n private readonly logger?: AtomicNodeFsLogger;\n private readonly flushIntervalMs: number;\n\n private dirty = false;\n private flushTimer?: ReturnType<typeof setTimeout>;\n private flushInFlight?: Promise<void>;\n\n constructor(\n hostDir: string,\n optionsOrLogger?: AtomicNodeFsOptions | AtomicNodeFsLogger,\n ) {\n super();\n this.hostDir = path.resolve(hostDir);\n const options = normalizeOptions(optionsOrLogger);\n this.logger = options.logger;\n this.flushIntervalMs = Math.max(0, options.flushIntervalMs ?? 0);\n }\n\n async initialSyncFs(): Promise<void> {\n await fs.mkdir(this.hostDir, { recursive: true });\n const snapPath = path.join(this.hostDir, SNAPSHOT_NAME);\n const tmpPath = path.join(this.hostDir, SNAPSHOT_TMP);\n\n // Drop any leftover staging file from a prior crashed write.\n await fs.rm(tmpPath, { force: true });\n\n const memFs = this.pg!.Module.FS as MemFs;\n\n if (await fileExists(snapPath)) {\n const bytes = await fs.readFile(snapPath);\n restoreMemfs(memFs, PGDATA, bytes);\n return;\n }\n\n const legacyMarker = path.join(this.hostDir, \"PG_VERSION\");\n if (await fileExists(legacyMarker)) {\n this.logger?.warn(\n `Migrating legacy PGLite data dir at ${this.hostDir} to atomic snapshot. Original files retained alongside snapshot.bin as a backup; remove them once the new snapshot is verified.`,\n );\n await loadLegacyIntoMemfs(memFs, PGDATA, this.hostDir);\n return;\n }\n }\n\n async syncToFs(relaxedDurability?: boolean): Promise<void> {\n if (this.flushIntervalMs === 0) {\n await this.writeSnapshot(relaxedDurability ?? false);\n return;\n }\n this.dirty = true;\n this.scheduleDeferredFlush(relaxedDurability ?? false);\n }\n\n async closeFs(): Promise<void> {\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n while (this.flushInFlight) {\n await this.flushInFlight;\n }\n this.dirty = false;\n await this.writeSnapshot(false);\n await super.closeFs();\n }\n\n private scheduleDeferredFlush(relaxedDurability: boolean): void {\n if (this.flushTimer || this.flushInFlight) return;\n this.flushTimer = setTimeout(() => {\n this.flushTimer = undefined;\n this.flushInFlight = this.drainDirty(relaxedDurability)\n .catch((err) => {\n this.logger?.warn(\n `AtomicNodeFs deferred flush failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n })\n .finally(() => {\n this.flushInFlight = undefined;\n });\n }, this.flushIntervalMs);\n // Don't keep the event loop alive solely for a pending flush; closeFs is\n // responsible for draining before shutdown.\n this.flushTimer.unref?.();\n }\n\n private async drainDirty(relaxedDurability: boolean): Promise<void> {\n while (this.dirty) {\n this.dirty = false;\n await this.writeSnapshot(relaxedDurability);\n }\n }\n\n private async writeSnapshot(relaxedDurability: boolean): Promise<void> {\n const memFs = this.pg!.Module.FS as MemFs;\n const bytes = serializeMemfs(memFs, PGDATA);\n const snapPath = path.join(this.hostDir, SNAPSHOT_NAME);\n const tmpPath = path.join(this.hostDir, SNAPSHOT_TMP);\n\n const fh = await fs.open(tmpPath, \"w\");\n try {\n await fh.write(bytes);\n if (!relaxedDurability) await fh.sync();\n } finally {\n await fh.close();\n }\n\n await fs.rename(tmpPath, snapPath);\n\n if (!relaxedDurability) {\n try {\n const dirFh = await fs.open(this.hostDir, \"r\");\n try {\n await dirFh.sync();\n } finally {\n await dirFh.close();\n }\n } catch {\n // Some platforms reject fsync on a directory fd. The rename itself is\n // still atomic at the inode level; durability of the directory entry\n // is best-effort.\n }\n }\n }\n}\n\nfunction normalizeOptions(\n optionsOrLogger: AtomicNodeFsOptions | AtomicNodeFsLogger | undefined,\n): AtomicNodeFsOptions {\n if (!optionsOrLogger) return {};\n if (isLogger(optionsOrLogger)) {\n return { logger: optionsOrLogger };\n }\n return optionsOrLogger;\n}\n\nfunction isLogger(\n value: AtomicNodeFsOptions | AtomicNodeFsLogger,\n): value is AtomicNodeFsLogger {\n return (\n \"warn\" in value && typeof (value as AtomicNodeFsLogger).warn === \"function\"\n );\n}\n\nasync function fileExists(p: string): Promise<boolean> {\n try {\n await fs.stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction ensureDir(FS: MemFs, dir: string): void {\n const parts = dir.split(\"/\").filter(Boolean);\n let cur = \"\";\n for (const p of parts) {\n cur += \"/\" + p;\n if (!FS.analyzePath(cur).exists) FS.mkdir(cur);\n }\n}\n\ninterface Entry {\n type: EntryType;\n mode: number;\n relPath: string;\n data?: Uint8Array;\n}\n\nfunction serializeMemfs(FS: MemFs, root: string): Uint8Array {\n const entries: Entry[] = [];\n\n const walk = (dir: string, rel: string) => {\n const names = FS.readdir(dir);\n for (const name of names) {\n if (name === \".\" || name === \"..\") continue;\n const full = dir + \"/\" + name;\n const r = rel === \"\" ? name : rel + \"/\" + name;\n const stat = FS.stat(full);\n if (FS.isDir(stat.mode)) {\n entries.push({ type: 0, mode: stat.mode & 0o7777, relPath: r });\n walk(full, r);\n } else if (FS.isFile(stat.mode)) {\n const data = FS.readFile(full, { encoding: \"binary\" });\n entries.push({\n type: 1,\n mode: stat.mode & 0o7777,\n relPath: r,\n data,\n });\n }\n // skip symlinks, sockets, etc. — PGLite doesn't create them in PGDATA.\n }\n };\n walk(root, \"\");\n\n const encoder = new TextEncoder();\n const encodedPaths = entries.map((e) => encoder.encode(e.relPath));\n\n let size = 4 + 4 + 4; // magic + version + count\n for (let i = 0; i < entries.length; i++) {\n size += 1 + 4 + 4 + encodedPaths[i].byteLength + 4;\n size += entries[i].data?.byteLength ?? 0;\n }\n\n const out = new Uint8Array(size);\n const view = new DataView(out.buffer);\n let off = 0;\n\n out.set(MAGIC, off);\n off += 4;\n view.setUint32(off, FORMAT_VERSION, true);\n off += 4;\n view.setUint32(off, entries.length, true);\n off += 4;\n\n for (let i = 0; i < entries.length; i++) {\n const e = entries[i];\n const pathBytes = encodedPaths[i];\n view.setUint8(off, e.type);\n off += 1;\n view.setUint32(off, e.mode, true);\n off += 4;\n view.setUint32(off, pathBytes.byteLength, true);\n off += 4;\n out.set(pathBytes, off);\n off += pathBytes.byteLength;\n const dataLen = e.data?.byteLength ?? 0;\n view.setUint32(off, dataLen, true);\n off += 4;\n if (dataLen > 0 && e.data) {\n out.set(e.data, off);\n off += dataLen;\n }\n }\n\n return out;\n}\n\nfunction restoreMemfs(FS: MemFs, root: string, bytes: Uint8Array): void {\n ensureDir(FS, root);\n\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n let off = 0;\n\n for (let i = 0; i < 4; i++) {\n if (bytes[off + i] !== MAGIC[i]) {\n throw new Error(\"AtomicNodeFs: invalid snapshot magic\");\n }\n }\n off += 4;\n\n const version = view.getUint32(off, true);\n off += 4;\n if (version !== FORMAT_VERSION) {\n throw new Error(`AtomicNodeFs: unsupported snapshot version ${version}`);\n }\n const count = view.getUint32(off, true);\n off += 4;\n\n const decoder = new TextDecoder();\n\n for (let i = 0; i < count; i++) {\n const type = view.getUint8(off);\n off += 1;\n const mode = view.getUint32(off, true);\n off += 4;\n const pathLen = view.getUint32(off, true);\n off += 4;\n const relPath = decoder.decode(bytes.subarray(off, off + pathLen));\n off += pathLen;\n const dataLen = view.getUint32(off, true);\n off += 4;\n const full = root + \"/\" + relPath;\n\n if (type === 0) {\n if (!FS.analyzePath(full).exists) FS.mkdir(full, mode);\n else FS.chmod(full, mode);\n } else {\n const data = bytes.subarray(off, off + dataLen);\n FS.writeFile(full, data);\n FS.chmod(full, mode);\n }\n off += dataLen;\n }\n}\n\nasync function loadLegacyIntoMemfs(\n FS: MemFs,\n root: string,\n hostDir: string,\n): Promise<void> {\n ensureDir(FS, root);\n\n const skip = new Set([SNAPSHOT_NAME, SNAPSHOT_TMP, \"postmaster.pid\"]);\n\n const walk = async (diskPath: string, memPath: string) => {\n const ents = await fs.readdir(diskPath, { withFileTypes: true });\n for (const ent of ents) {\n if (skip.has(ent.name)) continue;\n const diskFull = path.join(diskPath, ent.name);\n const memFull = memPath + \"/\" + ent.name;\n const stat = await fs.stat(diskFull);\n if (ent.isDirectory()) {\n if (!FS.analyzePath(memFull).exists) {\n FS.mkdir(memFull, stat.mode & 0o7777);\n }\n await walk(diskFull, memFull);\n } else if (ent.isFile()) {\n const data = await fs.readFile(diskFull);\n FS.writeFile(memFull, data);\n FS.chmod(memFull, stat.mode & 0o7777);\n }\n }\n };\n\n await walk(hostDir, root);\n}\n"],"mappings":";;;;AAOA,MAAM,SAAS;AACf,MAAM,gBAAgB;AACtB,MAAM,eAAe;AACrB,MAAM,QAAQ,IAAI,WAAW;CAAC;CAAM;CAAM;CAAM;CAAK,CAAC;AACtD,MAAM,iBAAiB;;;;;;;;;;;AA+CvB,IAAa,eAAb,cAAkC,SAAS;CACzC;CACA;CACA;CAEA,QAAgB;CAChB;CACA;CAEA,YACE,SACA,iBACA;AACA,SAAO;AACP,OAAK,UAAU,KAAK,QAAQ,QAAQ;EACpC,MAAM,UAAU,iBAAiB,gBAAgB;AACjD,OAAK,SAAS,QAAQ;AACtB,OAAK,kBAAkB,KAAK,IAAI,GAAG,QAAQ,mBAAmB,EAAE;;CAGlE,MAAM,gBAA+B;AACnC,QAAMA,SAAG,MAAM,KAAK,SAAS,EAAE,WAAW,MAAM,CAAC;EACjD,MAAM,WAAW,KAAK,KAAK,KAAK,SAAS,cAAc;EACvD,MAAM,UAAU,KAAK,KAAK,KAAK,SAAS,aAAa;AAGrD,QAAMA,SAAG,GAAG,SAAS,EAAE,OAAO,MAAM,CAAC;EAErC,MAAM,QAAQ,KAAK,GAAI,OAAO;AAE9B,MAAI,MAAM,WAAW,SAAS,EAAE;AAE9B,gBAAa,OAAO,QADN,MAAMA,SAAG,SAAS,SAAS,CACP;AAClC;;AAIF,MAAI,MAAM,WADW,KAAK,KAAK,KAAK,SAAS,aAAa,CACxB,EAAE;AAClC,QAAK,QAAQ,KACX,uCAAuC,KAAK,QAAQ,iIACrD;AACD,SAAM,oBAAoB,OAAO,QAAQ,KAAK,QAAQ;AACtD;;;CAIJ,MAAM,SAAS,mBAA4C;AACzD,MAAI,KAAK,oBAAoB,GAAG;AAC9B,SAAM,KAAK,cAAc,qBAAqB,MAAM;AACpD;;AAEF,OAAK,QAAQ;AACb,OAAK,sBAAsB,qBAAqB,MAAM;;CAGxD,MAAM,UAAyB;AAC7B,MAAI,KAAK,YAAY;AACnB,gBAAa,KAAK,WAAW;AAC7B,QAAK,aAAa,KAAA;;AAEpB,SAAO,KAAK,cACV,OAAM,KAAK;AAEb,OAAK,QAAQ;AACb,QAAM,KAAK,cAAc,MAAM;AAC/B,QAAM,MAAM,SAAS;;CAGvB,sBAA8B,mBAAkC;AAC9D,MAAI,KAAK,cAAc,KAAK,cAAe;AAC3C,OAAK,aAAa,iBAAiB;AACjC,QAAK,aAAa,KAAA;AAClB,QAAK,gBAAgB,KAAK,WAAW,kBAAkB,CACpD,OAAO,QAAQ;AACd,SAAK,QAAQ,KACX,uCAAuC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACxF;KACD,CACD,cAAc;AACb,SAAK,gBAAgB,KAAA;KACrB;KACH,KAAK,gBAAgB;AAGxB,OAAK,WAAW,SAAS;;CAG3B,MAAc,WAAW,mBAA2C;AAClE,SAAO,KAAK,OAAO;AACjB,QAAK,QAAQ;AACb,SAAM,KAAK,cAAc,kBAAkB;;;CAI/C,MAAc,cAAc,mBAA2C;EACrE,MAAM,QAAQ,KAAK,GAAI,OAAO;EAC9B,MAAM,QAAQ,eAAe,OAAO,OAAO;EAC3C,MAAM,WAAW,KAAK,KAAK,KAAK,SAAS,cAAc;EACvD,MAAM,UAAU,KAAK,KAAK,KAAK,SAAS,aAAa;EAErD,MAAM,KAAK,MAAMA,SAAG,KAAK,SAAS,IAAI;AACtC,MAAI;AACF,SAAM,GAAG,MAAM,MAAM;AACrB,OAAI,CAAC,kBAAmB,OAAM,GAAG,MAAM;YAC/B;AACR,SAAM,GAAG,OAAO;;AAGlB,QAAMA,SAAG,OAAO,SAAS,SAAS;AAElC,MAAI,CAAC,kBACH,KAAI;GACF,MAAM,QAAQ,MAAMA,SAAG,KAAK,KAAK,SAAS,IAAI;AAC9C,OAAI;AACF,UAAM,MAAM,MAAM;aACV;AACR,UAAM,MAAM,OAAO;;UAEf;;;AASd,SAAS,iBACP,iBACqB;AACrB,KAAI,CAAC,gBAAiB,QAAO,EAAE;AAC/B,KAAI,SAAS,gBAAgB,CAC3B,QAAO,EAAE,QAAQ,iBAAiB;AAEpC,QAAO;;AAGT,SAAS,SACP,OAC6B;AAC7B,QACE,UAAU,SAAS,OAAQ,MAA6B,SAAS;;AAIrE,eAAe,WAAW,GAA6B;AACrD,KAAI;AACF,QAAMA,SAAG,KAAK,EAAE;AAChB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,IAAW,KAAmB;CAC/C,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,OAAO,QAAQ;CAC5C,IAAI,MAAM;AACV,MAAK,MAAM,KAAK,OAAO;AACrB,SAAO,MAAM;AACb,MAAI,CAAC,GAAG,YAAY,IAAI,CAAC,OAAQ,IAAG,MAAM,IAAI;;;AAWlD,SAAS,eAAe,IAAW,MAA0B;CAC3D,MAAM,UAAmB,EAAE;CAE3B,MAAM,QAAQ,KAAa,QAAgB;EACzC,MAAM,QAAQ,GAAG,QAAQ,IAAI;AAC7B,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,SAAS,OAAO,SAAS,KAAM;GACnC,MAAM,OAAO,MAAM,MAAM;GACzB,MAAM,IAAI,QAAQ,KAAK,OAAO,MAAM,MAAM;GAC1C,MAAM,OAAO,GAAG,KAAK,KAAK;AAC1B,OAAI,GAAG,MAAM,KAAK,KAAK,EAAE;AACvB,YAAQ,KAAK;KAAE,MAAM;KAAG,MAAM,KAAK,OAAO;KAAQ,SAAS;KAAG,CAAC;AAC/D,SAAK,MAAM,EAAE;cACJ,GAAG,OAAO,KAAK,KAAK,EAAE;IAC/B,MAAM,OAAO,GAAG,SAAS,MAAM,EAAE,UAAU,UAAU,CAAC;AACtD,YAAQ,KAAK;KACX,MAAM;KACN,MAAM,KAAK,OAAO;KAClB,SAAS;KACT;KACD,CAAC;;;;AAKR,MAAK,MAAM,GAAG;CAEd,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,eAAe,QAAQ,KAAK,MAAM,QAAQ,OAAO,EAAE,QAAQ,CAAC;CAElE,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAQ,IAAY,aAAa,GAAG,aAAa;AACjD,UAAQ,QAAQ,GAAG,MAAM,cAAc;;CAGzC,MAAM,MAAM,IAAI,WAAW,KAAK;CAChC,MAAM,OAAO,IAAI,SAAS,IAAI,OAAO;CACrC,IAAI,MAAM;AAEV,KAAI,IAAI,OAAO,IAAI;AACnB,QAAO;AACP,MAAK,UAAU,KAAK,gBAAgB,KAAK;AACzC,QAAO;AACP,MAAK,UAAU,KAAK,QAAQ,QAAQ,KAAK;AACzC,QAAO;AAEP,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,IAAI,QAAQ;EAClB,MAAM,YAAY,aAAa;AAC/B,OAAK,SAAS,KAAK,EAAE,KAAK;AAC1B,SAAO;AACP,OAAK,UAAU,KAAK,EAAE,MAAM,KAAK;AACjC,SAAO;AACP,OAAK,UAAU,KAAK,UAAU,YAAY,KAAK;AAC/C,SAAO;AACP,MAAI,IAAI,WAAW,IAAI;AACvB,SAAO,UAAU;EACjB,MAAM,UAAU,EAAE,MAAM,cAAc;AACtC,OAAK,UAAU,KAAK,SAAS,KAAK;AAClC,SAAO;AACP,MAAI,UAAU,KAAK,EAAE,MAAM;AACzB,OAAI,IAAI,EAAE,MAAM,IAAI;AACpB,UAAO;;;AAIX,QAAO;;AAGT,SAAS,aAAa,IAAW,MAAc,OAAyB;AACtE,WAAU,IAAI,KAAK;CAEnB,MAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,WAAW;CAC3E,IAAI,MAAM;AAEV,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IACrB,KAAI,MAAM,MAAM,OAAO,MAAM,GAC3B,OAAM,IAAI,MAAM,uCAAuC;AAG3D,QAAO;CAEP,MAAM,UAAU,KAAK,UAAU,KAAK,KAAK;AACzC,QAAO;AACP,KAAI,YAAY,eACd,OAAM,IAAI,MAAM,8CAA8C,UAAU;CAE1E,MAAM,QAAQ,KAAK,UAAU,KAAK,KAAK;AACvC,QAAO;CAEP,MAAM,UAAU,IAAI,aAAa;AAEjC,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,OAAO,KAAK,SAAS,IAAI;AAC/B,SAAO;EACP,MAAM,OAAO,KAAK,UAAU,KAAK,KAAK;AACtC,SAAO;EACP,MAAM,UAAU,KAAK,UAAU,KAAK,KAAK;AACzC,SAAO;EACP,MAAM,UAAU,QAAQ,OAAO,MAAM,SAAS,KAAK,MAAM,QAAQ,CAAC;AAClE,SAAO;EACP,MAAM,UAAU,KAAK,UAAU,KAAK,KAAK;AACzC,SAAO;EACP,MAAM,OAAO,OAAO,MAAM;AAE1B,MAAI,SAAS,EACX,KAAI,CAAC,GAAG,YAAY,KAAK,CAAC,OAAQ,IAAG,MAAM,MAAM,KAAK;MACjD,IAAG,MAAM,MAAM,KAAK;OACpB;GACL,MAAM,OAAO,MAAM,SAAS,KAAK,MAAM,QAAQ;AAC/C,MAAG,UAAU,MAAM,KAAK;AACxB,MAAG,MAAM,MAAM,KAAK;;AAEtB,SAAO;;;AAIX,eAAe,oBACb,IACA,MACA,SACe;AACf,WAAU,IAAI,KAAK;CAEnB,MAAM,OAAO,IAAI,IAAI;EAAC;EAAe;EAAc;EAAiB,CAAC;CAErE,MAAM,OAAO,OAAO,UAAkB,YAAoB;EACxD,MAAM,OAAO,MAAMA,SAAG,QAAQ,UAAU,EAAE,eAAe,MAAM,CAAC;AAChE,OAAK,MAAM,OAAO,MAAM;AACtB,OAAI,KAAK,IAAI,IAAI,KAAK,CAAE;GACxB,MAAM,WAAW,KAAK,KAAK,UAAU,IAAI,KAAK;GAC9C,MAAM,UAAU,UAAU,MAAM,IAAI;GACpC,MAAM,OAAO,MAAMA,SAAG,KAAK,SAAS;AACpC,OAAI,IAAI,aAAa,EAAE;AACrB,QAAI,CAAC,GAAG,YAAY,QAAQ,CAAC,OAC3B,IAAG,MAAM,SAAS,KAAK,OAAO,KAAO;AAEvC,UAAM,KAAK,UAAU,QAAQ;cACpB,IAAI,QAAQ,EAAE;IACvB,MAAM,OAAO,MAAMA,SAAG,SAAS,SAAS;AACxC,OAAG,UAAU,SAAS,KAAK;AAC3B,OAAG,MAAM,SAAS,KAAK,OAAO,KAAO;;;;AAK3C,OAAM,KAAK,SAAS,KAAK"}
|