diff --git a/README.md b/README.md index efb6146..baa5cd4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ While the mutex is being held by another thread, any fs operations will be stuck ### `new FS(name, opts?)` First, create or open a "filesystem". (The name is used to determine the IndexedDb store name.) -``` +```js import FS from '@isomorphic-git/lightning-fs'; const fs = new FS("testfs") @@ -59,11 +59,32 @@ const fs = new FS("testfs") Options object: -| Param | Type [= default] | Description | -| --------- | ------------------ | --------------------------------------------------------------------- | -| `wipe` | boolean = false | Delete the database and start with an empty filesystem | -| `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL | -| `urlauto` | boolean = false | Fall back to HTTP for every read of a missing file, even if unbacked | +| Param | Type [= default] | Description | +| --------------- | ------------------ | --------------------------------------------------------------------- | +| `wipe` | boolean = false | Delete the database and start with an empty filesystem | +| `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL | +| `urlauto` | boolean = false | Fall back to HTTP for every read of a missing file, even if unbacked | +| `fileDbName` | string | Customize the database name | +| `fileStoreName` | string | Customize the store name | +| `lockDbName` | string | Customize the database name for the lock mutex | +| `lockStoreName` | string | Customize the store name for the lock mutex | + +#### Advanced usage + +You can procrastinate initializing the FS object until later. +And, if you're really adventurous, you can _re-initialize_ it with a different name to switch between IndexedDb databases. + +```js +import FS from '@isomorphic-git/lightning-fs'; + +const fs = new FS() + +// Some time later... +fs.init(name, options) + +// Some time later... +fs.init(different_name, different_options) +``` ### `fs.mkdir(filepath, opts?, cb)` @@ -157,9 +178,9 @@ Note that stat data is made automatically from the file `/.superblock.txt` if fo Options object: -| Param | Type [= default] | Description | -| ---------- | ------------------ | -------------------------------- | -| `mode` | number = 0o666 | Posix mode permissions | +| Param | Type [= default] | Description | +| ------ | ---------------- | ---------------------- | +| `mode` | number = 0o666 | Posix mode permissions | ### `fs.promises` diff --git a/package-lock.json b/package-lock.json index 80d0b96..6f23af3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@isomorphic-git/idb-keyval": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.1.tgz", - "integrity": "sha512-1KoEHIlzNKzX1I5c6wKTB9hjf4pGfcAKRha2ct32v4hsqYcKGy5e8escHbdjhwcKU1Xzm9vcblwP1yeWb0O+lw==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz", + "integrity": "sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA==" }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", diff --git a/package.json b/package.json index 5380a66..3ffdaa8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "semantic-release": "semantic-release" }, "dependencies": { - "@isomorphic-git/idb-keyval": "^3.3.1", + "@isomorphic-git/idb-keyval": "3.3.2", "isomorphic-textencoder": "1.0.1", "just-debounce-it": "1.1.0", "just-once": "1.1.0" diff --git a/src/IdbBackend.js b/src/IdbBackend.js index c729cae..c7af775 100644 --- a/src/IdbBackend.js +++ b/src/IdbBackend.js @@ -1,9 +1,10 @@ const idb = require("@isomorphic-git/idb-keyval"); module.exports = class IdbBackend { - constructor(name) { - this._database = name; - this._store = new idb.Store(this._database, this._database + "_files"); + constructor(dbname, storename) { + this._database = dbname; + this._storename = storename; + this._store = new idb.Store(this._database, this._storename); } saveSuperblock(superblock) { return idb.set("!root", superblock, this._store); diff --git a/src/Mutex.js b/src/Mutex.js index 63eebfc..d7df357 100644 --- a/src/Mutex.js +++ b/src/Mutex.js @@ -3,10 +3,11 @@ const idb = require("@isomorphic-git/idb-keyval"); const sleep = ms => new Promise(r => setTimeout(r, ms)) module.exports = class Mutex { - constructor(name) { + constructor(dbname, storename) { this._id = Math.random() - this._database = name - this._store = new idb.Store(this._database + "_lock", this._database + "_lock") + this._database = dbname + this._storename = storename + this._store = new idb.Store(this._database, this._storename) this._lock = null } async has ({ margin = 2000 } = {}) { diff --git a/src/PromisifiedFS.js b/src/PromisifiedFS.js index 415deaa..6e2fb66 100644 --- a/src/PromisifiedFS.js +++ b/src/PromisifiedFS.js @@ -34,22 +34,8 @@ function cleanParams2(oldFilepath, newFilepath) { } module.exports = class PromisifiedFS { - constructor(name, { wipe, url, urlauto } = {}) { - this._name = name - this._idb = new IdbBackend(name); - this._mutex = navigator.locks ? new Mutex2(name) : new Mutex(name); - this._cache = new CacheFS(name); - this._opts = { wipe, url }; - this._needsWipe = !!wipe; - this.saveSuperblock = debounce(() => { - this._saveSuperblock(); - }, 500); - if (url) { - this._http = new HttpBackend(url) - this._urlauto = !!urlauto - } - this._operations = new Set() - + constructor(name, options) { + this.init = this.init.bind(this) this.readFile = this._wrap(this.readFile, false) this.writeFile = this._wrap(this.writeFile, true) this.unlink = this._wrap(this.unlink, true) @@ -63,17 +49,60 @@ module.exports = class PromisifiedFS { this.symlink = this._wrap(this.symlink, true) this.backFile = this._wrap(this.backFile, true) + this.saveSuperblock = debounce(() => { + this._saveSuperblock(); + }, 500); + this._deactivationPromise = null this._deactivationTimeout = null this._activationPromise = null + + this._operations = new Set() + + if (name) { + this.init(name, options) + } + } + async init (...args) { + if (this._initPromiseResolve) await this._initPromise; + this._initPromise = this._init(...args) + return this._initPromise + } + async _init (name, { + wipe, + url, + urlauto, + fileDbName = name, + fileStoreName = name + "_files", + lockDbName = name + "_lock", + lockStoreName = name + "_lock", + } = {}) { + await this._gracefulShutdown() + this._name = name + this._idb = new IdbBackend(fileDbName, fileStoreName); + this._mutex = navigator.locks ? new Mutex2(name) : new Mutex(lockDbName, lockStoreName); + this._cache = new CacheFS(name); + this._opts = { wipe, url }; + this._needsWipe = !!wipe; + if (url) { + this._http = new HttpBackend(url) + this._urlauto = !!urlauto + } + if (this._initPromiseResolve) { + this._initPromiseResolve(); + this._initPromiseResolve = null; + } // The fs is initially activated when constructed (in order to wipe/save the superblock) - // but there might not be any other fs operations needed until later. Therefore we - // need to attempt to release the mutex - this._activate().then(() => { - if (this._operations.size === 0 && !this._deactivationTimeout) { - this._deactivationTimeout = setTimeout(this._deactivate.bind(this), 100) - } - }) + // This is not awaited, because that would create a cycle. + this.stat('/') + } + async _gracefulShutdown () { + if (this._operations.size > 0) { + this._isShuttingDown = true + await new Promise(resolve => this._gracefulShutdownResolve = resolve); + this._isShuttingDown = false + this._gracefulShutdownResolve = null + } } _wrap (fn, mutating) { let i = 0 @@ -97,6 +126,8 @@ module.exports = class PromisifiedFS { } } async _activate() { + if (!this._initPromise) console.warn(new Error(`Attempted to use LightningFS ${this._name} before it was initialized.`)) + await this._initPromise if (this._deactivationTimeout) { clearTimeout(this._deactivationTimeout) this._deactivationTimeout = null @@ -138,6 +169,7 @@ module.exports = class PromisifiedFS { if (this._activationPromise) await this._activationPromise if (!this._deactivationPromise) this._deactivationPromise = this.__deactivate() this._activationPromise = null + if (this._gracefulShutdownResolve) this._gracefulShutdownResolve() return this._deactivationPromise } async __deactivate() { diff --git a/src/index.js b/src/index.js index 1c15f1a..1fc7036 100755 --- a/src/index.js +++ b/src/index.js @@ -28,6 +28,9 @@ module.exports = class FS { this.symlink = this.symlink.bind(this) this.backFile = this.backFile.bind(this) } + init(name, options) { + this.promises.init(name, options) + } readFile(filepath, opts, cb) { const [resolve, reject] = wrapCallback(opts, cb); this.promises.readFile(filepath, opts).then(resolve).catch(reject)