UNPKG

lexicodec

Version:

A library for lexicographical encoding.

252 lines 9.23 kB
"use strict"; // This codec is should create a component-wise lexicographically sortable array. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.jsonCodec = exports.Codec = exports.MaxEncoding = exports.MinEncoding = exports.MAX = exports.MIN = exports.ObjectLegacyEncoding = exports.ObjectEncoding = exports.ArrayEncoding = exports.NumberEncoding = exports.StringEncoding = exports.BooleanEncoding = exports.NullEncoding = void 0; const elen = __importStar(require("elen")); const compare_1 = require("./compare"); exports.NullEncoding = { match: (value) => value === null, encode: () => "", decode: () => null, compare: () => 0, }; exports.BooleanEncoding = { match: (value) => value === true || value === false, encode: (value) => JSON.stringify(value), decode: (value) => JSON.parse(value), compare: compare_1.compare, }; exports.StringEncoding = { match: (value) => typeof value === "string", encode: (value) => value, decode: (value) => value, compare: compare_1.compare, }; exports.NumberEncoding = { match: (value) => typeof value === "number", encode: (value) => elen.encode(value), decode: (value) => elen.decode(value), compare: compare_1.compare, }; // Databases like LMDB avoid escaping null bytes by requiring all values // don't contain null bytes. But this means you can't have nested tuples. exports.ArrayEncoding = { match: (value) => Array.isArray(value), encode: (array, encode) => array .map((value, i) => { const encoded = encode(value); return (encoded // B -> BB or \ -> \\ .replace(/\x01/g, "\x01\x01") // A -> BA or x -> \x .replace(/\x00/g, "\x01\x00") + "\x00"); }) .join(""), decode: (value, decode) => { if (value === "") { return []; } // Capture all of the escaped BB and BA pairs and wait // til we find an exposed A. const re = /(\x01(\x01|\x00)|\x00)/g; const array = []; let start = 0; while (true) { const match = re.exec(value); if (match === null) { return array; } if (match[0][0] === "\x01") { // If we match a \x01\x01 or \x01\x00 then keep going. continue; } const end = match.index; const escaped = value.slice(start, end); const unescaped = escaped // BB -> B .replace(/\x01\x01/g, "\x01") // BA -> A .replace(/\x01\x00/g, "\x00"); const decoded = decode(unescaped); array.push(decoded); // Skip over the \x00. start = end + 1; } }, compare: (a, b, cmp) => { const len = Math.min(a.length, b.length); for (let i = 0; i < len; i++) { const dir = cmp(a[i], b[i]); if (dir !== 0) return dir; } return (0, compare_1.compare)(a.length, b.length); }, }; function flatten(array) { const result = []; for (const inner of array) { for (const item of inner) { result.push(item); } } return result; } function chunk(array, size) { const result = []; for (let i = 0; i < array.length; i += size) { const chunk = array.slice(i, i + size); result.push(chunk); } return result; } exports.ObjectEncoding = { match: (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype, encode: (value, encode) => { const entries = Object.entries(value).sort(([k1], [k2]) => (0, compare_1.compare)(k1, k2)); const items = flatten(entries); return exports.ArrayEncoding.encode(items, encode); }, decode: (value, decode) => { const items = exports.ArrayEncoding.decode(value, decode); const entries = chunk(items, 2); const obj = {}; for (const [key, value] of entries) { obj[key] = value; } return obj; }, compare: (a, b, cmp) => { const ae = Object.entries(a).sort(([k1], [k2]) => (0, compare_1.compare)(k1, k2)); const be = Object.entries(b).sort(([k1], [k2]) => (0, compare_1.compare)(k1, k2)); const len = Math.min(ae.length, be.length); for (let i = 0; i < len; i++) { const [ak, av] = ae[i]; const [bk, bv] = be[i]; const dir = (0, compare_1.compare)(ak, bk); if (dir !== 0) return dir; const dir2 = cmp(av, bv); if (dir2 !== 0) return dir2; } return (0, compare_1.compare)(ae.length, be.length); }, }; exports.ObjectLegacyEncoding = { match: (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype, encode: (value, encode) => { const entries = Object.entries(value).sort(([k1], [k2]) => (0, compare_1.compare)(k1, k2)); return exports.ArrayEncoding.encode(entries, encode); }, decode: (value, decode) => { const entries = exports.ArrayEncoding.decode(value, decode); const obj = {}; for (const [key, value] of entries) { obj[key] = value; } return obj; }, compare: exports.ObjectEncoding.compare, }; // MIN and MAX are useful for array prefix queries and stuff like that. exports.MIN = Symbol("min"); exports.MAX = Symbol("max"); exports.MinEncoding = { match: (value) => value === exports.MIN, encode: () => "", decode: () => exports.MIN, compare: () => 0, }; exports.MaxEncoding = { match: (value) => value === exports.MAX, encode: () => "", decode: () => exports.MAX, compare: () => 0, }; class Codec { constructor(encodings) { this.encodings = encodings; this.encode = (value) => { for (const [prefixByte, encoding] of Object.entries(this.encodings)) if (encoding.match(value)) return prefixByte + encoding.encode(value, this.encode); throw new Error(`Missing encoding for value: ${value}`); }; this.decode = (value) => { const prefix = value[0]; const rest = value.slice(1); for (const [prefixByte, encoding] of Object.entries(this.encodings)) if (prefixByte === prefix) return encoding.decode(rest, this.decode); throw new Error(`Missing encoding for value: ${value}`); }; this.compare = (a, b) => { if (a === b) return 0; let ae; let be; for (const [prefix, encoding] of Object.entries(this.encodings)) { if (!ae && encoding.match(a)) ae = [prefix, encoding]; if (!be && encoding.match(b)) be = [prefix, encoding]; if (ae && be) break; } if (!ae) throw new Error(`Missing encoding for value: ${a}`); if (!be) throw new Error(`Missing encoding for value: ${b}`); // Type prefix comparison. if (ae[0] !== be[0]) return (0, compare_1.compare)(ae[0], be[0]); // Value comparison. return ae[1].compare(a, b, this.compare); }; for (const prefixByte in encodings) if (prefixByte.length !== 1) throw new Error(`Encoding prefix is not 1 byte: ${prefixByte}`); } } exports.Codec = Codec; exports.jsonCodec = new Codec({ // Prefixes are based on legacy implementation. // MIN < null < object < array < number < string < boolean < MAX "\x00": exports.MinEncoding, b: exports.NullEncoding, c: exports.ObjectEncoding, d: exports.ArrayEncoding, e: exports.NumberEncoding, f: exports.StringEncoding, g: exports.BooleanEncoding, "\xff": exports.MaxEncoding, }); //# sourceMappingURL=codec.js.map