@eliware/snowflake 12.0.0 → 14.0.0
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/AGENTS.md +18 -26
- package/README.md +32 -23
- package/RELEASE_NOTES.md +38 -31
- package/docs/README.md +11 -2
- package/docs/troubleshooting.md +30 -17
- package/docs/usage.md +134 -24
- package/package.json +5 -2
- package/specs/directives.yaml +9 -8
- package/specs/out-of-scope.yaml +1 -3
- package/specs/requirements.yaml +3 -5
- package/src/generator-options.mjs +9 -2
- package/src/generator.mjs +57 -21
- package/src/index.d.ts +5 -0
package/AGENTS.md
CHANGED
|
@@ -2,53 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## Project
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Maintain `@eliware/snowflake`, a Snowflake ID library owned by Eliware.
|
|
6
6
|
|
|
7
7
|
## Scope and boundaries
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Do not use validation to publish, deploy, or change external systems.
|
|
9
|
+
Limit this package to Snowflake ID generation.
|
|
11
10
|
|
|
12
11
|
## Layout
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
Keep user guides under `docs/`, examples under `examples/`, and rules under `specs/`.
|
|
13
|
+
Use `README.md` and `specs/README.md` to find repository paths.
|
|
16
14
|
|
|
17
15
|
## Development
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
Keep TypeScript declarations beside their matching ESM modules.
|
|
17
|
+
Use Node.js 26 and native ESM for maintained JavaScript.
|
|
21
18
|
|
|
22
19
|
## Validation
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
Run `npm
|
|
26
|
-
Run `npm run
|
|
27
|
-
Run `npm run
|
|
28
|
-
Run `npm run format
|
|
29
|
-
Run `npm run format:check` to check file format.
|
|
30
|
-
Run `npm run typecheck` to check TypeScript declarations.
|
|
31
|
-
Run `npm run pack` to check package contents.
|
|
21
|
+
Run `npm test`.
|
|
22
|
+
Run `npm run lint`.
|
|
23
|
+
Run `npm run audit`.
|
|
24
|
+
Run `npm run format`.
|
|
25
|
+
Run `npm run format:check`.
|
|
32
26
|
|
|
33
27
|
## Security
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
Treat Snowflake IDs as identifiers, not secrets.
|
|
29
|
+
Keep credentials, private data, and runtime state out of Git.
|
|
37
30
|
|
|
38
31
|
## Changes
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
Follow the Eliware Operations approval process for release changes.
|
|
42
|
-
Do not treat validation as release approval.
|
|
33
|
+
Update the nearest index when you add a document.
|
|
43
34
|
|
|
44
35
|
## Library
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
Run focused tests first during development.
|
|
38
|
+
Run one test file with `npm test -- tests/path/to/example.test.mjs`.
|
|
39
|
+
Run multiple test files with `npm test -- tests/path/to/first.test.mjs tests/path/to/second.test.mjs`.
|
|
40
|
+
Prevent repeated full-suite runs during development.
|
|
41
|
+
Run the full `npm test` suite for final validation before a commit, milestone, or release.
|
|
49
42
|
|
|
50
43
|
## npm publication
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Run package validation before publication review.
|
|
45
|
+
Run `npm run pack` to check package contents.
|
|
46
|
+
Follow the approved release procedure.
|
package/README.md
CHANGED
|
@@ -23,19 +23,17 @@ A Node.js Snowflake ID generator with BigInt support and a configurable epoch.
|
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
|
-
Create time-ordered IDs
|
|
27
|
-
|
|
28
|
-
Receive decimal strings that preserve every ID bit in JavaScript and JSON.
|
|
29
|
-
The generator handles clock rollback and reports sequence overflow.
|
|
26
|
+
Create time-ordered Snowflake IDs as decimal strings.
|
|
27
|
+
Set a custom epoch, worker ID, process ID, clock, or logger.
|
|
30
28
|
|
|
31
29
|
## Requirements
|
|
32
30
|
|
|
33
31
|
Use Node.js 26 or later.
|
|
34
|
-
|
|
32
|
+
Use native ECMAScript modules (ESM).
|
|
35
33
|
|
|
36
34
|
## Setup
|
|
37
35
|
|
|
38
|
-
Install the package:
|
|
36
|
+
Install the package from npm:
|
|
39
37
|
|
|
40
38
|
```text
|
|
41
39
|
npm install @eliware/snowflake
|
|
@@ -43,7 +41,7 @@ npm install @eliware/snowflake
|
|
|
43
41
|
|
|
44
42
|
## Usage
|
|
45
43
|
|
|
46
|
-
Create a generator with a unique
|
|
44
|
+
Create a generator with a unique worker and process ID pair:
|
|
47
45
|
|
|
48
46
|
```js
|
|
49
47
|
import { createSnowflakeGenerator } from "@eliware/snowflake";
|
|
@@ -52,43 +50,54 @@ const generate = createSnowflakeGenerator({ workerId: 3, processId: 7 });
|
|
|
52
50
|
console.log(generate());
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
Each call returns one decimal string.
|
|
54
|
+
Read the [usage guide](docs/usage.md) for generator options and ID fields.
|
|
55
|
+
Read the [release notes](RELEASE_NOTES.md) for changes by version.
|
|
56
|
+
Read the [specifications index](specs/README.md) for repository requirements.
|
|
57
57
|
|
|
58
58
|
## Development
|
|
59
59
|
|
|
60
|
-
Read [
|
|
60
|
+
Read the [documentation index](docs/README.md) for maintained guides.
|
|
61
61
|
|
|
62
62
|
## Testing
|
|
63
63
|
|
|
64
|
-
Run `npm test` for
|
|
65
|
-
Run `npm run lint`, `npm run format:check`, `npm run audit`, `npm run typecheck`, or `npm run pack` for focused checks.
|
|
64
|
+
Run `npm test` from a repository clone for aggregate validation.
|
|
66
65
|
|
|
67
66
|
## Troubleshooting
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
Read the [troubleshooting guide](docs/troubleshooting.md) for error causes and actions.
|
|
70
69
|
|
|
71
70
|
## Security
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
IDs
|
|
75
|
-
Do not use IDs as authentication tokens.
|
|
72
|
+
Each ID contains its generation time and identity fields.
|
|
73
|
+
Do not use IDs as secrets or authentication tokens.
|
|
76
74
|
|
|
77
75
|
## API
|
|
78
76
|
|
|
79
|
-
`createSnowflakeGenerator
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
Import `createSnowflakeGenerator` to create a generator with custom options.
|
|
78
|
+
Import `generate`, `snowflake`, or the default export to use the default generator.
|
|
79
|
+
Import `constants` to read the default epoch and field sizes.
|
|
80
|
+
Read the [usage guide](docs/usage.md) for API details and TypeScript declarations.
|
|
83
81
|
|
|
84
82
|
## Packaging
|
|
85
83
|
|
|
86
|
-
The package
|
|
87
|
-
|
|
84
|
+
The package uses native ESM, includes TypeScript declarations, and depends on `@eliware/log`.
|
|
85
|
+
Its root entrypoint is `src/index.mjs` and its declaration file is `src/index.d.ts`.
|
|
86
|
+
The npm package includes these paths from `package.json`:
|
|
87
|
+
|
|
88
|
+
- `src/`
|
|
89
|
+
- `docs/`
|
|
90
|
+
- `README.md`, `AGENTS.md`, `LICENSE`, and `RELEASE_NOTES.md`
|
|
91
|
+
- `examples/` and `specs/`
|
|
92
|
+
|
|
93
|
+
Use numeric `MAJOR.MINOR.PATCH` versions.
|
|
94
|
+
Push a matching `v<version>` tag to start validation and npm publication.
|
|
95
|
+
Follow the approved release procedure before you create a release tag.
|
|
88
96
|
|
|
89
97
|
## Examples
|
|
90
98
|
|
|
91
|
-
Run
|
|
99
|
+
Run `node examples/example.mjs` to create sample IDs.
|
|
100
|
+
Read [examples/README.md](examples/README.md) for the example index.
|
|
92
101
|
|
|
93
102
|
## Support
|
|
94
103
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## 14.0.0 — 2026-10-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added optional structured logging through `@eliware/log` with a custom logger option.
|
|
8
|
+
- Added tests for logger records and the full elapsed timestamp range with the maximum epoch.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Updated the package to version 14.0.0 and aligned its specifications with Eliware Test 14.
|
|
13
|
+
- Clarified the authoritative rule for first timestamps and later pre-epoch rollbacks.
|
|
14
|
+
- Updated npm dependencies and added `@eliware/log` as a direct dependency.
|
|
15
|
+
- Expanded the user guides with logging, API, decoding, timestamp limits, identity, and storage details.
|
|
16
|
+
- Reduced `README.md` and `AGENTS.md` to convention-required content.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Kept the full elapsed timestamp range available when the configured epoch is at its maximum.
|
|
21
|
+
- Kept IDs increasing when the clock returns a time before the epoch after the first valid ID.
|
|
22
|
+
|
|
23
|
+
## 13.0.0 — 2026-10-19
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Added the canonical npm publication workflow.
|
|
28
|
+
- Added full Git history to CI.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Updated the package to version 13.0.0 and upgraded `@eliware/test` and npm dependencies.
|
|
33
|
+
- Aligned CI and npm publication workflows with the current templates and checkout action.
|
|
34
|
+
- Updated Knit deployment commands and the repository path.
|
|
35
|
+
- Updated specifications and repository guidance for Convention 13.
|
|
36
|
+
- Expanded user guides with API details, ID limits, timestamp decoding, and storage guidance.
|
|
37
|
+
- Documented the custom epoch for Discord timestamp decoding. It does not create official Discord IDs.
|
|
38
|
+
- Changed the invalid clock source and sequence overflow error messages.
|
|
39
|
+
- Reduced the root README and contributor guidance to convention-required content.
|
|
40
|
+
|
|
3
41
|
## 12.0.0 — 2026-10-12
|
|
4
42
|
|
|
5
43
|
### Changed
|
|
@@ -32,34 +70,3 @@
|
|
|
32
70
|
### Breaking changes
|
|
33
71
|
|
|
34
72
|
- Changed the `test` and `lint` scripts to use `@eliware/test`.
|
|
35
|
-
|
|
36
|
-
## 1.1.5 — 2026-08-07
|
|
37
|
-
|
|
38
|
-
### Changed
|
|
39
|
-
|
|
40
|
-
- Aligned the repository layout, scripts, documentation, and package contents.
|
|
41
|
-
- Added TypeScript declaration checks and package validation.
|
|
42
|
-
- Moved tests under `tests/` and added a runnable native ESM example.
|
|
43
|
-
- Removed the legacy CommonJS package entry point.
|
|
44
|
-
|
|
45
|
-
## 1.1.4 — 2026-08-07
|
|
46
|
-
|
|
47
|
-
### Changed
|
|
48
|
-
|
|
49
|
-
- Removed the unused `@eliware/common` runtime dependency.
|
|
50
|
-
- Added manual GitHub Actions workflow dispatch support.
|
|
51
|
-
|
|
52
|
-
## 1.1.2 — 2026-08-06
|
|
53
|
-
|
|
54
|
-
### Added
|
|
55
|
-
|
|
56
|
-
- Added a configurable BigInt-safe Snowflake ID generator.
|
|
57
|
-
- Added the Eliware epoch, worker IDs, and process IDs.
|
|
58
|
-
- Added clock rollback protection and sequence overflow handling.
|
|
59
|
-
- Added injectable clocks, range checks, and timestamp capacity checks.
|
|
60
|
-
- Added TypeScript declarations for public options and exports.
|
|
61
|
-
|
|
62
|
-
### Changed
|
|
63
|
-
|
|
64
|
-
- Removed CommonJS entry points and tests. The package now uses native ESM.
|
|
65
|
-
- Expanded deployment and bit layout guidance.
|
package/docs/README.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# Documentation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Use these guides to install the package, create IDs, and resolve common errors.
|
|
4
|
+
|
|
5
|
+
## Guides
|
|
6
|
+
|
|
7
|
+
- [Usage](usage.md) explains installation, generator options, ID fields, decoding, logging, and storage.
|
|
8
|
+
- [Troubleshooting](troubleshooting.md) explains validation errors, sequence limits, and duplicate IDs.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
Use Node.js 26 or later.
|
|
13
|
+
Use native ECMAScript modules (ESM).
|
package/docs/troubleshooting.md
CHANGED
|
@@ -2,44 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## Error reference
|
|
4
4
|
|
|
5
|
-
| Error | Cause | Action
|
|
6
|
-
| --------------------------------------------- | ---------------------------------------------------- |
|
|
7
|
-
| `RangeError` for `workerId` or `processId` | The value is outside `0` through `31`. | Set an integer from `0` through `31`.
|
|
8
|
-
| `TypeError` for `now` | The option is not a function. | Pass a function that returns Unix time in milliseconds.
|
|
9
|
-
| `
|
|
10
|
-
| `RangeError` for
|
|
11
|
-
| `RangeError` for
|
|
12
|
-
| `RangeError` for
|
|
13
|
-
| `RangeError` for
|
|
14
|
-
| `
|
|
5
|
+
| Error | Cause | Action |
|
|
6
|
+
| --------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
|
|
7
|
+
| `RangeError` for `workerId` or `processId` | The value is outside `0` through `31`. | Set an integer from `0` through `31`. |
|
|
8
|
+
| `TypeError` for `now` | The option is not a function. | Pass a function that returns Unix time in milliseconds. |
|
|
9
|
+
| `TypeError` for `logger` | The logger lacks a required level method. | Pass a logger with `error`, `warn`, `info`, and `debug` methods. |
|
|
10
|
+
| `RangeError` for `epoch` | The epoch is outside the supported range. | Set the epoch from zero through the timestamp limit. |
|
|
11
|
+
| `RangeError` for invalid clock number | The clock returned a fractional or unsafe number. | Return a safe integer number or a bigint. |
|
|
12
|
+
| `RangeError` for first time before epoch | The first clock value is lower than `epoch`. | Set the correct epoch or set the clock source. |
|
|
13
|
+
| `RangeError` for timestamp capacity | Elapsed time is greater than the 42-bit limit. | Use a valid epoch for the required date range. |
|
|
14
|
+
| `RangeError` for sequence overflow | The generator created 4096 IDs at one timestamp. | Retry when the clock returns a greater timestamp. |
|
|
15
|
+
| `TypeError` or `RangeError` during conversion | An option or clock value cannot convert to `BigInt`. | Pass an integer number or bigint value. |
|
|
15
16
|
|
|
16
17
|
## Sequence overflow
|
|
17
18
|
|
|
18
19
|
The generator does not wait for the clock.
|
|
19
20
|
It fails when one timestamp reaches the 4096-ID limit.
|
|
20
|
-
|
|
21
|
+
Retry when the clock returns a timestamp greater than the last timestamp.
|
|
21
22
|
If the clock stalls, check the system clock or the `now` function.
|
|
22
23
|
|
|
23
24
|
## Duplicate IDs
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Give each active generator a unique worker and process ID pair.
|
|
26
27
|
Use the same epoch for each generator in one application.
|
|
27
28
|
The default worker ID is `0`.
|
|
28
29
|
The default process ID is `process.pid % 32` and can repeat across hosts.
|
|
29
|
-
Set both IDs
|
|
30
|
+
Set both IDs in deployment configuration when some hosts create IDs.
|
|
31
|
+
|
|
32
|
+
Do not create multiple generators with the same pair while their IDs can overlap in time.
|
|
33
|
+
Each generator starts its sequence at zero.
|
|
34
|
+
Use one generator instance for each pair in a process.
|
|
35
|
+
|
|
36
|
+
## Clock rollback
|
|
37
|
+
|
|
38
|
+
The generator keeps IDs in increasing order when its clock returns an earlier time.
|
|
39
|
+
It uses the last valid timestamp and advances the sequence.
|
|
40
|
+
It logs a warning when you provide a logger.
|
|
41
|
+
The first clock value must be at or after the configured epoch.
|
|
42
|
+
After the first ID, a clock value lower than the epoch uses rollback handling.
|
|
30
43
|
|
|
31
44
|
## Invalid timestamps
|
|
32
45
|
|
|
33
46
|
The clock must return a safe integer number or a bigint.
|
|
34
47
|
The value must be Unix time in milliseconds.
|
|
35
|
-
The timestamp must be at or after the configured epoch.
|
|
36
|
-
The elapsed time must
|
|
48
|
+
The first timestamp must be at or after the configured epoch.
|
|
49
|
+
The elapsed time must be 42 bits or less.
|
|
37
50
|
Check the clock and epoch values when timestamp validation fails.
|
|
38
51
|
|
|
39
52
|
## Large ID values
|
|
40
53
|
|
|
41
|
-
IDs are decimal strings because JavaScript numbers cannot
|
|
42
|
-
Keep the string in JSON and in database fields that
|
|
54
|
+
IDs are decimal strings because JavaScript numbers cannot store every ID exactly.
|
|
55
|
+
Keep the string in JSON and in database fields that store exact decimal values.
|
|
43
56
|
Use `BigInt(id)` for exact integer operations.
|
|
44
57
|
|
|
45
58
|
## Node.js version
|
package/docs/usage.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
## Requirements
|
|
4
4
|
|
|
5
5
|
Use Node.js 26 or later.
|
|
6
|
-
Use
|
|
6
|
+
Use native ECMAScript modules (ESM).
|
|
7
|
+
Give each active generator a unique worker and process ID pair.
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -25,7 +26,11 @@ const id = generate();
|
|
|
25
26
|
console.log(id);
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
The
|
|
29
|
+
The command prints one decimal string. The value changes on each call.
|
|
30
|
+
|
|
31
|
+
The package exports `createSnowflakeGenerator`, `generate`, `snowflake`, `constants`, and a default generator.
|
|
32
|
+
The `generate`, `snowflake`, and default exports use the same generator instance.
|
|
33
|
+
Use `createSnowflakeGenerator()` to set an epoch, identity, clock, or logger.
|
|
29
34
|
|
|
30
35
|
## Set generator options
|
|
31
36
|
|
|
@@ -37,21 +42,40 @@ Pass an options object to `createSnowflakeGenerator`.
|
|
|
37
42
|
| `workerId` | number or bigint | `0` | Worker value from `0` through `31`. |
|
|
38
43
|
| `processId` | number or bigint | `process.pid % 32` | Process value from `0` through `31`. |
|
|
39
44
|
| `now` | function | `Date.now` | Function that returns Unix time in milliseconds. |
|
|
45
|
+
| `logger` | logger | none | Optional structured logger for generator events. |
|
|
46
|
+
|
|
47
|
+
`createSnowflakeGenerator(options)` returns a function that creates IDs.
|
|
48
|
+
Each call returns one decimal string.
|
|
49
|
+
Set `epoch` as a safe integer number or a bigint from zero through `(1n << 42n) - 1n`.
|
|
50
|
+
Set `workerId` and `processId` to integer values from `0` through `31`.
|
|
51
|
+
Use the same epoch for all generators in one application.
|
|
52
|
+
|
|
53
|
+
Give each active generator a unique pair of `workerId` and `processId` values.
|
|
54
|
+
Keep each pair unique while IDs from that pair remain in use.
|
|
55
|
+
The fields allow 32 worker values and 32 process values, for 1,024 pairs.
|
|
56
|
+
The default process ID is `process.pid % 32` and can repeat across hosts.
|
|
57
|
+
Set both values in deployment configuration when different hosts can create IDs at the same time.
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
To align timestamp decoding with Discord, set `epoch` to `1420070400000n`.
|
|
60
|
+
This epoch matches Discord's timestamp epoch.
|
|
61
|
+
It does not create an official Discord resource ID.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const createDiscordTimeGenerator = createSnowflakeGenerator({
|
|
65
|
+
epoch: 1420070400000n,
|
|
66
|
+
workerId: 3,
|
|
67
|
+
processId: 7,
|
|
68
|
+
});
|
|
69
|
+
```
|
|
48
70
|
|
|
49
71
|
## Understand the ID fields
|
|
50
72
|
|
|
51
73
|
Each ID has a 42-bit timestamp, a 5-bit worker ID, a 5-bit process ID, and a 12-bit sequence.
|
|
52
74
|
The timestamp stores milliseconds since the configured epoch.
|
|
53
|
-
The sequence starts at zero when the timestamp
|
|
75
|
+
The sequence starts at zero when the timestamp changes.
|
|
54
76
|
The default epoch is `365662380000n`, or `1981-08-03T04:53:00Z`.
|
|
77
|
+
The timestamp field sorts before the worker, process, and sequence fields.
|
|
78
|
+
The complete ID has 64 bits.
|
|
55
79
|
|
|
56
80
|
The generator combines the fields as follows:
|
|
57
81
|
|
|
@@ -59,15 +83,50 @@ The generator combines the fields as follows:
|
|
|
59
83
|
((time - epoch) << 22) | (workerId << 17) | (processId << 12) | sequence
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
The
|
|
63
|
-
|
|
86
|
+
The epoch option accepts values through `(1n << 42n) - 1n` milliseconds.
|
|
87
|
+
The timestamp field separately stores a maximum of `(1n << 42n) - 1n` elapsed milliseconds.
|
|
88
|
+
The epoch limit applies to the configured epoch, not the final Unix time.
|
|
89
|
+
The maximum supported Unix time equals the epoch plus the maximum elapsed timestamp.
|
|
90
|
+
IDs can be greater than JavaScript's safe integer limit.
|
|
64
91
|
|
|
65
92
|
## Use the default generator
|
|
66
93
|
|
|
67
94
|
Use `generate`, `snowflake`, or the default export to create IDs.
|
|
68
95
|
These exports refer to the same generator instance.
|
|
69
96
|
The default generator uses worker ID `0` and process ID `process.pid % 32`.
|
|
70
|
-
Create a custom generator
|
|
97
|
+
Create a custom generator to set a different identity, epoch, or clock.
|
|
98
|
+
|
|
99
|
+
The `constants` export has `epoch`, `sequenceBits`, `processBits`, and `workerBits` fields.
|
|
100
|
+
Each field has a `bigint` value.
|
|
101
|
+
The sequence, process, and worker fields use 12, 5, and 5 bits.
|
|
102
|
+
The `constants` object is frozen.
|
|
103
|
+
|
|
104
|
+
## Decode an ID timestamp
|
|
105
|
+
|
|
106
|
+
Convert the ID string to a `bigint` before you read its timestamp field.
|
|
107
|
+
Move the value right by 22 bits to read elapsed milliseconds.
|
|
108
|
+
Add the configured epoch to get Unix time in milliseconds.
|
|
109
|
+
Convert that value to a JavaScript `Date` to get its ISO 8601 value.
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const epoch = 365662380000n;
|
|
113
|
+
const timestamp = new Date(Number((BigInt(id) >> 22n) + epoch));
|
|
114
|
+
console.log(timestamp.toISOString());
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use the same epoch that the generator used to create the ID.
|
|
118
|
+
|
|
119
|
+
Read the worker, process, and sequence fields with bit shifts and masks:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const value = BigInt(id);
|
|
123
|
+
const workerId = (value >> 17n) & 31n;
|
|
124
|
+
const processId = (value >> 12n) & 31n;
|
|
125
|
+
const sequence = value & 4095n;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The package does not export a decoder.
|
|
129
|
+
Use the same field layout and epoch that the generator used to create the ID.
|
|
71
130
|
|
|
72
131
|
## Use a custom clock
|
|
73
132
|
|
|
@@ -76,29 +135,80 @@ Return a safe integer number or a bigint from that function.
|
|
|
76
135
|
The value must be Unix time in milliseconds.
|
|
77
136
|
Use this option for deterministic tests or a controlled clock source.
|
|
78
137
|
|
|
79
|
-
|
|
138
|
+
The first clock value must be at or after the configured epoch.
|
|
139
|
+
After the first ID, an earlier clock value uses the last valid timestamp.
|
|
140
|
+
The generator increments the sequence during that rollback.
|
|
80
141
|
|
|
81
|
-
|
|
82
|
-
It increments the sequence while that timestamp remains active.
|
|
83
|
-
Each generator can create up to 4096 IDs for one millisecond.
|
|
84
|
-
The next call throws `RangeError` until the clock advances past the last timestamp.
|
|
85
|
-
Wait for the clock to advance before retry.
|
|
86
|
-
The generator throws `RangeError` for a timestamp before the epoch or beyond its 42-bit limit.
|
|
142
|
+
## Add structured logging
|
|
87
143
|
|
|
88
|
-
|
|
144
|
+
Install `@eliware/log` to create a logger. Pass the logger to the generator with the `logger` option.
|
|
145
|
+
The generator writes records only when you provide a logger.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { createLogger } from "@eliware/log";
|
|
149
|
+
import { createSnowflakeGenerator } from "@eliware/snowflake";
|
|
150
|
+
|
|
151
|
+
const logger = createLogger({ level: "debug", format: "json" });
|
|
152
|
+
const generate = createSnowflakeGenerator({ logger });
|
|
153
|
+
const id = generate();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The generator writes `info` when it starts, `debug` when it creates an ID, and `warn` when its clock returns an earlier time.
|
|
157
|
+
It writes `error` when ID generation fails.
|
|
158
|
+
Records do not include generated IDs, credentials, tokens, or error messages.
|
|
159
|
+
|
|
160
|
+
Records include the operation, component, and related timestamp or identity fields.
|
|
161
|
+
The logger supports the `error`, `warn`, `info`, `http`, `verbose`, `debug`, and `silly` levels.
|
|
162
|
+
The generator uses only levels that apply to its operations.
|
|
163
|
+
Set the logger level to control which records it writes.
|
|
164
|
+
The `logger` option requires `error`, `warn`, `info`, and `debug` methods.
|
|
165
|
+
The `LoggerWithConfiguredLevels` TypeScript type describes the supported logger interface.
|
|
166
|
+
|
|
167
|
+
## Time and sequence limits
|
|
168
|
+
|
|
169
|
+
The generator uses the last timestamp when the clock returns a lower value.
|
|
170
|
+
The generator sets the sequence to zero when the timestamp changes.
|
|
171
|
+
The generator increments the sequence while the timestamp stays the same.
|
|
172
|
+
|
|
173
|
+
Each generator can create 4096 IDs in one millisecond.
|
|
174
|
+
The generator throws `RangeError` after that limit.
|
|
175
|
+
Retry when the clock returns a timestamp greater than the last timestamp.
|
|
176
|
+
The generator throws `RangeError` for a timestamp before the epoch or greater than its 42-bit limit.
|
|
177
|
+
|
|
178
|
+
## Store and share IDs
|
|
89
179
|
|
|
90
180
|
Keep each ID as a decimal string in JavaScript and JSON.
|
|
91
181
|
Do not convert an ID to `number`.
|
|
182
|
+
JSON cannot encode a `bigint` value without custom code.
|
|
92
183
|
Use a text field or an unsigned 64-bit field in a database.
|
|
93
|
-
Use `BigInt(id)`
|
|
184
|
+
Use `BigInt(id)` for exact integer math in JavaScript.
|
|
185
|
+
|
|
186
|
+
Each ID contains its generation time, worker ID, process ID, and sequence.
|
|
187
|
+
Do not use an ID as a password, credential, or authentication token.
|
|
188
|
+
|
|
189
|
+
## Use TypeScript
|
|
190
|
+
|
|
191
|
+
The package includes TypeScript declarations for its public API.
|
|
192
|
+
Import `SnowflakeGeneratorOptions`, `SnowflakeConstants`, and `LoggerWithConfiguredLevels` as types.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { createSnowflakeGenerator, constants } from "@eliware/snowflake";
|
|
196
|
+
import type { SnowflakeGeneratorOptions } from "@eliware/snowflake";
|
|
197
|
+
|
|
198
|
+
const options: SnowflakeGeneratorOptions = { workerId: 3, processId: 7 };
|
|
199
|
+
const generate = createSnowflakeGenerator(options);
|
|
200
|
+
const id: string = generate();
|
|
201
|
+
const epoch: bigint = constants.epoch;
|
|
202
|
+
```
|
|
94
203
|
|
|
95
204
|
## Run the example
|
|
96
205
|
|
|
97
|
-
Use Node.js 26 or later.
|
|
206
|
+
Use Node.js 26 or later.
|
|
207
|
+
Run this command from the repository root:
|
|
98
208
|
|
|
99
209
|
```text
|
|
100
210
|
node examples/example.mjs
|
|
101
211
|
```
|
|
102
212
|
|
|
103
213
|
The command prints two decimal strings. The values depend on the current time and process ID.
|
|
104
|
-
|
|
214
|
+
The example writes no files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eliware/snowflake",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Node.js Snowflake ID generator with BigInt support and a configurable epoch.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,8 +41,11 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=26"
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@eliware/log": "^14.0.0"
|
|
46
|
+
},
|
|
44
47
|
"devDependencies": {
|
|
45
|
-
"@eliware/test": "^
|
|
48
|
+
"@eliware/test": "^14.0.0",
|
|
46
49
|
"typescript": "^7.0.2"
|
|
47
50
|
},
|
|
48
51
|
"jest": {
|
package/specs/directives.yaml
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
version: "
|
|
1
|
+
version: "14.0"
|
|
2
2
|
description: Defines behavior and limits for the Snowflake ID generator.
|
|
3
|
-
requires: []
|
|
4
|
-
conflicts: []
|
|
5
3
|
directives:
|
|
6
4
|
- id: E-106.0
|
|
7
5
|
dos:
|
|
8
|
-
-
|
|
6
|
+
- Create time-ordered Snowflake IDs.
|
|
9
7
|
- Return each ID as a decimal string.
|
|
10
8
|
- Use configurable epoch and identity values.
|
|
11
9
|
donts:
|
|
12
|
-
- Do not
|
|
10
|
+
- Do not show generated IDs as secrets.
|
|
13
11
|
children:
|
|
14
12
|
- id: E-106.0.0
|
|
15
13
|
dos:
|
|
16
14
|
- Validate worker and process IDs before generation.
|
|
17
|
-
- Reject
|
|
15
|
+
- Reject the first clock value when it is before the configured epoch.
|
|
16
|
+
- Apply rollback handling to a later clock value before the configured epoch.
|
|
17
|
+
- Use the last valid timestamp after a rollback.
|
|
18
18
|
donts:
|
|
19
|
-
- Do not
|
|
19
|
+
- Do not create an ID from a timestamp before the configured epoch.
|
|
20
|
+
- Do not use identity values outside the supported bit range.
|
|
20
21
|
- id: E-106.0.1
|
|
21
22
|
dos:
|
|
22
|
-
-
|
|
23
|
+
- Keep IDs unique when the clock returns a lower value.
|
|
23
24
|
- Report sequence overflow before an ID can repeat.
|
|
24
25
|
donts:
|
|
25
26
|
- Do not return an ID when the sequence overflows.
|
package/specs/out-of-scope.yaml
CHANGED
package/specs/requirements.yaml
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
version: "
|
|
1
|
+
version: "14.0"
|
|
2
2
|
description: Defines repository requirements for the Snowflake ID library.
|
|
3
|
-
requires: []
|
|
4
|
-
conflicts: []
|
|
5
3
|
directives:
|
|
6
4
|
- id: E-106.1
|
|
7
5
|
dos:
|
|
8
6
|
- Preserve the documented Snowflake ID behavior.
|
|
9
7
|
- Return IDs as decimal strings.
|
|
10
|
-
-
|
|
11
|
-
- Pass
|
|
8
|
+
- Keep credentials and private data private.
|
|
9
|
+
- Pass repository validation before inspection.
|
|
12
10
|
donts:
|
|
13
11
|
- Do not use IDs as secrets or authentication tokens.
|
|
@@ -8,6 +8,7 @@ export function validateGeneratorOptions({
|
|
|
8
8
|
workerId = 0,
|
|
9
9
|
processId = process.pid % 32,
|
|
10
10
|
now = defaultNow,
|
|
11
|
+
logger,
|
|
11
12
|
} = {}) {
|
|
12
13
|
if (typeof epoch === "number" && !Number.isSafeInteger(epoch))
|
|
13
14
|
throw new RangeError("epoch must be between 0 and the timestamp limit");
|
|
@@ -15,7 +16,13 @@ export function validateGeneratorOptions({
|
|
|
15
16
|
if (baseEpoch < 0n || baseEpoch > MAX_TIMESTAMP)
|
|
16
17
|
throw new RangeError("epoch must be between 0 and the timestamp limit");
|
|
17
18
|
const identity = validateIdentity(workerId, processId);
|
|
18
|
-
if (typeof now !== "function") throw new TypeError("
|
|
19
|
+
if (typeof now !== "function") throw new TypeError("clock source must be a function");
|
|
20
|
+
if (
|
|
21
|
+
logger !== undefined &&
|
|
22
|
+
(!logger ||
|
|
23
|
+
["error", "warn", "info", "debug"].some((level) => typeof logger[level] !== "function"))
|
|
24
|
+
)
|
|
25
|
+
throw new TypeError("logger must provide error, warn, info, and debug methods");
|
|
19
26
|
|
|
20
|
-
return { baseEpoch, ...identity, now };
|
|
27
|
+
return { baseEpoch, ...identity, now, logger };
|
|
21
28
|
}
|
package/src/generator.mjs
CHANGED
|
@@ -8,40 +8,76 @@ import {
|
|
|
8
8
|
import { validateGeneratorOptions } from "./generator-options.mjs";
|
|
9
9
|
|
|
10
10
|
export function createGenerator(options = {}) {
|
|
11
|
-
const { baseEpoch, worker, processNumber, now } = validateGeneratorOptions(options);
|
|
11
|
+
const { baseEpoch, worker, processNumber, now, logger } = validateGeneratorOptions(options);
|
|
12
12
|
|
|
13
13
|
let lastTimestamp = -1n;
|
|
14
14
|
let sequence = 0n;
|
|
15
15
|
|
|
16
|
+
logger?.info("Snowflake generator created", {
|
|
17
|
+
component: "snowflake",
|
|
18
|
+
operation: "create-generator",
|
|
19
|
+
epoch: baseEpoch.toString(),
|
|
20
|
+
workerId: worker.toString(),
|
|
21
|
+
processId: processNumber.toString(),
|
|
22
|
+
});
|
|
23
|
+
|
|
16
24
|
function readTime() {
|
|
17
25
|
const clockValue = now();
|
|
18
26
|
if (typeof clockValue === "number" && !Number.isSafeInteger(clockValue))
|
|
19
27
|
throw new RangeError("current time must be a safe integer or bigint");
|
|
20
28
|
const timestamp = BigInt(clockValue);
|
|
21
|
-
if (timestamp < baseEpoch
|
|
29
|
+
if (timestamp < baseEpoch && lastTimestamp < baseEpoch)
|
|
30
|
+
throw new RangeError("current time must not be before epoch");
|
|
22
31
|
return timestamp;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
return function generate() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
try {
|
|
36
|
+
let timestamp = readTime();
|
|
37
|
+
if (timestamp < lastTimestamp) {
|
|
38
|
+
logger?.warn("Snowflake clock moved backward", {
|
|
39
|
+
component: "snowflake",
|
|
40
|
+
operation: "generate",
|
|
41
|
+
event: "clock-rollback",
|
|
42
|
+
timestamp: timestamp.toString(),
|
|
43
|
+
lastTimestamp: lastTimestamp.toString(),
|
|
44
|
+
});
|
|
45
|
+
timestamp = lastTimestamp;
|
|
46
|
+
}
|
|
47
|
+
let nextSequence;
|
|
48
|
+
if (timestamp === lastTimestamp) {
|
|
49
|
+
if (sequence === SEQUENCE_MASK)
|
|
50
|
+
throw new RangeError(
|
|
51
|
+
"sequence overflow. Retry when the clock returns a greater timestamp.",
|
|
52
|
+
);
|
|
53
|
+
nextSequence = sequence + 1n;
|
|
54
|
+
} else {
|
|
55
|
+
nextSequence = 0n;
|
|
56
|
+
}
|
|
57
|
+
const elapsed = timestamp - baseEpoch;
|
|
58
|
+
if (elapsed > MAX_TIMESTAMP) throw new RangeError("timestamp exceeds Snowflake capacity");
|
|
59
|
+
lastTimestamp = timestamp;
|
|
60
|
+
sequence = nextSequence;
|
|
61
|
+
const id = (
|
|
62
|
+
(elapsed << (WORKER_BITS + PROCESS_BITS + SEQUENCE_BITS)) |
|
|
63
|
+
(worker << (PROCESS_BITS + SEQUENCE_BITS)) |
|
|
64
|
+
(processNumber << SEQUENCE_BITS) |
|
|
65
|
+
nextSequence
|
|
66
|
+
).toString();
|
|
67
|
+
logger?.debug("Snowflake ID generated", {
|
|
68
|
+
component: "snowflake",
|
|
69
|
+
operation: "generate",
|
|
70
|
+
timestamp: timestamp.toString(),
|
|
71
|
+
sequence: nextSequence.toString(),
|
|
72
|
+
});
|
|
73
|
+
return id;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger?.error("Snowflake ID generation failed", {
|
|
76
|
+
component: "snowflake",
|
|
77
|
+
operation: "generate",
|
|
78
|
+
errorType: error.name,
|
|
79
|
+
});
|
|
80
|
+
throw error;
|
|
35
81
|
}
|
|
36
|
-
const elapsed = timestamp - baseEpoch;
|
|
37
|
-
if (elapsed > MAX_TIMESTAMP) throw new RangeError("timestamp exceeds Snowflake capacity");
|
|
38
|
-
lastTimestamp = timestamp;
|
|
39
|
-
sequence = nextSequence;
|
|
40
|
-
return (
|
|
41
|
-
(elapsed << (WORKER_BITS + PROCESS_BITS + SEQUENCE_BITS)) |
|
|
42
|
-
(worker << (PROCESS_BITS + SEQUENCE_BITS)) |
|
|
43
|
-
(processNumber << SEQUENCE_BITS) |
|
|
44
|
-
nextSequence
|
|
45
|
-
).toString();
|
|
46
82
|
};
|
|
47
83
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import type { LoggerWithConfiguredLevels } from "@eliware/log";
|
|
2
|
+
|
|
1
3
|
export interface SnowflakeGeneratorOptions {
|
|
2
4
|
epoch?: number | bigint;
|
|
3
5
|
workerId?: number | bigint;
|
|
4
6
|
processId?: number | bigint;
|
|
5
7
|
now?: () => number | bigint;
|
|
8
|
+
logger?: LoggerWithConfiguredLevels;
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
export type { LoggerWithConfiguredLevels } from "@eliware/log";
|
|
12
|
+
|
|
8
13
|
export interface SnowflakeConstants {
|
|
9
14
|
readonly epoch: bigint;
|
|
10
15
|
readonly sequenceBits: bigint;
|