Skip to content

Commit ded7f73

Browse files
authored
fix: allow changing 'backend' during 'init' (#65)
1 parent 04bb61d commit ded7f73

7 files changed

+177
-6
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ interface IBackend {
251251
// bonus - not part of the standard `fs` module
252252
backFile(filepath: string, opts: any): void;
253253
du(filepath: string): Awaited<number>;
254+
255+
// lifecycle - useful if your backend needs setup and teardown
256+
init?(name: string, opts: any): Awaited<void>; // passes initialization options
257+
activate?(): Awaited<void>; // called before fs operations are started
258+
deactivate?(): Awaited<void>; // called after fs has been idle for a while
259+
destroy?(): Awaited<void>; // called before hotswapping backends
254260
}
255261
```
256262

src/PromisifiedFS.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ module.exports = class PromisifiedFS {
5757
this.backFile = this._wrap(this.backFile, cleanParamsFilepathOpts, true)
5858
this.du = this._wrap(this.du, cleanParamsFilepathOpts, false);
5959

60-
this._backend = options.backend || new DefaultBackend();
61-
6260
this._deactivationPromise = null
6361
this._deactivationTimeout = null
6462
this._activationPromise = null
@@ -76,8 +74,15 @@ module.exports = class PromisifiedFS {
7674
}
7775
async _init (name, options = {}) {
7876
await this._gracefulShutdown();
77+
if (this._activationPromise) await this._deactivate()
7978

80-
await this._backend.init(name, options);
79+
if (this._backend && this._backend.destroy) {
80+
await this._backend.destroy();
81+
}
82+
this._backend = options.backend || new DefaultBackend();
83+
if (this._backend.init) {
84+
await this._backend.init(name, options);
85+
}
8186

8287
if (this._initPromiseResolve) {
8388
this._initPromiseResolve();
@@ -130,12 +135,17 @@ module.exports = class PromisifiedFS {
130135
}
131136
if (this._deactivationPromise) await this._deactivationPromise
132137
this._deactivationPromise = null
133-
if (!this._activationPromise) this._activationPromise = this._backend.activate();
138+
if (!this._activationPromise) {
139+
this._activationPromise = this._backend.activate ? this._backend.activate() : Promise.resolve();
140+
}
134141
await this._activationPromise
135142
}
136143
async _deactivate() {
137144
if (this._activationPromise) await this._activationPromise
138-
if (!this._deactivationPromise) this._deactivationPromise = this._backend.deactivate();
145+
146+
if (!this._deactivationPromise) {
147+
this._deactivationPromise = this._backend.deactivate ? this._backend.deactivate() : Promise.resolve();
148+
}
139149
this._activationPromise = null
140150
if (this._gracefulShutdownResolve) this._gracefulShutdownResolve()
141151
return this._deactivationPromise
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import FS from "../index.js";
2+
3+
const fs = new FS();
4+
const pfs = fs.promises;
5+
6+
describe("hotswap backends", () => {
7+
8+
it("re-init with new backend", async () => {
9+
// write a file
10+
fs.init("testfs-1", { wipe: true })
11+
await pfs.writeFile('/a.txt', 'HELLO');
12+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
13+
14+
// we swap backends. file is gone
15+
fs.init('testfs-2', { wipe: true })
16+
let err = null
17+
try {
18+
await pfs.readFile('/a.txt', 'utf8')
19+
} catch (e) {
20+
err = e
21+
}
22+
expect(err).not.toBeNull();
23+
expect(err.code).toBe('ENOENT');
24+
});
25+
26+
});
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import FS from "../index.js";
2+
3+
const fs = new FS();
4+
const pfs = fs.promises;
5+
6+
describe("hotswap backends", () => {
7+
8+
it("swap back and forth between two backends", async () => {
9+
// write a file to backend A
10+
fs.init('testfs-A', { wipe: true });
11+
await pfs.writeFile('/a.txt', 'HELLO');
12+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
13+
14+
// write a file to backend B
15+
fs.init('testfs-B', { wipe: true })
16+
await pfs.writeFile('/a.txt', 'WORLD');
17+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD');
18+
19+
// read a file from backend A
20+
fs.init('testfs-A');
21+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
22+
23+
// read a file from backend B
24+
fs.init('testfs-B');
25+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD');
26+
27+
// read a file from backend A
28+
fs.init('testfs-A');
29+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
30+
31+
// read a file from backend B
32+
fs.init('testfs-B');
33+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD');
34+
});
35+
36+
});
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import FS from "../index.js";
2+
3+
const fs = new FS();
4+
const pfs = fs.promises;
5+
6+
describe("hotswap backends", () => {
7+
8+
it("a custom backend", async () => {
9+
// we started with a default backend.
10+
fs.init('testfs-default', { wipe: true })
11+
await pfs.writeFile('/a.txt', 'HELLO');
12+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
13+
14+
// we swap backends.
15+
let ranInit = false;
16+
let ranDestroy = false;
17+
fs.init('testfs-custom', {
18+
backend: {
19+
init() { ranInit = true },
20+
readFile() { return 'dummy' },
21+
destroy() { ranDestroy = true },
22+
}
23+
});
24+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('dummy');
25+
expect(ranInit).toBe(true);
26+
expect(ranDestroy).toBe(false);
27+
28+
// we swap back
29+
fs.init('testfs-default');
30+
expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO');
31+
expect(ranDestroy).toBe(true);
32+
});
33+
34+
});
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import FS from "../index.js";
2+
3+
const fs = new FS();
4+
const pfs = fs.promises;
5+
6+
// IDK it's broke. It's time to rewrite LightningFS basically.
7+
8+
xdescribe("hotswap backends", () => {
9+
10+
it("graceful transition", async () => {
11+
const N = 1
12+
class MockBackend {
13+
constructor() {
14+
this.count = 0
15+
this.writeFile = this.writeFile.bind(this);
16+
}
17+
async writeFile () {
18+
await new Promise(r => setTimeout(r, 100 * Math.random()))
19+
this.count++
20+
}
21+
async readFile () {
22+
return 'foo'
23+
}
24+
}
25+
26+
const b1 = new MockBackend();
27+
const b2 = new MockBackend();
28+
29+
// write N files to mock backend 1
30+
await fs.init('testfs-custom-1', { backend: b1, defer: true });
31+
for (let i = 0; i < N; i++) {
32+
// we don't await
33+
pfs.writeFile('hello', 'foo');
34+
}
35+
36+
// swap backends without waiting
37+
await fs.init('testfs-custom-2', { backend: b2, defer: true });
38+
expect(pfs._operations.size).toBe(N);
39+
40+
// write N files to mock backend 2
41+
for (let i = 0; i < N; i++) {
42+
// we don't await
43+
pfs.writeFile('hello', 'foo');
44+
}
45+
46+
// swap backend back without waiting
47+
fs.init('testfs-custom-1', { backend: b1, defer: true });
48+
expect(pfs._operations.size).toBe(N);
49+
50+
// but now we have to wait. because we're dumb and the hotswapping isn't perfect
51+
await new Promise(r => setTimeout(r, 250));
52+
expect(pfs._operations.size).toBe(0);
53+
54+
// everything should be synced now
55+
expect(b1.count).toBe(N)
56+
expect(b2.count).toBe(N)
57+
});
58+
59+
});

src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = class FS {
3131
this.du = this.du.bind(this)
3232
}
3333
init(name, options) {
34-
this.promises.init(name, options)
34+
return this.promises.init(name, options)
3535
}
3636
readFile(filepath, opts, cb) {
3737
const [resolve, reject] = wrapCallback(opts, cb);

0 commit comments

Comments
 (0)