UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

396 lines (323 loc) • 10.7 kB
import { assert } from "../../../core/assert.js"; import { dec2hex } from "../../../core/binary/dec2hex.js"; import { hex2dec } from "../../../core/binary/hex2dec.js"; import { array_copy } from "../../../core/collection/array/array_copy.js"; import { randomBytes } from "../../../core/math/random/randomBytes.js"; import { randomUint8 } from "../../../core/math/random/randomUint8.js"; import { seededRandom } from "../../../core/math/random/seededRandom.js"; // Previous uuid creation time let _last_milliseconds = 0; let _last_nanoseconds = 0; // seed random with current time to lower potential correlation const random = seededRandom(Date.now()); // node and clockseq need to be initialized to random values. const _node_id = new Uint8Array(6); // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) randomBytes(_node_id, 0, random, 6); _node_id[0] |= 0x01; // Per 4.2.2, randomize (14 bit) clockseq let _clock_seq = ((randomUint8(random) << 8) | randomUint8(random)) & 0x3fff; /** * Universally Unique Identifier (UUID), a 128 bit label used to uniquely identify resources. * The default is Nil UUID, where all bits are set to 0, see {@link UUID.nil}. * Also known as GUID ( Globally Unique IDentifier). * * @see IETF RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 * @example * class Student{ * id: UUID * name: string * age: number * } * @example * const id_a = UUID.v1(); * const id_b = UUID.v1(); * * id_a.equals(id_b); // false * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class UUID { #data = new Uint8Array(16); /** * * @param {number[]|Uint8Array|ArrayLike<number>} bytes */ set data(bytes) { assert.isArrayLike(bytes, 'bytes'); assert.greaterThanOrEqual(bytes.length, 16, 'bytes.length < 16'); array_copy(bytes, 0, this.#data, 0, 16); } /** * * @returns {Uint8Array} */ get data() { return this.#data; } /** * The version number is in the most significant 4 bits of the timestamp (bits 4 through 7 of the time_hi_and_version field). * * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.3 * @return {number} * @example * UUID.v1().version === 1 * UUID.v4().version === 4 */ get version() { const bits = this.#data[6]; // using bits 4 through 7 return (bits >> 4) & 15; } /** * The nil UUID is special form of UUID that is specified to have all 128 bits set to zero. * Clears all bits of this UUID. * @returns {void} */ nil() { this.#data.fill(0); } /** * Generate Variant 1 UUID * @returns {void} */ v1() { // @see https://github.com/uuidjs/uuid/blob/8f028c4ea42ce41a9a9dc5fa634abe525b2e2066/src/v1.js#L17 const b = this.#data; let clock_seq = _clock_seq; // UUID timestamps are 100 nano-second units since the Gregorian epoch, // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. let msecs = Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock // cycle to simulate higher resolution clock let nanoseconds = _last_nanoseconds + 1; // Time since last uuid creation (in msecs) const dt = msecs - _last_milliseconds + (nanoseconds - _last_nanoseconds) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression if (dt < 0) { clock_seq = (clock_seq + 1) & 0x3fff; } // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new // time interval if ((dt < 0 || msecs > _last_milliseconds)) { nanoseconds = 0; } // Per 4.2.1.2 Throw error if too many uuids are requested if (nanoseconds >= 10000) { throw new Error(".v1(): Can't create more than 10M uuids/sec"); } _last_milliseconds = msecs; _last_nanoseconds = nanoseconds; _clock_seq = clock_seq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch msecs += 12219292800000; let i = 0 // `time_low` const tl = ((msecs & 0xfffffff) * 10000 + nanoseconds) % 0x100000000; b[i++] = (tl >>> 24) & 0xff; b[i++] = (tl >>> 16) & 0xff; b[i++] = (tl >>> 8) & 0xff; b[i++] = tl & 0xff; // `time_mid` const tmh = ((msecs / 0x100000000) * 10000) & 0xfffffff; b[i++] = (tmh >>> 8) & 0xff; b[i++] = tmh & 0xff; // `time_high_and_version` b[i++] = ((tmh >>> 24) & 0xf) | 0x10; // include version b[i++] = (tmh >>> 16) & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) b[i++] = (clock_seq >>> 8) | 0x80; // `clock_seq_low` b[i++] = clock_seq & 0xff; // `node` for (let n = 0; n < 6; ++n) { b[i + n] = _node_id[n]; } } /** * * @return {UUID} */ static v1() { const uuid = new UUID(); uuid.v1(); return uuid; } /** * Generate Variant 4 UUID (fully random) * @returns {void} */ v4() { const data = this.data; for (let i = 0; i < 16; i += 4) { const r = (random() * 4294967296) >>> 0; data[i] = r & 0xff; data[i + 1] = (r >>> 8) & 0xff; data[i + 2] = (r >>> 16) & 0xff; data[i + 3] = (r >>> 24) & 0xff; } // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` data[6] = (data[6] & 0x0f) | 0x40; data[8] = (data[8] & 0x3f) | 0x80; } /** * Variant 4 UUID generator (fully random) * @returns {UUID} */ static v4() { const r = new UUID(); r.v4(); return r; } /** * Shortcut to generate string-form ID * Uses v4(random) UUID * @return {string} */ static string() { return UUID.v4().toString(); } /*** * Parses standard UUID string of form AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE * @param {string} string String in UUID format */ parse(string) { assert.isString(string, 'string'); assert.greaterThanOrEqual(string.length, 36, 'string.length'); const arr = this.#data; // Parse ########-....-....-....-............ const v0 = hex2dec(string.slice(0, 8)); arr[0] = (v0) >>> 24; arr[1] = (v0 >>> 16) & 0xff; arr[2] = (v0 >>> 8) & 0xff; arr[3] = v0 & 0xff; // Parse ........-####-....-....-............ const v1 = hex2dec(string.slice(9, 13)); arr[4] = (v1) >>> 8; arr[5] = v1 & 0xff; // Parse ........-....-####-....-............ const v2 = hex2dec(string.slice(14, 18)); arr[6] = (v2) >>> 8; arr[7] = v2 & 0xff; // Parse ........-....-....-####-............ const v3 = hex2dec(string.slice(19, 23)); arr[8] = (v3) >>> 8; arr[9] = v3 & 0xff; // Parse ........-....-....-....-############ // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) const v4 = hex2dec(string.slice(24, 36)); arr[10] = (v4 / 0x10000000000) & 0xff; arr[11] = (v4 / 0x100000000) & 0xff; arr[12] = (v4 >>> 24) & 0xff; arr[13] = (v4 >>> 16) & 0xff; arr[14] = (v4 >>> 8) & 0xff; arr[15] = v4 & 0xff; } /** * * @param {string} string * @return {UUID} */ static parse(string) { const r = new UUID(); r.parse(string); return r; } /** * Standard UUID string in from: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE * Result is compatible with the {@link UUID.parse} method. * @example * a88bb73a-c89f-11ed-afa1-0242ac120002 * @returns {string} */ toString() { const bytes = this.#data; return ( dec2hex(bytes[0]) + dec2hex(bytes[1]) + dec2hex(bytes[2]) + dec2hex(bytes[3]) + '-' + dec2hex(bytes[4]) + dec2hex(bytes[5]) + '-' + dec2hex(bytes[6]) + dec2hex(bytes[7]) + '-' + dec2hex(bytes[8]) + dec2hex(bytes[9]) + '-' + dec2hex(bytes[10]) + dec2hex(bytes[11]) + dec2hex(bytes[12]) + dec2hex(bytes[13]) + dec2hex(bytes[14]) + dec2hex(bytes[15]) ); } /** * Sets this UUID to hold the same value as the other * @param {UUID} other */ copy(other) { this.#data.set(other.#data); } /** * * @return {UUID} */ clone() { const r = new UUID(); r.copy(this); return r; } /** * * @param {UUID} other * @returns {boolean} */ equals(other) { const this_data = this.#data; const other_data = other.#data; for (let i = 0; i < 16; i++) { if (this_data[i] !== other_data[i]) { return false; } } return true; } /** * Compute hash sum * @returns {number} 32bit integer value */ hash() { const data = this.#data; // use some non-metadata bytes as a hash, favoring speed as this is expected to be used a lot return data[3] // low byte of "time_low", very likely to differ between two UUIDs | data[5] << 8 // low byte of "time_mid" | data[9] << 16 // "clock_seq_low" | data[15] << 24 // low byte of "node" ; } } /** * @readonly */ UUID.prototype.toJSON = UUID.prototype.toString; /** * @readonly */ UUID.prototype.fromJSON = UUID.prototype.parse; /** * @readonly * @type {string} */ UUID.typeName = "UUID"; /** * @readonly * @type {boolean} */ UUID.prototype.isUUID = true;