@eliware/snowflake 1.1.1 → 1.1.4

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 CHANGED
@@ -1,128 +1,63 @@
1
- # [![eliware.org](https://eliware.org/logos/brand.png)](https://discord.gg/M6aTR9eTwN)
1
+ # @eliware/snowflake
2
2
 
3
- ## @eliware/snowflake [![npm version](https://img.shields.io/npm/v/@eliware/snowflake.svg)](https://www.npmjs.com/package/@eliware/snowflake)[![license](https://img.shields.io/github/license/eliware/snowflake.svg)](LICENSE)[![build status](https://github.com/eliware/snowflake/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eliware/snowflake/actions)
3
+ [![npm version](https://img.shields.io/npm/v/@eliware/snowflake.svg)](https://www.npmjs.com/package/@eliware/snowflake) [![build status](https://github.com/eliware/snowflake/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eliware/snowflake/actions)
4
4
 
5
- > A snowflake for Node.js (Insert Brief Description)
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
- - Minimal, dependency-light Snowflake ID generator compatible with Discord-style bit layout.
24
- - BigInt-safe implementation to preserve full 64-bit IDs.
25
- - Works in both ESM and CommonJS environments.
26
- - Configurable epoch and worker ID via a factory function.
27
- - Includes TypeScript declarations and basic tests.
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
- // ESM (index.mjs)
43
- import generate, { createSnowflakeGenerator } from './index.mjs';
26
+ import generate, { createSnowflakeGenerator } from '@eliware/snowflake';
44
27
 
45
- // default generator (workerId: 0)
46
- console.log('id:', generate());
28
+ console.log(generate());
47
29
 
48
- // create a generator for a specific worker ID
49
- const gen = createSnowflakeGenerator({ workerId: 1 });
50
- console.log('worker 1 id:', gen());
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
- ### CommonJS Example
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
- ### createSnowflakeGenerator(options?) => () => string
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
- A ready-to-use generator (workerId 0). It returns a decimal string representation of the Snowflake ID.
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
- ## TypeScript
51
+ The exports `generate` and `snowflake` are ready-to-use default generators. `constants` exposes the bit layout and default epoch.
85
52
 
86
- Type definitions are included (index.d.ts). Example signatures:
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
- [![Discord](https://eliware.org/logos/discord_96.png)](https://discord.gg/M6aTR9eTwN)[![eliware.org](https://eliware.org/logos/eliware_96.png)](https://discord.gg/M6aTR9eTwN)
116
-
117
- **[eliware.org on Discord](https://discord.gg/M6aTR9eTwN)**
118
-
119
61
  ## License
120
62
 
121
- [MIT © 2025 Eli Sterling, eliware.org](LICENSE)
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 function createSnowflakeGenerator(options?: { epoch?: number | bigint; workerId?: number }): () => string;
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
- // Snowflake generator (ESM)
2
- const DEFAULT_EPOCH = 1420070400000n; // Discord epoch: 2015-01-01T00:00:00.000Z
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
- export function createSnowflakeGenerator({ epoch = DEFAULT_EPOCH, workerId = 0 } = {}) {
5
- const _epoch = BigInt(epoch);
6
- const _workerId = BigInt(workerId) & 0x3ffn; // 10 bits
7
- let lastTimestamp = 0n;
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 now = BigInt(Date.now());
54
+ let timestamp = readTime();
55
+ if (timestamp < lastTimestamp) timestamp = lastTimestamp;
12
56
 
13
- if (now === lastTimestamp) {
14
- sequence = (sequence + 1n) & 0xfffn; // 12 bits
57
+ if (timestamp === lastTimestamp) {
58
+ sequence = (sequence + 1n) & SEQUENCE_MASK;
15
59
  if (sequence === 0n) {
16
- // Sequence overflow in the same millisecond — wait for next ms
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
- lastTimestamp = now;
26
-
27
- const id = ((now - _epoch) << 22n) | (_workerId << 12n) | sequence;
28
- return id.toString();
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.1",
5
- "description": "A minimal Node.js Snowflake ID generator (Discord-compatible). Provides BigInt-safe generators for ESM and CommonJS with configurable epoch and worker ID.",
6
- "main": "index.cjs",
7
- "module": "index.mjs",
4
+ "version": "1.1.4",
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
- "import": "./index.mjs",
19
- "require": "./index.cjs",
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,13 @@
45
43
  },
46
44
  "homepage": "https://github.com/eliware/snowflake#readme",
47
45
  "devDependencies": {
48
- "jest": "^30.0.4"
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
- },
58
- "dependencies": {
59
- "@eliware/common": "^1.1.1"
60
54
  }
61
55
  }
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;