import fs from 'node:fs'; function DEFAULT_SERIALIZER_FACTORY() { const s = new Serializer(); //TODO - buffers and possibly typed arrays s.register_constructor(BigInt, 'Bi', (raw) => raw.toString(), (stored) => BigInt(stored)); s.register_constructor(Buffer, 'Bu', (raw) => raw.toString('base64'), (stored) => Buffer.from(stored, 'base64')); s.register_default('P', (raw) => (JSON.stringify(raw), raw), (stored) => stored); return s; } class Serializer { constructor(constructor_lut=new Map(), identity_lut=new Map(), type_lut=new Map()) { Object.assign(this, { constructor_lut, identity_lut, type_lut, default: null }); } serialize(value) { const { constructor_lut, identity_lut } = this; const by_identity = identity_lut.get(value); if (by_identity) { const { type_id, serialize_value } = by_identity; return [ type_id, serialize_value(value) ]; } const by_constructor = constructor_lut.get(value.constructor); if (by_constructor) { const { type_id, serialize_value } = by_constructor; return [ type_id, serialize_value(value) ]; } return [this.default.type_id, this.default.serialize_value(value)]; } deserialize(value) { if (value === undefined) { throw new Error(`Deserialize called without value`); } const [ type_id, raw_value ] = value; const { type_lut } = this; const operation = type_lut.get(type_id); if (!operation) { throw new Error(`Unknown type_id: ${type_id}`); } const { deserialize_value } = operation; return deserialize_value(raw_value); } register_default(type_id, serialize_value, deserialize_value) { const { type_lut } = this; const operation = { selector: 'default', type_id, serialize_value, deserialize_value }; type_lut.set(type_id, operation); this.default = operation; } register_constructor(constructor, type_id, serialize_value, deserialize_value) { const { constructor_lut, type_lut } = this; const operation = { selector: 'constructor', constructor, type_id, serialize_value, deserialize_value }; constructor_lut.set(constructor, operation); type_lut.set(type_id, operation); } register_identity(identity, type_id, serialize_value, deserialize_value) { const { identity_lut, type_lut } = this; const operation = { selector: 'identity', identity, type_id, serialize_value, deserialize_value }; identity_lut.set(identity, operation); type_lut.set(type_id, operation); } } const DEFAULT_SETTINGS = { auto_load: false, auto_store: false, auto_store_events: [ 'SIGINT', 'SIGTERM', 'exit', ], debounce_flush_timeout: null, } export class Simple_KeyValue_Store { #flush_debounce_timer = null constructor(storage_path, settings=DEFAULT_SETTINGS, data=new Map(), serializer=DEFAULT_SERIALIZER_FACTORY()) { const actual_settings = { ...DEFAULT_SETTINGS, ...settings }; Object.assign(this, { storage_path, data, serializer, ...actual_settings }); if (this.auto_load) { this.load(); } if (this.auto_store) { for (const event of this.auto_store_events) { process.on(event, () => (this.store(), process.exit()) ); } } } trigger_flush_debouncer() { if (this.#flush_debounce_timer) { clearTimeout(this.#flush_debounce_timer); this.#flush_debounce_timer = null; } const { debounce_flush_timeout } = this; if (debounce_flush_timeout) { this.#flush_debounce_timer = setTimeout(() => { this.#flush_debounce_timer = null; this.store(); }, debounce_flush_timeout); } } create_pending_filename() { const suffix = process.hrtime.bigint().toString(16); const { storage_path } = this; return `${storage_path}-${suffix}.tmp`; } get(key) { const { serializer } = this; const raw_entry = this.data.get(key); if (raw_entry !== undefined) { return serializer.deserialize(raw_entry); } } set(key, value) { const { serializer } = this; this.data.set(key, serializer.serialize(value)); this.trigger_flush_debouncer(); } delete(key) { const result = this.data.delete(key); if (result) { this.trigger_flush_debouncer(); } return result; } load() { const { data, storage_path } = this; if (!fs.existsSync(storage_path)) { return; } const file_contents = fs.readFileSync(storage_path, 'utf-8'); for (const line of file_contents.split('\n')) { if (!line) continue; const [key, value] = JSON.parse(line); data.set(key, value); } } store() { if (this.#flush_debounce_timer) { clearTimeout(this.#flush_debounce_timer); this.#flush_debounce_timer = null; } const { data, storage_path } = this; const pending_out_path = this.create_pending_filename(); const out_fd = fs.openSync(pending_out_path, 'w'); try { for (const [key, value] of data.entries()) { fs.writeSync(out_fd, JSON.stringify([key, value]) + '\n'); } fs.closeSync(out_fd); fs.renameSync(pending_out_path, storage_path); } finally { try { fs.closeSync(out_fd); } catch {} try { fs.unlinkSync(pending_out_path); } catch {} } } } /* const kvs = new Simple_KeyValue_Store('./data.ndjson', { auto_load: true, auto_store: true, debounce_flush_timeout: 10_000 }); console.log(kvs.get('hello'), kvs.get('shello'), kvs.get('Path')); kvs.set('hello', 123456789n) console.log(Buffer.from([123, 10, 20])); kvs.set('Path', Buffer.from([123, 10, 20])); process.exit() //Exit immediately instead of waiting for debounce_timer (this also prevents double store - we should document this properly) */