| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import fs from 'node:fs'; |
| import path from 'node:path'; |
| import type { ValueLoc } from './store.js'; |
|
|
| |
| |
| |
| export function readAtAsync(fd: number, buf: Buffer, bufOff: number, len: number, pos: number): Promise<number> { |
| return new Promise((resolve, reject) => { |
| fs.read(fd, buf, bufOff, len, pos, (err, bytesRead) => (err ? reject(err) : resolve(bytesRead))); |
| }); |
| } |
|
|
| export class ValueReader { |
| readonly snapshotPath: string; |
| readonly walPath: string; |
| private snapshotFd: number | null = null; |
| private walFd: number | null = null; |
|
|
| constructor(dir: string) { |
| this.snapshotPath = path.join(dir, 'db.snapshot'); |
| this.walPath = path.join(dir, 'db.wal'); |
| } |
|
|
| |
| |
| |
| |
| |
| open(): { snapshot: { dev: number; ino: number } | null; wal: { dev: number; ino: number } | null } { |
| this.snapshotFd = this.openIfExists(this.snapshotPath); |
| this.walFd = this.openIfExists(this.walPath); |
| return { snapshot: this.ident(this.snapshotFd), wal: this.ident(this.walFd) }; |
| } |
|
|
| private ident(fd: number | null): { dev: number; ino: number } | null { |
| if (fd === null) return null; |
| const st = fs.fstatSync(fd); |
| return { dev: st.dev, ino: st.ino }; |
| } |
|
|
| private openIfExists(file: string): number | null { |
| try { |
| return fs.openSync(file, 'r'); |
| } catch (e) { |
| if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; |
| throw e; |
| } |
| } |
|
|
| private fdFor(loc: ValueLoc): number { |
| const fd = loc.file === 'snapshot' ? this.snapshotFd : this.walFd; |
| if (fd === null) throw new Error(`value reader: ${loc.file} file is not open`); |
| return fd; |
| } |
|
|
| read(loc: ValueLoc): Buffer { |
| if (loc.len === 0) return Buffer.alloc(0); |
| const fd = this.fdFor(loc); |
| const buf = Buffer.allocUnsafe(loc.len); |
| let got = 0; |
| while (got < loc.len) { |
| const r = fs.readSync(fd, buf, got, loc.len - got, loc.off + got); |
| if (r === 0) throw new Error(`value reader: short read from ${loc.file} at ${loc.off + got}`); |
| got += r; |
| } |
| return buf; |
| } |
|
|
| |
| |
| |
| async readAsync(loc: ValueLoc): Promise<Buffer> { |
| if (loc.len === 0) return Buffer.alloc(0); |
| const fd = this.fdFor(loc); |
| const buf = Buffer.allocUnsafe(loc.len); |
| let got = 0; |
| while (got < loc.len) { |
| const r = await readAtAsync(fd, buf, got, loc.len - got, loc.off + got); |
| if (r === 0) throw new Error(`value reader: short read from ${loc.file} at ${loc.off + got}`); |
| got += r; |
| } |
| return buf; |
| } |
|
|
| reopenSnapshot(): void { |
| if (this.snapshotFd !== null) { |
| fs.closeSync(this.snapshotFd); |
| this.snapshotFd = null; |
| } |
| this.snapshotFd = this.openIfExists(this.snapshotPath); |
| } |
|
|
| reopenWal(): void { |
| if (this.walFd !== null) { |
| fs.closeSync(this.walFd); |
| this.walFd = null; |
| } |
| this.walFd = this.openIfExists(this.walPath); |
| } |
|
|
| reopenBoth(): void { |
| this.reopenSnapshot(); |
| this.reopenWal(); |
| } |
|
|
| close(): void { |
| if (this.snapshotFd !== null) { |
| fs.closeSync(this.snapshotFd); |
| this.snapshotFd = null; |
| } |
| if (this.walFd !== null) { |
| fs.closeSync(this.walFd); |
| this.walFd = null; |
| } |
| } |
| } |
|
|