KV-store backed Express 5 app for tracking electronic components, their arbitrary fields, and inventory locations (physical, BOM, digital). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
5.3 KiB
JavaScript
201 lines
5.3 KiB
JavaScript
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)
|
|
*/ |