@php-wasm/fs-journal 3.0.15 → 3.0.16

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/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../../../../packages/php-wasm/fs-journal/src/lib/fs-journal.ts"],"sourcesContent":["import type { PHP, UniversalPHP } from '@php-wasm/universal';\nimport { __private__dont__use } from '@php-wasm/universal';\nimport { Semaphore, basename, joinPaths } from '@php-wasm/util';\nimport { logger } from '@php-wasm/logger';\n\nexport type EmscriptenFS = any;\n\n/**\n * Represents a stream in the Emscripten file system.\n */\nexport type EmscriptenFSStream = {\n\t/** The path of the node associated with this stream. */\n\tpath: string;\n\t/** The node associated with the stream. */\n\tnode: EmscriptenFSNode;\n};\n\n/**\n * Represents a node in the Emscripten file system.\n */\nexport type EmscriptenFSNode = {\n\t/**\n\t * The name of the file or directory.\n\t */\n\tname: string;\n\t/**\n\t * A binary flag encoding information about this note,\n\t * e.g. whether it's file or a directory.\n\t */\n\tmode: number;\n\t/**\n\t * A dictionary of functions representing operations\n\t * that can be performed on the node.\n\t */\n\tnode_ops: any;\n};\n\n/**\n * Represents the type of node in PHP file system.\n */\nexport type FSNodeType = 'file' | 'directory';\n\n/**\n * Represents an update operation on a file system node.\n */\nexport type UpdateFileOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'WRITE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** Optional. The new contents of the file. */\n\tdata?: Uint8Array;\n\tnodeType: 'file';\n};\n\n/**\n * Represents a directory operation.\n */\nexport type CreateOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'CREATE';\n\t/** The path of the node being created. */\n\tpath: string;\n\t/** The type of the node being created. */\n\tnodeType: FSNodeType;\n};\n\nexport type DeleteOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'DELETE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** The type of the node being updated. */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a rename operation on a file or directory in PHP file system.\n */\nexport type RenameOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'RENAME';\n\t/** The original path of the file or directory being renamed. */\n\tpath: string;\n\t/** The new path of the file or directory after the rename operation. */\n\ttoPath: string;\n\t/** The type of node being renamed (file or directory). */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a node in the file system.\n */\nexport type FSNode = {\n\t/** The name of this file or directory. */\n\tname: string;\n\t/** The type of this node (file or directory). */\n\ttype: FSNodeType;\n\t/** The contents of the file, if it is a file and it's stored in memory. */\n\tcontents?: string;\n\t/** The child nodes of the directory, if it is a directory. */\n\tchildren?: FSNode[];\n};\n\nexport type FilesystemOperation =\n\t| CreateOperation\n\t| UpdateFileOperation\n\t| DeleteOperation\n\t| RenameOperation;\n\nexport function journalFSEvents(\n\tphp: PHP,\n\tfsRoot: string,\n\tonEntry: (entry: FilesystemOperation) => void = () => {}\n) {\n\tfunction bindToCurrentRuntime() {\n\t\tfsRoot = normalizePath(fsRoot);\n\t\tconst FS = php[__private__dont__use].FS;\n\t\tconst FSHooks = createFSHooks(FS, (entry: FilesystemOperation) => {\n\t\t\t// Only journal entries inside the specified root directory.\n\t\t\tif (entry.path.startsWith(fsRoot)) {\n\t\t\t\tonEntry(entry);\n\t\t\t} else if (\n\t\t\t\tentry.operation === 'RENAME' &&\n\t\t\t\tentry.toPath.startsWith(fsRoot)\n\t\t\t) {\n\t\t\t\tfor (const op of recordExistingPath(\n\t\t\t\t\tphp,\n\t\t\t\t\tentry.path,\n\t\t\t\t\tentry.toPath\n\t\t\t\t)) {\n\t\t\t\t\tonEntry(op);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t/**\n\t\t * Override the original FS functions with ones running the hooks.\n\t\t * We could use a Proxy object here if the Emscripten JavaScript module\n\t\t * did not use hard-coded references to the FS object.\n\t\t */\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n\t\tconst originalFunctions: Record<string, Function> = {};\n\t\tfor (const [name] of Object.entries(FSHooks)) {\n\t\t\toriginalFunctions[name] = FS[name];\n\t\t}\n\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction bind() {\n\t\t\tfor (const [name, hook] of Object.entries(FSHooks)) {\n\t\t\t\tFS[name] = function (...args: any[]) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\thook(...args);\n\t\t\t\t\treturn originalFunctions[name].apply(this, args);\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction unbind() {\n\t\t\t// Restore the original FS functions.\n\t\t\tfor (const [name, fn] of Object.entries(originalFunctions)) {\n\t\t\t\tphp[__private__dont__use].FS[name] = fn;\n\t\t\t}\n\t\t}\n\n\t\tphp[__private__dont__use].journal = {\n\t\t\tbind,\n\t\t\tunbind,\n\t\t};\n\t\tbind();\n\t}\n\tphp.addEventListener('runtime.initialized', bindToCurrentRuntime);\n\tif (php[__private__dont__use]) {\n\t\tbindToCurrentRuntime();\n\t}\n\n\tfunction unbindFromOldRuntime() {\n\t\tphp[__private__dont__use].journal.unbind();\n\t\tdelete php[__private__dont__use].journal;\n\t}\n\tphp.addEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\n\treturn function unbind() {\n\t\tphp.removeEventListener('runtime.initialized', bindToCurrentRuntime);\n\t\tphp.removeEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\t\treturn php[__private__dont__use].journal.unbind();\n\t};\n}\n\nconst createFSHooks = (\n\tFS: EmscriptenFS,\n\trecordEntry: (entry: FilesystemOperation) => void = () => {}\n) => ({\n\twrite(stream: EmscriptenFSStream) {\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: stream.path,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\ttruncate(path: string) {\n\t\tlet node;\n\t\tif (typeof path == 'string') {\n\t\t\tconst lookup = FS.lookupPath(path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tnode = lookup.node;\n\t\t} else {\n\t\t\tnode = path;\n\t\t}\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: FS.getPath(node),\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tunlink(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tmknod(path: string, mode: number) {\n\t\tif (FS.isFile(mode)) {\n\t\t\trecordEntry({\n\t\t\t\toperation: 'CREATE',\n\t\t\t\tpath,\n\t\t\t\tnodeType: 'file',\n\t\t\t});\n\t\t}\n\t},\n\tmkdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'CREATE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trmdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trename(old_path: string, new_path: string) {\n\t\ttry {\n\t\t\tconst oldLookup = FS.lookupPath(old_path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tconst newParentPath = FS.lookupPath(new_path, {\n\t\t\t\tparent: true,\n\t\t\t}).path;\n\n\t\t\trecordEntry({\n\t\t\t\toperation: 'RENAME',\n\t\t\t\tnodeType: FS.isDir(oldLookup.node.mode) ? 'directory' : 'file',\n\t\t\t\tpath: oldLookup.path,\n\t\t\t\ttoPath: joinPaths(newParentPath, basename(new_path)),\n\t\t\t});\n\t\t} catch {\n\t\t\t// We're running a bunch of FS lookups that may fail at this point.\n\t\t\t// Let's ignore the failures and let the actual rename operation\n\t\t\t// fail if it needs to.\n\t\t}\n\t},\n});\n\n/**\n * Replays a list of filesystem operations on a PHP instance.\n *\n * @param php\n * @param entries\n */\nexport function replayFSJournal(php: PHP, entries: FilesystemOperation[]) {\n\t// We need to restore the original functions to the FS object\n\t// before proceeding, or each replayed FS operation will be journaled.\n\t//\n\t// Unfortunately we can't just call the non-journaling versions directly,\n\t// because they call other low-level FS functions like `FS.mkdir()`\n\t// and will trigger the journaling hooks anyway.\n\tphp[__private__dont__use].journal.unbind();\n\ttry {\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.operation === 'CREATE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.writeFile(entry.path, ' ');\n\t\t\t\t} else {\n\t\t\t\t\tphp.mkdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'DELETE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.unlink(entry.path);\n\t\t\t\t} else {\n\t\t\t\t\tphp.rmdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'WRITE') {\n\t\t\t\tphp.writeFile(entry.path, entry.data!);\n\t\t\t} else if (entry.operation === 'RENAME') {\n\t\t\t\tphp.mv(entry.path, entry.toPath);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tphp[__private__dont__use].journal.bind();\n\t}\n}\n\nexport function* recordExistingPath(\n\tphp: PHP,\n\tfromPath: string,\n\ttoPath: string\n): Generator<FilesystemOperation> {\n\tif (php.isDir(fromPath)) {\n\t\t// The rename operation moved a directory from outside root directory\n\t\t// into the root directory. We need to traverse the entire tree\n\t\t// and provide a create operation for each file and directory.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'directory',\n\t\t};\n\t\tfor (const file of php.listFiles(fromPath)) {\n\t\t\tyield* recordExistingPath(\n\t\t\t\tphp,\n\t\t\t\tjoinPaths(fromPath, file),\n\t\t\t\tjoinPaths(toPath, file)\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// The rename operation moved a file from outside root directory\n\t\t// into the root directory. Let's rewrite it as a create operation.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'file',\n\t\t};\n\t\tyield {\n\t\t\toperation: 'WRITE',\n\t\t\tnodeType: 'file',\n\t\t\tpath: toPath,\n\t\t};\n\t}\n}\n\nfunction normalizePath(path: string) {\n\treturn path.replace(/\\/$/, '').replace(/\\/\\/+/g, '/');\n}\n\n/**\n * Normalizes a list of filesystem operations to remove\n * redundant operations.\n *\n * This is crucial because the journal doesn't store the file contents\n * on write, but only the information that the write happened. We only\n * read the contents of the file on flush. However, at that time the file\n * could have been moved to another location so we need this function to\n * rewrite the journal to reflect the current file location. Only then\n * will the hydrateUpdateFileOps() function be able to do its job.\n *\n * @param journal The original journal.\n * @returns The normalized journal.\n */\nexport function normalizeFilesystemOperations(\n\tjournal: FilesystemOperation[]\n): FilesystemOperation[] {\n\tconst substitutions: Record<number, any> = {};\n\tfor (let i = journal.length - 1; i >= 0; i--) {\n\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\tconst formerType = checkRelationship(journal[i], journal[j]);\n\t\t\tif (formerType === 'none') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst latter = journal[i];\n\t\t\tconst former = journal[j];\n\t\t\tif (\n\t\t\t\tlatter.operation === 'RENAME' &&\n\t\t\t\tformer.operation === 'RENAME'\n\t\t\t) {\n\t\t\t\t// Normalizing a double rename is a complex scenario so let's just give\n\t\t\t\t// up. There's just too many possible scenarios to handle.\n\t\t\t\t//\n\t\t\t\t// For example, the following scenario may not be possible to normalize:\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/subdir /dir_c\n\t\t\t\t// RENAME /dir_b /dir_d\n\t\t\t\t//\n\t\t\t\t// Similarly, how should we normalize the following list?\n\t\t\t\t// CREATE_FILE /file\n\t\t\t\t// CREATE_DIR /dir_a\n\t\t\t\t// RENAME /file /dir_a/file\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/file /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// The shortest way to recreate the same structure would be this:\n\t\t\t\t// CREATE_DIR /dir_b\n\t\t\t\t// CREATE_FILE /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// But that's not a straightforward transformation so let's just not\n\t\t\t\t// handle it for now.\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'[FS Journal] Normalizing a double rename is not yet supported:',\n\t\t\t\t\t{\n\t\t\t\t\t\tcurrent: latter,\n\t\t\t\t\t\tlast: former,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (former.operation === 'CREATE' || former.operation === 'WRITE') {\n\t\t\t\tif (latter.operation === 'RENAME') {\n\t\t\t\t\tif (formerType === 'same_node') {\n\t\t\t\t\t\t// Creating a node and then renaming it is equivalent to creating\n\t\t\t\t\t\t// it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: latter.toPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t} else if (formerType === 'descendant') {\n\t\t\t\t\t\t// Creating a node and then renaming its parent directory is\n\t\t\t\t\t\t// equivalent to creating it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: joinPaths(\n\t\t\t\t\t\t\t\t\tlatter.toPath,\n\t\t\t\t\t\t\t\t\tformer.path.substring(latter.path.length)\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'WRITE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// Updating the same node twice is equivalent to updating it once\n\t\t\t\t\t// at the later time.\n\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'DELETE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// A CREATE/WRITE followed by a DELETE on the same node.\n\t\t\t\t\t// The CREATE/WRITE is redundant.\n\t\t\t\t\tsubstitutions[j] = [];\n\n\t\t\t\t\t// The DELETE is redundant only if the node was created\n\t\t\t\t\t// in this journal.\n\t\t\t\t\tif (former.operation === 'CREATE') {\n\t\t\t\t\t\tsubstitutions[i] = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Any substitutions? Apply them and start over.\n\t\t// We can't just continue as the current operation may\n\t\t// have been replaced.\n\t\tif (Object.entries(substitutions).length > 0) {\n\t\t\tconst updated = journal.flatMap((op, index) => {\n\t\t\t\tif (!(index in substitutions)) {\n\t\t\t\t\treturn [op];\n\t\t\t\t}\n\t\t\t\treturn substitutions[index];\n\t\t\t});\n\t\t\treturn normalizeFilesystemOperations(updated);\n\t\t}\n\t}\n\treturn journal;\n}\n\ntype RelatedOperationInfo = 'same_node' | 'ancestor' | 'descendant' | 'none';\nfunction checkRelationship(\n\tlatter: FilesystemOperation,\n\tformer: FilesystemOperation\n): RelatedOperationInfo {\n\tconst latterPath = latter.path;\n\tconst latterIsDir =\n\t\tlatter.operation !== 'WRITE' && latter.nodeType === 'directory';\n\tconst formerIsDir =\n\t\tformer.operation !== 'WRITE' && former.nodeType === 'directory';\n\tconst formerPath =\n\t\tformer.operation === 'RENAME' ? former.toPath : former.path;\n\n\tif (formerPath === latterPath) {\n\t\treturn 'same_node';\n\t} else if (formerIsDir && latterPath.startsWith(formerPath + '/')) {\n\t\treturn 'ancestor';\n\t} else if (latterIsDir && formerPath.startsWith(latterPath + '/')) {\n\t\treturn 'descendant';\n\t}\n\treturn 'none';\n}\n\n/**\n * Populates each WRITE operation with the contents of\n * said file.\n *\n * Mutates the original array.\n *\n * @param php\n * @param entries\n */\nexport async function hydrateUpdateFileOps(\n\tphp: UniversalPHP,\n\tentries: FilesystemOperation[]\n) {\n\tconst updateFileOps = entries.filter(\n\t\t(op): op is UpdateFileOperation => op.operation === 'WRITE'\n\t);\n\tconst updates = updateFileOps.map((op) => hydrateOp(php, op));\n\tawait Promise.all(updates);\n\treturn entries;\n}\n\nconst hydrateLock = new Semaphore({ concurrency: 15 });\nasync function hydrateOp(php: UniversalPHP, op: UpdateFileOperation) {\n\tconst release = await hydrateLock.acquire();\n\n\t// There is a race condition here:\n\t// The file could have been removed from the filesystem\n\t// between the flush() call and now. If that happens, we won't\n\t// be able to read it here.\n\t//\n\t// If the file was DELETEd, we're fine as the next flush() will\n\t// propagate the DELETE operation.\n\t//\n\t// If the file was RENAMEd, we're in trouble as we're about to\n\t// tell the other peer to create an empty file and the next\n\t// flush() will rename that empty file.\n\t//\n\t// This issue requires a particular timing and is unlikely to ever happen,\n\t// but is definitely possible. We could mitigate it by either:\n\t//\n\t// * Peeking into the buffered journal entries since the last flush() to\n\t// source the file path from\n\t// * Storing the data at the journaling stage instead of the flush() stage,\n\t// (and using potentially a lot of additional memory to keep track of all\n\t// the intermediate stages)\n\t//\n\t// For now, htough, let's just add error logging and keep an eye on this\n\t// to see if this actually ever happens.\n\ttry {\n\t\top.data = await php.readFileAsBuffer(op.path);\n\t} catch (e) {\n\t\t// Log the error but don't throw.\n\t\tlogger.warn(\n\t\t\t`Journal failed to hydrate a file on flush: the ` +\n\t\t\t\t`path ${op.path} no longer exists`\n\t\t);\n\t\tlogger.error(e);\n\t}\n\n\trelease();\n}\n"],"names":["journalFSEvents","php","fsRoot","onEntry","bindToCurrentRuntime","normalizePath","FS","__private__dont__use","FSHooks","createFSHooks","entry","op","recordExistingPath","originalFunctions","name","bind","hook","args","unbind","fn","unbindFromOldRuntime","recordEntry","stream","path","node","mode","old_path","new_path","oldLookup","newParentPath","joinPaths","basename","replayFSJournal","entries","fromPath","toPath","file","normalizeFilesystemOperations","journal","substitutions","i","j","formerType","checkRelationship","latter","former","logger","updated","index","latterPath","latterIsDir","formerIsDir","formerPath","hydrateUpdateFileOps","updates","hydrateOp","hydrateLock","Semaphore","release","e"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../../../../packages/php-wasm/fs-journal/src/lib/fs-journal.ts"],"sourcesContent":["import type { PHP, UniversalPHP } from '@php-wasm/universal';\nimport { __private__dont__use } from '@php-wasm/universal';\nimport { Semaphore, basename, joinPaths } from '@php-wasm/util';\nimport { logger } from '@php-wasm/logger';\n\nexport type EmscriptenFS = any;\n\n/**\n * Represents a stream in the Emscripten file system.\n */\nexport type EmscriptenFSStream = {\n\t/** The path of the node associated with this stream. */\n\tpath: string;\n\t/** The node associated with the stream. */\n\tnode: EmscriptenFSNode;\n};\n\n/**\n * Represents a node in the Emscripten file system.\n */\nexport type EmscriptenFSNode = {\n\t/**\n\t * The name of the file or directory.\n\t */\n\tname: string;\n\t/**\n\t * A binary flag encoding information about this note,\n\t * e.g. whether it's file or a directory.\n\t */\n\tmode: number;\n\t/**\n\t * A dictionary of functions representing operations\n\t * that can be performed on the node.\n\t */\n\tnode_ops: any;\n};\n\n/**\n * Represents the type of node in PHP file system.\n */\nexport type FSNodeType = 'file' | 'directory';\n\n/**\n * Represents an update operation on a file system node.\n */\nexport type UpdateFileOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'WRITE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** Optional. The new contents of the file. */\n\tdata?: Uint8Array;\n\tnodeType: 'file';\n};\n\n/**\n * Represents a directory operation.\n */\nexport type CreateOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'CREATE';\n\t/** The path of the node being created. */\n\tpath: string;\n\t/** The type of the node being created. */\n\tnodeType: FSNodeType;\n};\n\nexport type DeleteOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'DELETE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** The type of the node being updated. */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a rename operation on a file or directory in PHP file system.\n */\nexport type RenameOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'RENAME';\n\t/** The original path of the file or directory being renamed. */\n\tpath: string;\n\t/** The new path of the file or directory after the rename operation. */\n\ttoPath: string;\n\t/** The type of node being renamed (file or directory). */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a node in the file system.\n */\nexport type FSNode = {\n\t/** The name of this file or directory. */\n\tname: string;\n\t/** The type of this node (file or directory). */\n\ttype: FSNodeType;\n\t/** The contents of the file, if it is a file and it's stored in memory. */\n\tcontents?: string;\n\t/** The child nodes of the directory, if it is a directory. */\n\tchildren?: FSNode[];\n};\n\nexport type FilesystemOperation =\n\t| CreateOperation\n\t| UpdateFileOperation\n\t| DeleteOperation\n\t| RenameOperation;\n\nexport function journalFSEvents(\n\tphp: PHP,\n\tfsRoot: string,\n\tonEntry: (entry: FilesystemOperation) => void = () => {}\n) {\n\tfunction bindToCurrentRuntime() {\n\t\tfsRoot = normalizePath(fsRoot);\n\t\tconst FS = php[__private__dont__use].FS;\n\t\tconst FSHooks = createFSHooks(FS, (entry: FilesystemOperation) => {\n\t\t\t// Only journal entries inside the specified root directory.\n\t\t\tif (entry.path.startsWith(fsRoot)) {\n\t\t\t\tonEntry(entry);\n\t\t\t} else if (\n\t\t\t\tentry.operation === 'RENAME' &&\n\t\t\t\tentry.toPath.startsWith(fsRoot)\n\t\t\t) {\n\t\t\t\tfor (const op of recordExistingPath(\n\t\t\t\t\tphp,\n\t\t\t\t\tentry.path,\n\t\t\t\t\tentry.toPath\n\t\t\t\t)) {\n\t\t\t\t\tonEntry(op);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t/**\n\t\t * Override the original FS functions with ones running the hooks.\n\t\t * We could use a Proxy object here if the Emscripten JavaScript module\n\t\t * did not use hard-coded references to the FS object.\n\t\t */\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n\t\tconst originalFunctions: Record<string, Function> = {};\n\t\tfor (const [name] of Object.entries(FSHooks)) {\n\t\t\toriginalFunctions[name] = FS[name];\n\t\t}\n\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction bind() {\n\t\t\tfor (const [name, hook] of Object.entries(FSHooks)) {\n\t\t\t\tFS[name] = function (...args: any[]) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\thook(...args);\n\t\t\t\t\treturn originalFunctions[name].apply(this, args);\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction unbind() {\n\t\t\t// Restore the original FS functions.\n\t\t\tfor (const [name, fn] of Object.entries(originalFunctions)) {\n\t\t\t\tphp[__private__dont__use].FS[name] = fn;\n\t\t\t}\n\t\t}\n\n\t\tphp[__private__dont__use].journal = {\n\t\t\tbind,\n\t\t\tunbind,\n\t\t};\n\t\tbind();\n\t}\n\tphp.addEventListener('runtime.initialized', bindToCurrentRuntime);\n\tif (php[__private__dont__use]) {\n\t\tbindToCurrentRuntime();\n\t}\n\n\tfunction unbindFromOldRuntime() {\n\t\tphp[__private__dont__use].journal.unbind();\n\t\tdelete php[__private__dont__use].journal;\n\t}\n\tphp.addEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\n\treturn function unbind() {\n\t\tphp.removeEventListener('runtime.initialized', bindToCurrentRuntime);\n\t\tphp.removeEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\t\treturn php[__private__dont__use].journal.unbind();\n\t};\n}\n\nconst createFSHooks = (\n\tFS: EmscriptenFS,\n\trecordEntry: (entry: FilesystemOperation) => void = () => {}\n) => ({\n\twrite(stream: EmscriptenFSStream) {\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: stream.path,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\ttruncate(path: string) {\n\t\tlet node;\n\t\tif (typeof path == 'string') {\n\t\t\tconst lookup = FS.lookupPath(path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tnode = lookup.node;\n\t\t} else {\n\t\t\tnode = path;\n\t\t}\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: FS.getPath(node),\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tunlink(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tmknod(path: string, mode: number) {\n\t\tif (FS.isFile(mode)) {\n\t\t\trecordEntry({\n\t\t\t\toperation: 'CREATE',\n\t\t\t\tpath,\n\t\t\t\tnodeType: 'file',\n\t\t\t});\n\t\t}\n\t},\n\tmkdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'CREATE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trmdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trename(old_path: string, new_path: string) {\n\t\ttry {\n\t\t\tconst oldLookup = FS.lookupPath(old_path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tconst newParentPath = FS.lookupPath(new_path, {\n\t\t\t\tparent: true,\n\t\t\t}).path;\n\n\t\t\trecordEntry({\n\t\t\t\toperation: 'RENAME',\n\t\t\t\tnodeType: FS.isDir(oldLookup.node.mode) ? 'directory' : 'file',\n\t\t\t\tpath: oldLookup.path,\n\t\t\t\ttoPath: joinPaths(newParentPath, basename(new_path)),\n\t\t\t});\n\t\t} catch {\n\t\t\t// We're running a bunch of FS lookups that may fail at this point.\n\t\t\t// Let's ignore the failures and let the actual rename operation\n\t\t\t// fail if it needs to.\n\t\t}\n\t},\n});\n\n/**\n * Replays a list of filesystem operations on a PHP instance.\n *\n * @param php\n * @param entries\n */\nexport function replayFSJournal(php: PHP, entries: FilesystemOperation[]) {\n\t// We need to restore the original functions to the FS object\n\t// before proceeding, or each replayed FS operation will be journaled.\n\t//\n\t// Unfortunately we can't just call the non-journaling versions directly,\n\t// because they call other low-level FS functions like `FS.mkdir()`\n\t// and will trigger the journaling hooks anyway.\n\tphp[__private__dont__use].journal.unbind();\n\ttry {\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.operation === 'CREATE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.writeFile(entry.path, ' ');\n\t\t\t\t} else {\n\t\t\t\t\tphp.mkdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'DELETE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.unlink(entry.path);\n\t\t\t\t} else {\n\t\t\t\t\tphp.rmdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'WRITE') {\n\t\t\t\tphp.writeFile(entry.path, entry.data!);\n\t\t\t} else if (entry.operation === 'RENAME') {\n\t\t\t\tphp.mv(entry.path, entry.toPath);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tphp[__private__dont__use].journal.bind();\n\t}\n}\n\nexport function* recordExistingPath(\n\tphp: PHP,\n\tfromPath: string,\n\ttoPath: string\n): Generator<FilesystemOperation> {\n\tif (php.isDir(fromPath)) {\n\t\t// The rename operation moved a directory from outside root directory\n\t\t// into the root directory. We need to traverse the entire tree\n\t\t// and provide a create operation for each file and directory.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'directory',\n\t\t};\n\t\tfor (const file of php.listFiles(fromPath)) {\n\t\t\tyield* recordExistingPath(\n\t\t\t\tphp,\n\t\t\t\tjoinPaths(fromPath, file),\n\t\t\t\tjoinPaths(toPath, file)\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// The rename operation moved a file from outside root directory\n\t\t// into the root directory. Let's rewrite it as a create operation.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'file',\n\t\t};\n\t\tyield {\n\t\t\toperation: 'WRITE',\n\t\t\tnodeType: 'file',\n\t\t\tpath: toPath,\n\t\t};\n\t}\n}\n\nfunction normalizePath(path: string) {\n\treturn path.replace(/\\/$/, '').replace(/\\/\\/+/g, '/');\n}\n\n/**\n * Normalizes a list of filesystem operations to remove\n * redundant operations.\n *\n * This is crucial because the journal doesn't store the file contents\n * on write, but only the information that the write happened. We only\n * read the contents of the file on flush. However, at that time the file\n * could have been moved to another location so we need this function to\n * rewrite the journal to reflect the current file location. Only then\n * will the hydrateUpdateFileOps() function be able to do its job.\n *\n * @param journal The original journal.\n * @returns The normalized journal.\n */\nexport function normalizeFilesystemOperations(\n\tjournal: FilesystemOperation[]\n): FilesystemOperation[] {\n\tconst substitutions: Record<number, any> = {};\n\tfor (let i = journal.length - 1; i >= 0; i--) {\n\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\tconst formerType = checkRelationship(journal[i], journal[j]);\n\t\t\tif (formerType === 'none') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst latter = journal[i];\n\t\t\tconst former = journal[j];\n\t\t\tif (\n\t\t\t\tlatter.operation === 'RENAME' &&\n\t\t\t\tformer.operation === 'RENAME'\n\t\t\t) {\n\t\t\t\t// Normalizing a double rename is a complex scenario so let's just give\n\t\t\t\t// up. There's just too many possible scenarios to handle.\n\t\t\t\t//\n\t\t\t\t// For example, the following scenario may not be possible to normalize:\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/subdir /dir_c\n\t\t\t\t// RENAME /dir_b /dir_d\n\t\t\t\t//\n\t\t\t\t// Similarly, how should we normalize the following list?\n\t\t\t\t// CREATE_FILE /file\n\t\t\t\t// CREATE_DIR /dir_a\n\t\t\t\t// RENAME /file /dir_a/file\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/file /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// The shortest way to recreate the same structure would be this:\n\t\t\t\t// CREATE_DIR /dir_b\n\t\t\t\t// CREATE_FILE /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// But that's not a straightforward transformation so let's just not\n\t\t\t\t// handle it for now.\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'[FS Journal] Normalizing a double rename is not yet supported:',\n\t\t\t\t\t{\n\t\t\t\t\t\tcurrent: latter,\n\t\t\t\t\t\tlast: former,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (former.operation === 'CREATE' || former.operation === 'WRITE') {\n\t\t\t\tif (latter.operation === 'RENAME') {\n\t\t\t\t\tif (formerType === 'same_node') {\n\t\t\t\t\t\t// Creating a node and then renaming it is equivalent to creating\n\t\t\t\t\t\t// it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: latter.toPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t} else if (formerType === 'descendant') {\n\t\t\t\t\t\t// Creating a node and then renaming its parent directory is\n\t\t\t\t\t\t// equivalent to creating it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: joinPaths(\n\t\t\t\t\t\t\t\t\tlatter.toPath,\n\t\t\t\t\t\t\t\t\tformer.path.substring(latter.path.length)\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'WRITE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// Updating the same node twice is equivalent to updating it once\n\t\t\t\t\t// at the later time.\n\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'DELETE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// A CREATE/WRITE followed by a DELETE on the same node.\n\t\t\t\t\t// The CREATE/WRITE is redundant.\n\t\t\t\t\tsubstitutions[j] = [];\n\n\t\t\t\t\t// The DELETE is redundant only if the node was created\n\t\t\t\t\t// in this journal.\n\t\t\t\t\tif (former.operation === 'CREATE') {\n\t\t\t\t\t\tsubstitutions[i] = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Any substitutions? Apply them and start over.\n\t\t// We can't just continue as the current operation may\n\t\t// have been replaced.\n\t\tif (Object.entries(substitutions).length > 0) {\n\t\t\tconst updated = journal.flatMap((op, index) => {\n\t\t\t\tif (!(index in substitutions)) {\n\t\t\t\t\treturn [op];\n\t\t\t\t}\n\t\t\t\treturn substitutions[index];\n\t\t\t});\n\t\t\treturn normalizeFilesystemOperations(updated);\n\t\t}\n\t}\n\treturn journal;\n}\n\ntype RelatedOperationInfo = 'same_node' | 'ancestor' | 'descendant' | 'none';\nfunction checkRelationship(\n\tlatter: FilesystemOperation,\n\tformer: FilesystemOperation\n): RelatedOperationInfo {\n\tconst latterPath = latter.path;\n\tconst latterIsDir =\n\t\tlatter.operation !== 'WRITE' && latter.nodeType === 'directory';\n\tconst formerIsDir =\n\t\tformer.operation !== 'WRITE' && former.nodeType === 'directory';\n\tconst formerPath =\n\t\tformer.operation === 'RENAME' ? former.toPath : former.path;\n\n\tif (formerPath === latterPath) {\n\t\treturn 'same_node';\n\t} else if (formerIsDir && latterPath.startsWith(formerPath + '/')) {\n\t\treturn 'ancestor';\n\t} else if (latterIsDir && formerPath.startsWith(latterPath + '/')) {\n\t\treturn 'descendant';\n\t}\n\treturn 'none';\n}\n\n/**\n * Populates each WRITE operation with the contents of\n * said file.\n *\n * Mutates the original array.\n *\n * @param php\n * @param entries\n */\nexport async function hydrateUpdateFileOps(\n\tphp: UniversalPHP,\n\tentries: FilesystemOperation[]\n) {\n\tconst updateFileOps = entries.filter(\n\t\t(op): op is UpdateFileOperation => op.operation === 'WRITE'\n\t);\n\tconst updates = updateFileOps.map((op) => hydrateOp(php, op));\n\tawait Promise.all(updates);\n\treturn entries;\n}\n\nconst hydrateLock = new Semaphore({ concurrency: 15 });\nasync function hydrateOp(php: UniversalPHP, op: UpdateFileOperation) {\n\tconst release = await hydrateLock.acquire();\n\n\t// There is a race condition here:\n\t// The file could have been removed from the filesystem\n\t// between the flush() call and now. If that happens, we won't\n\t// be able to read it here.\n\t//\n\t// If the file was DELETEd, we're fine as the next flush() will\n\t// propagate the DELETE operation.\n\t//\n\t// If the file was RENAMEd, we're in trouble as we're about to\n\t// tell the other peer to create an empty file and the next\n\t// flush() will rename that empty file.\n\t//\n\t// This issue requires a particular timing and is unlikely to ever happen,\n\t// but is definitely possible. We could mitigate it by either:\n\t//\n\t// * Peeking into the buffered journal entries since the last flush() to\n\t// source the file path from\n\t// * Storing the data at the journaling stage instead of the flush() stage,\n\t// (and using potentially a lot of additional memory to keep track of all\n\t// the intermediate stages)\n\t//\n\t// For now, htough, let's just add error logging and keep an eye on this\n\t// to see if this actually ever happens.\n\ttry {\n\t\top.data = await php.readFileAsBuffer(op.path);\n\t} catch (e) {\n\t\t// Log the error but don't throw.\n\t\tlogger.warn(\n\t\t\t`Journal failed to hydrate a file on flush: the ` +\n\t\t\t\t`path ${op.path} no longer exists`\n\t\t);\n\t\tlogger.error(e);\n\t}\n\n\trelease();\n}\n"],"names":["journalFSEvents","php","fsRoot","onEntry","bindToCurrentRuntime","normalizePath","FS","__private__dont__use","FSHooks","createFSHooks","entry","op","recordExistingPath","originalFunctions","name","bind","hook","args","unbind","fn","unbindFromOldRuntime","recordEntry","stream","path","node","mode","old_path","new_path","oldLookup","newParentPath","joinPaths","basename","replayFSJournal","entries","fromPath","toPath","file","normalizeFilesystemOperations","journal","substitutions","i","j","formerType","checkRelationship","latter","former","logger","updated","index","latterPath","latterIsDir","formerIsDir","formerPath","hydrateUpdateFileOps","updates","hydrateOp","hydrateLock","Semaphore","release","e"],"mappings":"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"}
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../packages/php-wasm/fs-journal/src/lib/fs-journal.ts"],"sourcesContent":["import type { PHP, UniversalPHP } from '@php-wasm/universal';\nimport { __private__dont__use } from '@php-wasm/universal';\nimport { Semaphore, basename, joinPaths } from '@php-wasm/util';\nimport { logger } from '@php-wasm/logger';\n\nexport type EmscriptenFS = any;\n\n/**\n * Represents a stream in the Emscripten file system.\n */\nexport type EmscriptenFSStream = {\n\t/** The path of the node associated with this stream. */\n\tpath: string;\n\t/** The node associated with the stream. */\n\tnode: EmscriptenFSNode;\n};\n\n/**\n * Represents a node in the Emscripten file system.\n */\nexport type EmscriptenFSNode = {\n\t/**\n\t * The name of the file or directory.\n\t */\n\tname: string;\n\t/**\n\t * A binary flag encoding information about this note,\n\t * e.g. whether it's file or a directory.\n\t */\n\tmode: number;\n\t/**\n\t * A dictionary of functions representing operations\n\t * that can be performed on the node.\n\t */\n\tnode_ops: any;\n};\n\n/**\n * Represents the type of node in PHP file system.\n */\nexport type FSNodeType = 'file' | 'directory';\n\n/**\n * Represents an update operation on a file system node.\n */\nexport type UpdateFileOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'WRITE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** Optional. The new contents of the file. */\n\tdata?: Uint8Array;\n\tnodeType: 'file';\n};\n\n/**\n * Represents a directory operation.\n */\nexport type CreateOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'CREATE';\n\t/** The path of the node being created. */\n\tpath: string;\n\t/** The type of the node being created. */\n\tnodeType: FSNodeType;\n};\n\nexport type DeleteOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'DELETE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** The type of the node being updated. */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a rename operation on a file or directory in PHP file system.\n */\nexport type RenameOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'RENAME';\n\t/** The original path of the file or directory being renamed. */\n\tpath: string;\n\t/** The new path of the file or directory after the rename operation. */\n\ttoPath: string;\n\t/** The type of node being renamed (file or directory). */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a node in the file system.\n */\nexport type FSNode = {\n\t/** The name of this file or directory. */\n\tname: string;\n\t/** The type of this node (file or directory). */\n\ttype: FSNodeType;\n\t/** The contents of the file, if it is a file and it's stored in memory. */\n\tcontents?: string;\n\t/** The child nodes of the directory, if it is a directory. */\n\tchildren?: FSNode[];\n};\n\nexport type FilesystemOperation =\n\t| CreateOperation\n\t| UpdateFileOperation\n\t| DeleteOperation\n\t| RenameOperation;\n\nexport function journalFSEvents(\n\tphp: PHP,\n\tfsRoot: string,\n\tonEntry: (entry: FilesystemOperation) => void = () => {}\n) {\n\tfunction bindToCurrentRuntime() {\n\t\tfsRoot = normalizePath(fsRoot);\n\t\tconst FS = php[__private__dont__use].FS;\n\t\tconst FSHooks = createFSHooks(FS, (entry: FilesystemOperation) => {\n\t\t\t// Only journal entries inside the specified root directory.\n\t\t\tif (entry.path.startsWith(fsRoot)) {\n\t\t\t\tonEntry(entry);\n\t\t\t} else if (\n\t\t\t\tentry.operation === 'RENAME' &&\n\t\t\t\tentry.toPath.startsWith(fsRoot)\n\t\t\t) {\n\t\t\t\tfor (const op of recordExistingPath(\n\t\t\t\t\tphp,\n\t\t\t\t\tentry.path,\n\t\t\t\t\tentry.toPath\n\t\t\t\t)) {\n\t\t\t\t\tonEntry(op);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t/**\n\t\t * Override the original FS functions with ones running the hooks.\n\t\t * We could use a Proxy object here if the Emscripten JavaScript module\n\t\t * did not use hard-coded references to the FS object.\n\t\t */\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n\t\tconst originalFunctions: Record<string, Function> = {};\n\t\tfor (const [name] of Object.entries(FSHooks)) {\n\t\t\toriginalFunctions[name] = FS[name];\n\t\t}\n\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction bind() {\n\t\t\tfor (const [name, hook] of Object.entries(FSHooks)) {\n\t\t\t\tFS[name] = function (...args: any[]) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\thook(...args);\n\t\t\t\t\treturn originalFunctions[name].apply(this, args);\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction unbind() {\n\t\t\t// Restore the original FS functions.\n\t\t\tfor (const [name, fn] of Object.entries(originalFunctions)) {\n\t\t\t\tphp[__private__dont__use].FS[name] = fn;\n\t\t\t}\n\t\t}\n\n\t\tphp[__private__dont__use].journal = {\n\t\t\tbind,\n\t\t\tunbind,\n\t\t};\n\t\tbind();\n\t}\n\tphp.addEventListener('runtime.initialized', bindToCurrentRuntime);\n\tif (php[__private__dont__use]) {\n\t\tbindToCurrentRuntime();\n\t}\n\n\tfunction unbindFromOldRuntime() {\n\t\tphp[__private__dont__use].journal.unbind();\n\t\tdelete php[__private__dont__use].journal;\n\t}\n\tphp.addEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\n\treturn function unbind() {\n\t\tphp.removeEventListener('runtime.initialized', bindToCurrentRuntime);\n\t\tphp.removeEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\t\treturn php[__private__dont__use].journal.unbind();\n\t};\n}\n\nconst createFSHooks = (\n\tFS: EmscriptenFS,\n\trecordEntry: (entry: FilesystemOperation) => void = () => {}\n) => ({\n\twrite(stream: EmscriptenFSStream) {\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: stream.path,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\ttruncate(path: string) {\n\t\tlet node;\n\t\tif (typeof path == 'string') {\n\t\t\tconst lookup = FS.lookupPath(path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tnode = lookup.node;\n\t\t} else {\n\t\t\tnode = path;\n\t\t}\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: FS.getPath(node),\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tunlink(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tmknod(path: string, mode: number) {\n\t\tif (FS.isFile(mode)) {\n\t\t\trecordEntry({\n\t\t\t\toperation: 'CREATE',\n\t\t\t\tpath,\n\t\t\t\tnodeType: 'file',\n\t\t\t});\n\t\t}\n\t},\n\tmkdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'CREATE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trmdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trename(old_path: string, new_path: string) {\n\t\ttry {\n\t\t\tconst oldLookup = FS.lookupPath(old_path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tconst newParentPath = FS.lookupPath(new_path, {\n\t\t\t\tparent: true,\n\t\t\t}).path;\n\n\t\t\trecordEntry({\n\t\t\t\toperation: 'RENAME',\n\t\t\t\tnodeType: FS.isDir(oldLookup.node.mode) ? 'directory' : 'file',\n\t\t\t\tpath: oldLookup.path,\n\t\t\t\ttoPath: joinPaths(newParentPath, basename(new_path)),\n\t\t\t});\n\t\t} catch {\n\t\t\t// We're running a bunch of FS lookups that may fail at this point.\n\t\t\t// Let's ignore the failures and let the actual rename operation\n\t\t\t// fail if it needs to.\n\t\t}\n\t},\n});\n\n/**\n * Replays a list of filesystem operations on a PHP instance.\n *\n * @param php\n * @param entries\n */\nexport function replayFSJournal(php: PHP, entries: FilesystemOperation[]) {\n\t// We need to restore the original functions to the FS object\n\t// before proceeding, or each replayed FS operation will be journaled.\n\t//\n\t// Unfortunately we can't just call the non-journaling versions directly,\n\t// because they call other low-level FS functions like `FS.mkdir()`\n\t// and will trigger the journaling hooks anyway.\n\tphp[__private__dont__use].journal.unbind();\n\ttry {\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.operation === 'CREATE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.writeFile(entry.path, ' ');\n\t\t\t\t} else {\n\t\t\t\t\tphp.mkdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'DELETE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.unlink(entry.path);\n\t\t\t\t} else {\n\t\t\t\t\tphp.rmdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'WRITE') {\n\t\t\t\tphp.writeFile(entry.path, entry.data!);\n\t\t\t} else if (entry.operation === 'RENAME') {\n\t\t\t\tphp.mv(entry.path, entry.toPath);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tphp[__private__dont__use].journal.bind();\n\t}\n}\n\nexport function* recordExistingPath(\n\tphp: PHP,\n\tfromPath: string,\n\ttoPath: string\n): Generator<FilesystemOperation> {\n\tif (php.isDir(fromPath)) {\n\t\t// The rename operation moved a directory from outside root directory\n\t\t// into the root directory. We need to traverse the entire tree\n\t\t// and provide a create operation for each file and directory.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'directory',\n\t\t};\n\t\tfor (const file of php.listFiles(fromPath)) {\n\t\t\tyield* recordExistingPath(\n\t\t\t\tphp,\n\t\t\t\tjoinPaths(fromPath, file),\n\t\t\t\tjoinPaths(toPath, file)\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// The rename operation moved a file from outside root directory\n\t\t// into the root directory. Let's rewrite it as a create operation.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'file',\n\t\t};\n\t\tyield {\n\t\t\toperation: 'WRITE',\n\t\t\tnodeType: 'file',\n\t\t\tpath: toPath,\n\t\t};\n\t}\n}\n\nfunction normalizePath(path: string) {\n\treturn path.replace(/\\/$/, '').replace(/\\/\\/+/g, '/');\n}\n\n/**\n * Normalizes a list of filesystem operations to remove\n * redundant operations.\n *\n * This is crucial because the journal doesn't store the file contents\n * on write, but only the information that the write happened. We only\n * read the contents of the file on flush. However, at that time the file\n * could have been moved to another location so we need this function to\n * rewrite the journal to reflect the current file location. Only then\n * will the hydrateUpdateFileOps() function be able to do its job.\n *\n * @param journal The original journal.\n * @returns The normalized journal.\n */\nexport function normalizeFilesystemOperations(\n\tjournal: FilesystemOperation[]\n): FilesystemOperation[] {\n\tconst substitutions: Record<number, any> = {};\n\tfor (let i = journal.length - 1; i >= 0; i--) {\n\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\tconst formerType = checkRelationship(journal[i], journal[j]);\n\t\t\tif (formerType === 'none') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst latter = journal[i];\n\t\t\tconst former = journal[j];\n\t\t\tif (\n\t\t\t\tlatter.operation === 'RENAME' &&\n\t\t\t\tformer.operation === 'RENAME'\n\t\t\t) {\n\t\t\t\t// Normalizing a double rename is a complex scenario so let's just give\n\t\t\t\t// up. There's just too many possible scenarios to handle.\n\t\t\t\t//\n\t\t\t\t// For example, the following scenario may not be possible to normalize:\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/subdir /dir_c\n\t\t\t\t// RENAME /dir_b /dir_d\n\t\t\t\t//\n\t\t\t\t// Similarly, how should we normalize the following list?\n\t\t\t\t// CREATE_FILE /file\n\t\t\t\t// CREATE_DIR /dir_a\n\t\t\t\t// RENAME /file /dir_a/file\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/file /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// The shortest way to recreate the same structure would be this:\n\t\t\t\t// CREATE_DIR /dir_b\n\t\t\t\t// CREATE_FILE /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// But that's not a straightforward transformation so let's just not\n\t\t\t\t// handle it for now.\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'[FS Journal] Normalizing a double rename is not yet supported:',\n\t\t\t\t\t{\n\t\t\t\t\t\tcurrent: latter,\n\t\t\t\t\t\tlast: former,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (former.operation === 'CREATE' || former.operation === 'WRITE') {\n\t\t\t\tif (latter.operation === 'RENAME') {\n\t\t\t\t\tif (formerType === 'same_node') {\n\t\t\t\t\t\t// Creating a node and then renaming it is equivalent to creating\n\t\t\t\t\t\t// it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: latter.toPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t} else if (formerType === 'descendant') {\n\t\t\t\t\t\t// Creating a node and then renaming its parent directory is\n\t\t\t\t\t\t// equivalent to creating it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: joinPaths(\n\t\t\t\t\t\t\t\t\tlatter.toPath,\n\t\t\t\t\t\t\t\t\tformer.path.substring(latter.path.length)\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'WRITE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// Updating the same node twice is equivalent to updating it once\n\t\t\t\t\t// at the later time.\n\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'DELETE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// A CREATE/WRITE followed by a DELETE on the same node.\n\t\t\t\t\t// The CREATE/WRITE is redundant.\n\t\t\t\t\tsubstitutions[j] = [];\n\n\t\t\t\t\t// The DELETE is redundant only if the node was created\n\t\t\t\t\t// in this journal.\n\t\t\t\t\tif (former.operation === 'CREATE') {\n\t\t\t\t\t\tsubstitutions[i] = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Any substitutions? Apply them and start over.\n\t\t// We can't just continue as the current operation may\n\t\t// have been replaced.\n\t\tif (Object.entries(substitutions).length > 0) {\n\t\t\tconst updated = journal.flatMap((op, index) => {\n\t\t\t\tif (!(index in substitutions)) {\n\t\t\t\t\treturn [op];\n\t\t\t\t}\n\t\t\t\treturn substitutions[index];\n\t\t\t});\n\t\t\treturn normalizeFilesystemOperations(updated);\n\t\t}\n\t}\n\treturn journal;\n}\n\ntype RelatedOperationInfo = 'same_node' | 'ancestor' | 'descendant' | 'none';\nfunction checkRelationship(\n\tlatter: FilesystemOperation,\n\tformer: FilesystemOperation\n): RelatedOperationInfo {\n\tconst latterPath = latter.path;\n\tconst latterIsDir =\n\t\tlatter.operation !== 'WRITE' && latter.nodeType === 'directory';\n\tconst formerIsDir =\n\t\tformer.operation !== 'WRITE' && former.nodeType === 'directory';\n\tconst formerPath =\n\t\tformer.operation === 'RENAME' ? former.toPath : former.path;\n\n\tif (formerPath === latterPath) {\n\t\treturn 'same_node';\n\t} else if (formerIsDir && latterPath.startsWith(formerPath + '/')) {\n\t\treturn 'ancestor';\n\t} else if (latterIsDir && formerPath.startsWith(latterPath + '/')) {\n\t\treturn 'descendant';\n\t}\n\treturn 'none';\n}\n\n/**\n * Populates each WRITE operation with the contents of\n * said file.\n *\n * Mutates the original array.\n *\n * @param php\n * @param entries\n */\nexport async function hydrateUpdateFileOps(\n\tphp: UniversalPHP,\n\tentries: FilesystemOperation[]\n) {\n\tconst updateFileOps = entries.filter(\n\t\t(op): op is UpdateFileOperation => op.operation === 'WRITE'\n\t);\n\tconst updates = updateFileOps.map((op) => hydrateOp(php, op));\n\tawait Promise.all(updates);\n\treturn entries;\n}\n\nconst hydrateLock = new Semaphore({ concurrency: 15 });\nasync function hydrateOp(php: UniversalPHP, op: UpdateFileOperation) {\n\tconst release = await hydrateLock.acquire();\n\n\t// There is a race condition here:\n\t// The file could have been removed from the filesystem\n\t// between the flush() call and now. If that happens, we won't\n\t// be able to read it here.\n\t//\n\t// If the file was DELETEd, we're fine as the next flush() will\n\t// propagate the DELETE operation.\n\t//\n\t// If the file was RENAMEd, we're in trouble as we're about to\n\t// tell the other peer to create an empty file and the next\n\t// flush() will rename that empty file.\n\t//\n\t// This issue requires a particular timing and is unlikely to ever happen,\n\t// but is definitely possible. We could mitigate it by either:\n\t//\n\t// * Peeking into the buffered journal entries since the last flush() to\n\t// source the file path from\n\t// * Storing the data at the journaling stage instead of the flush() stage,\n\t// (and using potentially a lot of additional memory to keep track of all\n\t// the intermediate stages)\n\t//\n\t// For now, htough, let's just add error logging and keep an eye on this\n\t// to see if this actually ever happens.\n\ttry {\n\t\top.data = await php.readFileAsBuffer(op.path);\n\t} catch (e) {\n\t\t// Log the error but don't throw.\n\t\tlogger.warn(\n\t\t\t`Journal failed to hydrate a file on flush: the ` +\n\t\t\t\t`path ${op.path} no longer exists`\n\t\t);\n\t\tlogger.error(e);\n\t}\n\n\trelease();\n}\n"],"names":["journalFSEvents","php","fsRoot","onEntry","bindToCurrentRuntime","normalizePath","FS","__private__dont__use","FSHooks","createFSHooks","entry","op","recordExistingPath","originalFunctions","name","bind","hook","args","unbind","fn","unbindFromOldRuntime","recordEntry","stream","path","node","mode","old_path","new_path","oldLookup","newParentPath","joinPaths","basename","replayFSJournal","entries","fromPath","toPath","file","normalizeFilesystemOperations","journal","substitutions","i","j","formerType","checkRelationship","latter","former","logger","updated","index","latterPath","latterIsDir","formerIsDir","formerPath","hydrateUpdateFileOps","updates","hydrateOp","hydrateLock","Semaphore","release","e"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../packages/php-wasm/fs-journal/src/lib/fs-journal.ts"],"sourcesContent":["import type { PHP, UniversalPHP } from '@php-wasm/universal';\nimport { __private__dont__use } from '@php-wasm/universal';\nimport { Semaphore, basename, joinPaths } from '@php-wasm/util';\nimport { logger } from '@php-wasm/logger';\n\nexport type EmscriptenFS = any;\n\n/**\n * Represents a stream in the Emscripten file system.\n */\nexport type EmscriptenFSStream = {\n\t/** The path of the node associated with this stream. */\n\tpath: string;\n\t/** The node associated with the stream. */\n\tnode: EmscriptenFSNode;\n};\n\n/**\n * Represents a node in the Emscripten file system.\n */\nexport type EmscriptenFSNode = {\n\t/**\n\t * The name of the file or directory.\n\t */\n\tname: string;\n\t/**\n\t * A binary flag encoding information about this note,\n\t * e.g. whether it's file or a directory.\n\t */\n\tmode: number;\n\t/**\n\t * A dictionary of functions representing operations\n\t * that can be performed on the node.\n\t */\n\tnode_ops: any;\n};\n\n/**\n * Represents the type of node in PHP file system.\n */\nexport type FSNodeType = 'file' | 'directory';\n\n/**\n * Represents an update operation on a file system node.\n */\nexport type UpdateFileOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'WRITE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** Optional. The new contents of the file. */\n\tdata?: Uint8Array;\n\tnodeType: 'file';\n};\n\n/**\n * Represents a directory operation.\n */\nexport type CreateOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'CREATE';\n\t/** The path of the node being created. */\n\tpath: string;\n\t/** The type of the node being created. */\n\tnodeType: FSNodeType;\n};\n\nexport type DeleteOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'DELETE';\n\t/** The path of the node being updated. */\n\tpath: string;\n\t/** The type of the node being updated. */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a rename operation on a file or directory in PHP file system.\n */\nexport type RenameOperation = {\n\t/** The type of operation being performed. */\n\toperation: 'RENAME';\n\t/** The original path of the file or directory being renamed. */\n\tpath: string;\n\t/** The new path of the file or directory after the rename operation. */\n\ttoPath: string;\n\t/** The type of node being renamed (file or directory). */\n\tnodeType: FSNodeType;\n};\n\n/**\n * Represents a node in the file system.\n */\nexport type FSNode = {\n\t/** The name of this file or directory. */\n\tname: string;\n\t/** The type of this node (file or directory). */\n\ttype: FSNodeType;\n\t/** The contents of the file, if it is a file and it's stored in memory. */\n\tcontents?: string;\n\t/** The child nodes of the directory, if it is a directory. */\n\tchildren?: FSNode[];\n};\n\nexport type FilesystemOperation =\n\t| CreateOperation\n\t| UpdateFileOperation\n\t| DeleteOperation\n\t| RenameOperation;\n\nexport function journalFSEvents(\n\tphp: PHP,\n\tfsRoot: string,\n\tonEntry: (entry: FilesystemOperation) => void = () => {}\n) {\n\tfunction bindToCurrentRuntime() {\n\t\tfsRoot = normalizePath(fsRoot);\n\t\tconst FS = php[__private__dont__use].FS;\n\t\tconst FSHooks = createFSHooks(FS, (entry: FilesystemOperation) => {\n\t\t\t// Only journal entries inside the specified root directory.\n\t\t\tif (entry.path.startsWith(fsRoot)) {\n\t\t\t\tonEntry(entry);\n\t\t\t} else if (\n\t\t\t\tentry.operation === 'RENAME' &&\n\t\t\t\tentry.toPath.startsWith(fsRoot)\n\t\t\t) {\n\t\t\t\tfor (const op of recordExistingPath(\n\t\t\t\t\tphp,\n\t\t\t\t\tentry.path,\n\t\t\t\t\tentry.toPath\n\t\t\t\t)) {\n\t\t\t\t\tonEntry(op);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t/**\n\t\t * Override the original FS functions with ones running the hooks.\n\t\t * We could use a Proxy object here if the Emscripten JavaScript module\n\t\t * did not use hard-coded references to the FS object.\n\t\t */\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n\t\tconst originalFunctions: Record<string, Function> = {};\n\t\tfor (const [name] of Object.entries(FSHooks)) {\n\t\t\toriginalFunctions[name] = FS[name];\n\t\t}\n\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction bind() {\n\t\t\tfor (const [name, hook] of Object.entries(FSHooks)) {\n\t\t\t\tFS[name] = function (...args: any[]) {\n\t\t\t\t\t// @ts-ignore\n\t\t\t\t\thook(...args);\n\t\t\t\t\treturn originalFunctions[name].apply(this, args);\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\t// eslint-disable-next-line no-inner-declarations\n\t\tfunction unbind() {\n\t\t\t// Restore the original FS functions.\n\t\t\tfor (const [name, fn] of Object.entries(originalFunctions)) {\n\t\t\t\tphp[__private__dont__use].FS[name] = fn;\n\t\t\t}\n\t\t}\n\n\t\tphp[__private__dont__use].journal = {\n\t\t\tbind,\n\t\t\tunbind,\n\t\t};\n\t\tbind();\n\t}\n\tphp.addEventListener('runtime.initialized', bindToCurrentRuntime);\n\tif (php[__private__dont__use]) {\n\t\tbindToCurrentRuntime();\n\t}\n\n\tfunction unbindFromOldRuntime() {\n\t\tphp[__private__dont__use].journal.unbind();\n\t\tdelete php[__private__dont__use].journal;\n\t}\n\tphp.addEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\n\treturn function unbind() {\n\t\tphp.removeEventListener('runtime.initialized', bindToCurrentRuntime);\n\t\tphp.removeEventListener('runtime.beforeExit', unbindFromOldRuntime);\n\t\treturn php[__private__dont__use].journal.unbind();\n\t};\n}\n\nconst createFSHooks = (\n\tFS: EmscriptenFS,\n\trecordEntry: (entry: FilesystemOperation) => void = () => {}\n) => ({\n\twrite(stream: EmscriptenFSStream) {\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: stream.path,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\ttruncate(path: string) {\n\t\tlet node;\n\t\tif (typeof path == 'string') {\n\t\t\tconst lookup = FS.lookupPath(path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tnode = lookup.node;\n\t\t} else {\n\t\t\tnode = path;\n\t\t}\n\t\trecordEntry({\n\t\t\toperation: 'WRITE',\n\t\t\tpath: FS.getPath(node),\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tunlink(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'file',\n\t\t});\n\t},\n\tmknod(path: string, mode: number) {\n\t\tif (FS.isFile(mode)) {\n\t\t\trecordEntry({\n\t\t\t\toperation: 'CREATE',\n\t\t\t\tpath,\n\t\t\t\tnodeType: 'file',\n\t\t\t});\n\t\t}\n\t},\n\tmkdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'CREATE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trmdir(path: string) {\n\t\trecordEntry({\n\t\t\toperation: 'DELETE',\n\t\t\tpath,\n\t\t\tnodeType: 'directory',\n\t\t});\n\t},\n\trename(old_path: string, new_path: string) {\n\t\ttry {\n\t\t\tconst oldLookup = FS.lookupPath(old_path, {\n\t\t\t\tfollow: true,\n\t\t\t});\n\t\t\tconst newParentPath = FS.lookupPath(new_path, {\n\t\t\t\tparent: true,\n\t\t\t}).path;\n\n\t\t\trecordEntry({\n\t\t\t\toperation: 'RENAME',\n\t\t\t\tnodeType: FS.isDir(oldLookup.node.mode) ? 'directory' : 'file',\n\t\t\t\tpath: oldLookup.path,\n\t\t\t\ttoPath: joinPaths(newParentPath, basename(new_path)),\n\t\t\t});\n\t\t} catch {\n\t\t\t// We're running a bunch of FS lookups that may fail at this point.\n\t\t\t// Let's ignore the failures and let the actual rename operation\n\t\t\t// fail if it needs to.\n\t\t}\n\t},\n});\n\n/**\n * Replays a list of filesystem operations on a PHP instance.\n *\n * @param php\n * @param entries\n */\nexport function replayFSJournal(php: PHP, entries: FilesystemOperation[]) {\n\t// We need to restore the original functions to the FS object\n\t// before proceeding, or each replayed FS operation will be journaled.\n\t//\n\t// Unfortunately we can't just call the non-journaling versions directly,\n\t// because they call other low-level FS functions like `FS.mkdir()`\n\t// and will trigger the journaling hooks anyway.\n\tphp[__private__dont__use].journal.unbind();\n\ttry {\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.operation === 'CREATE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.writeFile(entry.path, ' ');\n\t\t\t\t} else {\n\t\t\t\t\tphp.mkdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'DELETE') {\n\t\t\t\tif (entry.nodeType === 'file') {\n\t\t\t\t\tphp.unlink(entry.path);\n\t\t\t\t} else {\n\t\t\t\t\tphp.rmdir(entry.path);\n\t\t\t\t}\n\t\t\t} else if (entry.operation === 'WRITE') {\n\t\t\t\tphp.writeFile(entry.path, entry.data!);\n\t\t\t} else if (entry.operation === 'RENAME') {\n\t\t\t\tphp.mv(entry.path, entry.toPath);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tphp[__private__dont__use].journal.bind();\n\t}\n}\n\nexport function* recordExistingPath(\n\tphp: PHP,\n\tfromPath: string,\n\ttoPath: string\n): Generator<FilesystemOperation> {\n\tif (php.isDir(fromPath)) {\n\t\t// The rename operation moved a directory from outside root directory\n\t\t// into the root directory. We need to traverse the entire tree\n\t\t// and provide a create operation for each file and directory.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'directory',\n\t\t};\n\t\tfor (const file of php.listFiles(fromPath)) {\n\t\t\tyield* recordExistingPath(\n\t\t\t\tphp,\n\t\t\t\tjoinPaths(fromPath, file),\n\t\t\t\tjoinPaths(toPath, file)\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// The rename operation moved a file from outside root directory\n\t\t// into the root directory. Let's rewrite it as a create operation.\n\t\tyield {\n\t\t\toperation: 'CREATE',\n\t\t\tpath: toPath,\n\t\t\tnodeType: 'file',\n\t\t};\n\t\tyield {\n\t\t\toperation: 'WRITE',\n\t\t\tnodeType: 'file',\n\t\t\tpath: toPath,\n\t\t};\n\t}\n}\n\nfunction normalizePath(path: string) {\n\treturn path.replace(/\\/$/, '').replace(/\\/\\/+/g, '/');\n}\n\n/**\n * Normalizes a list of filesystem operations to remove\n * redundant operations.\n *\n * This is crucial because the journal doesn't store the file contents\n * on write, but only the information that the write happened. We only\n * read the contents of the file on flush. However, at that time the file\n * could have been moved to another location so we need this function to\n * rewrite the journal to reflect the current file location. Only then\n * will the hydrateUpdateFileOps() function be able to do its job.\n *\n * @param journal The original journal.\n * @returns The normalized journal.\n */\nexport function normalizeFilesystemOperations(\n\tjournal: FilesystemOperation[]\n): FilesystemOperation[] {\n\tconst substitutions: Record<number, any> = {};\n\tfor (let i = journal.length - 1; i >= 0; i--) {\n\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\tconst formerType = checkRelationship(journal[i], journal[j]);\n\t\t\tif (formerType === 'none') {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst latter = journal[i];\n\t\t\tconst former = journal[j];\n\t\t\tif (\n\t\t\t\tlatter.operation === 'RENAME' &&\n\t\t\t\tformer.operation === 'RENAME'\n\t\t\t) {\n\t\t\t\t// Normalizing a double rename is a complex scenario so let's just give\n\t\t\t\t// up. There's just too many possible scenarios to handle.\n\t\t\t\t//\n\t\t\t\t// For example, the following scenario may not be possible to normalize:\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/subdir /dir_c\n\t\t\t\t// RENAME /dir_b /dir_d\n\t\t\t\t//\n\t\t\t\t// Similarly, how should we normalize the following list?\n\t\t\t\t// CREATE_FILE /file\n\t\t\t\t// CREATE_DIR /dir_a\n\t\t\t\t// RENAME /file /dir_a/file\n\t\t\t\t// RENAME /dir_a /dir_b\n\t\t\t\t// RENAME /dir_b/file /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// The shortest way to recreate the same structure would be this:\n\t\t\t\t// CREATE_DIR /dir_b\n\t\t\t\t// CREATE_FILE /dir_b/file_2\n\t\t\t\t//\n\t\t\t\t// But that's not a straightforward transformation so let's just not\n\t\t\t\t// handle it for now.\n\t\t\t\tlogger.warn(\n\t\t\t\t\t'[FS Journal] Normalizing a double rename is not yet supported:',\n\t\t\t\t\t{\n\t\t\t\t\t\tcurrent: latter,\n\t\t\t\t\t\tlast: former,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (former.operation === 'CREATE' || former.operation === 'WRITE') {\n\t\t\t\tif (latter.operation === 'RENAME') {\n\t\t\t\t\tif (formerType === 'same_node') {\n\t\t\t\t\t\t// Creating a node and then renaming it is equivalent to creating\n\t\t\t\t\t\t// it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: latter.toPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t} else if (formerType === 'descendant') {\n\t\t\t\t\t\t// Creating a node and then renaming its parent directory is\n\t\t\t\t\t\t// equivalent to creating it in the new location.\n\t\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t\t\tsubstitutions[i] = [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...former,\n\t\t\t\t\t\t\t\tpath: joinPaths(\n\t\t\t\t\t\t\t\t\tlatter.toPath,\n\t\t\t\t\t\t\t\t\tformer.path.substring(latter.path.length)\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(substitutions[i] || []),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'WRITE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// Updating the same node twice is equivalent to updating it once\n\t\t\t\t\t// at the later time.\n\t\t\t\t\tsubstitutions[j] = [];\n\t\t\t\t} else if (\n\t\t\t\t\tlatter.operation === 'DELETE' &&\n\t\t\t\t\tformerType === 'same_node'\n\t\t\t\t) {\n\t\t\t\t\t// A CREATE/WRITE followed by a DELETE on the same node.\n\t\t\t\t\t// The CREATE/WRITE is redundant.\n\t\t\t\t\tsubstitutions[j] = [];\n\n\t\t\t\t\t// The DELETE is redundant only if the node was created\n\t\t\t\t\t// in this journal.\n\t\t\t\t\tif (former.operation === 'CREATE') {\n\t\t\t\t\t\tsubstitutions[i] = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Any substitutions? Apply them and start over.\n\t\t// We can't just continue as the current operation may\n\t\t// have been replaced.\n\t\tif (Object.entries(substitutions).length > 0) {\n\t\t\tconst updated = journal.flatMap((op, index) => {\n\t\t\t\tif (!(index in substitutions)) {\n\t\t\t\t\treturn [op];\n\t\t\t\t}\n\t\t\t\treturn substitutions[index];\n\t\t\t});\n\t\t\treturn normalizeFilesystemOperations(updated);\n\t\t}\n\t}\n\treturn journal;\n}\n\ntype RelatedOperationInfo = 'same_node' | 'ancestor' | 'descendant' | 'none';\nfunction checkRelationship(\n\tlatter: FilesystemOperation,\n\tformer: FilesystemOperation\n): RelatedOperationInfo {\n\tconst latterPath = latter.path;\n\tconst latterIsDir =\n\t\tlatter.operation !== 'WRITE' && latter.nodeType === 'directory';\n\tconst formerIsDir =\n\t\tformer.operation !== 'WRITE' && former.nodeType === 'directory';\n\tconst formerPath =\n\t\tformer.operation === 'RENAME' ? former.toPath : former.path;\n\n\tif (formerPath === latterPath) {\n\t\treturn 'same_node';\n\t} else if (formerIsDir && latterPath.startsWith(formerPath + '/')) {\n\t\treturn 'ancestor';\n\t} else if (latterIsDir && formerPath.startsWith(latterPath + '/')) {\n\t\treturn 'descendant';\n\t}\n\treturn 'none';\n}\n\n/**\n * Populates each WRITE operation with the contents of\n * said file.\n *\n * Mutates the original array.\n *\n * @param php\n * @param entries\n */\nexport async function hydrateUpdateFileOps(\n\tphp: UniversalPHP,\n\tentries: FilesystemOperation[]\n) {\n\tconst updateFileOps = entries.filter(\n\t\t(op): op is UpdateFileOperation => op.operation === 'WRITE'\n\t);\n\tconst updates = updateFileOps.map((op) => hydrateOp(php, op));\n\tawait Promise.all(updates);\n\treturn entries;\n}\n\nconst hydrateLock = new Semaphore({ concurrency: 15 });\nasync function hydrateOp(php: UniversalPHP, op: UpdateFileOperation) {\n\tconst release = await hydrateLock.acquire();\n\n\t// There is a race condition here:\n\t// The file could have been removed from the filesystem\n\t// between the flush() call and now. If that happens, we won't\n\t// be able to read it here.\n\t//\n\t// If the file was DELETEd, we're fine as the next flush() will\n\t// propagate the DELETE operation.\n\t//\n\t// If the file was RENAMEd, we're in trouble as we're about to\n\t// tell the other peer to create an empty file and the next\n\t// flush() will rename that empty file.\n\t//\n\t// This issue requires a particular timing and is unlikely to ever happen,\n\t// but is definitely possible. We could mitigate it by either:\n\t//\n\t// * Peeking into the buffered journal entries since the last flush() to\n\t// source the file path from\n\t// * Storing the data at the journaling stage instead of the flush() stage,\n\t// (and using potentially a lot of additional memory to keep track of all\n\t// the intermediate stages)\n\t//\n\t// For now, htough, let's just add error logging and keep an eye on this\n\t// to see if this actually ever happens.\n\ttry {\n\t\top.data = await php.readFileAsBuffer(op.path);\n\t} catch (e) {\n\t\t// Log the error but don't throw.\n\t\tlogger.warn(\n\t\t\t`Journal failed to hydrate a file on flush: the ` +\n\t\t\t\t`path ${op.path} no longer exists`\n\t\t);\n\t\tlogger.error(e);\n\t}\n\n\trelease();\n}\n"],"names":["journalFSEvents","php","fsRoot","onEntry","bindToCurrentRuntime","normalizePath","FS","__private__dont__use","FSHooks","createFSHooks","entry","op","recordExistingPath","originalFunctions","name","bind","hook","args","unbind","fn","unbindFromOldRuntime","recordEntry","stream","path","node","mode","old_path","new_path","oldLookup","newParentPath","joinPaths","basename","replayFSJournal","entries","fromPath","toPath","file","normalizeFilesystemOperations","journal","substitutions","i","j","formerType","checkRelationship","latter","former","logger","updated","index","latterPath","latterIsDir","formerIsDir","formerPath","hydrateUpdateFileOps","updates","hydrateOp","hydrateLock","Semaphore","release","e"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@php-wasm/fs-journal",
3
- "version": "3.0.15",
3
+ "version": "3.0.16",
4
4
  "description": "Bindings to journal the PHP filesystem",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  "main": "./index.cjs",
38
38
  "module": "./index.js",
39
39
  "license": "GPL-2.0-or-later",
40
- "gitHead": "4e8addef4a0fa0e76f26785689a1a676bbb823e1",
40
+ "gitHead": "1f96a559fda4946f36f8543a2715cee411a5c8f2",
41
41
  "engines": {
42
42
  "node": ">=20.18.3",
43
43
  "npm": ">=10.1.0"
@@ -46,12 +46,12 @@
46
46
  "express": "4.21.2",
47
47
  "ini": "4.1.2",
48
48
  "wasm-feature-detect": "1.8.0",
49
- "ws": "8.18.1",
49
+ "ws": "8.18.3",
50
50
  "yargs": "17.7.2",
51
- "@php-wasm/universal": "3.0.15",
52
- "@php-wasm/util": "3.0.15",
53
- "@php-wasm/logger": "3.0.15",
54
- "@php-wasm/node": "3.0.15"
51
+ "@php-wasm/universal": "3.0.16",
52
+ "@php-wasm/util": "3.0.16",
53
+ "@php-wasm/logger": "3.0.16",
54
+ "@php-wasm/node": "3.0.16"
55
55
  },
56
56
  "packageManager": "npm@10.9.2",
57
57
  "overrides": {