@eliware/snowflake 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -96
- package/index.d.ts +17 -1
- package/index.mjs +69 -18
- package/package.json +12 -15
- package/index.cjs +0 -37
package/README.md
CHANGED
|
@@ -1,128 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @eliware/snowflake
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@eliware/snowflake) [](https://github.com/eliware/snowflake/actions)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
- [Features](#features)
|
|
12
|
-
- [Installation](#installation)
|
|
13
|
-
- [Usage](#usage)
|
|
14
|
-
- [ESM Example](#esm-example)
|
|
15
|
-
- [CommonJS Example](#commonjs-example)
|
|
16
|
-
- [API](#api)
|
|
17
|
-
- [TypeScript](#typescript)
|
|
18
|
-
- [Testing](#testing)
|
|
19
|
-
- [License](#license)
|
|
5
|
+
A production-ready, BigInt-safe Snowflake ID generator for Node.js. IDs are returned as decimal strings so they remain safe across JSON, databases, and JavaScript runtimes.
|
|
20
6
|
|
|
21
7
|
## Features
|
|
22
8
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- Configurable
|
|
27
|
-
-
|
|
9
|
+
- Custom epoch; the default is `365662380000n` (`1981-08-03T04:53:00Z`).
|
|
10
|
+
- 42-bit millisecond timestamp, 5-bit worker ID, 5-bit process ID, and 12-bit sequence.
|
|
11
|
+
- Clock rollback protection and sequence overflow handling.
|
|
12
|
+
- Configurable worker/process identity for distributed deployments.
|
|
13
|
+
- Injectable clock for deterministic tests.
|
|
14
|
+
- Range validation and timestamp-capacity checks.
|
|
15
|
+
- Native ESM, TypeScript declarations, and no runtime dependencies.
|
|
28
16
|
|
|
29
17
|
## Installation
|
|
30
18
|
|
|
31
|
-
Install from npm:
|
|
32
|
-
|
|
33
19
|
```bash
|
|
34
20
|
npm install @eliware/snowflake
|
|
35
21
|
```
|
|
36
22
|
|
|
37
23
|
## Usage
|
|
38
24
|
|
|
39
|
-
### ESM Example
|
|
40
|
-
|
|
41
25
|
```js
|
|
42
|
-
|
|
43
|
-
import generate, { createSnowflakeGenerator } from './index.mjs';
|
|
26
|
+
import generate, { createSnowflakeGenerator } from '@eliware/snowflake';
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
console.log('id:', generate());
|
|
28
|
+
console.log(generate());
|
|
47
29
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
30
|
+
// Set a unique worker ID for each replica or node.
|
|
31
|
+
const replicaGenerator = createSnowflakeGenerator({
|
|
32
|
+
workerId: 3,
|
|
33
|
+
processId: 7,
|
|
34
|
+
});
|
|
35
|
+
console.log(replicaGenerator());
|
|
51
36
|
```
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```js
|
|
56
|
-
// CommonJS (index.cjs)
|
|
57
|
-
const generate = require('./index.cjs');
|
|
58
|
-
const { createSnowflakeGenerator } = require('./index.cjs');
|
|
59
|
-
|
|
60
|
-
console.log('id:', generate());
|
|
61
|
-
|
|
62
|
-
const gen = createSnowflakeGenerator({ workerId: 2 });
|
|
63
|
-
console.log('worker 2 id:', gen());
|
|
64
|
-
```
|
|
38
|
+
For Kubernetes, assign each replica a stable `workerId` from configuration. `processId` defaults to `process.pid % 32`, but should be explicitly configured when process identity must be coordinated across hosts.
|
|
65
39
|
|
|
66
40
|
## API
|
|
67
41
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- options.epoch: number | bigint (default: 1420070400000) — base epoch in milliseconds (Discord epoch: 2015-01-01T00:00:00.000Z).
|
|
71
|
-
- options.workerId: number (default: 0) — numeric worker/node ID (10-bit value).
|
|
72
|
-
|
|
73
|
-
Returns a zero-argument function that generates a new Snowflake ID (as a decimal string) each call.
|
|
74
|
-
|
|
75
|
-
Behavior notes:
|
|
76
|
-
- Timestamp uses (Date.now() - epoch) shifted left 22 bits.
|
|
77
|
-
- Worker ID occupies 10 bits, sequence counter occupies 12 bits.
|
|
78
|
-
- The generator waits for the next millisecond if more than 4095 IDs are requested in the same millisecond.
|
|
79
|
-
|
|
80
|
-
### Default export: generate()
|
|
42
|
+
`createSnowflakeGenerator(options?)` returns a function that generates decimal-string IDs.
|
|
81
43
|
|
|
82
|
-
|
|
44
|
+
| Option | Default | Description |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `epoch` | `365662380000n` | Unix epoch in milliseconds. |
|
|
47
|
+
| `workerId` | `0` | Worker/node ID, from `0` to `31`. |
|
|
48
|
+
| `processId` | `process.pid % 32` | Process ID, from `0` to `31`. |
|
|
49
|
+
| `now` | `Date.now` | Injectable millisecond clock. |
|
|
83
50
|
|
|
84
|
-
|
|
51
|
+
The exports `generate` and `snowflake` are ready-to-use default generators. `constants` exposes the bit layout and default epoch.
|
|
85
52
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
export function createSnowflakeGenerator(options?: { epoch?: number | bigint; workerId?: number }): () => string;
|
|
90
|
-
export const generate: () => string;
|
|
91
|
-
export default generate;
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Testing
|
|
95
|
-
|
|
96
|
-
A small Jest test suite is included (index.test.mjs / index.test.cjs) that verifies:
|
|
97
|
-
|
|
98
|
-
- Generated IDs are numeric strings parseable to BigInt.
|
|
99
|
-
- Consecutive IDs are monotonically increasing.
|
|
100
|
-
- Worker ID bits are encoded correctly.
|
|
101
|
-
- Timestamp portion decodes to a recent time.
|
|
102
|
-
|
|
103
|
-
Run tests with:
|
|
53
|
+
## Testing and linting
|
|
104
54
|
|
|
105
55
|
```bash
|
|
106
56
|
npm test
|
|
57
|
+
npm run test:gaps
|
|
58
|
+
npm run lint
|
|
107
59
|
```
|
|
108
60
|
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Support
|
|
112
|
-
|
|
113
|
-
For help, questions, or to chat with the author and community, visit:
|
|
114
|
-
|
|
115
|
-
[](https://discord.gg/M6aTR9eTwN)[](https://discord.gg/M6aTR9eTwN)
|
|
116
|
-
|
|
117
|
-
**[eliware.org on Discord](https://discord.gg/M6aTR9eTwN)**
|
|
118
|
-
|
|
119
61
|
## License
|
|
120
62
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## Links
|
|
124
|
-
|
|
125
|
-
- [Home Page](https://eliware.org)
|
|
126
|
-
- [GitHub](https://github.com/eliware/snowflake)
|
|
127
|
-
- [npm](https://www.npmjs.com/package/@eliware/snowflake)
|
|
128
|
-
- [Discord](https://discord.gg/M6aTR9eTwN)
|
|
63
|
+
MIT © Eli Sterling, eliware.org
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface SnowflakeGeneratorOptions {
|
|
2
|
+
epoch?: number | bigint;
|
|
3
|
+
workerId?: number | bigint;
|
|
4
|
+
processId?: number | bigint;
|
|
5
|
+
now?: () => number | bigint;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SnowflakeConstants {
|
|
9
|
+
readonly epoch: bigint;
|
|
10
|
+
readonly sequenceBits: bigint;
|
|
11
|
+
readonly processBits: bigint;
|
|
12
|
+
readonly workerBits: bigint;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createSnowflakeGenerator(options?: SnowflakeGeneratorOptions): () => string;
|
|
2
16
|
export const generate: () => string;
|
|
17
|
+
export const snowflake: typeof generate;
|
|
18
|
+
export const constants: SnowflakeConstants;
|
|
3
19
|
export default generate;
|
package/index.mjs
CHANGED
|
@@ -1,34 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* BigInt-safe Snowflake ID generation.
|
|
3
|
+
*
|
|
4
|
+
* Layout (least significant bit first):
|
|
5
|
+
* sequence: 12 bits
|
|
6
|
+
* process: 5 bits
|
|
7
|
+
* worker: 5 bits
|
|
8
|
+
* timestamp: 42 bits
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_EPOCH = 365662380000n; // 1981-08-03T04:53:00.000Z (fixed EST input)
|
|
11
|
+
const SEQUENCE_BITS = 12n;
|
|
12
|
+
const PROCESS_BITS = 5n;
|
|
13
|
+
const WORKER_BITS = 5n;
|
|
14
|
+
const PROCESS_MASK = (1n << PROCESS_BITS) - 1n;
|
|
15
|
+
const WORKER_MASK = (1n << WORKER_BITS) - 1n;
|
|
16
|
+
const SEQUENCE_MASK = (1n << SEQUENCE_BITS) - 1n;
|
|
17
|
+
const MAX_TIMESTAMP = (1n << 42n) - 1n;
|
|
3
18
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
const defaultNow = () => Date.now();
|
|
20
|
+
|
|
21
|
+
function validatePart(value, name, mask) {
|
|
22
|
+
const parsed = BigInt(value);
|
|
23
|
+
if (parsed < 0n || parsed > mask) {
|
|
24
|
+
throw new RangeError(`${name} must be between 0 and ${mask}`);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a generator. Configure a unique workerId per replica and processId
|
|
31
|
+
* per process when generating IDs from multiple instances.
|
|
32
|
+
*/
|
|
33
|
+
export function createSnowflakeGenerator({
|
|
34
|
+
epoch = DEFAULT_EPOCH,
|
|
35
|
+
workerId = 0,
|
|
36
|
+
processId = process.pid % 32,
|
|
37
|
+
now = defaultNow,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const baseEpoch = BigInt(epoch);
|
|
40
|
+
const worker = validatePart(workerId, 'workerId', WORKER_MASK);
|
|
41
|
+
const processNumber = validatePart(processId, 'processId', PROCESS_MASK);
|
|
42
|
+
if (typeof now !== 'function') throw new TypeError('now must be a function');
|
|
43
|
+
|
|
44
|
+
let lastTimestamp = -1n;
|
|
8
45
|
let sequence = 0n;
|
|
9
46
|
|
|
47
|
+
function readTime() {
|
|
48
|
+
const timestamp = BigInt(now());
|
|
49
|
+
if (timestamp < baseEpoch) throw new RangeError('current time must not be before epoch');
|
|
50
|
+
return timestamp;
|
|
51
|
+
}
|
|
52
|
+
|
|
10
53
|
return function generate() {
|
|
11
|
-
let
|
|
54
|
+
let timestamp = readTime();
|
|
55
|
+
if (timestamp < lastTimestamp) timestamp = lastTimestamp;
|
|
12
56
|
|
|
13
|
-
if (
|
|
14
|
-
sequence = (sequence + 1n) &
|
|
57
|
+
if (timestamp === lastTimestamp) {
|
|
58
|
+
sequence = (sequence + 1n) & SEQUENCE_MASK;
|
|
15
59
|
if (sequence === 0n) {
|
|
16
|
-
|
|
17
|
-
while (now <= lastTimestamp) {
|
|
18
|
-
now = BigInt(Date.now());
|
|
19
|
-
}
|
|
60
|
+
do timestamp = readTime(); while (timestamp <= lastTimestamp);
|
|
20
61
|
}
|
|
21
62
|
} else {
|
|
22
63
|
sequence = 0n;
|
|
23
64
|
}
|
|
24
65
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
66
|
+
const elapsed = timestamp - baseEpoch;
|
|
67
|
+
if (elapsed > MAX_TIMESTAMP) throw new RangeError('timestamp exceeds Snowflake capacity');
|
|
68
|
+
lastTimestamp = timestamp;
|
|
69
|
+
return ((elapsed << (WORKER_BITS + PROCESS_BITS + SEQUENCE_BITS)) |
|
|
70
|
+
(worker << (PROCESS_BITS + SEQUENCE_BITS)) |
|
|
71
|
+
(processNumber << SEQUENCE_BITS) |
|
|
72
|
+
sequence).toString();
|
|
29
73
|
};
|
|
30
74
|
}
|
|
31
75
|
|
|
32
|
-
// Default generator (workerId 0)
|
|
33
76
|
export const generate = createSnowflakeGenerator();
|
|
77
|
+
export const snowflake = generate;
|
|
78
|
+
export const constants = Object.freeze({
|
|
79
|
+
epoch: DEFAULT_EPOCH,
|
|
80
|
+
sequenceBits: SEQUENCE_BITS,
|
|
81
|
+
processBits: PROCESS_BITS,
|
|
82
|
+
workerBits: WORKER_BITS,
|
|
83
|
+
});
|
|
84
|
+
|
|
34
85
|
export default generate;
|
package/package.json
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eliware/snowflake",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.1.
|
|
5
|
-
"description": "A minimal Node.js Snowflake ID generator (Discord-compatible). Provides BigInt-safe generators
|
|
6
|
-
"main": "index.
|
|
7
|
-
"module": "index.mjs",
|
|
4
|
+
"version": "1.1.3",
|
|
5
|
+
"description": "A minimal Node.js Snowflake ID generator (Discord-compatible). Provides BigInt-safe generators with configurable epoch and worker ID.",
|
|
6
|
+
"main": "index.mjs",
|
|
8
7
|
"types": "index.d.ts",
|
|
9
8
|
"files": [
|
|
10
9
|
"index.mjs",
|
|
11
|
-
"index.cjs",
|
|
12
10
|
"index.d.ts",
|
|
13
11
|
"README.md",
|
|
14
12
|
"LICENSE"
|
|
15
13
|
],
|
|
16
14
|
"exports": {
|
|
17
15
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"types": "./index.d.ts"
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"import": "./index.mjs"
|
|
21
18
|
}
|
|
22
19
|
},
|
|
23
20
|
"scripts": {
|
|
24
|
-
"test": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --detectOpenHandles --silent"
|
|
21
|
+
"test": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --detectOpenHandles --silent --coverage",
|
|
22
|
+
"test:gaps": "npm test -- --coverageReporters=text 2>&1 | awk '!/100%/'",
|
|
23
|
+
"lint": "npx oxlint ."
|
|
25
24
|
},
|
|
26
25
|
"repository": {
|
|
27
26
|
"type": "git",
|
|
@@ -35,8 +34,7 @@
|
|
|
35
34
|
"unique-id",
|
|
36
35
|
"bigint",
|
|
37
36
|
"nodejs",
|
|
38
|
-
"esm"
|
|
39
|
-
"cjs"
|
|
37
|
+
"esm"
|
|
40
38
|
],
|
|
41
39
|
"author": "Eli Sterling, eliware.org <eli@eliware.org>",
|
|
42
40
|
"license": "MIT",
|
|
@@ -45,17 +43,16 @@
|
|
|
45
43
|
},
|
|
46
44
|
"homepage": "https://github.com/eliware/snowflake#readme",
|
|
47
45
|
"devDependencies": {
|
|
48
|
-
"jest": "^30.
|
|
46
|
+
"jest": "^30.4.2"
|
|
49
47
|
},
|
|
50
48
|
"jest": {
|
|
51
49
|
"testEnvironment": "node",
|
|
52
50
|
"testMatch": [
|
|
53
51
|
"**/?(*.)+(spec|test).[jt]s?(x)",
|
|
54
|
-
"**/?(*.)+(spec|test).mjs"
|
|
55
|
-
"**/?(*.)+(spec|test).cjs"
|
|
52
|
+
"**/?(*.)+(spec|test).mjs"
|
|
56
53
|
]
|
|
57
54
|
},
|
|
58
55
|
"dependencies": {
|
|
59
|
-
"@eliware/common": "^1.1.
|
|
56
|
+
"@eliware/common": "^1.1.4"
|
|
60
57
|
}
|
|
61
58
|
}
|
package/index.cjs
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Snowflake generator (CommonJS)
|
|
2
|
-
const DEFAULT_EPOCH = 1420070400000n; // Discord epoch: 2015-01-01T00:00:00.000Z
|
|
3
|
-
|
|
4
|
-
function createSnowflakeGenerator({ epoch = DEFAULT_EPOCH, workerId = 0 } = {}) {
|
|
5
|
-
const _epoch = BigInt(epoch);
|
|
6
|
-
const _workerId = BigInt(workerId) & 0x3ffn; // 10 bits
|
|
7
|
-
let lastTimestamp = 0n;
|
|
8
|
-
let sequence = 0n;
|
|
9
|
-
|
|
10
|
-
return function generate() {
|
|
11
|
-
let now = BigInt(Date.now());
|
|
12
|
-
|
|
13
|
-
if (now === lastTimestamp) {
|
|
14
|
-
sequence = (sequence + 1n) & 0xfffn; // 12 bits
|
|
15
|
-
if (sequence === 0n) {
|
|
16
|
-
// Sequence overflow in the same millisecond — wait for next ms
|
|
17
|
-
while (now <= lastTimestamp) {
|
|
18
|
-
now = BigInt(Date.now());
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
} else {
|
|
22
|
-
sequence = 0n;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
lastTimestamp = now;
|
|
26
|
-
|
|
27
|
-
const id = ((now - _epoch) << 22n) | (_workerId << 12n) | sequence;
|
|
28
|
-
return id.toString();
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Default generator (workerId 0)
|
|
33
|
-
const generate = createSnowflakeGenerator();
|
|
34
|
-
|
|
35
|
-
module.exports = generate;
|
|
36
|
-
module.exports.generate = generate;
|
|
37
|
-
module.exports.createSnowflakeGenerator = createSnowflakeGenerator;
|