y-indexeddb
Version:
IndexedDB database adapter for Yjs
1,872 lines (1,713 loc) • 357 kB
JavaScript
(function () {
'use strict';
/**
* Utility module to work with key-value stores.
*
* @module map
*/
/**
* Creates a new Map instance.
*
* @function
* @return {Map<any, any>}
*
* @function
*/
const create = () => new Map();
/**
* Copy a Map object into a fresh Map object.
*
* @function
* @template X,Y
* @param {Map<X,Y>} m
* @return {Map<X,Y>}
*/
const copy = m => {
const r = create();
m.forEach((v, k) => { r.set(k, v); });
return r
};
/**
* Get map property. Create T if property is undefined and set T on map.
*
* ```js
* const listeners = map.setIfUndefined(events, 'eventName', set.create)
* listeners.add(listener)
* ```
*
* @function
* @template V,K
* @template {Map<K,V>} MAP
* @param {MAP} map
* @param {K} key
* @param {function():V} createT
* @return {V}
*/
const setIfUndefined = (map, key, createT) => {
let set = map.get(key);
if (set === undefined) {
map.set(key, set = createT());
}
return set
};
/**
* Creates an Array and populates it with the content of all key-value pairs using the `f(value, key)` function.
*
* @function
* @template K
* @template V
* @template R
* @param {Map<K,V>} m
* @param {function(V,K):R} f
* @return {Array<R>}
*/
const map = (m, f) => {
const res = [];
for (const [key, value] of m) {
res.push(f(value, key));
}
return res
};
/**
* Tests whether any key-value pairs pass the test implemented by `f(value, key)`.
*
* @todo should rename to some - similarly to Array.some
*
* @function
* @template K
* @template V
* @param {Map<K,V>} m
* @param {function(V,K):boolean} f
* @return {boolean}
*/
const any = (m, f) => {
for (const [key, value] of m) {
if (f(value, key)) {
return true
}
}
return false
};
/**
* Utility module to work with sets.
*
* @module set
*/
const create$1 = () => new Set();
/**
* Utility module to work with Arrays.
*
* @module array
*/
/**
* Return the last element of an array. The element must exist
*
* @template L
* @param {ArrayLike<L>} arr
* @return {L}
*/
const last = arr => arr[arr.length - 1];
/**
* Append elements from src to dest
*
* @template M
* @param {Array<M>} dest
* @param {Array<M>} src
*/
const appendTo = (dest, src) => {
for (let i = 0; i < src.length; i++) {
dest.push(src[i]);
}
};
/**
* Transforms something array-like to an actual Array.
*
* @function
* @template T
* @param {ArrayLike<T>|Iterable<T>} arraylike
* @return {T}
*/
const from = Array.from;
const isArray = Array.isArray;
/**
* Observable class prototype.
*
* @module observable
*/
/**
* Handles named events.
*
* @template N
*/
class Observable {
constructor () {
/**
* Some desc.
* @type {Map<N, any>}
*/
this._observers = create();
}
/**
* @param {N} name
* @param {function} f
*/
on (name, f) {
setIfUndefined(this._observers, name, create$1).add(f);
}
/**
* @param {N} name
* @param {function} f
*/
once (name, f) {
/**
* @param {...any} args
*/
const _f = (...args) => {
this.off(name, _f);
f(...args);
};
this.on(name, _f);
}
/**
* @param {N} name
* @param {function} f
*/
off (name, f) {
const observers = this._observers.get(name);
if (observers !== undefined) {
observers.delete(f);
if (observers.size === 0) {
this._observers.delete(name);
}
}
}
/**
* Emit a named event. All registered event listeners that listen to the
* specified name will receive the event.
*
* @todo This should catch exceptions
*
* @param {N} name The event name.
* @param {Array<any>} args The arguments that are applied to the event listener.
*/
emit (name, args) {
// copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called.
return from((this._observers.get(name) || create()).values()).forEach(f => f(...args))
}
destroy () {
this._observers = create();
}
}
/**
* Common Math expressions.
*
* @module math
*/
const floor = Math.floor;
const ceil = Math.ceil;
const abs = Math.abs;
const round = Math.round;
const log10 = Math.log10;
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The sum of a and b
*/
const add = (a, b) => a + b;
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The smaller element of a and b
*/
const min = (a, b) => a < b ? a : b;
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The bigger element of a and b
*/
const max = (a, b) => a > b ? a : b;
/**
* Base 10 exponential function. Returns the value of 10 raised to the power of pow.
*
* @param {number} exp
* @return {number}
*/
const exp10 = exp => Math.pow(10, exp);
/**
* @param {number} n
* @return {boolean} Wether n is negative. This function also differentiates between -0 and +0
*/
const isNegativeZero = n => n !== 0 ? n < 0 : 1 / n < 0;
/**
* @param {string} s
* @return {string}
*/
const toLowerCase = s => s.toLowerCase();
const trimLeftRegex = /^\s*/g;
/**
* @param {string} s
* @return {string}
*/
const trimLeft = s => s.replace(trimLeftRegex, '');
const fromCamelCaseRegex = /([A-Z])/g;
/**
* @param {string} s
* @param {string} separator
* @return {string}
*/
const fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, match => `${separator}${toLowerCase(match)}`));
/**
* @param {string} str
* @return {Uint8Array}
*/
const _encodeUtf8Polyfill = str => {
const encodedString = unescape(encodeURIComponent(str));
const len = encodedString.length;
const buf = new Uint8Array(len);
for (let i = 0; i < len; i++) {
buf[i] = /** @type {number} */ (encodedString.codePointAt(i));
}
return buf
};
/* c8 ignore next */
const utf8TextEncoder = /** @type {TextEncoder} */ (typeof TextEncoder !== 'undefined' ? new TextEncoder() : null);
/**
* @param {string} str
* @return {Uint8Array}
*/
const _encodeUtf8Native = str => utf8TextEncoder.encode(str);
/**
* @param {string} str
* @return {Uint8Array}
*/
/* c8 ignore next */
const encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill;
/* c8 ignore next */
let utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
/* c8 ignore start */
if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) {
// Safari doesn't handle BOM correctly.
// This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called.
// utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and
// utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call
// Another issue is that from then on no BOM chars are recognized anymore
/* c8 ignore next */
utf8TextDecoder = null;
}
/**
* Often used conditions.
*
* @module conditions
*/
/**
* @template T
* @param {T|null|undefined} v
* @return {T|null}
*/
/* c8 ignore next */
const undefinedToNull = v => v === undefined ? null : v;
/* eslint-env browser */
/**
* Isomorphic variable storage.
*
* Uses LocalStorage in the browser and falls back to in-memory storage.
*
* @module storage
*/
/* c8 ignore start */
class VarStoragePolyfill {
constructor () {
this.map = new Map();
}
/**
* @param {string} key
* @param {any} newValue
*/
setItem (key, newValue) {
this.map.set(key, newValue);
}
/**
* @param {string} key
*/
getItem (key) {
return this.map.get(key)
}
}
/* c8 ignore stop */
/**
* @type {any}
*/
let _localStorage = new VarStoragePolyfill();
let usePolyfill = true;
/* c8 ignore start */
try {
// if the same-origin rule is violated, accessing localStorage might thrown an error
if (typeof localStorage !== 'undefined') {
_localStorage = localStorage;
usePolyfill = false;
}
} catch (e) { }
/* c8 ignore stop */
/**
* This is basically localStorage in browser, or a polyfill in nodejs
*/
/* c8 ignore next */
const varStorage = _localStorage;
/**
* Utility functions for working with EcmaScript objects.
*
* @module object
*/
/**
* Object.assign
*/
const assign = Object.assign;
/**
* @param {Object<string,any>} obj
*/
const keys = Object.keys;
/**
* @template V
* @param {{[k:string]:V}} obj
* @param {function(V,string):any} f
*/
const forEach = (obj, f) => {
for (const key in obj) {
f(obj[key], key);
}
};
/**
* @todo implement mapToArray & map
*
* @template R
* @param {Object<string,any>} obj
* @param {function(any,string):R} f
* @return {Array<R>}
*/
const map$1 = (obj, f) => {
const results = [];
for (const key in obj) {
results.push(f(obj[key], key));
}
return results
};
/**
* @param {Object<string,any>} obj
* @return {number}
*/
const length = obj => keys(obj).length;
/**
* @param {Object|undefined} obj
*/
const isEmpty = obj => {
for (const _k in obj) {
return false
}
return true
};
/**
* @param {Object<string,any>} obj
* @param {function(any,string):boolean} f
* @return {boolean}
*/
const every = (obj, f) => {
for (const key in obj) {
if (!f(obj[key], key)) {
return false
}
}
return true
};
/**
* Calls `Object.prototype.hasOwnProperty`.
*
* @param {any} obj
* @param {string|symbol} key
* @return {boolean}
*/
const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
/**
* @param {Object<string,any>} a
* @param {Object<string,any>} b
* @return {boolean}
*/
const equalFlat = (a, b) => a === b || (length(a) === length(b) && every(a, (val, key) => (val !== undefined || hasProperty(b, key)) && b[key] === val));
/**
* Common functions and function call helpers.
*
* @module function
*/
/**
* Calls all functions in `fs` with args. Only throws after all functions were called.
*
* @param {Array<function>} fs
* @param {Array<any>} args
*/
const callAll = (fs, args, i = 0) => {
try {
for (; i < fs.length; i++) {
fs[i](...args);
}
} finally {
if (i < fs.length) {
callAll(fs, args, i + 1);
}
}
};
/**
* @template A
*
* @param {A} a
* @return {A}
*/
const id = a => a;
/**
* @template V
* @template {V} OPTS
*
* @param {V} value
* @param {Array<OPTS>} options
*/
// @ts-ignore
const isOneOf = (value, options) => options.includes(value);
/* c8 ignore stop */
/**
* Isomorphic module to work access the environment (query params, env variables).
*
* @module map
*/
/* c8 ignore next */
// @ts-ignore
const isNode = typeof process !== 'undefined' && process.release &&
/node|io\.js/.test(process.release.name);
/* c8 ignore next */
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && !isNode;
/* c8 ignore next 3 */
const isMac = typeof navigator !== 'undefined'
? /Mac/.test(navigator.platform)
: false;
/**
* @type {Map<string,string>}
*/
let params;
/* c8 ignore start */
const computeParams = () => {
if (params === undefined) {
if (isNode) {
params = create();
const pargs = process.argv;
let currParamName = null;
for (let i = 0; i < pargs.length; i++) {
const parg = pargs[i];
if (parg[0] === '-') {
if (currParamName !== null) {
params.set(currParamName, '');
}
currParamName = parg;
} else {
if (currParamName !== null) {
params.set(currParamName, parg);
currParamName = null;
}
}
}
if (currParamName !== null) {
params.set(currParamName, '');
}
// in ReactNative for example this would not be true (unless connected to the Remote Debugger)
} else if (typeof location === 'object') {
params = create(); // eslint-disable-next-line no-undef
(location.search || '?').slice(1).split('&').forEach((kv) => {
if (kv.length !== 0) {
const [key, value] = kv.split('=');
params.set(`--${fromCamelCase(key, '-')}`, value);
params.set(`-${fromCamelCase(key, '-')}`, value);
}
});
} else {
params = create();
}
}
return params
};
/* c8 ignore stop */
/**
* @param {string} name
* @return {boolean}
*/
/* c8 ignore next */
const hasParam = (name) => computeParams().has(name);
/**
* @param {string} name
* @param {string} defaultVal
* @return {string}
*/
/* c8 ignore next 2 */
const getParam = (name, defaultVal) =>
computeParams().get(name) || defaultVal;
/**
* @param {string} name
* @return {string|null}
*/
/* c8 ignore next 4 */
const getVariable = (name) =>
isNode
? undefinedToNull(process.env[name.toUpperCase()])
: undefinedToNull(varStorage.getItem(name));
/**
* @param {string} name
* @return {boolean}
*/
/* c8 ignore next 2 */
const hasConf = (name) =>
hasParam('--' + name) || getVariable(name) !== null;
/* c8 ignore next */
const production = hasConf('production');
/* c8 ignore next 2 */
const forceColor = isNode &&
isOneOf(process.env.FORCE_COLOR, ['true', '1', '2']);
/* c8 ignore start */
const supportsColor = !hasParam('no-colors') &&
(!isNode || process.stdout.isTTY || forceColor) && (
!isNode || hasParam('color') || forceColor ||
getVariable('COLORTERM') !== null ||
(getVariable('TERM') || '').includes('color')
);
/* c8 ignore stop */
/* eslint-env browser */
/**
* Binary data constants.
*
* @module binary
*/
/**
* n-th bit activated.
*
* @type {number}
*/
const BIT1 = 1;
const BIT2 = 2;
const BIT3 = 4;
const BIT4 = 8;
const BIT6 = 32;
const BIT7 = 64;
const BIT8 = 128;
const BITS5 = 31;
const BITS6 = 63;
const BITS7 = 127;
/**
* @type {number}
*/
const BITS31 = 0x7FFFFFFF;
/**
* @type {number}
*/
const BITS32 = 0xFFFFFFFF;
/**
* Utility helpers for working with numbers.
*
* @module number
*/
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
/**
* @module number
*/
/* c8 ignore next */
const isInteger = Number.isInteger || (num => typeof num === 'number' && isFinite(num) && floor(num) === num);
/**
* Error helpers.
*
* @module error
*/
/**
* @param {string} s
* @return {Error}
*/
/* c8 ignore next */
const create$2 = s => new Error(s);
/**
* @throws {Error}
* @return {never}
*/
/* c8 ignore next 3 */
const methodUnimplemented = () => {
throw create$2('Method unimplemented')
};
/**
* @throws {Error}
* @return {never}
*/
/* c8 ignore next 3 */
const unexpectedCase = () => {
throw create$2('Unexpected case')
};
/**
* Efficient schema-less binary decoding with support for variable length encoding.
*
* Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function.
*
* Encodes numbers in little-endian order (least to most significant byte order)
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
* which is also used in Protocol Buffers.
*
* ```js
* // encoding step
* const encoder = encoding.createEncoder()
* encoding.writeVarUint(encoder, 256)
* encoding.writeVarString(encoder, 'Hello world!')
* const buf = encoding.toUint8Array(encoder)
* ```
*
* ```js
* // decoding step
* const decoder = decoding.createDecoder(buf)
* decoding.readVarUint(decoder) // => 256
* decoding.readVarString(decoder) // => 'Hello world!'
* decoding.hasContent(decoder) // => false - all data is read
* ```
*
* @module decoding
*/
const errorUnexpectedEndOfArray = create$2('Unexpected end of array');
const errorIntegerOutOfRange = create$2('Integer out of Range');
/**
* A Decoder handles the decoding of an Uint8Array.
*/
class Decoder {
/**
* @param {Uint8Array} uint8Array Binary data to decode
*/
constructor (uint8Array) {
/**
* Decoding target.
*
* @type {Uint8Array}
*/
this.arr = uint8Array;
/**
* Current decoding position.
*
* @type {number}
*/
this.pos = 0;
}
}
/**
* @function
* @param {Uint8Array} uint8Array
* @return {Decoder}
*/
const createDecoder = uint8Array => new Decoder(uint8Array);
/**
* @function
* @param {Decoder} decoder
* @return {boolean}
*/
const hasContent = decoder => decoder.pos !== decoder.arr.length;
/**
* Create an Uint8Array view of the next `len` bytes and advance the position by `len`.
*
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
*
* @function
* @param {Decoder} decoder The decoder instance
* @param {number} len The length of bytes to read
* @return {Uint8Array}
*/
const readUint8Array = (decoder, len) => {
const view = createUint8ArrayViewFromArrayBuffer(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len);
decoder.pos += len;
return view
};
/**
* Read variable length Uint8Array.
*
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
*
* @function
* @param {Decoder} decoder
* @return {Uint8Array}
*/
const readVarUint8Array = decoder => readUint8Array(decoder, readVarUint(decoder));
/**
* Read one byte as unsigned integer.
* @function
* @param {Decoder} decoder The decoder instance
* @return {number} Unsigned 8-bit integer
*/
const readUint8 = decoder => decoder.arr[decoder.pos++];
/**
* Read unsigned integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
const readVarUint = decoder => {
let num = 0;
let mult = 1;
const len = decoder.arr.length;
while (decoder.pos < len) {
const r = decoder.arr[decoder.pos++];
// num = num | ((r & binary.BITS7) << len)
num = num + (r & BITS7) * mult; // shift $r << (7*#iterations) and add it to num
mult *= 128; // next iteration, shift 7 "more" to the left
if (r < BIT8) {
return num
}
/* c8 ignore start */
if (num > MAX_SAFE_INTEGER) {
throw errorIntegerOutOfRange
}
/* c8 ignore stop */
}
throw errorUnexpectedEndOfArray
};
/**
* Read signed integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
* @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
const readVarInt = decoder => {
let r = decoder.arr[decoder.pos++];
let num = r & BITS6;
let mult = 64;
const sign = (r & BIT7) > 0 ? -1 : 1;
if ((r & BIT8) === 0) {
// don't continue reading
return sign * num
}
const len = decoder.arr.length;
while (decoder.pos < len) {
r = decoder.arr[decoder.pos++];
// num = num | ((r & binary.BITS7) << len)
num = num + (r & BITS7) * mult;
mult *= 128;
if (r < BIT8) {
return sign * num
}
/* c8 ignore start */
if (num > MAX_SAFE_INTEGER) {
throw errorIntegerOutOfRange
}
/* c8 ignore stop */
}
throw errorUnexpectedEndOfArray
};
/**
* We don't test this function anymore as we use native decoding/encoding by default now.
* Better not modify this anymore..
*
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
* when String.fromCodePoint is fed with all characters as arguments.
* But most environments have a maximum number of arguments per functions.
* For effiency reasons we apply a maximum of 10000 characters at once.
*
* @function
* @param {Decoder} decoder
* @return {String} The read String.
*/
/* c8 ignore start */
const _readVarStringPolyfill = decoder => {
let remainingLen = readVarUint(decoder);
if (remainingLen === 0) {
return ''
} else {
let encodedString = String.fromCodePoint(readUint8(decoder)); // remember to decrease remainingLen
if (--remainingLen < 100) { // do not create a Uint8Array for small strings
while (remainingLen--) {
encodedString += String.fromCodePoint(readUint8(decoder));
}
} else {
while (remainingLen > 0) {
const nextLen = remainingLen < 10000 ? remainingLen : 10000;
// this is dangerous, we create a fresh array view from the existing buffer
const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen);
decoder.pos += nextLen;
// Starting with ES5.1 we can supply a generic array-like object as arguments
encodedString += String.fromCodePoint.apply(null, /** @type {any} */ (bytes));
remainingLen -= nextLen;
}
}
return decodeURIComponent(escape(encodedString))
}
};
/* c8 ignore stop */
/**
* @function
* @param {Decoder} decoder
* @return {String} The read String
*/
const _readVarStringNative = decoder =>
/** @type any */ (utf8TextDecoder).decode(readVarUint8Array(decoder));
/**
* Read string of variable length
* * varUint is used to store the length of the string
*
* @function
* @param {Decoder} decoder
* @return {String} The read String
*
*/
/* c8 ignore next */
const readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill;
/**
* @param {Decoder} decoder
* @param {number} len
* @return {DataView}
*/
const readFromDataView = (decoder, len) => {
const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len);
decoder.pos += len;
return dv
};
/**
* @param {Decoder} decoder
*/
const readFloat32 = decoder => readFromDataView(decoder, 4).getFloat32(0, false);
/**
* @param {Decoder} decoder
*/
const readFloat64 = decoder => readFromDataView(decoder, 8).getFloat64(0, false);
/**
* @param {Decoder} decoder
*/
const readBigInt64 = decoder => /** @type {any} */ (readFromDataView(decoder, 8)).getBigInt64(0, false);
/**
* @type {Array<function(Decoder):any>}
*/
const readAnyLookupTable = [
decoder => undefined, // CASE 127: undefined
decoder => null, // CASE 126: null
readVarInt, // CASE 125: integer
readFloat32, // CASE 124: float32
readFloat64, // CASE 123: float64
readBigInt64, // CASE 122: bigint
decoder => false, // CASE 121: boolean (false)
decoder => true, // CASE 120: boolean (true)
readVarString, // CASE 119: string
decoder => { // CASE 118: object<string,any>
const len = readVarUint(decoder);
/**
* @type {Object<string,any>}
*/
const obj = {};
for (let i = 0; i < len; i++) {
const key = readVarString(decoder);
obj[key] = readAny(decoder);
}
return obj
},
decoder => { // CASE 117: array<any>
const len = readVarUint(decoder);
const arr = [];
for (let i = 0; i < len; i++) {
arr.push(readAny(decoder));
}
return arr
},
readVarUint8Array // CASE 116: Uint8Array
];
/**
* @param {Decoder} decoder
*/
const readAny = decoder => readAnyLookupTable[127 - readUint8(decoder)](decoder);
/**
* T must not be null.
*
* @template T
*/
class RleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
* @param {function(Decoder):T} reader
*/
constructor (uint8Array, reader) {
super(uint8Array);
/**
* The reader
*/
this.reader = reader;
/**
* Current state
* @type {T|null}
*/
this.s = null;
this.count = 0;
}
read () {
if (this.count === 0) {
this.s = this.reader(this);
if (hasContent(this)) {
this.count = readVarUint(this) + 1; // see encoder implementation for the reason why this is incremented
} else {
this.count = -1; // read the current value forever
}
}
this.count--;
return /** @type {T} */ (this.s)
}
}
class UintOptRleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
super(uint8Array);
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
}
read () {
if (this.count === 0) {
this.s = readVarInt(this);
// if the sign is negative, we read the count too, otherwise count is 1
const isNegative = isNegativeZero(this.s);
this.count = 1;
if (isNegative) {
this.s = -this.s;
this.count = readVarUint(this) + 2;
}
}
this.count--;
return /** @type {number} */ (this.s)
}
}
class IntDiffOptRleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
super(uint8Array);
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
this.diff = 0;
}
/**
* @return {number}
*/
read () {
if (this.count === 0) {
const diff = readVarInt(this);
// if the first bit is set, we read more data
const hasCount = diff & 1;
this.diff = floor(diff / 2); // shift >> 1
this.count = 1;
if (hasCount) {
this.count = readVarUint(this) + 2;
}
}
this.s += this.diff;
this.count--;
return this.s
}
}
class StringDecoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
this.decoder = new UintOptRleDecoder(uint8Array);
this.str = readVarString(this.decoder);
/**
* @type {number}
*/
this.spos = 0;
}
/**
* @return {string}
*/
read () {
const end = this.spos + this.decoder.read();
const res = this.str.slice(this.spos, end);
this.spos = end;
return res
}
}
/**
* Utility functions to work with buffers (Uint8Array).
*
* @module buffer
*/
/**
* @param {number} len
*/
const createUint8ArrayFromLen = len => new Uint8Array(len);
/**
* Create Uint8Array with initial content from buffer
*
* @param {ArrayBuffer} buffer
* @param {number} byteOffset
* @param {number} length
*/
const createUint8ArrayViewFromArrayBuffer = (buffer, byteOffset, length) => new Uint8Array(buffer, byteOffset, length);
/**
* Copy the content of an Uint8Array view to a new ArrayBuffer.
*
* @param {Uint8Array} uint8Array
* @return {Uint8Array}
*/
const copyUint8Array = uint8Array => {
const newBuf = createUint8ArrayFromLen(uint8Array.byteLength);
newBuf.set(uint8Array);
return newBuf
};
/**
* Efficient schema-less binary encoding with support for variable length encoding.
*
* Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function.
*
* Encodes numbers in little-endian order (least to most significant byte order)
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
* which is also used in Protocol Buffers.
*
* ```js
* // encoding step
* const encoder = encoding.createEncoder()
* encoding.writeVarUint(encoder, 256)
* encoding.writeVarString(encoder, 'Hello world!')
* const buf = encoding.toUint8Array(encoder)
* ```
*
* ```js
* // decoding step
* const decoder = decoding.createDecoder(buf)
* decoding.readVarUint(decoder) // => 256
* decoding.readVarString(decoder) // => 'Hello world!'
* decoding.hasContent(decoder) // => false - all data is read
* ```
*
* @module encoding
*/
/**
* A BinaryEncoder handles the encoding to an Uint8Array.
*/
class Encoder {
constructor () {
this.cpos = 0;
this.cbuf = new Uint8Array(100);
/**
* @type {Array<Uint8Array>}
*/
this.bufs = [];
}
}
/**
* @function
* @return {Encoder}
*/
const createEncoder = () => new Encoder();
/**
* The current length of the encoded data.
*
* @function
* @param {Encoder} encoder
* @return {number}
*/
const length$1 = encoder => {
let len = encoder.cpos;
for (let i = 0; i < encoder.bufs.length; i++) {
len += encoder.bufs[i].length;
}
return len
};
/**
* Transform to Uint8Array.
*
* @function
* @param {Encoder} encoder
* @return {Uint8Array} The created ArrayBuffer.
*/
const toUint8Array = encoder => {
const uint8arr = new Uint8Array(length$1(encoder));
let curPos = 0;
for (let i = 0; i < encoder.bufs.length; i++) {
const d = encoder.bufs[i];
uint8arr.set(d, curPos);
curPos += d.length;
}
uint8arr.set(createUint8ArrayViewFromArrayBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos);
return uint8arr
};
/**
* Verify that it is possible to write `len` bytes wtihout checking. If
* necessary, a new Buffer with the required length is attached.
*
* @param {Encoder} encoder
* @param {number} len
*/
const verifyLen = (encoder, len) => {
const bufferLen = encoder.cbuf.length;
if (bufferLen - encoder.cpos < len) {
encoder.bufs.push(createUint8ArrayViewFromArrayBuffer(encoder.cbuf.buffer, 0, encoder.cpos));
encoder.cbuf = new Uint8Array(max(bufferLen, len) * 2);
encoder.cpos = 0;
}
};
/**
* Write one byte to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {number} num The byte that is to be encoded.
*/
const write = (encoder, num) => {
const bufferLen = encoder.cbuf.length;
if (encoder.cpos === bufferLen) {
encoder.bufs.push(encoder.cbuf);
encoder.cbuf = new Uint8Array(bufferLen * 2);
encoder.cpos = 0;
}
encoder.cbuf[encoder.cpos++] = num;
};
/**
* Write one byte as an unsigned integer.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeUint8 = write;
/**
* Write a variable length unsigned integer. Max encodable integer is 2^53.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeVarUint = (encoder, num) => {
while (num > BITS7) {
write(encoder, BIT8 | (BITS7 & num));
num = floor(num / 128); // shift >>> 7
}
write(encoder, BITS7 & num);
};
/**
* Write a variable length integer.
*
* We use the 7th bit instead for signaling that this is a negative number.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeVarInt = (encoder, num) => {
const isNegative = isNegativeZero(num);
if (isNegative) {
num = -num;
}
// |- whether to continue reading |- whether is negative |- number
write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | (BITS6 & num));
num = floor(num / 64); // shift >>> 6
// We don't need to consider the case of num === 0 so we can use a different
// pattern here than above.
while (num > 0) {
write(encoder, (num > BITS7 ? BIT8 : 0) | (BITS7 & num));
num = floor(num / 128); // shift >>> 7
}
};
/**
* A cache to store strings temporarily
*/
const _strBuffer = new Uint8Array(30000);
const _maxStrBSize = _strBuffer.length / 3;
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
const _writeVarStringNative = (encoder, str) => {
if (str.length < _maxStrBSize) {
// We can encode the string into the existing buffer
/* c8 ignore next */
const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0;
writeVarUint(encoder, written);
for (let i = 0; i < written; i++) {
write(encoder, _strBuffer[i]);
}
} else {
writeVarUint8Array(encoder, encodeUtf8(str));
}
};
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
const _writeVarStringPolyfill = (encoder, str) => {
const encodedString = unescape(encodeURIComponent(str));
const len = encodedString.length;
writeVarUint(encoder, len);
for (let i = 0; i < len; i++) {
write(encoder, /** @type {number} */ (encodedString.codePointAt(i)));
}
};
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
/* c8 ignore next */
const writeVarString = (utf8TextEncoder && /** @type {any} */ (utf8TextEncoder).encodeInto) ? _writeVarStringNative : _writeVarStringPolyfill;
/**
* Append fixed-length Uint8Array to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {Uint8Array} uint8Array
*/
const writeUint8Array = (encoder, uint8Array) => {
const bufferLen = encoder.cbuf.length;
const cpos = encoder.cpos;
const leftCopyLen = min(bufferLen - cpos, uint8Array.length);
const rightCopyLen = uint8Array.length - leftCopyLen;
encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos);
encoder.cpos += leftCopyLen;
if (rightCopyLen > 0) {
// Still something to write, write right half..
// Append new buffer
encoder.bufs.push(encoder.cbuf);
// must have at least size of remaining buffer
encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen));
// copy array
encoder.cbuf.set(uint8Array.subarray(leftCopyLen));
encoder.cpos = rightCopyLen;
}
};
/**
* Append an Uint8Array to Encoder.
*
* @function
* @param {Encoder} encoder
* @param {Uint8Array} uint8Array
*/
const writeVarUint8Array = (encoder, uint8Array) => {
writeVarUint(encoder, uint8Array.byteLength);
writeUint8Array(encoder, uint8Array);
};
/**
* Create an DataView of the next `len` bytes. Use it to write data after
* calling this function.
*
* ```js
* // write float32 using DataView
* const dv = writeOnDataView(encoder, 4)
* dv.setFloat32(0, 1.1)
* // read float32 using DataView
* const dv = readFromDataView(encoder, 4)
* dv.getFloat32(0) // => 1.100000023841858 (leaving it to the reader to find out why this is the correct result)
* ```
*
* @param {Encoder} encoder
* @param {number} len
* @return {DataView}
*/
const writeOnDataView = (encoder, len) => {
verifyLen(encoder, len);
const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len);
encoder.cpos += len;
return dview
};
/**
* @param {Encoder} encoder
* @param {number} num
*/
const writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false);
/**
* @param {Encoder} encoder
* @param {number} num
*/
const writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false);
/**
* @param {Encoder} encoder
* @param {bigint} num
*/
const writeBigInt64 = (encoder, num) => /** @type {any} */ (writeOnDataView(encoder, 8)).setBigInt64(0, num, false);
const floatTestBed = new DataView(new ArrayBuffer(4));
/**
* Check if a number can be encoded as a 32 bit float.
*
* @param {number} num
* @return {boolean}
*/
const isFloat32 = num => {
floatTestBed.setFloat32(0, num);
return floatTestBed.getFloat32(0) === num
};
/**
* Encode data with efficient binary format.
*
* Differences to JSON:
* • Transforms data to a binary format (not to a string)
* • Encodes undefined, NaN, and ArrayBuffer (these can't be represented in JSON)
* • Numbers are efficiently encoded either as a variable length integer, as a
* 32 bit float, as a 64 bit float, or as a 64 bit bigint.
*
* Encoding table:
*
* | Data Type | Prefix | Encoding Method | Comment |
* | ------------------- | -------- | ------------------ | ------- |
* | undefined | 127 | | Functions, symbol, and everything that cannot be identified is encoded as undefined |
* | null | 126 | | |
* | integer | 125 | writeVarInt | Only encodes 32 bit signed integers |
* | float32 | 124 | writeFloat32 | |
* | float64 | 123 | writeFloat64 | |
* | bigint | 122 | writeBigInt64 | |
* | boolean (false) | 121 | | True and false are different data types so we save the following byte |
* | boolean (true) | 120 | | - 0b01111000 so the last bit determines whether true or false |
* | string | 119 | writeVarString | |
* | object<string,any> | 118 | custom | Writes {length} then {length} key-value pairs |
* | array<any> | 117 | custom | Writes {length} then {length} json values |
* | Uint8Array | 116 | writeVarUint8Array | We use Uint8Array for any kind of binary data |
*
* Reasons for the decreasing prefix:
* We need the first bit for extendability (later we may want to encode the
* prefix with writeVarUint). The remaining 7 bits are divided as follows:
* [0-30] the beginning of the data range is used for custom purposes
* (defined by the function that uses this library)
* [31-127] the end of the data range is used for data encoding by
* lib0/encoding.js
*
* @param {Encoder} encoder
* @param {undefined|null|number|bigint|boolean|string|Object<string,any>|Array<any>|Uint8Array} data
*/
const writeAny = (encoder, data) => {
switch (typeof data) {
case 'string':
// TYPE 119: STRING
write(encoder, 119);
writeVarString(encoder, data);
break
case 'number':
if (isInteger(data) && abs(data) <= BITS31) {
// TYPE 125: INTEGER
write(encoder, 125);
writeVarInt(encoder, data);
} else if (isFloat32(data)) {
// TYPE 124: FLOAT32
write(encoder, 124);
writeFloat32(encoder, data);
} else {
// TYPE 123: FLOAT64
write(encoder, 123);
writeFloat64(encoder, data);
}
break
case 'bigint':
// TYPE 122: BigInt
write(encoder, 122);
writeBigInt64(encoder, data);
break
case 'object':
if (data === null) {
// TYPE 126: null
write(encoder, 126);
} else if (isArray(data)) {
// TYPE 117: Array
write(encoder, 117);
writeVarUint(encoder, data.length);
for (let i = 0; i < data.length; i++) {
writeAny(encoder, data[i]);
}
} else if (data instanceof Uint8Array) {
// TYPE 116: ArrayBuffer
write(encoder, 116);
writeVarUint8Array(encoder, data);
} else {
// TYPE 118: Object
write(encoder, 118);
const keys = Object.keys(data);
writeVarUint(encoder, keys.length);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
writeVarString(encoder, key);
writeAny(encoder, data[key]);
}
}
break
case 'boolean':
// TYPE 120/121: boolean (true/false)
write(encoder, data ? 120 : 121);
break
default:
// TYPE 127: undefined
write(encoder, 127);
}
};
/**
* Now come a few stateful encoder that have their own classes.
*/
/**
* Basic Run Length Encoder - a basic compression implementation.
*
* Encodes [1,1,1,7] to [1,3,7,1] (3 times 1, 1 time 7). This encoder might do more harm than good if there are a lot of values that are not repeated.
*
* It was originally used for image compression. Cool .. article http://csbruce.com/cbm/transactor/pdfs/trans_v7_i06.pdf
*
* @note T must not be null!
*
* @template T
*/
class RleEncoder extends Encoder {
/**
* @param {function(Encoder, T):void} writer
*/
constructor (writer) {
super();
/**
* The writer
*/
this.w = writer;
/**
* Current state
* @type {T|null}
*/
this.s = null;
this.count = 0;
}
/**
* @param {T} v
*/
write (v) {
if (this.s === v) {
this.count++;
} else {
if (this.count > 0) {
// flush counter, unless this is the first value (count = 0)
writeVarUint(this, this.count - 1); // since count is always > 0, we can decrement by one. non-standard encoding ftw
}
this.count = 1;
// write first value
this.w(this, v);
this.s = v;
}
}
}
/**
* @param {UintOptRleEncoder} encoder
*/
const flushUintOptRleEncoder = encoder => {
if (encoder.count > 0) {
// flush counter, unless this is the first value (count = 0)
// case 1: just a single value. set sign to positive
// case 2: write several values. set sign to negative to indicate that there is a length coming
writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s);
if (encoder.count > 1) {
writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw
}
}
};
/**
* Optimized Rle encoder that does not suffer from the mentioned problem of the basic Rle encoder.
*
* Internally uses VarInt encoder to write unsigned integers. If the input occurs multiple times, we write
* write it as a negative number. The UintOptRleDecoder then understands that it needs to read a count.
*
* Encodes [1,2,3,3,3] as [1,2,-3,3] (once 1, once 2, three times 3)
*/
class UintOptRleEncoder {
constructor () {
this.encoder = new Encoder();
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
}
/**
* @param {number} v
*/
write (v) {
if (this.s === v) {
this.count++;
} else {
flushUintOptRleEncoder(this);
this.count = 1;
this.s = v;
}
}
toUint8Array () {
flushUintOptRleEncoder(this);
return toUint8Array(this.encoder)
}
}
/**
* @param {IntDiffOptRleEncoder} encoder
*/
const flushIntDiffOptRleEncoder = encoder => {
if (encoder.count > 0) {
// 31 bit making up the diff | wether to write the counter
// const encodedDiff = encoder.diff << 1 | (encoder.count === 1 ? 0 : 1)
const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1);
// flush counter, unless this is the first value (count = 0)
// case 1: just a single value. set first bit to positive
// case 2: write several values. set first bit to negative to indicate that there is a length coming
writeVarInt(encoder.encoder, encodedDiff);
if (encoder.count > 1) {
writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw
}
}
};
/**
* A combination of the IntDiffEncoder and the UintOptRleEncoder.
*
* The count approach is similar to the UintDiffOptRleEncoder, but instead of using the negative bitflag, it encodes
* in the LSB whether a count is to be read. Therefore this Encoder only supports 31 bit integers!
*
* Encodes [1, 2, 3, 2] as [3, 1, 6, -1] (more specifically [(1 << 1) | 1, (3 << 0) | 0, -1])
*
* Internally uses variable length encoding. Contrary to normal UintVar encoding, the first byte contains:
* * 1 bit that denotes whether the next value is a count (LSB)
* * 1 bit that denotes whether this value is negative (MSB - 1)
* * 1 bit that denotes whether to continue reading the variable length integer (MSB)
*
* Therefore, only five bits remain to encode diff ranges.
*
* Use this Encoder only when appropriate. In most cases, this is probably a bad idea.
*/
class IntDiffOptRleEncoder {
constructor () {
this.encoder = new Encoder();
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
this.diff = 0;
}
/**
* @param {number} v
*/
write (v) {
if (this.diff === v - this.s) {
this.s = v;
this.count++;
} else {
flushIntDiffOptRleEncoder(this);
this.count = 1;
this.diff = v - this.s;
this.s = v;
}
}
toUint8Array () {
flushIntDiffOptRleEncoder(this);
return toUint8Array(this.encoder)
}
}
/**
* Optimized String Encoder.
*
* Encoding many small strings in a simple Encoder is not very efficient. The function call to decode a string takes some time and creates references that must be eventually deleted.
* In practice, when decoding several million small strings, the GC will kick in more and more often to collect orphaned string objects (or maybe there is another reason?).
*
* This string encoder solves the above problem. All strings are concatenated and written as a single string using a single encoding call.
*
* The lengths are encoded using a UintOptRleEncoder.
*/
class StringEncoder {
constructor () {
/**
* @type {Array<string>}
*/
this.sarr = [];
this.s = '';
this.lensE = new UintOptRleEncoder();
}
/**
* @param {string} string
*/
write (string) {
this.s += string;
if (this.s.length > 19) {
this.sarr.push(this.s);
this.s = '';
}
this.lensE.write(string.length);
}
toUint8Array () {
const encoder = new Encoder();
this.sarr.push(this.s);
this.s = '';
writeVarString(encoder, this.sarr.join(''));
writeUint8Array(encoder, this.lensE.toUint8Array());
return toUint8Array(encoder)
}
}
/* eslint-env browser */
const getRandomValues = crypto.getRandomValues.bind(crypto);
const uint32 = () => getRandomValues(new Uint32Array(1))[0];
// @ts-ignore
const uuidv4Template = [1e7] + -1e3 + -4e3 + -8e3 + -1e11;
const uuidv4 = () => uuidv4Template.replace(/[018]/g, /** @param {number} c */ c =>
(c ^ uint32() & 15 >> c / 4).toString(16)
);
/**
* Utility module to convert metric values.
*
* @module metric
*/
const prefixUp = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const prefixDown = ['', 'm', 'μ', 'n', 'p', 'f', 'a', 'z', 'y'];
/**
* Calculate the metric prefix for a number. Assumes E.g. `prefix(1000) = { n: 1, prefix: 'k' }`
*
* @param {number} n
* @param {number} [baseMultiplier] Multiplier of the base (10^(3*baseMultiplier)). E.g. `convert(time, -3)` if time is already in milli seconds
* @return {{n:number,prefix:string}}
*/
const prefix = (n, baseMultiplier = 0) => {
const nPow = n === 0 ? 0 : log10(n);
let mult = 0;
while (nPow < mult * 3 && baseMultiplier > -8) {
baseMultiplier--;
mult--;
}
while (nPow >= 3 + mult * 3 && baseMultiplier < 8) {
baseMultiplier++;
mult++;
}
const prefix = baseMultiplier < 0 ? prefixDown[-baseMultiplier] : prefixUp[baseMultiplier];
return {
n: round((mult > 0 ? n / exp10(mult * 3) : n * exp10(mult * -3)) * 1e12) / 1e12,
prefix
}
};
/**
* Utility module to work with time.
*
* @module time
*/
/**
* Transform time (in ms) to a human readable format. E.g. 1100 => 1.1s. 60s => 1min. .001 => 10μs.
*
* @param {number} d duration in milliseconds
* @return {string} humanized approximation of time
*/
const humanizeDuration = d => {
if (d < 60000) {
const p = prefix(d, -1);