423 lines
12 KiB
JavaScript
423 lines
12 KiB
JavaScript
"use strict";
|
|
// Preserves Values.
|
|
// Uses Immutable.js for many things; adds immutable values of its own for the rest.
|
|
|
|
if (require('./singletonmodule.js')('leastfixedpoint.com/preserves',
|
|
require('../package.json').version,
|
|
'values.js',
|
|
module)) return;
|
|
|
|
const util = require('util');
|
|
|
|
const Immutable = require('immutable');
|
|
const { List, Map, Set, is, hash } = Immutable;
|
|
|
|
const { PreserveOn, AsPreserve } = require('./symbols.js');
|
|
|
|
function _decode(bs) {
|
|
return Buffer.from(bs.buffer, bs.byteOffset, bs.byteLength).toString();
|
|
}
|
|
|
|
const encoder = (typeof TextEncoder === 'undefined') ? { encode: Buffer.from } : new TextEncoder();
|
|
const decoder = (typeof TextDecoder === 'undefined') ? { decode: _decode } : new TextDecoder();
|
|
|
|
function fromJS(x) {
|
|
switch (typeof x) {
|
|
case 'number':
|
|
if (!Number.isInteger(x)) {
|
|
// We require that clients be explicit about integer vs. non-integer types.
|
|
throw new TypeError("Refusing to autoconvert non-integer number to Single or Double");
|
|
}
|
|
// FALL THROUGH
|
|
case 'string':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return x;
|
|
case 'undefined':
|
|
throw new TypeError("Cannot represent JavaScript undefined value as Preserves");
|
|
case 'function':
|
|
// We are relaxed about these, for now, even though Preserves can't serialize them.
|
|
return x;
|
|
case 'object':
|
|
if (x === null) {
|
|
// We are relaxed about these, for now, even though null is
|
|
// strictly speaking not a Preserves Value.
|
|
return x;
|
|
}
|
|
if (typeof x[AsPreserve] === 'function') {
|
|
return x[AsPreserve]();
|
|
}
|
|
if (Array.isArray(x)) {
|
|
return List().withMutations((l) => {
|
|
for (let i = 0; i < x.length; i++) {
|
|
l.push(fromJS(x[i]));
|
|
}
|
|
});
|
|
}
|
|
if (ArrayBuffer.isView(x) || x instanceof ArrayBuffer) {
|
|
return Bytes(x);
|
|
}
|
|
return Immutable.fromJS(x);
|
|
}
|
|
}
|
|
|
|
function _Float(value) {
|
|
Object.defineProperty(this, 'value', {
|
|
value: value,
|
|
writable: false,
|
|
enumerable: true
|
|
});
|
|
}
|
|
|
|
function Float(value) {
|
|
if (!(this instanceof Float)) return new Float(value);
|
|
_Float.call(this, value);
|
|
}
|
|
|
|
Float.prototype.toString = function () {
|
|
return '' + this.value;
|
|
};
|
|
|
|
Float.prototype.hashCode = function () {
|
|
return this.value | 0; // TODO: something better?
|
|
};
|
|
|
|
Float.unwrap = function (v) {
|
|
if (typeof v === 'number') return v;
|
|
if (v instanceof Float) return v.value;
|
|
{
|
|
const e = new TypeError("Float.unwrap failed");
|
|
e.irritant = v;
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
function Single(value) {
|
|
if (!(this instanceof Single)) return new Single(value);
|
|
_Float.call(this, value);
|
|
}
|
|
Single.prototype = Float(NaN);
|
|
|
|
Single.prototype.equals = function (other) {
|
|
return (other instanceof Single) && (other.value === this.value);
|
|
};
|
|
|
|
Single.prototype[PreserveOn] = function (encoder) {
|
|
encoder.leadbyte(0, 0, 2);
|
|
encoder.makeroom(4);
|
|
encoder.view.setFloat32(encoder.index, this.value, false);
|
|
encoder.index += 4;
|
|
};
|
|
|
|
function Double(value) {
|
|
if (!(this instanceof Double)) return new Double(value);
|
|
_Float.call(this, value);
|
|
}
|
|
Double.prototype = Float(NaN);
|
|
|
|
Double.prototype.equals = function (other) {
|
|
return (other instanceof Double) && (other.value === this.value);
|
|
};
|
|
|
|
Double.prototype[PreserveOn] = function (encoder) {
|
|
encoder.leadbyte(0, 0, 3);
|
|
encoder.makeroom(8);
|
|
encoder.view.setFloat64(encoder.index, this.value, false);
|
|
encoder.index += 8;
|
|
};
|
|
|
|
function Bytes(maybeByteIterable) {
|
|
if (!(this instanceof Bytes)) return new Bytes(maybeByteIterable);
|
|
if (maybeByteIterable === void 0) {
|
|
_installView.call(this, new Uint8Array());
|
|
} else if (ArrayBuffer.isView(maybeByteIterable)) {
|
|
_installView.call(this, new Uint8Array(maybeByteIterable.buffer,
|
|
maybeByteIterable.byteOffset,
|
|
maybeByteIterable.byteLength));
|
|
} else if (maybeByteIterable instanceof ArrayBuffer) {
|
|
_installView.call(this, new Uint8Array(maybeByteIterable.slice()));
|
|
} else if (typeof maybeByteIterable === 'string') {
|
|
_installView.call(this, encoder.encode(maybeByteIterable));
|
|
} else if (typeof maybeByteIterable === 'number') {
|
|
_installView.call(this, new Uint8Array(maybeByteIterable));
|
|
} else if (typeof maybeByteIterable.length === 'number') {
|
|
_installView.call(this, Uint8Array.from(maybeByteIterable));
|
|
} else if (maybeByteIterable instanceof Bytes) {
|
|
_installView.call(this, maybeByteIterable._view);
|
|
} else if (typeof maybeByteIterable.size === 'number') {
|
|
_installView.call(this, new Uint8Array(maybeByteIterable.size));
|
|
for (let i = 0; i < this.size; i++) { this._view[i] = maybeByteIterable.get(i); }
|
|
} else {
|
|
const e = new TypeError("Attempt to initialize Bytes from unsupported value");
|
|
e.irritant = maybeByteIterable;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function _installView(view) {
|
|
Object.defineProperty(this, '_view', { value: view, writable: false });
|
|
Object.defineProperty(this, 'size', { value: view.length, writable: false, enumerable: true });
|
|
}
|
|
|
|
Bytes.from = Bytes;
|
|
Bytes.of = function (...args) { return Bytes(Uint8Array.of(...args)); };
|
|
|
|
function unhexDigit(asciiCode) {
|
|
if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
|
|
if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
|
|
if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
|
|
throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
|
|
}
|
|
|
|
Bytes.fromHex = function (s) {
|
|
if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
|
|
const len = s.length >> 1;
|
|
const result = Bytes(len);
|
|
for (let i = 0; i < len; i++) {
|
|
result._view[i] =
|
|
(unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
|
|
}
|
|
return result;
|
|
};
|
|
|
|
Bytes.fromIO = function (io) {
|
|
if (typeof io === 'string') return io;
|
|
if (io instanceof Bytes) return io;
|
|
if (io instanceof Uint8Array) return Bytes.from(io);
|
|
{
|
|
const e = new TypeError("Bytes.fromIO: unsupported value");
|
|
e.irritant = io;
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
Bytes.toIO = function (b) {
|
|
if (typeof b === 'string') return b;
|
|
if (b instanceof Bytes) return b._view;
|
|
if (b instanceof Uint8Array) return b;
|
|
{
|
|
const e = new TypeError("Bytes.toIO: unsupported value");
|
|
e.irritant = b;
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
function underlying(b) {
|
|
return b._view || b;
|
|
}
|
|
|
|
Bytes.concat = function (bss) {
|
|
let len = 0;
|
|
for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }
|
|
|
|
const result = Bytes(len);
|
|
let index = 0;
|
|
for (let i = 0; i < bss.length; i++) {
|
|
const bs = underlying(bss[i]);
|
|
result._view.set(bs, index);
|
|
index += bs.length;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
Bytes.prototype.get = function (index) {
|
|
return this._view[index];
|
|
};
|
|
|
|
Bytes.prototype.equals = function (other) {
|
|
if (!(other instanceof Bytes)) return false;
|
|
if (other.size !== this.size) return false;
|
|
const va = this._view;
|
|
const vb = other._view;
|
|
for (let i = 0; i < va.length; i++) {
|
|
if (va[i] !== vb[i]) return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
Bytes.prototype.hashCode = function () {
|
|
// Immutable.js uses this function for strings.
|
|
const v = this._view;
|
|
let hash = 0;
|
|
for (let i = 0; i < v.length; i++) {
|
|
hash = ((31 * hash) + v[i]) | 0;
|
|
}
|
|
return hash;
|
|
};
|
|
|
|
Bytes.decodeUtf8 = function (bs) {
|
|
return decoder.decode(underlying(bs));
|
|
};
|
|
|
|
Bytes.prototype.fromUtf8 = function () {
|
|
return decoder.decode(this._view);
|
|
};
|
|
|
|
Bytes.prototype.toString = function () {
|
|
return '#"' + this.__asciify() + '"';
|
|
};
|
|
|
|
if (util.inspect) {
|
|
Bytes.prototype[util.inspect.custom] = function (depth, options) {
|
|
return this.toString();
|
|
};
|
|
}
|
|
|
|
Bytes.prototype.__asciify = function () {
|
|
const pieces = [];
|
|
const v = this._view;
|
|
for (let i = 0; i < v.length; i++) {
|
|
const b = v[i];
|
|
if (b === 92 || b === 34) {
|
|
pieces.push('\\' + String.fromCharCode(b));
|
|
} else if (b >= 32 && b <= 126) {
|
|
pieces.push(String.fromCharCode(b));
|
|
} else {
|
|
pieces.push('\\x' + hexDigit(b >> 4) + hexDigit(b & 15));
|
|
}
|
|
}
|
|
return pieces.join('');
|
|
};
|
|
|
|
function hexDigit(n) { return '0123456789abcdef'[n]; }
|
|
|
|
Bytes.prototype.toHex = function () {
|
|
var nibbles = [];
|
|
for (let i = 0; i < this.size; i++) {
|
|
nibbles.push(hexDigit(this._view[i] >> 4));
|
|
nibbles.push(hexDigit(this._view[i] & 15));
|
|
}
|
|
return nibbles.join('');
|
|
};
|
|
|
|
Bytes.prototype[PreserveOn] = function (encoder) {
|
|
encoder.header(1, 2, this.size);
|
|
encoder.emitbytes(this._view);
|
|
};
|
|
|
|
// Uint8Array / TypedArray methods
|
|
(function () {
|
|
for (const k of `entries every find findIndex forEach includes indexOf join
|
|
keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/))
|
|
{
|
|
Bytes.prototype[k] = function (...args) { return this._view[k](...args); };
|
|
}
|
|
|
|
for (const k of `filter map slice subarray`.split(/\s+/))
|
|
{
|
|
Bytes.prototype[k] = function (...args) { return Bytes(this._view[k](...args)); };
|
|
}
|
|
|
|
for (const k of `reverse sort`.split(/\s+/))
|
|
{
|
|
Bytes.prototype[k] = function (...args) { return Bytes(this._view.slice()[k](...args)); };
|
|
}
|
|
|
|
Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); };
|
|
})();
|
|
|
|
function Record(label, fields) {
|
|
if (!(this instanceof Record)) {
|
|
throw new TypeError("Class constructor Record cannot be invoked without 'new'");
|
|
}
|
|
Object.defineProperty(this, 'label', { value: fromJS(label), writable: false, enumerable: true });
|
|
Object.defineProperty(this, 'fields', { value: fromJS(fields), writable: false, enumerable: true });
|
|
}
|
|
|
|
Record.prototype.equals = function (other) {
|
|
return (other instanceof Record) &&
|
|
is(this.label, other.label) &&
|
|
is(this.fields, other.fields);
|
|
};
|
|
|
|
Record.prototype.hashCode = function () {
|
|
return Immutable.List([this.label, this.fields]).hashCode();
|
|
};
|
|
|
|
Record.prototype.get = function (index, defaultValue) {
|
|
return this.fields.get(index, defaultValue);
|
|
};
|
|
|
|
Record.prototype.set = function (index, newValue) {
|
|
return new Record(this.label, this.fields.set(index, newValue));
|
|
};
|
|
|
|
Record.prototype[PreserveOn] = function (encoder) {
|
|
encoder.header(2, 0, this.fields.size + 1);
|
|
encoder.push(this.label);
|
|
for (const field of this.fields) { encoder.push(field); }
|
|
};
|
|
|
|
Record.prototype.getConstructorInfo = function () {
|
|
return new RecordConstructorInfo(this.label, this.fields.size);
|
|
};
|
|
|
|
Record.prototype.toString = function () {
|
|
return this.label.toString().replace(/^Symbol\((.*)\)$/, '$1') +
|
|
'(' + this.fields.map((f) => {
|
|
try {
|
|
return "" + f;
|
|
} catch (e) {
|
|
return (util.inspect || ((f) => '<unprintable_preserves_field_value>'))(f);
|
|
}
|
|
}).join(', ') + ')';
|
|
};
|
|
|
|
Record.makeConstructor = function (labelSymbolText, fieldNames) {
|
|
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
|
|
};
|
|
|
|
Record.makeBasicConstructor = function (label, fieldNames) {
|
|
const arity = fieldNames.length;
|
|
const ctor = (...fields) => {
|
|
if (fields.length !== arity) {
|
|
throw new Error("Record: cannot instantiate " + (label && label.toString()) +
|
|
" expecting " + arity + " fields with " + fields.length + " fields");
|
|
}
|
|
return new Record(label, fields);
|
|
};
|
|
ctor.constructorInfo = new RecordConstructorInfo(label, arity);
|
|
ctor.isClassOf = (v) => ((v instanceof Record) &&
|
|
is(label, v.label) &&
|
|
v.fields.size === arity);
|
|
fieldNames.forEach((name, i) => {
|
|
ctor['_'+name] = function (r) {
|
|
if (!ctor.isClassOf(r)) {
|
|
throw new Error("Record: attempt to retrieve field "+label.toString()+"."+name+
|
|
" from non-"+label.toString()+": "+(r && r.toString()));
|
|
}
|
|
return r.get(i);
|
|
};
|
|
});
|
|
return ctor;
|
|
};
|
|
|
|
function RecordConstructorInfo(label, arity) {
|
|
this.label = label;
|
|
this.arity = arity;
|
|
}
|
|
|
|
RecordConstructorInfo.prototype.equals = function (other) {
|
|
return (other instanceof RecordConstructorInfo) &&
|
|
is(this.label, other.label) &&
|
|
(this.arity === other.arity);
|
|
};
|
|
|
|
RecordConstructorInfo.prototype.hashCode = function () {
|
|
return Immutable.List([this.label, this.arity]).hashCode();
|
|
};
|
|
|
|
RecordConstructorInfo.prototype.isClassOf = function (v) {
|
|
return (v instanceof Record) &&
|
|
is(this.label, v.label) &&
|
|
(this.arity === v.fields.size);
|
|
};
|
|
|
|
Object.assign(module.exports, {
|
|
fromJS,
|
|
List, Map, Set, is, hash,
|
|
Float, Single, Double,
|
|
Bytes,
|
|
Record, RecordConstructorInfo,
|
|
});
|