snowflake-id-monotonic 1.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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +165 -0
- package/dist/index.mjs +173 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Princetoniño Piscos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# ❄️ Snowflake ID Generator (Monotonic)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/snowflake-id-monotonic)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
A performancent, monotonic Snowflake ID generator for Node.js and TypeScript applications. Inspired by Twitter's original Snowflake algorithm, time-ordered, unique IDs across distributed systems with zero dependencies,
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Monotonic**: IDs are always increasing, even when system clock moves backward
|
|
13
|
+
- **Async Support**: Non-blocking async version for concurrent environments
|
|
14
|
+
- **Timezone Aware**: Optional for timezone-specific decoding
|
|
15
|
+
- **ID Decoding**: Extracts creation timestamp from Snowflake ID base to its offset
|
|
16
|
+
- **Resilient**: Returns "0" on errors instead of throwing
|
|
17
|
+
- **TypeScript Native**: Full type definitions included
|
|
18
|
+
- **Flexible Configuration**: Custom epochs, machine IDs
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install snowflake-id-monotonic
|
|
24
|
+
# or
|
|
25
|
+
yarn add snowflake-id-monotonic
|
|
26
|
+
# or
|
|
27
|
+
pnpm add snowflake-id-monotonic
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Basic Usage
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { Snowflake } from 'snowflake-id-monotonic';
|
|
34
|
+
|
|
35
|
+
// Create generator with default settings
|
|
36
|
+
const snowflake = new Snowflake();
|
|
37
|
+
|
|
38
|
+
// Generate ID (synchronous - fastest)
|
|
39
|
+
const id = snowflake.GenerateID();
|
|
40
|
+
console.log(id); // "130303618715750401"
|
|
41
|
+
|
|
42
|
+
// Generate ID (asynchronous - non-blocking)
|
|
43
|
+
const asyncId = await snowflake.GenerateIDAsync();
|
|
44
|
+
console.log(asyncId); // "130303618715750402"
|
|
45
|
+
|
|
46
|
+
// Decode ID back to creation timestamp
|
|
47
|
+
const timestamp = snowflake.decodetoTimestamp(id);
|
|
48
|
+
console.log(timestamp); // "1745109200000" (epoch milliseconds)
|
|
49
|
+
|
|
50
|
+
// Format timestamp as Date
|
|
51
|
+
console.log(new Date(Number(timestamp)));
|
|
52
|
+
// Output: 2025-04-19T10:33:20.000Z
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Custom Config
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { Snowflake } from 'snowflake-id-monotonic';
|
|
59
|
+
|
|
60
|
+
// Create generator with custom settings
|
|
61
|
+
const snowflake = new Snowflake({
|
|
62
|
+
mid: 42, // Machine ID (0-1023)
|
|
63
|
+
offset: "2025-01-01T00:00:00Z", // Custom epoch (ISO string)
|
|
64
|
+
// offset: new Date("2025-01-01"), // Or Date object
|
|
65
|
+
// offset: 1735689600000, // Or milliseconds
|
|
66
|
+
// now: () => Date.now(), // Custom time source
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const id = snowflake.GenerateID();
|
|
70
|
+
console.log(id);
|
|
71
|
+
```
|
|
72
|
+
## 🎯 API Reference
|
|
73
|
+
|
|
74
|
+
__new Snowflake(options?)__ Creates a new Snowflake ID generator instance.
|
|
75
|
+
|
|
76
|
+
__GenerateID(): string__ Generates a unique Snowflake ID synchronously. Returns "0" on error.
|
|
77
|
+
|
|
78
|
+
__GenerateIDAsync(): Promise(string)__ Generates a unique Snowflake ID asynchronously. Returns "0" on error.
|
|
79
|
+
|
|
80
|
+
__decodetoTimestamp(id: string, offset?): string__ Decodes a Snowflake ID back to its creation timestamp (epoch milliseconds).
|
|
81
|
+
|
|
82
|
+
## Distributed System Setup
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
// Server 1
|
|
86
|
+
const server1 = new Snowflake({ mid: 1 });
|
|
87
|
+
|
|
88
|
+
// Server 2
|
|
89
|
+
const server2 = new Snowflake({ mid: 2 });
|
|
90
|
+
|
|
91
|
+
// Server 3
|
|
92
|
+
const server3 = new Snowflake({ mid: 3 });
|
|
93
|
+
|
|
94
|
+
// Each generates unique, non-overlapping IDs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Database Integration
|
|
98
|
+
```js
|
|
99
|
+
import { Snowflake } from 'snowflake-id-monotonic';
|
|
100
|
+
|
|
101
|
+
const snowflake = new Snowflake({ mid: process.env.MACHINE_ID || 1 });
|
|
102
|
+
|
|
103
|
+
// Create user with Snowflake ID
|
|
104
|
+
async function createUser(userData) {
|
|
105
|
+
const userId = await snowflake.GenerateIDAsync();
|
|
106
|
+
const createdAt = Number(snowflake.decodetoTimestamp(userId));
|
|
107
|
+
|
|
108
|
+
await db.users.insert({
|
|
109
|
+
id: userId,
|
|
110
|
+
...userData,
|
|
111
|
+
created_at: new Date(createdAt)
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return userId;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 📈 Performance
|
|
119
|
+
|
|
120
|
+
### Benchmarks (Node.js v24, Acer Aspire 5 WIN11 Core i5-11th Gen 16gb ram 1tb ssd)
|
|
121
|
+
|
|
122
|
+
| Method | Rate (IDs/sec) | Time per ID | Relative Speed |
|
|
123
|
+
|--------|----------------|-------------|----------------|
|
|
124
|
+
| `GenerateID()` | ~1.2 million | ~0.0008ms | 1.52x faster |
|
|
125
|
+
| `GenerateIDAsync()` | ~800,000 | ~0.0012ms | Baseline |
|
|
126
|
+
| **Concurrent (100 instances)** | ~340,000 | ~0.0029ms | System-wide |
|
|
127
|
+
|
|
128
|
+
### Performance Characteristics
|
|
129
|
+
|
|
130
|
+
1. **Synchronous (`GenerateID()`)**
|
|
131
|
+
- **Best for**: Bulk generation, single-threaded workloads
|
|
132
|
+
- **Overhead**: Minimal, but may block event loop briefly
|
|
133
|
+
- **Use when**: Generating IDs in loops or batches
|
|
134
|
+
|
|
135
|
+
2. **Asynchronous (`GenerateIDAsync()`)**
|
|
136
|
+
- **Best for**: High-concurrency, I/O-bound applications
|
|
137
|
+
- **Overhead**: ~52% slower due to Promise scheduling
|
|
138
|
+
- **Use when**: In async/await contexts, web servers, real-time systems
|
|
139
|
+
|
|
140
|
+
3. **Memory Efficiency**
|
|
141
|
+
- Minimal memory footprint (< 1MB for 1M IDs)
|
|
142
|
+
- No significant GC pressure
|
|
143
|
+
- Suitable for serverless and memory-constrained environments
|
|
144
|
+
|
|
145
|
+
## 🤝 Contributing
|
|
146
|
+
Every snowflake matters - and so does every contribution... Whether you're fixing a typo or adding a new feature...
|
|
147
|
+
|
|
148
|
+
How You Can Help:
|
|
149
|
+
- 🐛 Found a bug? File an issue with the snowflake ID that caused it!
|
|
150
|
+
- 💡 Have an idea? Suggest features that would make your snowfall heavier
|
|
151
|
+
- 📚 Better docs? Help others navigate the blizzard
|
|
152
|
+
- 🔧 Code contribution? Check our Contributing Guide to get started
|
|
153
|
+
|
|
154
|
+
### Fun fact: Why 'snowflake'?
|
|
155
|
+
Because a snowflake's shape evolves as it journeys through the air, no two will ever be the same.
|
|
156
|
+
Even two flakes floating side by side will each be blown through different levels
|
|
157
|
+
of humidity and vapour to create a shape that is truly unique. -BBC UK
|
|
158
|
+
|
|
159
|
+
### First Time? No Problem!
|
|
160
|
+
- Look for issues tagged good-first-issue ❄️ - perfect for your first contribution to an open source snowstorm!
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Quick start for contributors
|
|
164
|
+
git clone https://github.com/piscode/snowflake-id-monotonic.git
|
|
165
|
+
cd snowflake-id-monotonic
|
|
166
|
+
npm install
|
|
167
|
+
npm test
|
|
168
|
+
|
|
169
|
+
benchmark:
|
|
170
|
+
- path\test> node benchmark.js
|
|
171
|
+
- path\test> node benchmark-batch.js
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### The Originals
|
|
175
|
+
Twitter Engineering (2010) - For inventing the Snowflake concept that powers thousands of applications today.
|
|
176
|
+
Their brilliant 64-bit timestamp-based design revolutionized distributed ID generation.
|
|
177
|
+
_Dustin Rouillard_ - For the initial JavaScript implementation that made Snowflakes accessible
|
|
178
|
+
|
|
179
|
+
## Special Thanks To:
|
|
180
|
+
- The Open Source Community - For being the atmosphere where ideas can collide and form beautiful patterns
|
|
181
|
+
- Early Adopters - For braving the first snowfall and reporting the icy patches
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type SnowflakeOffset = number | Date | string;
|
|
2
|
+
|
|
3
|
+
export interface SnowflakeOptions
|
|
4
|
+
{
|
|
5
|
+
/** Machine ID (0–1023). Defaults to 1. */
|
|
6
|
+
mid?: number;
|
|
7
|
+
/** Custom epoch (ms since unix epoch), Date, or ISO string. Defaults to 2025-01-01T00:00:00Z. */
|
|
8
|
+
offset?: SnowflakeOffset;
|
|
9
|
+
/** Override time source: either a function returning epoch ms, or a fixed epoch ms number. */
|
|
10
|
+
now?: (() => number) | number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export declare class Snowflake
|
|
14
|
+
{
|
|
15
|
+
static DEFAULT_OFFSET: string;
|
|
16
|
+
static MIN_EPOCH_AGE_MS: number;
|
|
17
|
+
|
|
18
|
+
/** Sequence number (0–4095). */
|
|
19
|
+
seq: number;
|
|
20
|
+
/** Machine ID (0–1023). */
|
|
21
|
+
mid: number;
|
|
22
|
+
/** Custom epoch in ms. */
|
|
23
|
+
offset: number;
|
|
24
|
+
/** Time source. */
|
|
25
|
+
now: (() => number) | number;
|
|
26
|
+
/** Last timestamp used (epoch ms). */
|
|
27
|
+
lastTime: number;
|
|
28
|
+
|
|
29
|
+
constructor(options?: SnowflakeOptions);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generates a unique, time-ordered Snowflake ID.
|
|
33
|
+
*
|
|
34
|
+
* Note: the current implementation returns `0` on error.
|
|
35
|
+
*/
|
|
36
|
+
GenerateID(): string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a unique, time-ordered Snowflake ID (non-blocking version).
|
|
40
|
+
*
|
|
41
|
+
* Note: the current implementation returns `0` on error.
|
|
42
|
+
*/
|
|
43
|
+
GenerateIDAsync(): Promise<string>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decodes a Snowflake ID back to its creation timestamp (epoch ms).
|
|
47
|
+
*
|
|
48
|
+
* Note: the current implementation returns `0` on error.
|
|
49
|
+
*/
|
|
50
|
+
decodetoTimestamp(id: string, offset?: SnowflakeOffset): string;
|
|
51
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __typeError = (msg) => {
|
|
4
|
+
throw TypeError(msg);
|
|
5
|
+
};
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
8
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
9
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
10
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
11
|
+
|
|
12
|
+
// src/index.js
|
|
13
|
+
var _Snowflake_static, resolveOffsetMs_fn, _Snowflake_instances, getCurrentTime_fn, waitNextMs_fn;
|
|
14
|
+
var _Snowflake = class _Snowflake {
|
|
15
|
+
/**
|
|
16
|
+
* @constructor
|
|
17
|
+
* @param {Object} options Configuration options
|
|
18
|
+
* @param {number} options.mid Machine ID (0–1023)
|
|
19
|
+
* @param {number|Date|string} options.offset Custom epoch (ms, Date, or ISO string)
|
|
20
|
+
* @description creates a new Snowflake ID generator instance
|
|
21
|
+
*/
|
|
22
|
+
constructor(options) {
|
|
23
|
+
__privateAdd(this, _Snowflake_instances);
|
|
24
|
+
__publicField(this, "seq", 0);
|
|
25
|
+
__publicField(this, "mid");
|
|
26
|
+
__publicField(this, "offset");
|
|
27
|
+
__publicField(this, "now", Date.now);
|
|
28
|
+
__publicField(this, "lastTime", 0);
|
|
29
|
+
var _a;
|
|
30
|
+
this.mid = (options?.mid ?? 1) & 1023;
|
|
31
|
+
this.offset = __privateMethod(_a = _Snowflake, _Snowflake_static, resolveOffsetMs_fn).call(_a, options?.offset);
|
|
32
|
+
if (!Number.isFinite(this.offset))
|
|
33
|
+
throw new Error("Invalid offset: must be a valid epoch in milliseconds, or a parsable DEFAULT_OFFSET ISO string.");
|
|
34
|
+
this.now = options?.now ?? Date.now;
|
|
35
|
+
const nowValue = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
36
|
+
if (!Number.isFinite(nowValue))
|
|
37
|
+
throw new Error("Invalid now: must be a function returning epoch ms, or a finite number of epoch ms.");
|
|
38
|
+
if (this.offset > nowValue - _Snowflake.MIN_EPOCH_AGE_MS)
|
|
39
|
+
throw new Error("Invalid offset: custom epoch must not be in the future.");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @function GenerateID
|
|
43
|
+
* @description Generates a unique, time-ordered Snowflake ID
|
|
44
|
+
* Blocking version (synchronous); Fastest for single-threaded usage, may block the event loop briefly.
|
|
45
|
+
* @returns {string} The generated Snowflake ID as a string
|
|
46
|
+
*/
|
|
47
|
+
GenerateID() {
|
|
48
|
+
let time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
49
|
+
try {
|
|
50
|
+
if (time < this.lastTime)
|
|
51
|
+
time = this.lastTime;
|
|
52
|
+
if (time === this.lastTime) {
|
|
53
|
+
this.seq = this.seq + 1 & 4095;
|
|
54
|
+
if (this.seq === 0) {
|
|
55
|
+
while (__privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this) <= time)
|
|
56
|
+
time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
this.seq = 0;
|
|
60
|
+
}
|
|
61
|
+
this.lastTime = time;
|
|
62
|
+
const timePart = BigInt(time - this.offset) << 22n;
|
|
63
|
+
const midPart = BigInt(this.mid) << 12n;
|
|
64
|
+
const seqPart = BigInt(this.seq);
|
|
65
|
+
return (timePart | midPart | seqPart).toString(10);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return "0";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @function GenerateIDAsync
|
|
72
|
+
* @description Non-blocking version (asynchronous); Uses async/await to yield control instead of blocking event loop.
|
|
73
|
+
* Slightly slower due to async overhead, but better for high-concurrency scenarios.
|
|
74
|
+
* @returns {Promise<string>} Promise that resolves to the generated Snowflake ID as a string
|
|
75
|
+
*/
|
|
76
|
+
async GenerateIDAsync() {
|
|
77
|
+
let time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
78
|
+
try {
|
|
79
|
+
if (time < this.lastTime)
|
|
80
|
+
time = this.lastTime;
|
|
81
|
+
if (time === this.lastTime) {
|
|
82
|
+
this.seq = this.seq + 1 & 4095;
|
|
83
|
+
if (this.seq === 0) {
|
|
84
|
+
await __privateMethod(this, _Snowflake_instances, waitNextMs_fn).call(this, time);
|
|
85
|
+
time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
this.seq = 0;
|
|
89
|
+
}
|
|
90
|
+
this.lastTime = time;
|
|
91
|
+
const timePart = BigInt(time - this.offset) << 22n;
|
|
92
|
+
const midPart = BigInt(this.mid) << 12n;
|
|
93
|
+
const seqPart = BigInt(this.seq);
|
|
94
|
+
const id = timePart | midPart | seqPart;
|
|
95
|
+
return id.toString(10);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return "0";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* @function decodetoTimestamp
|
|
102
|
+
* @param {string} id The Snowflake ID to decode
|
|
103
|
+
* @returns {string} The timestamp (epoch ms) when the ID was generated, or "0" on error
|
|
104
|
+
* @description decodes a Snowflake ID back to its creation timestamp (epoch ms)
|
|
105
|
+
*/
|
|
106
|
+
decodetoTimestamp(id, offset = this.offset) {
|
|
107
|
+
var _a;
|
|
108
|
+
try {
|
|
109
|
+
const offsetMs = __privateMethod(_a = _Snowflake, _Snowflake_static, resolveOffsetMs_fn).call(_a, offset);
|
|
110
|
+
if (!Number.isFinite(offsetMs))
|
|
111
|
+
throw new Error("Invalid offset");
|
|
112
|
+
const timePart = BigInt(id) >> 22n;
|
|
113
|
+
const timestamp = timePart + BigInt(offsetMs);
|
|
114
|
+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
115
|
+
if (timestamp > maxSafe)
|
|
116
|
+
throw new Error("Timestamp exceeds MAX_SAFE_INTEGER");
|
|
117
|
+
return String(Number(timestamp));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return "0";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
_Snowflake_static = new WeakSet();
|
|
124
|
+
resolveOffsetMs_fn = function(offset) {
|
|
125
|
+
if (typeof offset === "number")
|
|
126
|
+
return offset;
|
|
127
|
+
if (offset instanceof Date)
|
|
128
|
+
return offset.getTime();
|
|
129
|
+
const parsed = Date.parse(offset ?? _Snowflake.DEFAULT_OFFSET);
|
|
130
|
+
return Number.isFinite(parsed) ? parsed : NaN;
|
|
131
|
+
};
|
|
132
|
+
_Snowflake_instances = new WeakSet();
|
|
133
|
+
/**
|
|
134
|
+
* Retrieves the current timestamp in milliseconds adjusted by offset
|
|
135
|
+
* @returns {number} Current timestamp in milliseconds adjusted by offset
|
|
136
|
+
*/
|
|
137
|
+
getCurrentTime_fn = function() {
|
|
138
|
+
if (typeof this.now === "function")
|
|
139
|
+
return this.now();
|
|
140
|
+
if (typeof this.now === "number")
|
|
141
|
+
return this.now;
|
|
142
|
+
return Date.now();
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* @function #waitNextMs
|
|
146
|
+
* @param {number} time The current timestamp in milliseconds
|
|
147
|
+
* @returns {Promise<void>} Promise that resolves when the next millisecond is reached
|
|
148
|
+
* @description Non-blocking wait until the next millisecond
|
|
149
|
+
*/
|
|
150
|
+
waitNextMs_fn = function(time) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const check = () => {
|
|
153
|
+
if (__privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this) > time)
|
|
154
|
+
resolve();
|
|
155
|
+
else
|
|
156
|
+
setTimeout(check, 0);
|
|
157
|
+
};
|
|
158
|
+
check();
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
__privateAdd(_Snowflake, _Snowflake_static);
|
|
162
|
+
__publicField(_Snowflake, "DEFAULT_OFFSET", "2000-01-01T00:00:00Z");
|
|
163
|
+
__publicField(_Snowflake, "MIN_EPOCH_AGE_MS", 0);
|
|
164
|
+
var Snowflake = _Snowflake;
|
|
165
|
+
module.exports = { Snowflake };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __typeError = (msg) => {
|
|
4
|
+
throw TypeError(msg);
|
|
5
|
+
};
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
11
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
12
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
13
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
14
|
+
|
|
15
|
+
// src/index.js
|
|
16
|
+
var require_index = __commonJS({
|
|
17
|
+
"src/index.js"(exports, module) {
|
|
18
|
+
var _Snowflake_static, resolveOffsetMs_fn, _Snowflake_instances, getCurrentTime_fn, waitNextMs_fn;
|
|
19
|
+
var _Snowflake = class _Snowflake {
|
|
20
|
+
/**
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {Object} options Configuration options
|
|
23
|
+
* @param {number} options.mid Machine ID (0–1023)
|
|
24
|
+
* @param {number|Date|string} options.offset Custom epoch (ms, Date, or ISO string)
|
|
25
|
+
* @description creates a new Snowflake ID generator instance
|
|
26
|
+
*/
|
|
27
|
+
constructor(options) {
|
|
28
|
+
__privateAdd(this, _Snowflake_instances);
|
|
29
|
+
__publicField(this, "seq", 0);
|
|
30
|
+
__publicField(this, "mid");
|
|
31
|
+
__publicField(this, "offset");
|
|
32
|
+
__publicField(this, "now", Date.now);
|
|
33
|
+
__publicField(this, "lastTime", 0);
|
|
34
|
+
var _a;
|
|
35
|
+
this.mid = (options?.mid ?? 1) & 1023;
|
|
36
|
+
this.offset = __privateMethod(_a = _Snowflake, _Snowflake_static, resolveOffsetMs_fn).call(_a, options?.offset);
|
|
37
|
+
if (!Number.isFinite(this.offset))
|
|
38
|
+
throw new Error("Invalid offset: must be a valid epoch in milliseconds, or a parsable DEFAULT_OFFSET ISO string.");
|
|
39
|
+
this.now = options?.now ?? Date.now;
|
|
40
|
+
const nowValue = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
41
|
+
if (!Number.isFinite(nowValue))
|
|
42
|
+
throw new Error("Invalid now: must be a function returning epoch ms, or a finite number of epoch ms.");
|
|
43
|
+
if (this.offset > nowValue - _Snowflake.MIN_EPOCH_AGE_MS)
|
|
44
|
+
throw new Error("Invalid offset: custom epoch must not be in the future.");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @function GenerateID
|
|
48
|
+
* @description Generates a unique, time-ordered Snowflake ID
|
|
49
|
+
* Blocking version (synchronous); Fastest for single-threaded usage, may block the event loop briefly.
|
|
50
|
+
* @returns {string} The generated Snowflake ID as a string
|
|
51
|
+
*/
|
|
52
|
+
GenerateID() {
|
|
53
|
+
let time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
54
|
+
try {
|
|
55
|
+
if (time < this.lastTime)
|
|
56
|
+
time = this.lastTime;
|
|
57
|
+
if (time === this.lastTime) {
|
|
58
|
+
this.seq = this.seq + 1 & 4095;
|
|
59
|
+
if (this.seq === 0) {
|
|
60
|
+
while (__privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this) <= time)
|
|
61
|
+
time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
this.seq = 0;
|
|
65
|
+
}
|
|
66
|
+
this.lastTime = time;
|
|
67
|
+
const timePart = BigInt(time - this.offset) << 22n;
|
|
68
|
+
const midPart = BigInt(this.mid) << 12n;
|
|
69
|
+
const seqPart = BigInt(this.seq);
|
|
70
|
+
return (timePart | midPart | seqPart).toString(10);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return "0";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @function GenerateIDAsync
|
|
77
|
+
* @description Non-blocking version (asynchronous); Uses async/await to yield control instead of blocking event loop.
|
|
78
|
+
* Slightly slower due to async overhead, but better for high-concurrency scenarios.
|
|
79
|
+
* @returns {Promise<string>} Promise that resolves to the generated Snowflake ID as a string
|
|
80
|
+
*/
|
|
81
|
+
async GenerateIDAsync() {
|
|
82
|
+
let time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
83
|
+
try {
|
|
84
|
+
if (time < this.lastTime)
|
|
85
|
+
time = this.lastTime;
|
|
86
|
+
if (time === this.lastTime) {
|
|
87
|
+
this.seq = this.seq + 1 & 4095;
|
|
88
|
+
if (this.seq === 0) {
|
|
89
|
+
await __privateMethod(this, _Snowflake_instances, waitNextMs_fn).call(this, time);
|
|
90
|
+
time = __privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
this.seq = 0;
|
|
94
|
+
}
|
|
95
|
+
this.lastTime = time;
|
|
96
|
+
const timePart = BigInt(time - this.offset) << 22n;
|
|
97
|
+
const midPart = BigInt(this.mid) << 12n;
|
|
98
|
+
const seqPart = BigInt(this.seq);
|
|
99
|
+
const id = timePart | midPart | seqPart;
|
|
100
|
+
return id.toString(10);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return "0";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @function decodetoTimestamp
|
|
107
|
+
* @param {string} id The Snowflake ID to decode
|
|
108
|
+
* @returns {string} The timestamp (epoch ms) when the ID was generated, or "0" on error
|
|
109
|
+
* @description decodes a Snowflake ID back to its creation timestamp (epoch ms)
|
|
110
|
+
*/
|
|
111
|
+
decodetoTimestamp(id, offset = this.offset) {
|
|
112
|
+
var _a;
|
|
113
|
+
try {
|
|
114
|
+
const offsetMs = __privateMethod(_a = _Snowflake, _Snowflake_static, resolveOffsetMs_fn).call(_a, offset);
|
|
115
|
+
if (!Number.isFinite(offsetMs))
|
|
116
|
+
throw new Error("Invalid offset");
|
|
117
|
+
const timePart = BigInt(id) >> 22n;
|
|
118
|
+
const timestamp = timePart + BigInt(offsetMs);
|
|
119
|
+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
120
|
+
if (timestamp > maxSafe)
|
|
121
|
+
throw new Error("Timestamp exceeds MAX_SAFE_INTEGER");
|
|
122
|
+
return String(Number(timestamp));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return "0";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
_Snowflake_static = new WeakSet();
|
|
129
|
+
resolveOffsetMs_fn = function(offset) {
|
|
130
|
+
if (typeof offset === "number")
|
|
131
|
+
return offset;
|
|
132
|
+
if (offset instanceof Date)
|
|
133
|
+
return offset.getTime();
|
|
134
|
+
const parsed = Date.parse(offset ?? _Snowflake.DEFAULT_OFFSET);
|
|
135
|
+
return Number.isFinite(parsed) ? parsed : NaN;
|
|
136
|
+
};
|
|
137
|
+
_Snowflake_instances = new WeakSet();
|
|
138
|
+
/**
|
|
139
|
+
* Retrieves the current timestamp in milliseconds adjusted by offset
|
|
140
|
+
* @returns {number} Current timestamp in milliseconds adjusted by offset
|
|
141
|
+
*/
|
|
142
|
+
getCurrentTime_fn = function() {
|
|
143
|
+
if (typeof this.now === "function")
|
|
144
|
+
return this.now();
|
|
145
|
+
if (typeof this.now === "number")
|
|
146
|
+
return this.now;
|
|
147
|
+
return Date.now();
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* @function #waitNextMs
|
|
151
|
+
* @param {number} time The current timestamp in milliseconds
|
|
152
|
+
* @returns {Promise<void>} Promise that resolves when the next millisecond is reached
|
|
153
|
+
* @description Non-blocking wait until the next millisecond
|
|
154
|
+
*/
|
|
155
|
+
waitNextMs_fn = function(time) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const check = () => {
|
|
158
|
+
if (__privateMethod(this, _Snowflake_instances, getCurrentTime_fn).call(this) > time)
|
|
159
|
+
resolve();
|
|
160
|
+
else
|
|
161
|
+
setTimeout(check, 0);
|
|
162
|
+
};
|
|
163
|
+
check();
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
__privateAdd(_Snowflake, _Snowflake_static);
|
|
167
|
+
__publicField(_Snowflake, "DEFAULT_OFFSET", "2000-01-01T00:00:00Z");
|
|
168
|
+
__publicField(_Snowflake, "MIN_EPOCH_AGE_MS", 0);
|
|
169
|
+
var Snowflake = _Snowflake;
|
|
170
|
+
module.exports = { Snowflake };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
export default require_index();
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "snowflake-id-monotonic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny module to generate monotonic time-based 64-bit unique IDs and decode them back to their creation timestamp, inspired by Twitter Snowflake.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"snowflake",
|
|
7
|
+
"snowflake-id",
|
|
8
|
+
"snowflake-id-generator",
|
|
9
|
+
"monotonic-id",
|
|
10
|
+
"unique-id",
|
|
11
|
+
"id-generator",
|
|
12
|
+
"distributed-id",
|
|
13
|
+
"time-based-id"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/piscodev/snowflake-id-monotonic#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/piscodev/snowflake-id-monotonic/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/piscodev/snowflake-id-monotonic.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Princetoniño Piscos",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"require": "./dist/index.js",
|
|
34
|
+
"import": "./dist/index.mjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node scripts/clean-dist.cjs && tsup src/index.js --format cjs,esm --out-dir dist && node scripts/copy-types.cjs",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"test": "npm run typecheck",
|
|
48
|
+
"prepublish": "npm run build && npm test"
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
}
|
|
55
|
+
}
|