-
Notifications
You must be signed in to change notification settings - Fork 330
Description
My use case
I use https://pglite.dev/ and want to keep FS in RAM, but dump this FS as ArrayBuffer once changes occurs.
PGLite have method https://pglite.dev/docs/api#dumpdatadir that even with no compression takes 100ms and this is too long time, since data dump may be called frequently.
Since PGLite uses a MEMFS i think the problem is they iterate whole FS content to build archive every time we call dump.
As potential solution I see a FS that will store data in single ArrayBuffer and will let us dump this buffer immediately (only time to copy buffer).
I found package @zenfs/core that implements exactly I need
import { configureSingle, fs, SingleBuffer } from "@zenfs/core";
const sharedArrayBuffer = new ArrayBuffer(100000);
await configureSingle({
backend: SingleBuffer,
buffer: sharedArrayBuffer,
});
fs.writeFileSync('/test.txt', 'You can do this anywhere, including browsers!');The problem
Now I trying to wrap this FS via pglite custom FS API
pglite/packages/pglite/src/fs/base.ts
Lines 75 to 79 in a5d8a21
| /** | |
| * Abstract base class for all custom virtual filesystems. | |
| * Each custom filesystem needs to implement an interface similar to the NodeJS FS API. | |
| */ | |
| export abstract class BaseFilesystem implements Filesystem { |
It does not work for me and throws random errors and behave randomly too.
My code with debug prints is:
/* eslint-disable spellcheck/spell-checker */
import { PGlite } from '@electric-sql/pglite';
import { BaseFilesystem } from '@electric-sql/pglite/dist/fs/base';
import { x } from '@electric-sql/pglite/dist/pglite-CyDq4d4K';
import { configure, fs, SingleBuffer } from '@zenfs/core';
class MyFS extends BaseFilesystem {
protected readonly dataDir = '/';
chmod(path: string, mode: number): void {
fs.chmodSync(path, mode);
}
close(fd: number): void {
fs.closeSync(fd);
}
fstat(fd: number): x {
console.log('fstat', fd);
const stats = fs.fstatSync(fd);
return {
blocks: stats.blocks,
atime: stats.atimeMs,
mtime: stats.mtimeMs,
ctime: stats.ctimeMs,
dev: stats.dev,
ino: stats.ino,
mode: stats.mode,
nlink: stats.nlink,
uid: stats.uid,
gid: stats.gid,
rdev: stats.rdev,
size: stats.size,
blksize: stats.blksize,
};
}
lstat(path: string): x {
console.log('lstat', path);
try {
const stats = fs.lstatSync(path);
const result = {
blocks: stats.blocks,
atime: stats.atimeMs,
mtime: stats.mtimeMs,
ctime: stats.ctimeMs,
dev: stats.dev,
ino: stats.ino,
mode: stats.mode,
nlink: stats.nlink,
uid: stats.uid,
gid: stats.gid,
rdev: stats.rdev,
size: stats.size,
blksize: stats.blksize,
};
return result;
} catch (error) {
throw error;
}
}
mkdir(
path: string,
options?:
| { recursive?: boolean | undefined; mode?: number | undefined }
| undefined,
): void {
console.log('mkdir', path);
fs.mkdirSync(path, options);
}
open(path: string, flags?: string | undefined, mode?: number | undefined): number {
console.log('open', path);
return fs.openSync(path, flags ?? 'rw', mode);
}
readdir(path: string): string[] {
console.log('readdir', path);
return fs.readdirSync(path);
}
read(
fd: number,
buffer: Uint8Array,
offset: number,
length: number,
position: number,
): number {
console.log('>> read', fd);
return fs.readSync(fd, buffer, offset, length, position);
}
rename(oldPath: string, newPath: string): void {
fs.renameSync(oldPath, newPath);
}
rmdir(path: string): void {
fs.rmdirSync(path);
}
truncate(path: string, len: number): void {
fs.truncateSync(path, len);
}
unlink(path: string): void {
fs.unlinkSync(path);
}
utimes(path: string, atime: number, mtime: number): void {
fs.utimesSync(path, atime, mtime);
}
writeFile(
path: string,
data: string | Uint8Array,
options?:
| {
encoding?: string | undefined;
mode?: number | undefined;
flag?: string | undefined;
}
| undefined,
): void {
console.log('WRITE FILE', path);
const { encoding, ...restOptions } = options ?? {};
fs.writeFileSync(path, data, {
...restOptions,
encoding: encoding as BufferEncoding,
});
}
write(
fd: number,
buffer: Uint8Array,
offset: number,
length: number,
position: number,
): number {
console.log('>> write');
return fs.writeSync(fd, buffer, offset, length, position);
}
}
function proxyMethods(obj: any, onCall: any) {
return new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value !== "function") return value;
return function (...args) {
const original = value.bind(this);
onCall(target, prop, args, original);
return original(...args);
};
}
});
}
test('debug', async () => {
const sharedArrayBuffer = new ArrayBuffer(52_428_800);
await configure({
mounts: {
'/': {
backend: SingleBuffer,
buffer: sharedArrayBuffer,
},
},
});
const pg = new PGlite({
fs: proxyMethods(new MyFS(), (_target, prop, args) => {
console.log("Call for", prop, args);
})
});
await pg.waitReady;
console.log(fs.readdirSync('/', { recursive: true }));
});The output looks weird, it looks code calls lstat with empty path '' and never tries to create file, then promise waitReady rejects with message "unreachable":
stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for initialSyncFs []
stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION
stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION
Call for lstat [ '' ]
lstat
Call for lstat [ '/postgresql.conf' ]
lstat /postgresql.conf
Call for lstat [ '' ]
lstat
❯ src/core/storage/database/pglite/zenfs.test.ts (1 test | 1 failed) 129ms
× debug 129ms
→ unreachable
The request
It is not clear about current status of custom FS in pglite, so let's figure it out first. Does pglite provides any API to implement custom FS on user side?
If so, we need in docs and maybe trivial example how to implement custom FS.
I found issue #533 where @tdrz said
We are looking into making it easier to add custom filesystems but it will take a while...
It was almost year ago. I no need in API to make custom FS easier, I want to implement custom FS in any way, to address real use case.
With no custom FS pglite have no sense in my case, because the use case is privacy focused app that can't keep data in FS/IDB/etc with no encryption. That's why we use in-memory DB. But persistence and performance still does matter.
In case I can't implement custom FS, the only way is to dump data as tar archive, but this way show very poor performance for frequent syncs.