value-graph 0.1.0: JSON-free, bigint-free binary snapshot layer
- src/snapshot.js: compact portable wire (VGST store / VGGP graph), binary maps for params/pattern/objects (no JSON), buffer values (no JS bigint), CRC-32 guarded. - createFileStore: binary per-key files (encodeEntryBytes/decodeEntryBytes). - ValueGraph snapshot/snapshotFile/restore/loadFile; keys base64url(params-bytes). - 60 tests (node:test + rigor incl. 'flip ANY byte never decodes silently').
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @arbiter/value-graph
|
||||
*
|
||||
* The "second graph": a graph-structured, lazy, TTL + version-invalidated store
|
||||
* of derived values, built on @push-stream-std. Queries are duplex streams
|
||||
* (push `{ get }` up, receive values down); invalidation pushes `{ stale }`
|
||||
* triggers down the graph and cascades to dependents. Backing store is
|
||||
* swappable (in-memory default; `createFileStore` disk-backed for graphs
|
||||
* larger than memory).
|
||||
*
|
||||
* Persistence & transport: `ValueGraph.snapshot()` / `snapshotFile(path)` /
|
||||
* `ValueGraph.restore(buf)` / `ValueGraph.loadFile(path)` move a graph between
|
||||
* processes and machines as a single compact, CRC-guarded binary file — schema
|
||||
* metadata plus every entry (functions re-registered via `{ resolvers }`).
|
||||
* Stores expose `snapshotFile`/`restoreFile` (`createFileStore` also accepts
|
||||
* `{ loadFrom }`) for raw key->entry dumps.
|
||||
*/
|
||||
export { ValueGraph, createMapStore, createFileStore, owa, weightFor, validateValueType } from './value-graph.js';
|
||||
export {
|
||||
encodeSnapshot, decodeSnapshot, encodeGraphSnapshot, decodeGraphSnapshot,
|
||||
encodeEntryBytes, decodeEntryBytes, encodeParams, decodeParams, crc32
|
||||
} from './snapshot.js';
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* Compact, portable binary snapshots for @arbiter/value-graph.
|
||||
*
|
||||
* Two snapshot flavours share one value/entry wire format:
|
||||
*
|
||||
* STORE snapshot ('VGST') — an opaque dump of a backing store: key -> entry.
|
||||
* Transportable between stores/processes/machines; the working per-key
|
||||
* files of createFileStore are NOT portable, this is. Bounded-memory
|
||||
* writing (record at a time) so a larger-than-memory graph can be moved.
|
||||
*
|
||||
* GRAPH snapshot ('VGGP') — schema (relation metadata, minus functions) +
|
||||
* structured records { subject, relation, params, entry }. This is the
|
||||
* seamless artifact: ValueGraph.restore(buf) / ValueGraph.loadFile(path)
|
||||
* reconstruct a working graph (resolvers re-registered by the caller).
|
||||
*
|
||||
* DESIGN RULES (hard constraints, not preferences):
|
||||
* - NO JSON anywhere on the wire. params/pattern/objects are encoded as
|
||||
* deterministic BINARY MAPS (sorted keys, recursive value encoding) —
|
||||
* JSON.stringify/parse is banned (slow, no zero-copy).
|
||||
* - NO JS bigint. Large integers are Buffers/Uint8Array (tag 0x04, raw
|
||||
* bytes) — JS bigints are not a value-graph type.
|
||||
* - Everything is CRC-32 guarded and parse-guarded on load; a corrupt or
|
||||
* truncated snapshot throws, it never silently mis-reads.
|
||||
*
|
||||
* Wire format (all integers little-endian, varints LEB128):
|
||||
*
|
||||
* HEADER: MAGIC(4 ASCII) VERSION(u8=1) FLAGS(u8)
|
||||
* CREATED(f64 ms) [SCHEMA_COUNT(varint) ENTRY_COUNT(varint)]
|
||||
* SCHEMA record (graph snapshots only): REL(varstr) OPERATOR(varstr)
|
||||
* PARENTS(varint, varstr*) ATTR(varstr) PATTERN(value)
|
||||
* WEIGHTS(varint, f64*) PRIORITIES(varint, f64*) CAPSUM(u8)
|
||||
* HAS_TTL(u8[, f64]) RETURNTYPE(varstr) PARAMS(value)
|
||||
* HAS_VALIDATE(u8)
|
||||
* ENTRY record: KEY(varstr) [subject+relation+params for graph snapshots]
|
||||
* VALUE(v) UNIT(u8 tag + payload) AT(f64) SOURCE(varstr)
|
||||
* VERSION(varint) DEPS(varint, KEYVAR+VALUE*)
|
||||
* TRAILER: CRC32(u32) over every byte before it.
|
||||
*
|
||||
* VALUE(v): tag u8 + payload:
|
||||
* 0 null | 1 f64 | 2 string(varstr) | 3 bool(u8) | 4 buffer(bytes: varint
|
||||
* len + raw) | 5 array(varint, v*) | 6 interval(v lower, v upper) |
|
||||
* 7 object (binary map: varint count + sorted str-key + value per entry)
|
||||
*
|
||||
* Transportability: no paths, no machine-dependent floats beyond IEEE-754
|
||||
* doubles, big integers as raw bytes, no JSON parsing.
|
||||
*
|
||||
* This module is fs-free; the store / ValueGraph do the file I/O, streaming
|
||||
* record-at-a-time via the header/record encoders + snapshotCrc32().
|
||||
*/
|
||||
export const STORE_MAGIC = 'VGST';
|
||||
export const GRAPH_MAGIC = 'VGGP';
|
||||
export const SNAPSHOT_VERSION = 1;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CRC-32 (IEEE 802.3 / zlib polynomial) — integrity guard for every snapshot.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
t[i] = c >>> 0;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
|
||||
export function crc32(bytes) {
|
||||
let c = 0xFFFFFFFF;
|
||||
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incremental CRC-32 for streaming writes: `update(chunk)` per chunk, `digest()`
|
||||
* at the end. Keeps memory bounded for larger-than-memory snapshot files.
|
||||
*/
|
||||
export function snapshotCrc32() {
|
||||
let c = 0xFFFFFFFF;
|
||||
return {
|
||||
update(bytes) {
|
||||
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
||||
},
|
||||
digest() { return (c ^ 0xFFFFFFFF) >>> 0; }
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Writer / Reader — little-endian byte accumulation and parse.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Writer {
|
||||
constructor(cap = 1024) {
|
||||
this.buf = new Uint8Array(cap);
|
||||
this.len = 0;
|
||||
}
|
||||
|
||||
ensure(n) {
|
||||
if (this.len + n <= this.buf.length) return;
|
||||
let cap = this.buf.length * 2;
|
||||
while (cap < this.len + n) cap *= 2;
|
||||
const nb = new Uint8Array(cap);
|
||||
nb.set(this.buf.subarray(0, this.len));
|
||||
this.buf = nb;
|
||||
}
|
||||
|
||||
u8(v) { this.ensure(1); this.buf[this.len++] = v & 0xff; return this; }
|
||||
u32(v) {
|
||||
this.ensure(4);
|
||||
const dv = new DataView(this.buf.buffer);
|
||||
dv.setUint32(this.len, v, true);
|
||||
this.len += 4;
|
||||
return this;
|
||||
}
|
||||
f64(v) {
|
||||
this.ensure(8);
|
||||
const dv = new DataView(this.buf.buffer);
|
||||
dv.setFloat64(this.len, v, true);
|
||||
this.len += 8;
|
||||
return this;
|
||||
}
|
||||
varint(v) {
|
||||
let n = v >>> 0;
|
||||
while (n >= 0x80) { this.u8((n & 0x7f) | 0x80); n >>>= 7; }
|
||||
this.u8(n);
|
||||
return this;
|
||||
}
|
||||
bytes(b) {
|
||||
this.varint(b.length);
|
||||
this.ensure(b.length);
|
||||
this.buf.set(b, this.len);
|
||||
this.len += b.length;
|
||||
return this;
|
||||
}
|
||||
// Append already-encoded bytes verbatim (records/headers are self-describing).
|
||||
raw(b) {
|
||||
this.ensure(b.length);
|
||||
this.buf.set(b, this.len);
|
||||
this.len += b.length;
|
||||
return this;
|
||||
}
|
||||
str(s) { return this.bytes(Buffer.from(s, 'utf8')); }
|
||||
toBytes() { return this.buf.subarray(0, this.len); }
|
||||
}
|
||||
|
||||
class Reader {
|
||||
constructor(buf) {
|
||||
this.buf = buf;
|
||||
this.dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||
this.off = 0;
|
||||
}
|
||||
u8() { const v = this.dv.getUint8(this.off); this.off += 1; return v; }
|
||||
u32() { const v = this.dv.getUint32(this.off, true); this.off += 4; return v; }
|
||||
f64() { const v = this.dv.getFloat64(this.off, true); this.off += 8; return v; }
|
||||
varint() {
|
||||
let shift = 0;
|
||||
let v = 0;
|
||||
for (;;) {
|
||||
const b = this.u8();
|
||||
v |= (b & 0x7f) << shift;
|
||||
if (!(b & 0x80)) return v >>> 0;
|
||||
shift += 7;
|
||||
if (shift > 28) throw new Error('value-graph: snapshot malformed (varint overflow)');
|
||||
}
|
||||
}
|
||||
take(n) {
|
||||
if (n > this.remaining()) throw new Error('value-graph: snapshot corrupt (length past end of buffer)');
|
||||
const v = this.buf.subarray(this.off, this.off + n);
|
||||
this.off += n;
|
||||
return v;
|
||||
}
|
||||
bytes() { return this.take(this.varint()); }
|
||||
str() { return Buffer.from(this.bytes()).toString('utf8'); }
|
||||
remaining() { return this.buf.length - this.off; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Binary map + params — the JSON-free encoding for objects (params, pattern,
|
||||
// nested object values). Keys are sorted by UTF-8 so identical objects always
|
||||
// encode to identical bytes (determinism: same params → same store key).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function encodeMap(obj, w) {
|
||||
const keys = Object.keys(obj).sort();
|
||||
w.varint(keys.length);
|
||||
for (const k of keys) { w.str(k); encodeValue(obj[k], w); }
|
||||
}
|
||||
|
||||
function decodeMap(r) {
|
||||
const count = r.varint();
|
||||
if (count > r.remaining()) throw new Error('value-graph: snapshot corrupt (map count past end of buffer)');
|
||||
const o = {};
|
||||
for (let i = 0; i < count; i++) o[r.str()] = decodeValue(r);
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a params object to bytes for use as the key tail (JSON-free) — the
|
||||
* same bytes every call for the same params, so the store key is stable.
|
||||
*/
|
||||
export function encodeParams(params) {
|
||||
const w = new Writer();
|
||||
encodeValue(params ?? {}, w);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
/** Reverse of encodeParams — decodes the key tail back to a params object. */
|
||||
export function decodeParams(bytes) {
|
||||
const r = new Reader(bytes);
|
||||
const v = decodeValue(r);
|
||||
return (v === null || v === undefined) ? {} : v;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Value encoding (recursive) — the stored `value` payload of every entry.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function encodeValue(v, w) {
|
||||
if (v === null || v === undefined) { w.u8(0); return; }
|
||||
const t = typeof v;
|
||||
if (t === 'number') { w.u8(1); w.f64(v); return; }
|
||||
if (t === 'string') { w.u8(2); w.str(v); return; }
|
||||
if (t === 'boolean') { w.u8(3); w.u8(v ? 1 : 0); return; }
|
||||
if (v instanceof Uint8Array) { w.u8(4); w.bytes(v); return; }
|
||||
if (Array.isArray(v)) {
|
||||
w.u8(5); w.varint(v.length);
|
||||
for (const x of v) encodeValue(x, w);
|
||||
return;
|
||||
}
|
||||
if (typeof v === 'object' && 'lower' in v && 'upper' in v) {
|
||||
w.u8(6);
|
||||
encodeValue(v.lower, w);
|
||||
encodeValue(v.upper, w);
|
||||
return;
|
||||
}
|
||||
// Plain object (params, pattern, nested objects): deterministic binary map.
|
||||
w.u8(7);
|
||||
encodeMap(v, w);
|
||||
}
|
||||
|
||||
export function decodeValue(r) {
|
||||
const tag = r.u8();
|
||||
switch (tag) {
|
||||
case 0: return null;
|
||||
case 1: return r.f64();
|
||||
case 2: return r.str();
|
||||
case 3: return r.u8() === 1;
|
||||
case 4: return r.bytes(); // Uint8Array view (zero-copy, no JSON, no bigint)
|
||||
case 5: {
|
||||
const n = r.varint();
|
||||
if (n > r.remaining()) throw new Error('value-graph: snapshot corrupt (array count past end of buffer)');
|
||||
const a = new Array(n);
|
||||
for (let i = 0; i < n; i++) a[i] = decodeValue(r);
|
||||
return a;
|
||||
}
|
||||
case 6: {
|
||||
const lower = decodeValue(r);
|
||||
const upper = decodeValue(r);
|
||||
return { lower, upper };
|
||||
}
|
||||
case 7: return decodeMap(r);
|
||||
default: throw new Error(`value-graph: snapshot unknown value tag ${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeUnit(unit, w) {
|
||||
if (unit === null || unit === undefined) { w.u8(0); return; }
|
||||
if (typeof unit === 'number') { w.u8(1); w.f64(unit); return; }
|
||||
w.u8(2); w.str(String(unit));
|
||||
}
|
||||
|
||||
function decodeUnit(r) {
|
||||
const t = r.u8();
|
||||
if (t === 0) return null;
|
||||
if (t === 1) return r.f64();
|
||||
return r.str();
|
||||
}
|
||||
|
||||
// Entry payload shared by both snapshot flavours: the value, its unit, the
|
||||
// wall-clock it was computed at, provenance, version stamp and deps metadata.
|
||||
function encodeEntryPayload(entry, w) {
|
||||
encodeValue(entry.value, w);
|
||||
encodeUnit(entry.unit, w);
|
||||
w.f64(typeof entry.at === 'number' ? entry.at : 0);
|
||||
w.str(entry.source || '');
|
||||
w.varint(typeof entry.version === 'number' ? entry.version : 0);
|
||||
const deps = entry.deps || {};
|
||||
const dk = Object.keys(deps).sort();
|
||||
w.varint(dk.length);
|
||||
for (const k of dk) { w.str(k); encodeValue(deps[k], w); }
|
||||
}
|
||||
|
||||
function decodeEntryPayload(r) {
|
||||
const value = decodeValue(r);
|
||||
const unit = decodeUnit(r);
|
||||
const at = r.f64();
|
||||
const source = r.str();
|
||||
const version = r.varint();
|
||||
const depsCount = r.varint();
|
||||
if (depsCount > r.remaining()) throw new Error('value-graph: snapshot corrupt (deps count past end of buffer)');
|
||||
const deps = {};
|
||||
for (let i = 0; i < depsCount; i++) deps[r.str()] = decodeValue(r);
|
||||
return { value, unit, at, source, version, deps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode JUST the entry payload (no key) to bytes — the on-disk representation
|
||||
* for createFileStore's binary per-key files (JSON-free).
|
||||
*/
|
||||
export function encodeEntryBytes(entry) {
|
||||
const w = new Writer();
|
||||
encodeEntryPayload(entry, w);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
/** Reverse of encodeEntryBytes — decode a bare entry payload. */
|
||||
export function decodeEntryBytes(buffer) {
|
||||
const r = new Reader(buffer);
|
||||
return decodeEntryPayload(r);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Headers + record encoders — shared by the in-memory encoders and the
|
||||
// streaming file writers in the store / ValueGraph.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function storeHeader(entryCount, { createdAt } = {}) {
|
||||
const w = new Writer();
|
||||
w.raw(Buffer.from(STORE_MAGIC, 'ascii'));
|
||||
w.u8(SNAPSHOT_VERSION);
|
||||
w.u8(0); // flags (unused)
|
||||
w.f64(typeof createdAt === 'number' ? createdAt : Date.now());
|
||||
w.varint(entryCount);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
export function graphHeader(schemaCount, entryCount, { createdAt } = {}) {
|
||||
const w = new Writer();
|
||||
w.raw(Buffer.from(GRAPH_MAGIC, 'ascii'));
|
||||
w.u8(SNAPSHOT_VERSION);
|
||||
w.u8(0); // flags (unused)
|
||||
w.f64(typeof createdAt === 'number' ? createdAt : Date.now());
|
||||
w.varint(schemaCount);
|
||||
w.varint(entryCount);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
export function encodeStoreRecord(key, entry) {
|
||||
const w = new Writer();
|
||||
w.str(key);
|
||||
encodeEntryPayload(entry, w);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
export function encodeSchemaRecord(rel, meta) {
|
||||
const w = new Writer();
|
||||
w.str(rel);
|
||||
w.str(meta.operator || '');
|
||||
const parents = meta.parents ? [...meta.parents] : [];
|
||||
w.varint(parents.length);
|
||||
for (const p of parents) w.str(p);
|
||||
w.str(meta.attribute || '');
|
||||
encodeValue(meta.pattern ?? null, w);
|
||||
const weights = meta.weights || [];
|
||||
w.varint(weights.length);
|
||||
for (const x of weights) w.f64(x);
|
||||
const priorities = meta.priorities || [];
|
||||
w.varint(priorities.length);
|
||||
for (const x of priorities) w.f64(x);
|
||||
w.u8(meta.capSum ? 1 : 0);
|
||||
w.u8(meta.ttl === undefined || meta.ttl === null ? 0 : 1);
|
||||
if (meta.ttl !== undefined && meta.ttl !== null) w.f64(meta.ttl);
|
||||
w.str(meta.returnType || '');
|
||||
encodeValue(meta.params ?? null, w);
|
||||
w.u8(meta.validate ? 1 : 0);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
export function encodeGraphRecord({ subject, relation, params, entry }) {
|
||||
const w = new Writer();
|
||||
w.str(subject);
|
||||
w.str(relation);
|
||||
encodeValue(params ?? {}, w);
|
||||
encodeEntryPayload(entry, w);
|
||||
return w.toBytes();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// In-memory encoders (whole snapshot as one Buffer) and decoders (with
|
||||
// integrity verification). File streaming lives in the store / ValueGraph.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function encodeSnapshot(entries, { createdAt } = {}) {
|
||||
const list = [...entries];
|
||||
const w = new Writer(4096);
|
||||
w.raw(storeHeader(list.length, { createdAt }));
|
||||
for (const [key, entry] of list) w.raw(encodeStoreRecord(key, entry));
|
||||
const body = w.toBytes();
|
||||
const tail = Buffer.alloc(4);
|
||||
tail.writeUInt32LE(crc32(body));
|
||||
return Buffer.concat([body, tail]);
|
||||
}
|
||||
|
||||
export function encodeGraphSnapshot({ schema = [], records = [] } = {}, { createdAt } = {}) {
|
||||
const slist = [...schema];
|
||||
const rlist = [...records];
|
||||
const w = new Writer(4096);
|
||||
w.raw(graphHeader(slist.length, rlist.length, { createdAt }));
|
||||
for (const [rel, meta] of slist) w.raw(encodeSchemaRecord(rel, meta));
|
||||
for (const rec of rlist) w.raw(encodeGraphRecord(rec));
|
||||
const body = w.toBytes();
|
||||
const tail = Buffer.alloc(4);
|
||||
tail.writeUInt32LE(crc32(body));
|
||||
return Buffer.concat([body, tail]);
|
||||
}
|
||||
|
||||
function verifyCrc(buffer, r) {
|
||||
if (r.remaining() !== 4) throw new Error('value-graph: snapshot truncated (missing CRC)');
|
||||
const stored = r.u32();
|
||||
const actual = crc32(buffer.subarray(0, buffer.length - 4));
|
||||
if (stored !== actual) throw new Error('value-graph: snapshot CRC mismatch (corrupt or truncated)');
|
||||
}
|
||||
|
||||
function checkMagic(r, expected) {
|
||||
const magic = Buffer.from(r.take(4)).toString('ascii');
|
||||
if (magic !== expected) {
|
||||
throw new Error(`value-graph: not a value-graph snapshot (magic '${magic}')`);
|
||||
}
|
||||
const version = r.u8();
|
||||
if (version !== SNAPSHOT_VERSION) {
|
||||
throw new Error(`value-graph: unsupported snapshot version ${version} (this build reads v${SNAPSHOT_VERSION})`);
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeSnapshot(buffer) {
|
||||
const r = new Reader(buffer);
|
||||
checkMagic(r, STORE_MAGIC);
|
||||
return decodeBody(buffer, r, () => {
|
||||
r.u8(); // flags
|
||||
const createdAt = r.f64();
|
||||
const entryCount = r.varint();
|
||||
if (entryCount > r.remaining()) throw new Error('value-graph: snapshot corrupt (entry count past end of buffer)');
|
||||
const entries = new Array(entryCount);
|
||||
for (let i = 0; i < entryCount; i++) entries[i] = decodeStoreRecord(r);
|
||||
verifyCrc(buffer, r);
|
||||
return { createdAt, entries };
|
||||
});
|
||||
}
|
||||
|
||||
export function decodeGraphSnapshot(buffer) {
|
||||
const r = new Reader(buffer);
|
||||
checkMagic(r, GRAPH_MAGIC);
|
||||
return decodeBody(buffer, r, () => {
|
||||
r.u8(); // flags
|
||||
const createdAt = r.f64();
|
||||
const schemaCount = r.varint();
|
||||
const entryCount = r.varint();
|
||||
if (schemaCount > r.remaining()) throw new Error('value-graph: snapshot corrupt (schema count past end of buffer)');
|
||||
const schema = new Array(schemaCount);
|
||||
for (let i = 0; i < schemaCount; i++) schema[i] = [r.str(), decodeSchema(r)];
|
||||
if (entryCount > r.remaining()) throw new Error('value-graph: snapshot corrupt (entry count past end of buffer)');
|
||||
const records = new Array(entryCount);
|
||||
for (let i = 0; i < entryCount; i++) records[i] = decodeGraphRecord(r);
|
||||
verifyCrc(buffer, r);
|
||||
return { createdAt, schema, records };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A malformed record (corrupt bytes, truncated file, bad version) throws a
|
||||
* coherent `value-graph: snapshot corrupt (...)` error rather than leaking a
|
||||
* low-level RangeError — snapshots are CRC-guarded AND parse-guarded. Already-
|
||||
* prefixed errors (magic/version/CRC) pass through unchanged.
|
||||
*/
|
||||
function decodeBody(buffer, r, bodyFn) {
|
||||
try {
|
||||
return bodyFn();
|
||||
} catch (e) {
|
||||
if (e && typeof e.message === 'string' && e.message.startsWith('value-graph: snapshot')) throw e;
|
||||
throw new Error(`value-graph: snapshot corrupt (${e && e.message ? e.message : e})`);
|
||||
}
|
||||
}
|
||||
|
||||
function decodeStoreRecord(r) {
|
||||
const key = r.str();
|
||||
return [key, decodeEntryPayload(r)];
|
||||
}
|
||||
|
||||
function decodeGraphRecord(r) {
|
||||
const subject = r.str();
|
||||
const relation = r.str();
|
||||
const params = decodeValue(r);
|
||||
const entry = decodeEntryPayload(r);
|
||||
return { subject, relation, params: (params === null || params === undefined) ? {} : params, entry };
|
||||
}
|
||||
|
||||
function decodeSchema(r) {
|
||||
const operator = r.str();
|
||||
const parentCount = r.varint();
|
||||
if (parentCount > r.remaining()) throw new Error('value-graph: snapshot corrupt (parent count past end of buffer)');
|
||||
const parents = new Array(parentCount);
|
||||
for (let i = 0; i < parentCount; i++) parents[i] = r.str();
|
||||
const attribute = r.str() || null;
|
||||
const pattern = decodeValue(r);
|
||||
const weightCount = r.varint();
|
||||
if (weightCount > r.remaining() / 8) throw new Error('value-graph: snapshot corrupt (weights count past end of buffer)');
|
||||
const weights = new Array(weightCount);
|
||||
for (let i = 0; i < weightCount; i++) weights[i] = r.f64();
|
||||
const priorityCount = r.varint();
|
||||
if (priorityCount > r.remaining() / 8) throw new Error('value-graph: snapshot corrupt (priorities count past end of buffer)');
|
||||
const priorities = new Array(priorityCount);
|
||||
for (let i = 0; i < priorityCount; i++) priorities[i] = r.f64();
|
||||
const capSum = r.u8() === 1;
|
||||
const hasTtl = r.u8() === 1;
|
||||
const ttl = hasTtl ? r.f64() : undefined;
|
||||
const returnType = r.str() || null;
|
||||
const params = decodeValue(r);
|
||||
const hasValidate = r.u8() === 1;
|
||||
return {
|
||||
operator,
|
||||
parents,
|
||||
attribute,
|
||||
pattern: (pattern === undefined) ? null : pattern,
|
||||
weights,
|
||||
priorities,
|
||||
capSum,
|
||||
ttl,
|
||||
returnType,
|
||||
params: (params === null || params === undefined) ? null : params,
|
||||
hasValidate
|
||||
};
|
||||
}
|
||||
+1152
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user