Comparing version 0.2.1 to 0.3.0-beta.1
@@ -26,10 +26,4 @@ /** | ||
*/ | ||
static ofInner(bytes: Uint8Array): Scru64Id; | ||
static ofInner(bytes: Readonly<Uint8Array>): Scru64Id; | ||
/** | ||
* Returns the 12-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toString(): string; | ||
/** | ||
* Creates an object from a 12-digit string representation. | ||
@@ -50,2 +44,16 @@ * | ||
private static fromDigitValues; | ||
/** | ||
* Returns the 12-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toString(): string; | ||
/** | ||
* Creates a value from the `timestamp` and the combined `nodeCtr` field | ||
* value. | ||
* | ||
* @throws RangeError if any argument is out of the valid value range. | ||
* @category Conversion | ||
*/ | ||
static fromParts(timestamp: number, nodeCtr: number): Scru64Id; | ||
/** Returns the `timestamp` field value. */ | ||
@@ -59,16 +67,14 @@ get timestamp(): number; | ||
/** | ||
* Creates a value from the `timestamp` and the combined `nodeCtr` field | ||
* value. | ||
* Creates an object from a 64-bit unsigned integer. | ||
* | ||
* @throws RangeError if any argument is out of the valid value range. | ||
* @throws RangeError if the argument is out of the valid value range. | ||
* @category Conversion | ||
*/ | ||
static fromParts(timestamp: number, nodeCtr: number): Scru64Id; | ||
static fromBigInt(value: bigint): Scru64Id; | ||
/** | ||
* Returns the 64-bit unsigned integer representation as a 16-digit | ||
* hexadecimal string prefixed with "0x". | ||
* Returns the 64-bit unsigned integer representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toHex(): string; | ||
toBigInt(): bigint; | ||
/** Represents `this` in JSON as a 12-digit canonical string. */ | ||
@@ -108,9 +114,9 @@ toJSON(): string; | ||
* All of these methods return monotonically increasing IDs unless a timestamp | ||
* provided is significantly (by default, approx. 10 seconds or more) smaller | ||
* than the one embedded in the immediately preceding ID. If such a significant | ||
* clock rollback is detected, (1) the `generate` (OrAbort) method aborts and | ||
* returns `undefined`; (2) the `OrReset` variants reset the generator and | ||
* return a new ID based on the given timestamp; and, (3) the `OrSleep` and | ||
* `OrAwait` methods sleep and wait for the next timestamp tick. The `Core` | ||
* functions offer low-level primitives. | ||
* provided is significantly (by default, approx. 10 seconds) smaller than the | ||
* one embedded in the immediately preceding ID. If such a significant clock | ||
* rollback is detected, (1) the `generate` (OrAbort) method aborts and returns | ||
* `undefined`; (2) the `OrReset` variants reset the generator and return a new | ||
* ID based on the given timestamp; and, (3) the `OrSleep` and `OrAwait` methods | ||
* sleep and wait for the next timestamp tick. The `Core` functions offer | ||
* low-level primitives. | ||
*/ | ||
@@ -120,33 +126,29 @@ export declare class Scru64Generator { | ||
private prevNodeCtr; | ||
private counterSize; | ||
private readonly counterSize; | ||
private readonly counterMode; | ||
/** | ||
* Creates a generator with a node configuration. | ||
* Creates a new generator with the given node configuration and counter mode. | ||
* | ||
* The `nodeId` must fit in `nodeIdSize` bits, where `nodeIdSize` ranges from | ||
* 1 to 23, inclusive. | ||
* | ||
* @throws RangeError if the arguments represent an invalid node | ||
* configuration. | ||
* @throws `SyntaxError` if an invalid string `nodeSpec` is passed or | ||
* `RangeError` if an invalid object `nodeSpec` is passed. | ||
*/ | ||
constructor(nodeId: number, nodeIdSize: number); | ||
constructor(nodeSpec: NodeSpec, counterMode?: CounterMode); | ||
/** Returns the `nodeId` of the generator. */ | ||
getNodeId(): number; | ||
/** | ||
* Creates a generator by parsing a node spec string that describes the node | ||
* configuration. | ||
* | ||
* A node spec string consists of `nodeId` and `nodeIdSize` separated by a | ||
* slash (e.g., `"42/8"`, `"12345/16"`). | ||
* | ||
* @throws Error if the node spec does not conform to the valid syntax or | ||
* represents an invalid node configuration. | ||
* Returns the `nodePrev` value if the generator is constructed with one or | ||
* `undefined` otherwise. | ||
*/ | ||
static parse(nodeSpec: string): Scru64Generator; | ||
/** Returns the `nodeId` of the generator. */ | ||
getNodeId(): number; | ||
getNodePrev(): Scru64Id | undefined; | ||
/** Returns the size in bits of the `nodeId` adopted by the generator. */ | ||
getNodeIdSize(): number; | ||
/** | ||
* Returns the node configuration specifier describing the generator state. | ||
*/ | ||
getNodeSpec(): string; | ||
/** | ||
* Calculates the combined `nodeCtr` field value for the next `timestamp` | ||
* tick. | ||
*/ | ||
private initNodeCtr; | ||
private renewNodeCtr; | ||
/** | ||
@@ -209,7 +211,104 @@ * Generates a new SCRU64 ID object from the current `timestamp`, or returns | ||
/** | ||
* Represents a node configuration specifier used to build a | ||
* {@link Scru64Generator}. | ||
* | ||
* A `NodeSpec` is usually expressed as a node spec string, which starts with a | ||
* decimal `nodeId`, a hexadecimal `nodeId` prefixed with `"0x"`, or a 12-digit | ||
* `nodePrev` SCRU64 ID value, followed by a slash and a decimal `nodeIdSize` | ||
* value ranging from 1 to 23 (e.g., `"42/8"`, `"0xb00/12"`, `"0u2r85hm2pt3/16"`). | ||
* The first and second forms create a fresh new generator with the given | ||
* `nodeId`, while the third form constructs one that generates subsequent | ||
* SCRU64 IDs to the `nodePrev`. | ||
*/ | ||
export type NodeSpec = string | { | ||
nodeId: number; | ||
nodeIdSize: number; | ||
} | { | ||
nodePrev: Scru64Id; | ||
nodeIdSize: number; | ||
}; | ||
/** | ||
* An interface of objects to customize the initial counter value for each new | ||
* `timestamp`. | ||
* | ||
* {@link Scru64Generator} calls `renew()` to obtain the initial counter value | ||
* when the `timestamp` field has changed since the immediately preceding ID. | ||
* Types implementing this interface may apply their respective logic to | ||
* calculate the initial counter value. | ||
*/ | ||
export type CounterMode = { | ||
/** | ||
* Returns the next initial counter value of `counterSize` bits. | ||
* | ||
* {@link Scru64Generator} passes the `counterSize` (from 1 to 23) and other | ||
* context information that may be useful for counter renewal. The returned | ||
* value must be within the range of `counterSize`-bit unsigned integer. | ||
*/ | ||
renew(counterSize: number, context: { | ||
timestamp: number; | ||
nodeId: number; | ||
}): number; | ||
}; | ||
/** | ||
* The default "initialize a portion counter" strategy. | ||
* | ||
* With this strategy, the counter is reset to a random number for each new | ||
* `timestamp` tick, but some specified leading bits are set to zero to reserve | ||
* space as the counter overflow guard. | ||
* | ||
* Note that the random number generator employed is not cryptographically | ||
* strong. This mode does not pay for security because a small random number is | ||
* insecure anyway. | ||
*/ | ||
export declare class DefaultCounterMode { | ||
private readonly overflowGuardSize; | ||
/** Creates a new instance with the size (in bits) of overflow guard bits. */ | ||
constructor(overflowGuardSize: number); | ||
/** Returns the next initial counter value of `counterSize` bits. */ | ||
renew(counterSize: number, context: {}): number; | ||
} | ||
/** | ||
* The gateway object that forwards supported method calls to the process-wide | ||
* global generator. | ||
*/ | ||
export declare class GlobalGenerator { | ||
private constructor(); | ||
/** | ||
* Initializes the global generator, if not initialized, with the node spec | ||
* passed. | ||
* | ||
* This method tries to configure the global generator with the argument only | ||
* when the global generator is not yet initialized. Otherwise, it preserves | ||
* the existing configuration. | ||
* | ||
* @throws `SyntaxError` or `RangeError` according to the semantics of | ||
* {@link Scru64Generator.constructor | new Scru64Generator(nodeSpec)} if the | ||
* argument represents an invalid node spec. | ||
* @returns `true` if this method configures the global generator or `false` | ||
* if it preserves the existing configuration. | ||
*/ | ||
static initialize(nodeSpec: NodeSpec): boolean; | ||
/** Calls {@link Scru64Generator.generate} of the global generator. */ | ||
static generate(): Scru64Id | undefined; | ||
/** Calls {@link Scru64Generator.generateOrSleep} of the global generator. */ | ||
static generateOrSleep(): Scru64Id; | ||
/** Calls {@link Scru64Generator.generateOrAwait} of the global generator. */ | ||
static generateOrAwait(): Promise<Scru64Id>; | ||
/** Calls {@link Scru64Generator.getNodeId} of the global generator. */ | ||
static getNodeId(): number; | ||
/** Calls {@link Scru64Generator.getNodePrev} of the global generator. */ | ||
static getNodePrev(): Scru64Id | undefined; | ||
/** Calls {@link Scru64Generator.getNodeIdSize} of the global generator. */ | ||
static getNodeIdSize(): number; | ||
/** Calls {@link Scru64Generator.getNodeSpec} of the global generator. */ | ||
static getNodeSpec(): string; | ||
} | ||
/** | ||
* Generates a new SCRU64 ID object using the global generator. | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -220,4 +319,3 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
@@ -229,5 +327,7 @@ export declare const scru64Sync: () => Scru64Id; | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -238,4 +338,3 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
@@ -246,5 +345,7 @@ export declare const scru64StringSync: () => string; | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -254,4 +355,3 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
@@ -263,5 +363,7 @@ export declare const scru64: () => Promise<Scru64Id>; | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -271,5 +373,4 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
export declare const scru64String: () => Promise<string>; |
@@ -61,31 +61,2 @@ /** | ||
/** | ||
* Returns the 12-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toString() { | ||
const dst = new Uint8Array(12); | ||
let minIndex = 99; // any number greater than size of output array | ||
for (let i = -2; i < 8; i += 5) { | ||
// implement Base36 using 40-bit words | ||
let carry = this.subUint(i < 0 ? 0 : i, i + 5); | ||
// iterate over output array from right to left while carry != 0 but at | ||
// least up to place already filled | ||
let j = dst.length - 1; | ||
for (; carry > 0 || j > minIndex; j--) { | ||
console.assert(j >= 0); | ||
carry += dst[j] * 1099511627776; | ||
const quo = Math.trunc(carry / 36); | ||
dst[j] = carry - quo * 36; // remainder | ||
carry = quo; | ||
} | ||
minIndex = j; | ||
} | ||
let text = ""; | ||
for (const d of dst) { | ||
text += DIGITS.charAt(d); | ||
} | ||
return text; | ||
} | ||
/** | ||
* Creates an object from a 12-digit string representation. | ||
@@ -145,12 +116,30 @@ * | ||
} | ||
/** Returns the `timestamp` field value. */ | ||
get timestamp() { | ||
return this.subUint(0, 5); | ||
} | ||
/** | ||
* Returns the `nodeId` and `counter` field values combined as a single | ||
* integer. | ||
* Returns the 12-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
get nodeCtr() { | ||
return this.subUint(5, 8); | ||
toString() { | ||
const dst = new Uint8Array(12); | ||
let minIndex = 99; // any number greater than size of output array | ||
for (let i = -2; i < 8; i += 5) { | ||
// implement Base36 using 40-bit words | ||
let carry = this.subUint(i < 0 ? 0 : i, i + 5); | ||
// iterate over output array from right to left while carry != 0 but at | ||
// least up to place already filled | ||
let j = dst.length - 1; | ||
for (; carry > 0 || j > minIndex; j--) { | ||
console.assert(j >= 0); | ||
carry += dst[j] * 1099511627776; | ||
const quo = Math.trunc(carry / 36); | ||
dst[j] = carry - quo * 36; // remainder | ||
carry = quo; | ||
} | ||
minIndex = j; | ||
} | ||
let text = ""; | ||
for (const d of dst) { | ||
text += DIGITS.charAt(d); | ||
} | ||
return text; | ||
} | ||
@@ -184,19 +173,43 @@ /** | ||
bytes[7] = nodeCtr; | ||
return new Scru64Id(bytes); | ||
// upper bound check is necessary when `timestamp` is at max | ||
return timestamp === MAX_TIMESTAMP | ||
? Scru64Id.ofInner(bytes) | ||
: new Scru64Id(bytes); | ||
} | ||
/** Returns the `timestamp` field value. */ | ||
get timestamp() { | ||
return this.subUint(0, 5); | ||
} | ||
/** | ||
* Returns the 64-bit unsigned integer representation as a 16-digit | ||
* hexadecimal string prefixed with "0x". | ||
* Returns the `nodeId` and `counter` field values combined as a single | ||
* integer. | ||
*/ | ||
get nodeCtr() { | ||
return this.subUint(5, 8); | ||
} | ||
/** | ||
* Creates an object from a 64-bit unsigned integer. | ||
* | ||
* @throws RangeError if the argument is out of the valid value range. | ||
* @category Conversion | ||
*/ | ||
toHex() { | ||
const digits = "0123456789abcdef"; | ||
let text = "0x"; | ||
for (const e of this.bytes) { | ||
text += digits.charAt(e >>> 4); | ||
text += digits.charAt(e & 0xf); | ||
static fromBigInt(value) { | ||
if (value < 0 || value >> BigInt(64) > 0) { | ||
throw new RangeError("out of 64-bit value range"); | ||
} | ||
return text; | ||
const bytes = new Uint8Array(8); | ||
for (let i = 7; i >= 0; i--) { | ||
bytes[i] = Number(value & BigInt(0xff)); | ||
value >>= BigInt(8); | ||
} | ||
return Scru64Id.ofInner(bytes); | ||
} | ||
/** | ||
* Returns the 64-bit unsigned integer representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toBigInt() { | ||
return this.bytes.reduce((acc, curr) => (acc << BigInt(8)) | BigInt(curr), BigInt(0)); | ||
} | ||
/** Represents `this` in JSON as a 12-digit canonical string. */ | ||
@@ -256,51 +269,70 @@ toJSON() { | ||
* All of these methods return monotonically increasing IDs unless a timestamp | ||
* provided is significantly (by default, approx. 10 seconds or more) smaller | ||
* than the one embedded in the immediately preceding ID. If such a significant | ||
* clock rollback is detected, (1) the `generate` (OrAbort) method aborts and | ||
* returns `undefined`; (2) the `OrReset` variants reset the generator and | ||
* return a new ID based on the given timestamp; and, (3) the `OrSleep` and | ||
* `OrAwait` methods sleep and wait for the next timestamp tick. The `Core` | ||
* functions offer low-level primitives. | ||
* provided is significantly (by default, approx. 10 seconds) smaller than the | ||
* one embedded in the immediately preceding ID. If such a significant clock | ||
* rollback is detected, (1) the `generate` (OrAbort) method aborts and returns | ||
* `undefined`; (2) the `OrReset` variants reset the generator and return a new | ||
* ID based on the given timestamp; and, (3) the `OrSleep` and `OrAwait` methods | ||
* sleep and wait for the next timestamp tick. The `Core` functions offer | ||
* low-level primitives. | ||
*/ | ||
export class Scru64Generator { | ||
/** | ||
* Creates a generator with a node configuration. | ||
* Creates a new generator with the given node configuration and counter mode. | ||
* | ||
* The `nodeId` must fit in `nodeIdSize` bits, where `nodeIdSize` ranges from | ||
* 1 to 23, inclusive. | ||
* | ||
* @throws RangeError if the arguments represent an invalid node | ||
* configuration. | ||
* @throws `SyntaxError` if an invalid string `nodeSpec` is passed or | ||
* `RangeError` if an invalid object `nodeSpec` is passed. | ||
*/ | ||
constructor(nodeId, nodeIdSize) { | ||
if (nodeIdSize <= 0 || | ||
constructor(nodeSpec, counterMode) { | ||
let errType = RangeError; | ||
if (typeof nodeSpec === "string") { | ||
// convert string `nodeSpec` to object | ||
errType = SyntaxError; | ||
const m = nodeSpec.match(/^(?:([0-9a-z]{12})|([0-9]{1,8}|0x[0-9a-f]{1,6}))\/([0-9]{1,3})$/i); | ||
if (m === null) { | ||
throw new errType('could not parse string as node spec (expected: e.g., "42/8", "0xb00/12", "0u2r85hm2pt3/16")'); | ||
} | ||
else if (typeof m[1] === "string") { | ||
nodeSpec = { | ||
nodePrev: Scru64Id.fromString(m[1]), | ||
nodeIdSize: parseInt(m[3], 10), | ||
}; | ||
} | ||
else if (typeof m[2] === "string") { | ||
nodeSpec = { | ||
nodeId: parseInt(m[2]), | ||
nodeIdSize: parseInt(m[3], 10), | ||
}; | ||
} | ||
else { | ||
throw new Error("unreachable"); | ||
} | ||
} | ||
// process object `nodeSpec` | ||
const nodeIdSize = nodeSpec.nodeIdSize; | ||
if (nodeIdSize < 1 || | ||
nodeIdSize >= NODE_CTR_SIZE || | ||
!Number.isInteger(nodeIdSize)) { | ||
throw new RangeError("`nodeIdSize` must range from 1 to 23"); | ||
throw new errType(`\`nodeIdSize\` (${nodeIdSize}) must range from 1 to 23`); | ||
} | ||
else if (nodeId < 0 || | ||
nodeId >= 1 << nodeIdSize || | ||
!Number.isInteger(nodeId)) { | ||
throw new RangeError("`nodeId` must fit in `nodeIdSize` bits"); | ||
} | ||
this.counterSize = NODE_CTR_SIZE - nodeIdSize; | ||
this.prevTimestamp = 0; | ||
this.prevNodeCtr = nodeId << this.counterSize; | ||
} | ||
/** | ||
* Creates a generator by parsing a node spec string that describes the node | ||
* configuration. | ||
* | ||
* A node spec string consists of `nodeId` and `nodeIdSize` separated by a | ||
* slash (e.g., `"42/8"`, `"12345/16"`). | ||
* | ||
* @throws Error if the node spec does not conform to the valid syntax or | ||
* represents an invalid node configuration. | ||
*/ | ||
static parse(nodeSpec) { | ||
const m = nodeSpec.match(/^([0-9]{1,10})\/([0-9]{1,3})$/); | ||
if (m === null) { | ||
throw new SyntaxError("invalid `nodeSpec`; it looks like: `42/8`, `12345/16`"); | ||
if ("nodePrev" in nodeSpec && typeof nodeSpec.nodePrev === "object") { | ||
this.prevTimestamp = nodeSpec.nodePrev.timestamp; | ||
this.prevNodeCtr = nodeSpec.nodePrev.nodeCtr; | ||
} | ||
return new Scru64Generator(parseInt(m[1], 10), parseInt(m[2], 10)); | ||
else if ("nodeId" in nodeSpec && typeof nodeSpec.nodeId === "number") { | ||
this.prevTimestamp = 0; | ||
const nodeId = nodeSpec.nodeId; | ||
if (nodeId < 0 || | ||
nodeId >= 1 << nodeIdSize || | ||
!Number.isInteger(nodeId)) { | ||
throw new errType(`\`nodeId\` (${nodeId}) must fit in \`nodeIdSize\` (${nodeIdSize}) bits`); | ||
} | ||
this.prevNodeCtr = nodeId << this.counterSize; | ||
} | ||
else { | ||
throw new errType("invalid `nodeSpec` argument"); | ||
} | ||
// reserve one overflow guard bit if `counterSize` is four or less | ||
this.counterMode = | ||
counterMode !== null && counterMode !== void 0 ? counterMode : new DefaultCounterMode(this.counterSize <= 4 ? 1 : 0); | ||
} | ||
@@ -311,2 +343,14 @@ /** Returns the `nodeId` of the generator. */ | ||
} | ||
/** | ||
* Returns the `nodePrev` value if the generator is constructed with one or | ||
* `undefined` otherwise. | ||
*/ | ||
getNodePrev() { | ||
if (this.prevTimestamp > 0) { | ||
return Scru64Id.fromParts(this.prevTimestamp, this.prevNodeCtr); | ||
} | ||
else { | ||
return undefined; | ||
} | ||
} | ||
/** Returns the size in bits of the `nodeId` adopted by the generator. */ | ||
@@ -317,11 +361,22 @@ getNodeIdSize() { | ||
/** | ||
* Returns the node configuration specifier describing the generator state. | ||
*/ | ||
getNodeSpec() { | ||
const nodePrev = this.getNodePrev(); | ||
return nodePrev !== undefined | ||
? `${nodePrev.toString()}/${this.getNodeIdSize()}` | ||
: `${this.getNodeId()}/${this.getNodeIdSize()}`; | ||
} | ||
/** | ||
* Calculates the combined `nodeCtr` field value for the next `timestamp` | ||
* tick. | ||
*/ | ||
initNodeCtr() { | ||
// initialize counter at `counter_size - 1`-bit random number | ||
const OVERFLOW_GUARD_SIZE = 1; | ||
const limit = 1 << (this.counterSize - OVERFLOW_GUARD_SIZE); | ||
const counter = Math.trunc(Math.random() * limit); | ||
return (this.getNodeId() << this.counterSize) | counter; | ||
renewNodeCtr(timestamp) { | ||
const nodeId = this.getNodeId(); | ||
const context = { timestamp, nodeId }; | ||
const counter = this.counterMode.renew(this.counterSize, context); | ||
if (counter >= 1 << this.counterSize) { | ||
throw new Error("illegal `CounterMode` implementation"); | ||
} | ||
return (nodeId << this.counterSize) | counter; | ||
} | ||
@@ -403,3 +458,3 @@ /** | ||
this.prevTimestamp = Math.trunc(unixTsMs / 0x100); | ||
this.prevNodeCtr = this.initNodeCtr(); | ||
this.prevNodeCtr = this.renewNodeCtr(this.prevTimestamp); | ||
return Scru64Id.fromParts(this.prevTimestamp, this.prevNodeCtr); | ||
@@ -430,5 +485,5 @@ } | ||
this.prevTimestamp = timestamp; | ||
this.prevNodeCtr = this.initNodeCtr(); | ||
this.prevNodeCtr = this.renewNodeCtr(this.prevTimestamp); | ||
} | ||
else if (timestamp + allowance > this.prevTimestamp) { | ||
else if (timestamp + allowance >= this.prevTimestamp) { | ||
// go on with previous timestamp if new one is not much smaller | ||
@@ -442,3 +497,3 @@ const counterMask = (1 << this.counterSize) - 1; | ||
this.prevTimestamp++; | ||
this.prevNodeCtr = this.initNodeCtr(); | ||
this.prevNodeCtr = this.renewNodeCtr(this.prevTimestamp); | ||
} | ||
@@ -453,18 +508,103 @@ } | ||
} | ||
let globalGenerator = undefined; | ||
/** | ||
* The default "initialize a portion counter" strategy. | ||
* | ||
* With this strategy, the counter is reset to a random number for each new | ||
* `timestamp` tick, but some specified leading bits are set to zero to reserve | ||
* space as the counter overflow guard. | ||
* | ||
* Note that the random number generator employed is not cryptographically | ||
* strong. This mode does not pay for security because a small random number is | ||
* insecure anyway. | ||
*/ | ||
export class DefaultCounterMode { | ||
/** Creates a new instance with the size (in bits) of overflow guard bits. */ | ||
constructor(overflowGuardSize) { | ||
this.overflowGuardSize = overflowGuardSize; | ||
if (overflowGuardSize < 0 || !Number.isInteger(overflowGuardSize)) { | ||
throw new RangeError("`overflowGuardSize` must be an unsigned integer"); | ||
} | ||
} | ||
/** Returns the next initial counter value of `counterSize` bits. */ | ||
renew(counterSize, context) { | ||
const k = Math.max(0, counterSize - this.overflowGuardSize); | ||
return Math.trunc(Math.random() * (1 << k)); | ||
} | ||
} | ||
let globalGen = undefined; | ||
const getGlobalGenerator = () => { | ||
if (globalGenerator === undefined) { | ||
if (globalGen === undefined) { | ||
if (typeof SCRU64_NODE_SPEC === "undefined") { | ||
throw new Error("scru64: could not read config from SCRU64_NODE_SPEC global var"); | ||
} | ||
globalGenerator = Scru64Generator.parse(SCRU64_NODE_SPEC); | ||
globalGen = new Scru64Generator(SCRU64_NODE_SPEC); | ||
} | ||
return globalGenerator; | ||
return globalGen; | ||
}; | ||
/** | ||
* The gateway object that forwards supported method calls to the process-wide | ||
* global generator. | ||
*/ | ||
export class GlobalGenerator { | ||
constructor() { } | ||
/** | ||
* Initializes the global generator, if not initialized, with the node spec | ||
* passed. | ||
* | ||
* This method tries to configure the global generator with the argument only | ||
* when the global generator is not yet initialized. Otherwise, it preserves | ||
* the existing configuration. | ||
* | ||
* @throws `SyntaxError` or `RangeError` according to the semantics of | ||
* {@link Scru64Generator.constructor | new Scru64Generator(nodeSpec)} if the | ||
* argument represents an invalid node spec. | ||
* @returns `true` if this method configures the global generator or `false` | ||
* if it preserves the existing configuration. | ||
*/ | ||
static initialize(nodeSpec) { | ||
if (globalGen === undefined) { | ||
globalGen = new Scru64Generator(nodeSpec); | ||
return true; | ||
} | ||
else { | ||
return false; | ||
} | ||
} | ||
/** Calls {@link Scru64Generator.generate} of the global generator. */ | ||
static generate() { | ||
return getGlobalGenerator().generate(); | ||
} | ||
/** Calls {@link Scru64Generator.generateOrSleep} of the global generator. */ | ||
static generateOrSleep() { | ||
return getGlobalGenerator().generateOrSleep(); | ||
} | ||
/** Calls {@link Scru64Generator.generateOrAwait} of the global generator. */ | ||
static async generateOrAwait() { | ||
return getGlobalGenerator().generateOrAwait(); | ||
} | ||
/** Calls {@link Scru64Generator.getNodeId} of the global generator. */ | ||
static getNodeId() { | ||
return getGlobalGenerator().getNodeId(); | ||
} | ||
/** Calls {@link Scru64Generator.getNodePrev} of the global generator. */ | ||
static getNodePrev() { | ||
return getGlobalGenerator().getNodePrev(); | ||
} | ||
/** Calls {@link Scru64Generator.getNodeIdSize} of the global generator. */ | ||
static getNodeIdSize() { | ||
return getGlobalGenerator().getNodeIdSize(); | ||
} | ||
/** Calls {@link Scru64Generator.getNodeSpec} of the global generator. */ | ||
static getNodeSpec() { | ||
return getGlobalGenerator().getNodeSpec(); | ||
} | ||
} | ||
/** | ||
* Generates a new SCRU64 ID object using the global generator. | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -475,6 +615,5 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
export const scru64Sync = () => getGlobalGenerator().generateOrSleep(); | ||
export const scru64Sync = () => GlobalGenerator.generateOrSleep(); | ||
/** | ||
@@ -484,5 +623,7 @@ * Generates a new SCRU64 ID encoded in the 12-digit canonical string | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -493,4 +634,3 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
@@ -501,5 +641,7 @@ export const scru64StringSync = () => scru64Sync().toString(); | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -509,6 +651,5 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
export const scru64 = async () => getGlobalGenerator().generateOrAwait(); | ||
export const scru64 = async () => GlobalGenerator.generateOrAwait(); | ||
/** | ||
@@ -518,5 +659,7 @@ * Generates a new SCRU64 ID encoded in the 12-digit canonical string | ||
* | ||
* The global generator reads the node configuration from the `SCRU64_NODE_SPEC` | ||
* global variable. A node spec string consists of `nodeId` and `nodeIdSize` | ||
* separated by a slash (e.g., `"42/8"`, `"12345/16"`). | ||
* The {@link GlobalGenerator} reads the node configuration from the | ||
* `SCRU64_NODE_SPEC` global variable by default, and it throws an error if it | ||
* fails to read a well-formed node spec string (e.g., `"42/8"`, `"0xb00/12"`, | ||
* `"0u2r85hm2pt3/16"`) when a generator method is first called. See also | ||
* {@link NodeSpec} for the node spec string format. | ||
* | ||
@@ -526,5 +669,4 @@ * This function usually returns a value immediately, but if not possible, it | ||
* | ||
* @throws Error if the global generator is not properly configured through the | ||
* global variable. | ||
* @throws Error if the global generator is not properly configured. | ||
*/ | ||
export const scru64String = async () => (await scru64()).toString(); |
{ | ||
"name": "scru64", | ||
"version": "0.2.1", | ||
"version": "0.3.0-beta.1", | ||
"description": "SCRU64: Sortable, Clock-based, Realm-specifically Unique identifier", | ||
@@ -33,5 +33,5 @@ "type": "module", | ||
"mocha": "^10.2.0", | ||
"typedoc": "^0.23.28", | ||
"typescript": "^5.0.2" | ||
"typedoc": "^0.24.8", | ||
"typescript": "^5.1.6" | ||
} | ||
} |
@@ -21,11 +21,11 @@ # SCRU64: Sortable, Clock-based, Realm-specifically Unique identifier | ||
// or on browsers: | ||
// import { scru64Sync, scru64StringSync } from "https://unpkg.com/scru64@^0.2"; | ||
// import { scru64Sync, scru64StringSync } from "https://unpkg.com/scru64@^0.3"; | ||
// generate a new identifier object | ||
const x = scru64Sync(); | ||
console.log(String(x)); // e.g. "0u2r85hm2pt3" | ||
console.log(BigInt(x.toHex())); // as a 64-bit unsigned integer | ||
console.log(String(x)); // e.g., "0u2r85hm2pt3" | ||
console.log(x.toBigInt()); // as a 64-bit unsigned integer | ||
// generate a textual representation directly | ||
console.log(scru64StringSync()); // e.g. "0u2r85hm2pt4" | ||
console.log(scru64StringSync()); // e.g., "0u2r85hm2pt4" | ||
``` | ||
@@ -32,0 +32,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
55099
1018