| |
| |
| |
| |
| |
| |
|
|
| import fs from 'node:fs/promises'; |
| import path from 'node:path'; |
| import { fsyncDir } from './compaction.js'; |
| import { SNAPSHOT_FILE, WAL_FILE } from './generation.js'; |
| import type { RangeOptions } from './skiplist.js'; |
| import type { ValueMode } from './recovery.js'; |
| import type { ValueCodec, ValueCodecName, ValueModeSetting } from './types.js'; |
|
|
| export const BUFFER: ValueCodec<Buffer> = { |
| encode: (v) => { |
| if (!Buffer.isBuffer(v)) throw new TypeError('value must be a Buffer (use valueCodec: "string" or "json")'); |
| return v; |
| }, |
| |
| |
| decode: (b) => Buffer.from(b), |
| }; |
| export const STRING: ValueCodec<string> = { |
| encode: (v) => Buffer.from(String(v), 'utf8'), |
| decode: (b) => b.toString('utf8'), |
| }; |
| export const JSON_CODEC: ValueCodec<unknown> = { |
| encode: (v) => Buffer.from(JSON.stringify(v), 'utf8'), |
| decode: (b) => JSON.parse(b.toString('utf8')), |
| }; |
| export const CODECS: Record<ValueCodecName, ValueCodec<unknown>> = { buffer: BUFFER, string: STRING, json: JSON_CODEC }; |
| export const MAX_KEY_LEN = 128; |
|
|
| export function toBuf(key: string | Buffer): Buffer { |
| return Buffer.isBuffer(key) ? key : Buffer.from(String(key), 'utf8'); |
| } |
| |
| |
| |
| |
| |
| |
| |
| export function toKStr(key: string | Buffer): string { |
| return typeof key === 'string' ? Buffer.from(key, 'utf8').toString('binary') : key.toString('binary'); |
| } |
| |
| |
| export function fromKStr(k: string): string { |
| return Buffer.from(k, 'binary').toString('utf8'); |
| } |
| |
| |
| export function canonRange(opts: RangeOptions<string>): RangeOptions<string> { |
| const out: RangeOptions<string> = { ...opts }; |
| if (out.gte !== undefined) out.gte = toKStr(out.gte); |
| if (out.gt !== undefined) out.gt = toKStr(out.gt); |
| if (out.lte !== undefined) out.lte = toKStr(out.lte); |
| if (out.lt !== undefined) out.lt = toKStr(out.lt); |
| return out; |
| } |
|
|
| export function normDt(dt?: Record<string, number | string> | null): Record<string, number> | null { |
| if (!dt) return null; |
| const out: Record<string, number> = {}; |
| for (const [k, v] of Object.entries(dt)) { |
| const ms = typeof v === 'number' ? v : Date.parse(v); |
| if (Number.isFinite(ms)) out[k] = ms; |
| } |
| return Object.keys(out).length ? out : null; |
| } |
|
|
| export async function fileSize(file: string): Promise<number> { |
| try { |
| return (await fs.stat(file)).size; |
| } catch (e) { |
| if ((e as NodeJS.ErrnoException).code === 'ENOENT') return 0; |
| throw e; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| let sidecarTmpSeq = 0; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function writeFileAtomic( |
| file: string, |
| data: string, |
| opts: { stats?: { dirFsyncUnsupported?: boolean } } = {}, |
| ): Promise<void> { |
| const tmp = `${file}.tmp-${process.pid}-${++sidecarTmpSeq}`; |
| try { |
| await fs.writeFile(tmp, data, 'utf8'); |
| await fs.rename(tmp, file); |
| } finally { |
| |
| |
| await fs.rm(tmp, { force: true }).catch(() => {}); |
| } |
| await fsyncDir(path.dirname(file), { strict: true, stats: opts.stats }); |
| } |
|
|
| export async function resolveValueMode(mode: ValueModeSetting, dir: string, maxMemoryBytes: number | null): Promise<ValueMode> { |
| if (mode !== 'auto') return mode; |
| if (maxMemoryBytes === null) return 'memory'; |
| const total = (await fileSize(path.join(dir, SNAPSHOT_FILE))) + (await fileSize(path.join(dir, WAL_FILE))); |
| return total > maxMemoryBytes ? 'disk' : 'memory'; |
| } |
|
|