UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

275 lines (274 loc) 8.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerClockVector = exports.ClockVector = exports.LogicalClock = exports.interval = exports.printTs = exports.containsId = exports.contains = exports.compare = exports.equal = exports.tick = exports.tss = exports.ts = exports.Timespan = exports.Timestamp = void 0; const enums_1 = require("../enums"); class Timestamp { constructor(sid, time) { this.sid = sid; this.time = time; } } exports.Timestamp = Timestamp; class Timespan { constructor(sid, time, span) { this.sid = sid; this.time = time; this.span = span; } } exports.Timespan = Timespan; /** * A factory function for creating a new timestamp. * * @param sid Session ID. * @param time Logical clock sequence number. * @returns A new timestamp. */ const ts = (sid, time) => new Timestamp(sid, time); exports.ts = ts; /** * A factory function for creating a new timespan. * * @param sid Session ID. * @param time Logical clock sequence number. * @param span Length of the timespan. * @returns A new timespan. */ const tss = (sid, time, span) => new Timespan(sid, time, span); exports.tss = tss; /** * Advance a timestamp by a number of cycles. * * @param stamp A reference timestamp. * @param cycles Number of cycles to advance. * @returns A new timestamp. */ const tick = (stamp, cycles) => (0, exports.ts)(stamp.sid, stamp.time + cycles); exports.tick = tick; /** * Compares for equality two timestamps, time first. * * @returns True if timestamps are equal. */ const equal = (ts1, ts2) => ts1.time === ts2.time && ts1.sid === ts2.sid; exports.equal = equal; /** * Compares two timestamps, time first. * * @returns 1 if current timestamp is larger, -1 if smaller, and 0 otherwise. */ const compare = (ts1, ts2) => { const t1 = ts1.time; const t2 = ts2.time; if (t1 > t2) return 1; if (t1 < t2) return -1; const s1 = ts1.sid; const s2 = ts2.sid; if (s1 > s2) return 1; if (s1 < s2) return -1; return 0; }; exports.compare = compare; /** * Checks if the first timespan contains the second timespan. * * @param ts1 Start of container timespan. * @param span1 Length of container timespan. * @param ts2 Start of contained timespan. * @param span2 Length of contained timespan. * @returns Returns true if the first timespan contains the second timespan. */ const contains = (ts1, span1, ts2, span2) => { if (ts1.sid !== ts2.sid) return false; const t1 = ts1.time; const t2 = ts2.time; if (t1 > t2) return false; if (t1 + span1 < t2 + span2) return false; return true; }; exports.contains = contains; /** * Checks if a timespan contains the `ts2` point. * * @param ts1 Start of container timespan. * @param span1 Length of container timespan. * @param ts2 A point in time. * @returns Returns true if the first timespan contains the `ts2` point. */ const containsId = (ts1, span1, ts2) => { if (ts1.sid !== ts2.sid) return false; const t1 = ts1.time; const t2 = ts2.time; if (t1 > t2) return false; if (t1 + span1 < t2 + 1) return false; return true; }; exports.containsId = containsId; /** * Returns a human-readable string representation of the timestamp. * * @param id A timestamp. * @returns Human-readable string representation of the timestamp. */ const printTs = (id) => { if (id.sid === enums_1.SESSION.SERVER) return '.' + id.time; let session = '' + id.sid; if (session.length > 4) session = '..' + session.slice(session.length - 4); return session + '.' + id.time; }; exports.printTs = printTs; /** * Advances a given timestamp by a number of cycles and then returns a timespan * starting from that position. * * @param ts A start timestamp. * @param tick Number of cycles to advance the starting timestamp. * @param span Length of the timespan. * @returns A new timespan. */ const interval = (ts, tick, span) => new Timespan(ts.sid, ts.time + tick, span); exports.interval = interval; /** * Represents a *Logical Clock*, which can be advanced by a number of cycles. */ class LogicalClock extends Timestamp { /** * Returns a new timestamp, which is the current clock value, and advances the * clock by a number of cycles. * * @param cycles Number of cycles to advance the clock. * @returns A new timestamp, which is the current clock value. */ tick(cycles) { const timestamp = new Timestamp(this.sid, this.time); this.time += cycles; return timestamp; } } exports.LogicalClock = LogicalClock; /** * Represents a clock vector, which is a local logical clock together with a set * of logical clocks of other peers. */ class ClockVector extends LogicalClock { constructor() { super(...arguments); /** * A set of logical clocks of other peers. */ this.peers = new Map(); } /** * Advances local time every time we see any timestamp with higher time value. * This is an idempotent method which can be called every time a new timestamp * is observed, it advances the local time only if the observed timestamp is * greater than the current local time. * * @param id The time stamp we observed. * @param span Length of the time span. */ observe(id, span) { // if (this.time < ts.time) throw new Error('TIME_TRAVEL'); const edge = id.time + span - 1; const sid = id.sid; if (sid !== this.sid) { const clock = this.peers.get(id.sid); if (!clock) this.peers.set(id.sid, (0, exports.ts)(sid, edge)); else if (edge > clock.time) clock.time = edge; } if (edge >= this.time) this.time = edge + 1; } /** * Returns a deep copy of the current vector clock with the same session ID. * * @returns A new vector clock, which is a clone of the current vector clock. */ clone() { return this.fork(this.sid); } /** * Returns a deep copy of the current vector clock with a different session ID. * * @param sessionId The session ID of the new vector clock. * @returns A new vector clock, which is a fork of the current vector clock. */ fork(sessionId) { const clock = new ClockVector(sessionId, this.time); if (sessionId !== this.sid) clock.observe((0, exports.tick)(this, -1), 1); // biome-ignore lint: using .forEach() on Map is the fastest way to iterate this.peers.forEach((peer) => { clock.observe(peer, 1); }); return clock; } /** * Returns a human-readable string representation of the clock vector. * * @param tab String to use for indentation. * @returns Human-readable string representation of the clock vector. */ toString(tab = '') { const last = this.peers.size; let i = 1; let lines = ''; // biome-ignore lint: using .forEach() on Map is the fastest way to iterate this.peers.forEach((clock) => { const isLast = i === last; lines += `\n${tab}${isLast ? '└─' : '├─'} ${clock.sid}.${clock.time}`; i++; }); return `clock ${this.sid}.${this.time}${lines}`; } } exports.ClockVector = ClockVector; /** * Implements a clock vector with a fixed session ID. The *server clock* * is used when the CRDT is powered by a central server. */ class ServerClockVector extends LogicalClock { constructor() { super(...arguments); /** A stub for other peers. Not used in the server clock. */ this.peers = new Map(); } observe(ts, span) { if (ts.sid > 8) throw new Error('INVALID_SERVER_SESSION'); if (this.time < ts.time) throw new Error('TIME_TRAVEL'); const time = ts.time + span; if (time > this.time) this.time = time; } clone() { return this.fork(); } fork() { return new ServerClockVector(enums_1.SESSION.SERVER, this.time); } /** * Returns a human-readable string representation of the clock vector. * * @returns Human-readable string representation of the clock vector. */ toString() { return `clock ${this.sid}.${this.time}`; } } exports.ServerClockVector = ServerClockVector;