lexicodec
Version:
A library for lexicographical encoding.
252 lines • 9.23 kB
JavaScript
;
// 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