{"id":18410055,"url":"https://github.com/isomorphic-git/lightning-fs","last_synced_at":"2025-05-16T05:05:03.958Z","repository":{"id":33861891,"uuid":"162842464","full_name":"isomorphic-git/lightning-fs","owner":"isomorphic-git","description":"A lean and fast 'fs' for the browser","archived":false,"fork":false,"pushed_at":"2025-05-07T11:11:00.000Z","size":1055,"stargazers_count":511,"open_issues_count":29,"forks_count":51,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-05-07T11:35:42.867Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/isomorphic-git.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-12-22T21:01:01.000Z","updated_at":"2025-05-07T10:42:22.000Z","dependencies_parsed_at":"2023-02-18T01:16:11.193Z","dependency_job_id":"32d50e8b-3dc6-4312-bee1-5826988edf5a","html_url":"https://github.com/isomorphic-git/lightning-fs","commit_stats":{"total_commits":70,"total_committers":9,"mean_commits":7.777777777777778,"dds":0.3857142857142857,"last_synced_commit":"b041900186c126691cddcfd0b355b417404f0c16"},"previous_names":[],"tags_count":33,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isomorphic-git%2Flightning-fs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isomorphic-git%2Flightning-fs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isomorphic-git%2Flightning-fs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/isomorphic-git%2Flightning-fs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/isomorphic-git","download_url":"https://codeload.github.com/isomorphic-git/lightning-fs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254471061,"owners_count":22076585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T03:28:47.632Z","updated_at":"2025-05-16T05:05:03.938Z","avatar_url":"https://github.com/isomorphic-git.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @isomorphic-git/lightning-fs\n\nA lean and fast 'fs' for the browser\n\n## Motivation\n\nI wanted to see if I could make something faster than [BrowserFS](https://github.com/jvilk/BrowserFS) or [filer](https://github.com/filerjs/filer) that still implements enough of the `fs` API to run the [`isomorphic-git`](https://github.com/isomorphic-git/isomorphic-git) test suite in browsers.\n\n## Comparison with other libraries\n\nThis library does not even come close to implementing the full [`fs`](https://nodejs.org/api/fs.html) API.\nInstead, it only implements [the subset used by isomorphic-git 'fs' plugin interface](https://isomorphic-git.org/docs/en/plugin_fs) plus the [`fs.promises`](https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_promises_api) versions of those functions.\n\nUnlike BrowserFS, which has a dozen backends and is highly configurable, `lightning-fs` has a single configuration that should Just Work for most users.\n\n## Philosophy\n\n### Basic requirements:\n\n1. needs to work in all modern browsers\n2. needs to work with large-ish files and directories\n3. needs to persist data\n4. needs to enable performant web apps\n\nReq #3 excludes pure in-memory solutions. Req #4 excludes `localStorage` because it blocks the DOM and cannot be run in a webworker. Req #1 excludes WebSQL and Chrome's FileSystem API. So that leaves us with IndexedDB as the only usable storage technology.\n\n### Optimization targets (in order of priority):\n\n1. speed (time it takes to execute file system operations)\n2. bundle size (time it takes to download the library)\n3. memory usage (will it work on mobile)\n\nIn order to get improve #1, I ended up making a hybrid in-memory / IndexedDB system:\n- `mkdir`, `rmdir`, `readdir`, `rename`, and `stat` are pure in-memory operations that take 0ms\n- `writeFile`, `readFile`, and `unlink` are throttled by IndexedDB\n\nThe in-memory portion of the filesystem is persisted to IndexedDB with a debounce of 500ms.\nThe files themselves are not currently cached in memory, because I don't want to waste a lot of memory.\nApplications can always *add* an LRU cache on top of `lightning-fs` - if I add one internally and it isn't tuned well for your application, it might be much harder to work around.\n\n### Multi-threaded filesystem access\n\nMultiple tabs (and web workers) can share a filesystem. However, because SharedArrayBuffer is still not available in most browsers, the in-memory cache that makes LightningFS fast cannot be shared. If each thread was allowed to update its cache independently, then you'd have a complex distributed system and would need a fancy algorithm to resolve conflicts. Instead, I'm counting on the fact that your multi-threaded applications will NOT be IO bound, and thus a simpler strategy for sharing the filesystem will work. Filesystem access is bottlenecked by a mutex (implemented via polling and an atomic compare-and-replace operation in IndexedDB) to ensure that only one thread has access to the filesystem at a time. If the active thread is constantly using the filesystem, no other threads will get a chance. However if the active thread's filesystem goes idle - no operations are pending and no new operations are started - then after 500ms its in-memory cache is serialized and saved to IndexedDB and the mutex is released. (500ms was chosen experimentally such that an [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) `clone` operation didn't thrash the mutex.)\n\nWhile the mutex is being held by another thread, any fs operations will be stuck waiting until the mutex becomes available. If the mutex is not available even after ten minutes then the filesystem operations will fail with an error. This could happen if say, you are trying to write to a log file every 100ms. You can overcome this by making sure that the filesystem is allowed to go idle for \u003e500ms every now and then.\n\n## Usage\n\n### `new FS(name, opts?)`\nFirst, create or open a \"filesystem\". (The name is used to determine the IndexedDb store name.)\n\n```js\nimport FS from '@isomorphic-git/lightning-fs';\n\nconst fs = new FS(\"testfs\")\n```\n\n**Note: It is better not to create multiple `FS` instances using the same name in a single thread.** Memory usage will be higher as each instance maintains its own cache, and throughput may be lower as each instance will have to compete over the mutex for access to the IndexedDb store.\n\nOptions object:\n\n| Param           | Type [= default]   | Description                                                                                                                                                                                |\n| --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `wipe`          | boolean = false    | Delete the database and start with an empty filesystem                                                                                                                                     |\n| `url`           | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL                                                                                                                      |\n| `urlauto`       | boolean = false    | Fall back to HTTP for every read of a missing file, even if unbacked                                                                                                                       |\n| `fileDbName`    | string             | Customize the database name                                                                                                                                                                |\n| `fileStoreName` | string             | Customize the store name                                                                                                                                                                   |\n| `lockDbName`    | string             | Customize the database name for the lock mutex                                                                                                                                             |\n| `lockStoreName` | string             | Customize the store name for the lock mutex                                                                                                                                                |\n| `defer`         | boolean = false    | If true, avoids mutex contention during initialization                                                                                                                                     |\n| `db`            | IDB                | Replacement for DB object that hold Filesystem data. It's low level replacement for `backend` option.  |\n| `backend`       | IBackend           | If present, none of the other arguments (except `defer`) have any effect, and instead of using the normal LightningFS stuff, LightningFS acts as a wrapper around the provided custom backend. |\n\n\n#### Advanced usage\n\nYou can procrastinate initializing the FS object until later.\nAnd, if you're really adventurous, you can _re-initialize_ it with a different name to switch between IndexedDb databases.\n\n```js\nimport FS from '@isomorphic-git/lightning-fs';\n\nconst fs = new FS()\n\n// Some time later...\nfs.init(name, options)\n\n// Some time later...\nfs.init(different_name, different_options)\n```\n\n### `fs.mkdir(filepath, opts?, cb)`\n\nMake directory\n\nOptions object:\n\n| Param  | Type [= default] | Description            |\n| ------ | ---------------- | ---------------------- |\n| `mode` | number = 0o777   | Posix mode permissions |\n\n### `fs.rmdir(filepath, opts?, cb)`\n\nRemove directory\n\n### `fs.readdir(filepath, opts?, cb)`\n\nRead directory\n\nThe callback return value is an Array of strings. NOTE: _To save time, it is NOT SORTED._ (Fun fact: Node.js' `readdir` output is not guaranteed to be sorted either. I learned that the hard way.)\n\n### `fs.writeFile(filepath, data, opts?, cb)`\n\n`data` should be a string of a Uint8Array.\n\nIf `opts` is a string, it is interpreted as `{ encoding: opts }`.\n\nOptions object:\n\n| Param      | Type [= default]   | Description                      |\n| ---------- | ------------------ | -------------------------------- |\n| `mode`     | number = 0o777     | Posix mode permissions           |\n| `encoding` | string = undefined | Only supported value is `'utf8'` |\n\n### `fs.readFile(filepath, opts?, cb)`\n\nThe result value will be a Uint8Array or (if `encoding` is `'utf8'`) a string.\n\nIf `opts` is a string, it is interpreted as `{ encoding: opts }`.\n\nOptions object:\n\n| Param      | Type [= default]   | Description                      |\n| ---------- | ------------------ | -------------------------------- |\n| `encoding` | string = undefined | Only supported value is `'utf8'` |\n\n### `fs.unlink(filepath, opts?, cb)`\n\nDelete a file\n\n### `fs.rename(oldFilepath, newFilepath, cb)`\n\nRename a file or directory\n\n### `fs.stat(filepath, opts?, cb)`\n\nThe result is a Stat object similar to the one used by Node but with fewer and slightly different properties and methods.\nThe included properties are:\n\n- `type` (\"file\" or \"dir\")\n- `mode`\n- `size`\n- `ino`\n- `mtimeMs`\n- `ctimeMs`\n- `uid` (fixed value of 1)\n- `gid` (fixed value of 1)\n- `dev` (fixed value of 1)\n\nThe included methods are:\n- `isFile()`\n- `isDirectory()`\n- `isSymbolicLink()`\n\n### `fs.lstat(filepath, opts?, cb)`\n\nLike `fs.stat` except that paths to symlinks return the symlink stats not the file stats of the symlink's target.\n\n### `fs.symlink(target, filepath, cb)`\n\nCreate a symlink at `filepath` that points to `target`.\n\n### `fs.readlink(filepath, opts?, cb)`\n\nRead the target of a symlink.\n\n### `fs.backFile(filepath, opts?, cb)`\n\nCreate or change the stat data for a file backed by HTTP.  Size is fetched with a HEAD request.  Useful when using an HTTP backend without `urlauto` set, as then files will only be readable if they have stat data.\nNote that stat data is made automatically from the file `/.superblock.txt` if found on the server.  `/.superblock.txt` can be generated or updated with the [included standalone script](src/superblocktxt.js).\n\nOptions object:\n\n| Param  | Type [= default] | Description            |\n| ------ | ---------------- | ---------------------- |\n| `mode` | number = 0o666   | Posix mode permissions |\n\n### `fs.du(filepath, cb)`\n\nReturns the size of a file or directory in bytes.\n\n### `fs.promises`\n\nAll the same functions as above, but instead of passing a callback they return a promise.\n\n## Providing a custom `backend` (advanced usage)\n\nThere are only two reasons I can think of that you would want to do this:\n\n1. The `fs` module is normally a singleton. LightningFS allows you to safely(ish) hotswap between various data sources by calling `init` multiple times with different options. (It keeps track of file system operations in flight and waits until there's an idle moment to do the switch.)\n\n2. LightningFS normalizes all the lovely variations of node's `fs` arguments:\n\n- `fs.writeFile('filename.txt', 'Hello', cb)`\n- `fs.writeFile('filename.txt', 'Hello', 'utf8', cb)`\n- `fs.writeFile('filename.txt', 'Hello', { encoding: 'utf8' }, cb)`\n- `fs.promises.writeFile('filename.txt', 'Hello')`\n- `fs.promises.writeFile('filename.txt', 'Hello', 'utf8')`\n- `fs.promises.writeFile('filename.txt', 'Hello', { encoding: 'utf8' })`\n\nAnd it normalizes filepaths. And will convert plain `StatLike` objects into `Stat` objects with methods like `isFile`, `isDirectory`, etc.\n\nIf that fits your needs, then you can provide a `backend` option and LightningFS will use that. Implement as few/many methods as you need for your application to work.\n\n**Note:** If you use a custom backend, you are responsible for managing multi-threaded access - there are no magic mutexes included by default.\n\nNote: throwing an error with the correct `.code` property for any given situation is often important for utilities like `mkdirp` and `rimraf` to work.\n\n```tsx\n\ntype EncodingOpts = {\n  encoding?: 'utf8';\n}\n\ntype StatLike = {\n  type: 'file' | 'dir' | 'symlink';\n  mode: number;\n  size: number;\n  ino: number | string | BigInt;\n  mtimeMs: number;\n  ctimeMs?: number;\n}\n\ninterface IBackend {\n  // highly recommended - usually necessary for apps to work\n  readFile(filepath: string, opts: EncodingOpts): Awaited\u003cUint8Array | string\u003e; // throws ENOENT\n  writeFile(filepath: string, data: Uint8Array | string, opts: EncodingOpts): void; // throws ENOENT\n  unlink(filepath: string, opts: any): void; // throws ENOENT\n  readdir(filepath: string, opts: any): Awaited\u003cstring[]\u003e; // throws ENOENT, ENOTDIR\n  mkdir(filepath: string, opts: any): void; // throws ENOENT, EEXIST\n  rmdir(filepath: string, opts: any): void; // throws ENOENT, ENOTDIR, ENOTEMPTY\n\n  // recommended - often necessary for apps to work\n  stat(filepath: string, opts: any): Awaited\u003cStatLike\u003e; // throws ENOENT\n  lstat(filepath: string, opts: any): Awaited\u003cStatLike\u003e; // throws ENOENT\n\n  // suggested - used occasionally by apps\n  rename(oldFilepath: string, newFilepath: string): void; // throws ENOENT\n  readlink(filepath: string, opts: any): Awaited\u003cstring\u003e; // throws ENOENT\n  symlink(target: string, filepath: string): void; // throws ENOENT\n\n  // bonus - not part of the standard `fs` module\n  backFile(filepath: string, opts: any): void;\n  du(filepath: string): Awaited\u003cnumber\u003e;\n\n  // lifecycle - useful if your backend needs setup and teardown\n  init?(name: string, opts: any): Awaited\u003cvoid\u003e; // passes initialization options\n  activate?(): Awaited\u003cvoid\u003e; // called before fs operations are started\n  deactivate?(): Awaited\u003cvoid\u003e; // called after fs has been idle for a while\n  destroy?(): Awaited\u003cvoid\u003e; // called before hotswapping backends\n}\n\ninterface IDB {\n  saveSuperblock(superblock): void;\n  loadSuperblock(): Awaited\u003cBuffer\u003e;\n  readFile(inode): Awaited\u003cBuffer\u003e;\n  writeFile(inode, data): Awaited\u003cvoid\u003e;\n  unlink(inode): Awaited\u003cvoid\u003e;\n  wipe(): void;\n  close(): void;\n}\n\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fisomorphic-git%2Flightning-fs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fisomorphic-git%2Flightning-fs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fisomorphic-git%2Flightning-fs/lists"}