UNPKG

@consento/hlc

Version:

A Hybrid Logical Clock implementation that comes with a codec to store it as Uint8Array (compatible to codecs)

190 lines (169 loc) 5.42 kB
const bigIntTime = require('bigint-time') const { toBytesBE, fromBigInt, fromInt, fromBytesBE, toBigInt, MAX_UNSIGNED_VALUE } = require('longfn') function writeUint32 (target, offset, num) { target[offset] = num >>> 24 target[offset + 1] = (num >>> 16) & 0xFF target[offset + 2] = (num >>> 8) & 0xFF target[offset + 3] = num & 0xFF } function readUint32 (target, offset) { return (target[offset] * 0x1000000) + ( (target[offset + 1] << 16) | (target[offset + 2] << 8) | target[offset + 3] ) } const TMP_INT = fromInt(0) const n1e6 = BigInt(1e6) const UINT64_MAX = toBigInt(MAX_UNSIGNED_VALUE) const UINT32_MAX = 0xFFFFFFFF function bigIntCoerce (input, fallback) { if (typeof input === 'bigint') return input if (typeof input === 'number' || typeof input === 'string') return BigInt(input) return fallback } function bigIntJSON (bigInt) { if (bigInt < Number.MAX_SAFE_INTEGER) { return Number(bigInt) } return '0x' + bigInt.toString(16) } class ClockOffsetError extends Error { constructor (offset, maxOffset) { super(`The received time is ${offset / n1e6}ms ahead of the wall time, exceeding the 'maxOffset' limit of ${maxOffset / n1e6}ms.`) this.offset = offset this.maxOffset = maxOffset } } ClockOffsetError.prototype.type = 'ClockOffsetError' class WallTimeOverflowError extends Error { constructor (time, maxTime) { super(`The wall time ${time / n1e6}ms exceeds the max time of ${maxTime / n1e6}ms.`) this.time = time this.maxTime = maxTime } } WallTimeOverflowError.prototype.type = 'WallTimeOverflowError' class ForwardJumpError extends Error { constructor (timejump, tolerance) { super(`Detected a forward time jump of ${timejump / n1e6}ms, which exceed the allowed tolerance of ${tolerance / n1e6}ms.`) this.timejump = timejump this.tolerance = tolerance } } ForwardJumpError.prototype.type = 'ForwardJumpError' const codec = Object.freeze({ name: 'hlc', encode (current, byob, offset = 0) { let out if (byob) { if (byob.byteLength < 12) { throw new Error(`The provided Uint8Array is too small. 12 byte required but only ${byob.byteLength} byte given.`) } out = byob } else { out = new Uint8Array(12) } offset = byob ? offset : 0 toBytesBE(fromBigInt(current.wallTime, true, TMP_INT), offset, out) writeUint32(out, offset + 8, current.logical) return out }, decode (array, offset = 0) { return new Timestamp(toBigInt(fromBytesBE(array, true, offset, TMP_INT)), readUint32(array, offset + 8)) } }) class Timestamp { constructor (wallTime, logical = 0) { if (typeof wallTime === 'object') { this.wallTime = bigIntCoerce(wallTime.wallTime, 0n) this.logical = wallTime.logical } else { this.wallTime = bigIntCoerce(wallTime, 0n) this.logical = logical } } static compare (a, b) { if (a.wallTime > b.wallTime) return 1 if (a.wallTime < b.wallTime) return -1 if (a.logical > b.logical) return 1 if (a.logical < b.logical) return -1 return 0 } static bigger (a, b) { return a.compare(b) === -1 ? b : a } encode (byob, offset = 0) { return codec.encode(this, byob, offset) } toJSON () { return { wallTime: bigIntJSON(this.wallTime), logical: this.logical } } compare (other) { return Timestamp.compare(this, other) } } class HLC { constructor ({ wallTime, maxOffset, wallTimeUpperBound, toleratedForwardClockJump, last } = {}) { this.wallTime = wallTime || bigIntTime this.maxOffset = bigIntCoerce(maxOffset, 0n) this.wallTimeUpperBound = bigIntCoerce(wallTimeUpperBound, 0n) this.toleratedForwardClockJump = bigIntCoerce(toleratedForwardClockJump, 0n) this.last = new Timestamp(this.wallTime()) if (last) { this.last = Timestamp.bigger(new Timestamp(last), this.last) } } toJSON () { return { maxOffset: bigIntJSON(this.maxOffset), wallTimeUpperBound: bigIntJSON(this.wallTimeUpperBound), toleratedForwardClockJump: bigIntJSON(this.toleratedForwardClockJump), last: this.last.toJSON() } } now () { return this.update(this.last) } validateOffset (offset) { if (this.toleratedForwardClockJump > 0n && -offset > this.toleratedForwardClockJump) { throw new ForwardJumpError(-offset, this.toleratedForwardClockJump) } if (this.maxOffset > 0n && offset > this.maxOffset) { throw new ClockOffsetError(offset, this.maxOffset) } } update (other) { const last = Timestamp.bigger(other, this.last) let wallTime = this.wallTime() const offset = last.wallTime - wallTime this.validateOffset(offset) let logical if (offset < 0n) { logical = 0 } else { wallTime = last.wallTime logical = last.logical + 1 if (logical > UINT32_MAX) { wallTime += 1n logical = 0 } } const maxWallTime = this.wallTimeUpperBound > 0n ? this.wallTimeUpperBound : UINT64_MAX if (wallTime > maxWallTime) { throw new WallTimeOverflowError(wallTime, maxWallTime) } this.last = new Timestamp(wallTime, logical) return this.last } } HLC.Timestamp = Timestamp HLC.WallTimeOverflowError = WallTimeOverflowError HLC.ClockOffsetError = ClockOffsetError HLC.ForwardJumpError = ForwardJumpError HLC.codec = codec module.exports = HLC