@tai-kun/surrealdb
Version:
The SurrealDB SDK for JavaScript
322 lines (271 loc) • 9.57 kB
text/typescript
import {
Uuid as Base,
type UuidSource,
} from "@tai-kun/surrealdb/encodable-datatypes";
import { SurrealValueError, unreachable } from "@tai-kun/surrealdb/errors";
import { isValidBytes } from "../_internals/uuid";
export type * from "../encodable/uuid";
// const UUID_36_REGEX =
// /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
export type UuidVariant =
| "NIL" // 00000000-0000-0000-0000-000000000000
| "MAX" // ffffffff-ffff-ffff-ffff-ffffffffffff
| "NCS" // 0xxx (0b0000 ~ 0b0111)
| "RFC" // 10xx (0b1000 ~ 0b1011)
| "MS" // 110x (0b1100 ~ 0b1101)
| "RESERVED"; // 111x (0b1110 ~ 0b1111)
export type UuidVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7;
/**
* ```ts
* [
* "00", // BYTE_TO_HEX[0]
* "01", // BYTE_TO_HEX[1]
* ...
* "fe", // BYTE_TO_HEX[254]
* "ff", // BYTE_TO_HEX[255]
* ]
* ```
*/
const BYTE_TO_HEX = /* @__PURE__ */ Array.from(
{ length: 256 },
(_, i) => (i + 0x100).toString(16).slice(1),
);
/**
* ```ts
* [
* [
* 0x00, // HEX_TO_BYTES[0][0]
* 0x01, // HEX_TO_BYTES[0][1]
* ...
* ],
* ...
* [
* ...
* 0xfe, // HEX_TO_BYTES[15][14]
* 0xff, // HEX_TO_BYTES[15][15]
* ],
* ]
* ```
*/
const HEX_TO_BYTES = /* @__PURE__ */ Array.from(
{ length: 16 },
(_, i) => Array.from({ length: 16 }, (_, j) => i * 16 + j),
);
function parseUuid36(uuid: string): Uint8Array {
// if (uuid === "00000000-0000-0000-0000-000000000000") {
// return new Uint8Array(16);
// }
// if (uuid === "ffffffff-ffff-ffff-ffff-ffffffffffff") {
// return new Uint8Array(16).fill(0xff);
// }
if (uuid.length !== 36) {
throw new SurrealValueError("a valid uuid", uuid);
}
const bytes = new Uint8Array(16);
for (let a, b, i = 0, j = 0; i < 36; i++) {
if ((a = uuid.charCodeAt(i)) !== 45 /* "-" */) {
b = uuid.charCodeAt(++i);
a = a < 58 ? (a - 48) : (a < 71 ? (a - 55) : (a - 87));
b = b < 58 ? (b - 48) : (b < 71 ? (b - 55) : (b - 87));
bytes[j++] = HEX_TO_BYTES[a]![b]!;
}
}
return bytes;
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/data/uuid)
*/
export default class Uuid extends Base {
protected _variant: UuidVariant | undefined;
protected _version: UuidVersion | null | undefined;
protected _timestamp: number | null | undefined;
override get bytes(): Uint8Array {
return this._bytes;
}
override set bytes(v: Uint8Array) {
if (!isValidBytes(v)) {
throw new SurrealValueError("a valid uuid", v);
}
this._bytes = v;
// Clear cache
this._variant = undefined;
this._version = undefined;
this._timestamp = undefined;
}
constructor(source: UuidSource | string) {
super(typeof source === "string" ? parseUuid36(source) : source);
}
get variant(): UuidVariant {
if (this._variant === undefined) {
const bytes = this.bytes;
const v = bytes[8]! >>> 4;
switch (true) {
case v < 0b0000:
unreachable();
case v === 0b0000 && bytes.every(b => b === 0x00):
this._variant = "NIL";
break;
case v <= 0b0111:
this._variant = "NCS";
break;
case v <= 0b1011:
this._variant = "RFC";
break;
case v <= 0b1101:
this._variant = "MS";
break;
case v === 0b1111 && bytes.every(b => b === 0xff):
this._variant = "MAX";
break;
case v <= 0b1111:
this._variant = "RESERVED";
break;
default:
unreachable();
}
}
return this._variant;
}
get version(): UuidVersion | null {
if (this._version === undefined) {
this._version = this.variant === "RFC"
? (this.bytes[6]! >>> 4) as UuidVersion
: null;
}
return this._version;
}
get timestamp(): number | null {
if (this._timestamp === undefined) {
this._timestamp = unsafe_getUnixMillisecondsFromUuid(this);
}
return this._timestamp;
}
clone(): this {
const This = this.constructor as typeof Uuid;
return new This(this.bytes.slice()) as this;
}
// TODO(tai-kun): 時間でイコール判定? bytes でイコール判定?
// equals(other): boolean {}
compareTo(
other: {
readonly bytes: Uint8Array;
readonly version: UuidVersion | null;
readonly timestamp?: number | null;
},
): -1 | 0 | 1 {
let a = this.timestamp, b: number | null;
if (a === null) {
return 0;
}
b = "timestamp" in other
? other.timestamp
: unsafe_getUnixMillisecondsFromUuid(other);
if (b === null) {
return 0;
}
return a < b ? -1 : a > b ? 1 : 0;
}
}
function unsafe_getUnixMillisecondsFromUuid(
uuid: {
readonly bytes: Uint8Array;
readonly version: UuidVersion | null;
},
): number | null {
switch (uuid.version) {
case 1:
return unsafe_getUnixMillisecondsFromUuidV1(uuid.bytes);
case 6:
return unsafe_getUnixMillisecondsFromUuidV6(uuid.bytes);
case 7:
return unsafe_getUnixMillisecondsFromUuidV7(uuid.bytes);
default:
return null;
}
}
// グレゴリオ暦の開始日時: 1582-10-15T00:00:00.000Z
const GREGORIAN_OFFSET = 122_192_928_000_000_000n;
function unsafe_getUnixMillisecondsFromUuidV1(bytes: Uint8Array): number {
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | time_low |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | time_mid | time_high_and_version |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |clk_seq_hi_res | clk_seq_low | node (0-1) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | node (2-5) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
const hex = "0x"
// ________-____-1XXX-____-____________
+ BYTE_TO_HEX[bytes[6]!]![1]
+ BYTE_TO_HEX[bytes[7]!]
// ________-XXXX-1___-____-____________
+ BYTE_TO_HEX[bytes[4]!]
+ BYTE_TO_HEX[bytes[5]!]
// XXXXXXXX-____-1___-____-____________
+ BYTE_TO_HEX[bytes[0]!]
+ BYTE_TO_HEX[bytes[1]!]
+ BYTE_TO_HEX[bytes[2]!]
+ BYTE_TO_HEX[bytes[3]!];
const timestamp = BigInt(hex);
const fromUnixEpoch = timestamp - GREGORIAN_OFFSET;
const unixMilliseconds = fromUnixEpoch / 10_000n; // 100ns -> ms
return Number(unixMilliseconds);
}
function unsafe_getUnixMillisecondsFromUuidV6(bytes: Uint8Array): number {
// https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html_name-uuid-version-6
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | time_high |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | time_mid | time_low_and_version |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |clk_seq_hi_res | clk_seq_low | node (0-1) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | node (2-5) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
const hex = "0x"
// XXXXXXXX-____-6___-____-____________
+ BYTE_TO_HEX[bytes[0]!]
+ BYTE_TO_HEX[bytes[1]!]
+ BYTE_TO_HEX[bytes[2]!]
+ BYTE_TO_HEX[bytes[3]!]
// ________-XXXX-6___-____-____________
+ BYTE_TO_HEX[bytes[4]!]
+ BYTE_TO_HEX[bytes[5]!]
// ________-____-6XXX-____-____________
+ BYTE_TO_HEX[bytes[6]!]![1]
+ BYTE_TO_HEX[bytes[7]!];
const timestamp = BigInt(hex);
const fromUnixEpoch = timestamp - GREGORIAN_OFFSET;
const unixMilliseconds = fromUnixEpoch / 10_000n; // 100ns -> ms
return Number(unixMilliseconds);
}
function unsafe_getUnixMillisecondsFromUuidV7(bytes: Uint8Array): number {
// https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html_name-uuid-version-7
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | unix_ts_ms |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | unix_ts_ms | ver | rand_a |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |var| rand_b |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | rand_b |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
const hex = "0x"
// XXXXXXXX-____-7___-____-____________
+ BYTE_TO_HEX[bytes[0]!]
+ BYTE_TO_HEX[bytes[1]!]
+ BYTE_TO_HEX[bytes[2]!]
+ BYTE_TO_HEX[bytes[3]!]
// ________-XXXX-7___-____-____________
+ BYTE_TO_HEX[bytes[4]!]
+ BYTE_TO_HEX[bytes[5]!];
const unixMilliseconds = parseInt(hex);
return unixMilliseconds;
}