@shinsekai/snowflake 0.0.0-stage → 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 +22 -0
- package/README.md +101 -2
- package/dist/index.cjs +284 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.global.js +275 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +250 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @shinsekai/snowflake contributors
|
|
4
|
+
Copyright © 2020 The Sapphire Community and its contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shinsekai/snowflake
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Snowflake IDs as BigInts that are **unique and strictly increasing**, with a layout you can fit into a
|
|
4
|
+
signed 64-bit column. Invalid input throws instead of silently producing a wrong id.
|
|
5
|
+
|
|
6
|
+
No dependencies. ESM, CommonJS and a `<script>` build (global `ShinsekaiSnowflake`). Node 18+ and every modern browser.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Snowflake } from '@shinsekai/snowflake'
|
|
10
|
+
|
|
11
|
+
const snowflake = new Snowflake({
|
|
12
|
+
epoch: new Date('2026-01-01T00:00:00.000Z'),
|
|
13
|
+
workerId: 3, // one worker / process pair per generator running at the same time
|
|
14
|
+
processId: 7
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const id = snowflake.generate() // 1234567890123456n
|
|
18
|
+
const json = snowflake.generateString() // '1234567890123456', safe for JSON
|
|
19
|
+
snowflake.deconstruct(id) // { id, timestamp, workerId: 3n, processId: 7n, increment, epoch }
|
|
20
|
+
snowflake.timestampFrom(id) // ms since the Unix epoch, exact
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
| bits | 63 – 22 | 21 – 17 | 16 – 12 | 11 – 0 |
|
|
26
|
+
| ----- | --------- | ------- | ------- | --------- |
|
|
27
|
+
| field | timestamp | worker | process | increment |
|
|
28
|
+
| width | 42 | 5 | 5 | 12 |
|
|
29
|
+
|
|
30
|
+
The timestamp is milliseconds after your epoch. With the default 64 bits it lasts ~139 years; with
|
|
31
|
+
`bits: { total: 63 }` it lasts ~69 years and **every id fits a signed 64-bit integer** (PostgreSQL
|
|
32
|
+
`bigint`, MySQL `BIGINT`, Java `long`). With the default 64-bit layout, ids pass 2^63 halfway through,
|
|
33
|
+
which overflows those columns (with Discord's epoch: on 2084-09-06).
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// For a database: 63 bits, epoch close to today
|
|
37
|
+
const ids = new Snowflake({ epoch: Date.UTC(2026, 0, 1), bits: { total: 63 }, workerId: 1 })
|
|
38
|
+
|
|
39
|
+
// Any layout: 10-bit machine id, no process field
|
|
40
|
+
const custom = new Snowflake({ epoch: Date.UTC(2026, 0, 1), bits: { worker: 10, process: 0, increment: 12, total: 63 } })
|
|
41
|
+
custom.layout // { totalBits: 63, timestampBits: 41, maxWorkerId: 1023n, ... }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `new Snowflake(epoch | options)`
|
|
47
|
+
|
|
48
|
+
- `epoch` — `number | bigint | Date`. You can pass the epoch alone instead of an options object.
|
|
49
|
+
- `workerId` (default `0`), `processId` (default `1`) — must fit their fields, or a `RangeError` is thrown.
|
|
50
|
+
- `bits` — `{ worker = 5, process = 5, increment = 12, total = 64 }`. At least 35 bits must be left for the timestamp.
|
|
51
|
+
- `clock` — `() => number | bigint`, default `Date.now`. Inject one in tests.
|
|
52
|
+
|
|
53
|
+
### `generate(options?)` / `generateString(options?)`
|
|
54
|
+
|
|
55
|
+
With no options, ids come from the clock and a per-millisecond sequence:
|
|
56
|
+
|
|
57
|
+
- The sequence restarts at 0 every millisecond, and ids are strictly increasing.
|
|
58
|
+
- More than 4096 ids in a millisecond: the next id borrows the next millisecond instead of repeating.
|
|
59
|
+
- The clock goes back (NTP, VM resume): the last timestamp is held until the clock catches up, so ids never repeat or go backwards.
|
|
60
|
+
|
|
61
|
+
Options (all optional): `timestamp`, `increment`, `workerId`, `processId`.
|
|
62
|
+
|
|
63
|
+
- `timestamp` — make an id for that time. Repeated calls with the same timestamp continue its sequence
|
|
64
|
+
and throw once it's full; a timestamp earlier than the last one used gets increment 0 and doesn't move the generator back.
|
|
65
|
+
- `increment` — use exactly this sequence value (no state is touched).
|
|
66
|
+
- Every value is range-checked. A timestamp before the epoch or past the last one the layout holds throws a `RangeError`.
|
|
67
|
+
|
|
68
|
+
### Reading ids
|
|
69
|
+
|
|
70
|
+
- `deconstruct(id)` / `decode(id)` — fields as bigints. `decode` is bound, so `ids.map(snowflake.decode)` works.
|
|
71
|
+
- `timestampFrom(id)` — exact milliseconds as a number.
|
|
72
|
+
- `minIdAt(time)` / `maxIdAt(time)` — id bounds for range queries: `WHERE id BETWEEN $1 AND $2`.
|
|
73
|
+
- `Snowflake.compare(a, b)` — `-1 | 0 | 1` for strings or bigints, for sorting.
|
|
74
|
+
|
|
75
|
+
Ids can be a bigint, a safe integer number or a decimal string. Anything else (`'12abc'`, negatives, ids
|
|
76
|
+
wider than the layout, unsafe numbers) throws instead of producing a wrong answer.
|
|
77
|
+
|
|
78
|
+
### Presets
|
|
79
|
+
|
|
80
|
+
`DiscordEpoch`, `TwitterEpoch`, and shared `DiscordSnowflake` / `TwitterSnowflake` generators — handy for
|
|
81
|
+
reading Discord or Twitter ids. To generate your own, create a generator per process with its own worker / process ids.
|
|
82
|
+
|
|
83
|
+
## Guarantees
|
|
84
|
+
|
|
85
|
+
- **Unique:** one generator never returns the same id twice, even past 4096 ids per millisecond.
|
|
86
|
+
- **Ordered:** ids from one generator are strictly increasing, even when the clock goes back.
|
|
87
|
+
- **Exact:** `timestampFrom` and `deconstruct` return exactly what went in; no floating-point rounding.
|
|
88
|
+
- **Checked:** worker ids, process ids, increments and timestamps that don't fit their fields throw a `RangeError`
|
|
89
|
+
instead of being masked or overflowing. To fit a larger value such as a hash, mask it yourself: `workerId: hash & 31`.
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pnpm install
|
|
95
|
+
pnpm test # vitest
|
|
96
|
+
pnpm typecheck
|
|
97
|
+
pnpm build # dist/: ESM, CJS, IIFE, .d.ts
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. See [LICENSE](LICENSE).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DiscordEpoch: () => DiscordEpoch,
|
|
24
|
+
DiscordSnowflake: () => DiscordSnowflake,
|
|
25
|
+
MaximumIncrement: () => MaximumIncrement,
|
|
26
|
+
MaximumProcessId: () => MaximumProcessId,
|
|
27
|
+
MaximumWorkerId: () => MaximumWorkerId,
|
|
28
|
+
Snowflake: () => Snowflake,
|
|
29
|
+
TwitterEpoch: () => TwitterEpoch,
|
|
30
|
+
TwitterSnowflake: () => TwitterSnowflake
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/Snowflake.ts
|
|
35
|
+
var MaximumWorkerId = 0b11111n;
|
|
36
|
+
var MaximumProcessId = 0b11111n;
|
|
37
|
+
var MaximumIncrement = 0b111111111111n;
|
|
38
|
+
var mask = (bits) => (1n << BigInt(bits)) - 1n;
|
|
39
|
+
function toBigInt(value, name) {
|
|
40
|
+
if (typeof value === "bigint") return value;
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
if (!Number.isSafeInteger(value)) throw new RangeError(`"${name}" must be a safe integer (received ${value})`);
|
|
43
|
+
return BigInt(value);
|
|
44
|
+
}
|
|
45
|
+
throw new TypeError(`"${name}" must be a number or bigint (received ${typeof value})`);
|
|
46
|
+
}
|
|
47
|
+
function toTime(value, name) {
|
|
48
|
+
if (value instanceof Date) {
|
|
49
|
+
const time = value.getTime();
|
|
50
|
+
if (Number.isNaN(time)) throw new RangeError(`"${name}" is an invalid Date`);
|
|
51
|
+
return BigInt(time);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "number" || typeof value === "bigint") return toBigInt(value, name);
|
|
54
|
+
throw new TypeError(`"${name}" must be a number, bigint, or Date (received ${typeof value})`);
|
|
55
|
+
}
|
|
56
|
+
function inRange(value, max, name) {
|
|
57
|
+
const v = toBigInt(value, name);
|
|
58
|
+
if (v < 0n || v > max) throw new RangeError(`"${name}" must be between 0 and ${max} (received ${v})`);
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
function toId(id) {
|
|
62
|
+
let value;
|
|
63
|
+
if (typeof id === "bigint") value = id;
|
|
64
|
+
else if (typeof id === "string") {
|
|
65
|
+
if (!/^\d+$/.test(id)) throw new SyntaxError(`"${id}" is not a snowflake: expected decimal digits`);
|
|
66
|
+
value = BigInt(id);
|
|
67
|
+
} else if (typeof id === "number") value = toBigInt(id, "id");
|
|
68
|
+
else throw new TypeError(`A snowflake must be a bigint or decimal string (received ${typeof id})`);
|
|
69
|
+
if (value < 0n) throw new RangeError(`A snowflake can't be negative (received ${value})`);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
var Snowflake = class {
|
|
73
|
+
#epoch;
|
|
74
|
+
#epochNumber;
|
|
75
|
+
#layout;
|
|
76
|
+
#clock;
|
|
77
|
+
#shiftTimestamp;
|
|
78
|
+
#shiftWorker;
|
|
79
|
+
#shiftProcess;
|
|
80
|
+
#workerId;
|
|
81
|
+
#processId;
|
|
82
|
+
/** Last millisecond (absolute) the sequence was used for, and the sequence value used. */
|
|
83
|
+
#lastTimestamp = -1n;
|
|
84
|
+
#sequence = 0n;
|
|
85
|
+
/**
|
|
86
|
+
* @param options the epoch alone (as `@sapphire/snowflake` takes it), or {@link SnowflakeOptions}
|
|
87
|
+
*/
|
|
88
|
+
constructor(options) {
|
|
89
|
+
const opts = typeof options === "object" && !(options instanceof Date) ? options : { epoch: options };
|
|
90
|
+
this.#epoch = toTime(opts.epoch, "epoch");
|
|
91
|
+
if (this.#epoch < 0n) throw new RangeError(`"epoch" can't be before 1970 (received ${this.#epoch})`);
|
|
92
|
+
this.#epochNumber = Number(this.#epoch);
|
|
93
|
+
this.#clock = opts.clock ?? (() => Date.now());
|
|
94
|
+
const totalBits = opts.bits?.total ?? 64;
|
|
95
|
+
if (totalBits !== 63 && totalBits !== 64) throw new RangeError(`"bits.total" must be 63 or 64 (received ${totalBits})`);
|
|
96
|
+
const workerBits = opts.bits?.worker ?? 5;
|
|
97
|
+
const processBits = opts.bits?.process ?? 5;
|
|
98
|
+
const incrementBits = opts.bits?.increment ?? 12;
|
|
99
|
+
for (const [name, bits] of [["worker", workerBits], ["process", processBits], ["increment", incrementBits]]) {
|
|
100
|
+
if (!Number.isInteger(bits) || bits < 0) throw new RangeError(`"bits.${name}" must be a non-negative integer (received ${bits})`);
|
|
101
|
+
}
|
|
102
|
+
if (incrementBits < 1) throw new RangeError('"bits.increment" must be at least 1');
|
|
103
|
+
const timestampBits = totalBits - workerBits - processBits - incrementBits;
|
|
104
|
+
if (timestampBits < 35) throw new RangeError(`Only ${timestampBits} bits left for the timestamp: use fewer worker / process / increment bits`);
|
|
105
|
+
this.#layout = Object.freeze({
|
|
106
|
+
totalBits,
|
|
107
|
+
timestampBits,
|
|
108
|
+
workerBits,
|
|
109
|
+
processBits,
|
|
110
|
+
incrementBits,
|
|
111
|
+
maxWorkerId: mask(workerBits),
|
|
112
|
+
maxProcessId: mask(processBits),
|
|
113
|
+
maxIncrement: mask(incrementBits),
|
|
114
|
+
maxTimestampOffset: mask(timestampBits)
|
|
115
|
+
});
|
|
116
|
+
this.#shiftProcess = BigInt(incrementBits);
|
|
117
|
+
this.#shiftWorker = this.#shiftProcess + BigInt(processBits);
|
|
118
|
+
this.#shiftTimestamp = this.#shiftWorker + BigInt(workerBits);
|
|
119
|
+
this.#workerId = inRange(opts.workerId ?? 0n, this.#layout.maxWorkerId, "workerId");
|
|
120
|
+
this.#processId = inRange(opts.processId ?? (this.#layout.maxProcessId >= 1n ? 1n : 0n), this.#layout.maxProcessId, "processId");
|
|
121
|
+
}
|
|
122
|
+
/** The epoch, as a bigint. */
|
|
123
|
+
get epoch() {
|
|
124
|
+
return this.#epoch;
|
|
125
|
+
}
|
|
126
|
+
/** The epoch, as a number. */
|
|
127
|
+
get epochNumber() {
|
|
128
|
+
return this.#epochNumber;
|
|
129
|
+
}
|
|
130
|
+
/** Field widths and limits. */
|
|
131
|
+
get layout() {
|
|
132
|
+
return this.#layout;
|
|
133
|
+
}
|
|
134
|
+
/** The worker id used by default. */
|
|
135
|
+
get workerId() {
|
|
136
|
+
return this.#workerId;
|
|
137
|
+
}
|
|
138
|
+
/** Sets the default worker id. Throws a RangeError when it doesn't fit (it used to be silently masked). */
|
|
139
|
+
set workerId(value) {
|
|
140
|
+
this.#workerId = inRange(value, this.#layout.maxWorkerId, "workerId");
|
|
141
|
+
}
|
|
142
|
+
/** The process id used by default. */
|
|
143
|
+
get processId() {
|
|
144
|
+
return this.#processId;
|
|
145
|
+
}
|
|
146
|
+
/** Sets the default process id. Throws a RangeError when it doesn't fit (it used to be silently masked). */
|
|
147
|
+
set processId(value) {
|
|
148
|
+
this.#processId = inRange(value, this.#layout.maxProcessId, "processId");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Generates a snowflake.
|
|
152
|
+
*
|
|
153
|
+
* Without options, ids from one generator are unique and strictly increasing: the sequence
|
|
154
|
+
* restarts each millisecond; past its last value, or when the clock goes back, the id takes the
|
|
155
|
+
* next millisecond instead (ids may then run slightly ahead of the clock until it catches up).
|
|
156
|
+
*/
|
|
157
|
+
generate(options = {}) {
|
|
158
|
+
const workerId = options.workerId === void 0 ? this.#workerId : inRange(options.workerId, this.#layout.maxWorkerId, "workerId");
|
|
159
|
+
const processId = options.processId === void 0 ? this.#processId : inRange(options.processId, this.#layout.maxProcessId, "processId");
|
|
160
|
+
let timestamp;
|
|
161
|
+
let increment;
|
|
162
|
+
if (options.increment !== void 0) {
|
|
163
|
+
increment = inRange(options.increment, this.#layout.maxIncrement, "increment");
|
|
164
|
+
timestamp = options.timestamp === void 0 ? this.#now() : toTime(options.timestamp, "timestamp");
|
|
165
|
+
} else if (options.timestamp !== void 0) {
|
|
166
|
+
timestamp = toTime(options.timestamp, "timestamp");
|
|
167
|
+
this.#offset(timestamp);
|
|
168
|
+
if (timestamp < this.#lastTimestamp) {
|
|
169
|
+
increment = 0n;
|
|
170
|
+
} else {
|
|
171
|
+
if (timestamp === this.#lastTimestamp) {
|
|
172
|
+
if (this.#sequence === this.#layout.maxIncrement) {
|
|
173
|
+
throw new RangeError(`No more ids for timestamp ${timestamp}: all ${this.#layout.maxIncrement + 1n} have been used`);
|
|
174
|
+
}
|
|
175
|
+
this.#sequence++;
|
|
176
|
+
} else {
|
|
177
|
+
this.#sequence = 0n;
|
|
178
|
+
}
|
|
179
|
+
this.#lastTimestamp = timestamp;
|
|
180
|
+
increment = this.#sequence;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
timestamp = this.#next();
|
|
184
|
+
increment = this.#sequence;
|
|
185
|
+
}
|
|
186
|
+
return this.#compose(timestamp, workerId, processId, increment);
|
|
187
|
+
}
|
|
188
|
+
/** {@link generate}, as a decimal string: ready for JSON, which can't hold a bigint. */
|
|
189
|
+
generateString(options) {
|
|
190
|
+
return this.generate(options).toString();
|
|
191
|
+
}
|
|
192
|
+
/** Splits a snowflake into its fields. */
|
|
193
|
+
deconstruct(id) {
|
|
194
|
+
const value = this.#checkWidth(toId(id));
|
|
195
|
+
const { maxWorkerId, maxProcessId, maxIncrement } = this.#layout;
|
|
196
|
+
return {
|
|
197
|
+
id: value,
|
|
198
|
+
timestamp: (value >> this.#shiftTimestamp) + this.#epoch,
|
|
199
|
+
workerId: value >> this.#shiftWorker & maxWorkerId,
|
|
200
|
+
processId: value >> this.#shiftProcess & maxProcessId,
|
|
201
|
+
increment: value & maxIncrement,
|
|
202
|
+
epoch: this.#epoch
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Alias for {@link deconstruct}, bound to this generator: it still works detached (`const { decode } = snowflake`). */
|
|
206
|
+
decode = (id) => this.deconstruct(id);
|
|
207
|
+
/**
|
|
208
|
+
* The timestamp stored in a snowflake, in milliseconds since the Unix epoch. Computed with
|
|
209
|
+
* bigints: going through a floating-point number could be a millisecond off.
|
|
210
|
+
*/
|
|
211
|
+
timestampFrom(id) {
|
|
212
|
+
return Number(this.#checkWidth(toId(id)) >> this.#shiftTimestamp) + this.#epochNumber;
|
|
213
|
+
}
|
|
214
|
+
/** The smallest id possible at a timestamp: for "ids created from this time on" queries. */
|
|
215
|
+
minIdAt(timestamp) {
|
|
216
|
+
return this.#offset(toTime(timestamp, "timestamp")) << this.#shiftTimestamp;
|
|
217
|
+
}
|
|
218
|
+
/** The largest id possible at a timestamp: for "ids created up to this time" queries. */
|
|
219
|
+
maxIdAt(timestamp) {
|
|
220
|
+
return this.#offset(toTime(timestamp, "timestamp")) << this.#shiftTimestamp | mask(Number(this.#shiftTimestamp));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Orders two snowflakes: -1 if `a` is older than `b`, 0 if they are the same, 1 if `a` is newer.
|
|
224
|
+
* Works for any mix of bigints and decimal strings, leading zeros included.
|
|
225
|
+
*/
|
|
226
|
+
static compare(a, b) {
|
|
227
|
+
const x = toId(a);
|
|
228
|
+
const y = toId(b);
|
|
229
|
+
return x === y ? 0 : x < y ? -1 : 1;
|
|
230
|
+
}
|
|
231
|
+
/** The next millisecond for an automatic id, with the sequence set for it. */
|
|
232
|
+
#next() {
|
|
233
|
+
const now = this.#now();
|
|
234
|
+
if (now > this.#lastTimestamp) {
|
|
235
|
+
this.#lastTimestamp = now;
|
|
236
|
+
this.#sequence = 0n;
|
|
237
|
+
} else if (this.#sequence < this.#layout.maxIncrement) {
|
|
238
|
+
this.#sequence++;
|
|
239
|
+
} else {
|
|
240
|
+
this.#lastTimestamp++;
|
|
241
|
+
this.#sequence = 0n;
|
|
242
|
+
}
|
|
243
|
+
return this.#lastTimestamp;
|
|
244
|
+
}
|
|
245
|
+
#now() {
|
|
246
|
+
return toTime(this.#clock(), "clock()");
|
|
247
|
+
}
|
|
248
|
+
/** Milliseconds after the epoch, checked to fit the timestamp field. */
|
|
249
|
+
#offset(timestamp) {
|
|
250
|
+
const offset = timestamp - this.#epoch;
|
|
251
|
+
if (offset < 0n) throw new RangeError(`Timestamp ${timestamp} is before the epoch (${this.#epoch})`);
|
|
252
|
+
if (offset > this.#layout.maxTimestampOffset) {
|
|
253
|
+
throw new RangeError(
|
|
254
|
+
`Timestamp ${timestamp} is past the last one this layout can hold (${this.#epoch + this.#layout.maxTimestampOffset}): use a later epoch`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return offset;
|
|
258
|
+
}
|
|
259
|
+
#compose(timestamp, workerId, processId, increment) {
|
|
260
|
+
return this.#offset(timestamp) << this.#shiftTimestamp | workerId << this.#shiftWorker | processId << this.#shiftProcess | increment;
|
|
261
|
+
}
|
|
262
|
+
#checkWidth(id) {
|
|
263
|
+
if (id > mask(this.#layout.totalBits)) throw new RangeError(`${id} is wider than ${this.#layout.totalBits} bits`);
|
|
264
|
+
return id;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/presets.ts
|
|
269
|
+
var DiscordEpoch = 1420070400000n;
|
|
270
|
+
var TwitterEpoch = 1288834974657n;
|
|
271
|
+
var DiscordSnowflake = new Snowflake(DiscordEpoch);
|
|
272
|
+
var TwitterSnowflake = new Snowflake(TwitterEpoch);
|
|
273
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
274
|
+
0 && (module.exports = {
|
|
275
|
+
DiscordEpoch,
|
|
276
|
+
DiscordSnowflake,
|
|
277
|
+
MaximumIncrement,
|
|
278
|
+
MaximumProcessId,
|
|
279
|
+
MaximumWorkerId,
|
|
280
|
+
Snowflake,
|
|
281
|
+
TwitterEpoch,
|
|
282
|
+
TwitterSnowflake
|
|
283
|
+
});
|
|
284
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Snowflake.ts","../src/presets.ts"],"sourcesContent":["export * from './presets'\nexport * from './Snowflake'\n","/**\n * Snowflake IDs: 64-bit (or 63-bit) integers made of a timestamp, a worker id, a process id and a\n * per-millisecond sequence, so they are unique and sort by creation time.\n *\n * Twitter / Discord layout (the default):\n * ```\n * 64 22 17 12 0\n * 000000111011000111100001101001000101000000 00001 00000 000000000000\n * milliseconds since the epoch worker process increment\n * ```\n *\n * A drop-in successor to `@sapphire/snowflake` that, on one generator, guarantees unique and\n * increasing IDs: the sequence restarts every millisecond, and when it runs out (more than 4096\n * IDs in a millisecond) or the clock goes back, generation carries on from the last millisecond\n * used instead of repeating or going backwards. Out-of-range input throws instead of being masked.\n */\n\n/** A point in time: milliseconds since the Unix epoch, or a Date. */\nexport type TimeInput = number | bigint | Date\n\n/** Something that becomes a non-negative integer. */\nexport type IntegerInput = number | bigint\n\nexport interface SnowflakeBits {\n\t/** Bits of the worker id. @default 5 */\n\tworker?: number\n\t/** Bits of the process id. @default 5 */\n\tprocess?: number\n\t/** Bits of the per-millisecond sequence. @default 12 */\n\tincrement?: number\n\t/**\n\t * Width of the whole id. 64 is the Twitter / Discord layout; 63 keeps every id positive in a\n\t * signed 64-bit integer, such as PostgreSQL's `bigint` or Java's `long`. The timestamp gets the bits left over.\n\t * @default 64\n\t */\n\ttotal?: 63 | 64\n}\n\nexport interface SnowflakeOptions {\n\t/** Where timestamps count from. Pick one close to today: the timestamp field fills up after 2^timestampBits ms. */\n\tepoch: TimeInput\n\t/** Default worker id for generated ids. Give every generator that runs at the same time its own worker / process pair. @default 0 */\n\tworkerId?: IntegerInput\n\t/** Default process id for generated ids. @default 1 */\n\tprocessId?: IntegerInput\n\t/** Field widths. */\n\tbits?: SnowflakeBits\n\t/** The clock, in milliseconds since the Unix epoch. Replace it in tests. @default Date.now */\n\tclock?: () => number | bigint\n}\n\nexport interface SnowflakeGenerateOptions {\n\t/**\n\t * Timestamp of the id. Without it the clock is used, and ids are guaranteed unique and increasing.\n\t * With it, the id belongs to that millisecond: it continues the sequence when it is the latest\n\t * millisecond used, and starts at 0 for an earlier one (which doesn't move the generator back).\n\t * @default the clock\n\t */\n\ttimestamp?: TimeInput\n\t/**\n\t * Use this sequence value as is (0 to 4095 by default), outside the generator's own sequence.\n\t * Uniqueness is then up to you.\n\t */\n\tincrement?: IntegerInput\n\t/** Worker id for this id only. */\n\tworkerId?: IntegerInput\n\t/** Process id for this id only. */\n\tprocessId?: IntegerInput\n}\n\n/** Object returned by {@link Snowflake.deconstruct}. */\nexport interface DeconstructedSnowflake {\n\t/** The id as a bigint. */\n\tid: bigint\n\t/** Milliseconds since the Unix epoch, when the id was made. */\n\ttimestamp: bigint\n\tworkerId: bigint\n\tprocessId: bigint\n\tincrement: bigint\n\t/** The epoch the generator counts from. */\n\tepoch: bigint\n}\n\n/** The resolved field widths and limits of a {@link Snowflake}. */\nexport interface SnowflakeLayout {\n\ttotalBits: number\n\ttimestampBits: number\n\tworkerBits: number\n\tprocessBits: number\n\tincrementBits: number\n\tmaxWorkerId: bigint\n\tmaxProcessId: bigint\n\tmaxIncrement: bigint\n\t/** Largest timestamp offset (ms after the epoch) that still fits. */\n\tmaxTimestampOffset: bigint\n}\n\n/** The largest worker id in the default layout. */\nexport const MaximumWorkerId = 0b11111n\n/** The largest process id in the default layout. */\nexport const MaximumProcessId = 0b11111n\n/** The largest per-millisecond sequence value in the default layout. */\nexport const MaximumIncrement = 0b111111111111n\n\nconst mask = (bits: number) => (1n << BigInt(bits)) - 1n\n\nfunction toBigInt(value: unknown, name: string): bigint {\n\tif (typeof value === 'bigint') return value\n\tif (typeof value === 'number') {\n\t\tif (!Number.isSafeInteger(value)) throw new RangeError(`\"${name}\" must be a safe integer (received ${value})`)\n\t\treturn BigInt(value)\n\t}\n\tthrow new TypeError(`\"${name}\" must be a number or bigint (received ${typeof value})`)\n}\n\nfunction toTime(value: unknown, name: string): bigint {\n\tif (value instanceof Date) {\n\t\tconst time = value.getTime()\n\t\tif (Number.isNaN(time)) throw new RangeError(`\"${name}\" is an invalid Date`)\n\t\treturn BigInt(time)\n\t}\n\tif (typeof value === 'number' || typeof value === 'bigint') return toBigInt(value, name)\n\tthrow new TypeError(`\"${name}\" must be a number, bigint, or Date (received ${typeof value})`)\n}\n\nfunction inRange(value: IntegerInput, max: bigint, name: string): bigint {\n\tconst v = toBigInt(value, name)\n\tif (v < 0n || v > max) throw new RangeError(`\"${name}\" must be between 0 and ${max} (received ${v})`)\n\treturn v\n}\n\n/** A snowflake as a bigint: from a bigint, a safe integer, or a decimal string. */\nfunction toId(id: unknown): bigint {\n\tlet value: bigint\n\tif (typeof id === 'bigint') value = id\n\telse if (typeof id === 'string') {\n\t\tif (!/^\\d+$/.test(id)) throw new SyntaxError(`\"${id}\" is not a snowflake: expected decimal digits`)\n\t\tvalue = BigInt(id)\n\t} else if (typeof id === 'number') value = toBigInt(id, 'id')\n\telse throw new TypeError(`A snowflake must be a bigint or decimal string (received ${typeof id})`)\n\tif (value < 0n) throw new RangeError(`A snowflake can't be negative (received ${value})`)\n\treturn value\n}\n\nexport class Snowflake {\n\treadonly #epoch: bigint\n\treadonly #epochNumber: number\n\treadonly #layout: SnowflakeLayout\n\treadonly #clock: () => number | bigint\n\treadonly #shiftTimestamp: bigint\n\treadonly #shiftWorker: bigint\n\treadonly #shiftProcess: bigint\n\t#workerId: bigint\n\t#processId: bigint\n\t/** Last millisecond (absolute) the sequence was used for, and the sequence value used. */\n\t#lastTimestamp = -1n\n\t#sequence = 0n\n\n\t/**\n\t * @param options the epoch alone (as `@sapphire/snowflake` takes it), or {@link SnowflakeOptions}\n\t */\n\tpublic constructor(options: TimeInput | SnowflakeOptions) {\n\t\tconst opts: SnowflakeOptions =\n\t\t\ttypeof options === 'object' && !(options instanceof Date) ? options : { epoch: options }\n\n\t\tthis.#epoch = toTime(opts.epoch, 'epoch')\n\t\tif (this.#epoch < 0n) throw new RangeError(`\"epoch\" can't be before 1970 (received ${this.#epoch})`)\n\t\tthis.#epochNumber = Number(this.#epoch)\n\t\tthis.#clock = opts.clock ?? (() => Date.now())\n\n\t\tconst totalBits = opts.bits?.total ?? 64\n\t\tif (totalBits !== 63 && totalBits !== 64) throw new RangeError(`\"bits.total\" must be 63 or 64 (received ${totalBits})`)\n\t\tconst workerBits = opts.bits?.worker ?? 5\n\t\tconst processBits = opts.bits?.process ?? 5\n\t\tconst incrementBits = opts.bits?.increment ?? 12\n\t\tfor (const [name, bits] of [['worker', workerBits], ['process', processBits], ['increment', incrementBits]] as const) {\n\t\t\tif (!Number.isInteger(bits) || bits < 0) throw new RangeError(`\"bits.${name}\" must be a non-negative integer (received ${bits})`)\n\t\t}\n\t\tif (incrementBits < 1) throw new RangeError('\"bits.increment\" must be at least 1')\n\t\tconst timestampBits = totalBits - workerBits - processBits - incrementBits\n\t\t// below ~35 bits the id space runs out within a year: almost certainly a mistake\n\t\tif (timestampBits < 35) throw new RangeError(`Only ${timestampBits} bits left for the timestamp: use fewer worker / process / increment bits`)\n\n\t\tthis.#layout = Object.freeze({\n\t\t\ttotalBits,\n\t\t\ttimestampBits,\n\t\t\tworkerBits,\n\t\t\tprocessBits,\n\t\t\tincrementBits,\n\t\t\tmaxWorkerId: mask(workerBits),\n\t\t\tmaxProcessId: mask(processBits),\n\t\t\tmaxIncrement: mask(incrementBits),\n\t\t\tmaxTimestampOffset: mask(timestampBits)\n\t\t})\n\t\tthis.#shiftProcess = BigInt(incrementBits)\n\t\tthis.#shiftWorker = this.#shiftProcess + BigInt(processBits)\n\t\tthis.#shiftTimestamp = this.#shiftWorker + BigInt(workerBits)\n\n\t\tthis.#workerId = inRange(opts.workerId ?? 0n, this.#layout.maxWorkerId, 'workerId')\n\t\tthis.#processId = inRange(opts.processId ?? (this.#layout.maxProcessId >= 1n ? 1n : 0n), this.#layout.maxProcessId, 'processId')\n\t}\n\n\t/** The epoch, as a bigint. */\n\tpublic get epoch(): bigint {\n\t\treturn this.#epoch\n\t}\n\n\t/** The epoch, as a number. */\n\tpublic get epochNumber(): number {\n\t\treturn this.#epochNumber\n\t}\n\n\t/** Field widths and limits. */\n\tpublic get layout(): Readonly<SnowflakeLayout> {\n\t\treturn this.#layout\n\t}\n\n\t/** The worker id used by default. */\n\tpublic get workerId(): bigint {\n\t\treturn this.#workerId\n\t}\n\n\t/** Sets the default worker id. Throws a RangeError when it doesn't fit (it used to be silently masked). */\n\tpublic set workerId(value: IntegerInput) {\n\t\tthis.#workerId = inRange(value, this.#layout.maxWorkerId, 'workerId')\n\t}\n\n\t/** The process id used by default. */\n\tpublic get processId(): bigint {\n\t\treturn this.#processId\n\t}\n\n\t/** Sets the default process id. Throws a RangeError when it doesn't fit (it used to be silently masked). */\n\tpublic set processId(value: IntegerInput) {\n\t\tthis.#processId = inRange(value, this.#layout.maxProcessId, 'processId')\n\t}\n\n\t/**\n\t * Generates a snowflake.\n\t *\n\t * Without options, ids from one generator are unique and strictly increasing: the sequence\n\t * restarts each millisecond; past its last value, or when the clock goes back, the id takes the\n\t * next millisecond instead (ids may then run slightly ahead of the clock until it catches up).\n\t */\n\tpublic generate(options: SnowflakeGenerateOptions = {}): bigint {\n\t\tconst workerId = options.workerId === undefined ? this.#workerId : inRange(options.workerId, this.#layout.maxWorkerId, 'workerId')\n\t\tconst processId = options.processId === undefined ? this.#processId : inRange(options.processId, this.#layout.maxProcessId, 'processId')\n\n\t\tlet timestamp: bigint\n\t\tlet increment: bigint\n\n\t\tif (options.increment !== undefined) {\n\t\t\t// a fixed increment: exactly the id asked for, outside the generator's sequence\n\t\t\tincrement = inRange(options.increment, this.#layout.maxIncrement, 'increment')\n\t\t\ttimestamp = options.timestamp === undefined ? this.#now() : toTime(options.timestamp, 'timestamp')\n\t\t} else if (options.timestamp !== undefined) {\n\t\t\ttimestamp = toTime(options.timestamp, 'timestamp')\n\t\t\t// check the range before touching the sequence, so a rejected timestamp leaves no trace\n\t\t\tthis.#offset(timestamp)\n\t\t\tif (timestamp < this.#lastTimestamp) {\n\t\t\t\t// an earlier millisecond: its first id, without moving the generator back\n\t\t\t\tincrement = 0n\n\t\t\t} else {\n\t\t\t\tif (timestamp === this.#lastTimestamp) {\n\t\t\t\t\tif (this.#sequence === this.#layout.maxIncrement) {\n\t\t\t\t\t\tthrow new RangeError(`No more ids for timestamp ${timestamp}: all ${this.#layout.maxIncrement + 1n} have been used`)\n\t\t\t\t\t}\n\t\t\t\t\tthis.#sequence++\n\t\t\t\t} else {\n\t\t\t\t\tthis.#sequence = 0n\n\t\t\t\t}\n\t\t\t\tthis.#lastTimestamp = timestamp\n\t\t\t\tincrement = this.#sequence\n\t\t\t}\n\t\t} else {\n\t\t\ttimestamp = this.#next()\n\t\t\tincrement = this.#sequence\n\t\t}\n\n\t\treturn this.#compose(timestamp, workerId, processId, increment)\n\t}\n\n\t/** {@link generate}, as a decimal string: ready for JSON, which can't hold a bigint. */\n\tpublic generateString(options?: SnowflakeGenerateOptions): string {\n\t\treturn this.generate(options).toString()\n\t}\n\n\t/** Splits a snowflake into its fields. */\n\tpublic deconstruct(id: string | bigint): DeconstructedSnowflake {\n\t\tconst value = this.#checkWidth(toId(id))\n\t\tconst { maxWorkerId, maxProcessId, maxIncrement } = this.#layout\n\t\treturn {\n\t\t\tid: value,\n\t\t\ttimestamp: (value >> this.#shiftTimestamp) + this.#epoch,\n\t\t\tworkerId: (value >> this.#shiftWorker) & maxWorkerId,\n\t\t\tprocessId: (value >> this.#shiftProcess) & maxProcessId,\n\t\t\tincrement: value & maxIncrement,\n\t\t\tepoch: this.#epoch\n\t\t}\n\t}\n\n\t/** Alias for {@link deconstruct}, bound to this generator: it still works detached (`const { decode } = snowflake`). */\n\tpublic readonly decode = (id: string | bigint): DeconstructedSnowflake => this.deconstruct(id)\n\n\t/**\n\t * The timestamp stored in a snowflake, in milliseconds since the Unix epoch. Computed with\n\t * bigints: going through a floating-point number could be a millisecond off.\n\t */\n\tpublic timestampFrom(id: string | bigint): number {\n\t\treturn Number(this.#checkWidth(toId(id)) >> this.#shiftTimestamp) + this.#epochNumber\n\t}\n\n\t/** The smallest id possible at a timestamp: for \"ids created from this time on\" queries. */\n\tpublic minIdAt(timestamp: TimeInput): bigint {\n\t\treturn this.#offset(toTime(timestamp, 'timestamp')) << this.#shiftTimestamp\n\t}\n\n\t/** The largest id possible at a timestamp: for \"ids created up to this time\" queries. */\n\tpublic maxIdAt(timestamp: TimeInput): bigint {\n\t\treturn (this.#offset(toTime(timestamp, 'timestamp')) << this.#shiftTimestamp) | mask(Number(this.#shiftTimestamp))\n\t}\n\n\t/**\n\t * Orders two snowflakes: -1 if `a` is older than `b`, 0 if they are the same, 1 if `a` is newer.\n\t * Works for any mix of bigints and decimal strings, leading zeros included.\n\t */\n\tpublic static compare(a: string | bigint, b: string | bigint): -1 | 0 | 1 {\n\t\tconst x = toId(a)\n\t\tconst y = toId(b)\n\t\treturn x === y ? 0 : x < y ? -1 : 1\n\t}\n\n\t/** The next millisecond for an automatic id, with the sequence set for it. */\n\t#next(): bigint {\n\t\tconst now = this.#now()\n\t\tif (now > this.#lastTimestamp) {\n\t\t\tthis.#lastTimestamp = now\n\t\t\tthis.#sequence = 0n\n\t\t} else if (this.#sequence < this.#layout.maxIncrement) {\n\t\t\t// same millisecond, or the clock went back: keep counting in the last one used\n\t\t\tthis.#sequence++\n\t\t} else {\n\t\t\t// sequence used up: borrow the next millisecond rather than repeat an id or wait\n\t\t\tthis.#lastTimestamp++\n\t\t\tthis.#sequence = 0n\n\t\t}\n\t\treturn this.#lastTimestamp\n\t}\n\n\t#now(): bigint {\n\t\treturn toTime(this.#clock(), 'clock()')\n\t}\n\n\t/** Milliseconds after the epoch, checked to fit the timestamp field. */\n\t#offset(timestamp: bigint): bigint {\n\t\tconst offset = timestamp - this.#epoch\n\t\tif (offset < 0n) throw new RangeError(`Timestamp ${timestamp} is before the epoch (${this.#epoch})`)\n\t\tif (offset > this.#layout.maxTimestampOffset) {\n\t\t\tthrow new RangeError(\n\t\t\t\t`Timestamp ${timestamp} is past the last one this layout can hold (${this.#epoch + this.#layout.maxTimestampOffset}): use a later epoch`\n\t\t\t)\n\t\t}\n\t\treturn offset\n\t}\n\n\t#compose(timestamp: bigint, workerId: bigint, processId: bigint, increment: bigint): bigint {\n\t\treturn (this.#offset(timestamp) << this.#shiftTimestamp) | (workerId << this.#shiftWorker) | (processId << this.#shiftProcess) | increment\n\t}\n\n\t#checkWidth(id: bigint): bigint {\n\t\tif (id > mask(this.#layout.totalBits)) throw new RangeError(`${id} is wider than ${this.#layout.totalBits} bits`)\n\t\treturn id\n\t}\n}\n","import { Snowflake } from './Snowflake'\n\n/**\n * Discord's epoch: 2015-01-01T00:00:00.000Z.\n * {@link https://discord.com/developers/docs/reference#snowflakes}\n */\nexport const DiscordEpoch = 1420070400000n\n\n/**\n * Twitter's epoch: 2010-11-04T01:42:54.657Z, from the archived snowflake repository.\n * {@link https://github.com/twitter-archive/snowflake/blob/b3f6a3c6ca8e1b6847baa6ff42bf72201e2c2231/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L25}\n */\nexport const TwitterEpoch = 1288834974657n\n\n/**\n * Shared generator with Discord's epoch, mainly for reading Discord ids. To generate ids of your\n * own, make a `new Snowflake({ epoch: DiscordEpoch, workerId, processId })` per process instead,\n * so concurrent generators never share a worker / process pair.\n */\nexport const DiscordSnowflake = new Snowflake(DiscordEpoch)\n\n/** Shared generator with Twitter's epoch; same advice as {@link DiscordSnowflake}. */\nexport const TwitterSnowflake = new Snowflake(TwitterEpoch)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkGO,IAAM,kBAAkB;AAExB,IAAM,mBAAmB;AAEzB,IAAM,mBAAmB;AAEhC,IAAM,OAAO,CAAC,UAAkB,MAAM,OAAO,IAAI,KAAK;AAEtD,SAAS,SAAS,OAAgB,MAAsB;AACvD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,UAAU;AAC9B,QAAI,CAAC,OAAO,cAAc,KAAK,EAAG,OAAM,IAAI,WAAW,IAAI,IAAI,sCAAsC,KAAK,GAAG;AAC7G,WAAO,OAAO,KAAK;AAAA,EACpB;AACA,QAAM,IAAI,UAAU,IAAI,IAAI,0CAA0C,OAAO,KAAK,GAAG;AACtF;AAEA,SAAS,OAAO,OAAgB,MAAsB;AACrD,MAAI,iBAAiB,MAAM;AAC1B,UAAM,OAAO,MAAM,QAAQ;AAC3B,QAAI,OAAO,MAAM,IAAI,EAAG,OAAM,IAAI,WAAW,IAAI,IAAI,sBAAsB;AAC3E,WAAO,OAAO,IAAI;AAAA,EACnB;AACA,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,SAAU,QAAO,SAAS,OAAO,IAAI;AACvF,QAAM,IAAI,UAAU,IAAI,IAAI,iDAAiD,OAAO,KAAK,GAAG;AAC7F;AAEA,SAAS,QAAQ,OAAqB,KAAa,MAAsB;AACxE,QAAM,IAAI,SAAS,OAAO,IAAI;AAC9B,MAAI,IAAI,MAAM,IAAI,IAAK,OAAM,IAAI,WAAW,IAAI,IAAI,2BAA2B,GAAG,cAAc,CAAC,GAAG;AACpG,SAAO;AACR;AAGA,SAAS,KAAK,IAAqB;AAClC,MAAI;AACJ,MAAI,OAAO,OAAO,SAAU,SAAQ;AAAA,WAC3B,OAAO,OAAO,UAAU;AAChC,QAAI,CAAC,QAAQ,KAAK,EAAE,EAAG,OAAM,IAAI,YAAY,IAAI,EAAE,+CAA+C;AAClG,YAAQ,OAAO,EAAE;AAAA,EAClB,WAAW,OAAO,OAAO,SAAU,SAAQ,SAAS,IAAI,IAAI;AAAA,MACvD,OAAM,IAAI,UAAU,4DAA4D,OAAO,EAAE,GAAG;AACjG,MAAI,QAAQ,GAAI,OAAM,IAAI,WAAW,2CAA2C,KAAK,GAAG;AACxF,SAAO;AACR;AAEO,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EACA;AAAA;AAAA,EAEA,iBAAiB,CAAC;AAAA,EAClB,YAAY;AAAA;AAAA;AAAA;AAAA,EAKL,YAAY,SAAuC;AACzD,UAAM,OACL,OAAO,YAAY,YAAY,EAAE,mBAAmB,QAAQ,UAAU,EAAE,OAAO,QAAQ;AAExF,SAAK,SAAS,OAAO,KAAK,OAAO,OAAO;AACxC,QAAI,KAAK,SAAS,GAAI,OAAM,IAAI,WAAW,0CAA0C,KAAK,MAAM,GAAG;AACnG,SAAK,eAAe,OAAO,KAAK,MAAM;AACtC,SAAK,SAAS,KAAK,UAAU,MAAM,KAAK,IAAI;AAE5C,UAAM,YAAY,KAAK,MAAM,SAAS;AACtC,QAAI,cAAc,MAAM,cAAc,GAAI,OAAM,IAAI,WAAW,2CAA2C,SAAS,GAAG;AACtH,UAAM,aAAa,KAAK,MAAM,UAAU;AACxC,UAAM,cAAc,KAAK,MAAM,WAAW;AAC1C,UAAM,gBAAgB,KAAK,MAAM,aAAa;AAC9C,eAAW,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,UAAU,UAAU,GAAG,CAAC,WAAW,WAAW,GAAG,CAAC,aAAa,aAAa,CAAC,GAAY;AACrH,UAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,EAAG,OAAM,IAAI,WAAW,SAAS,IAAI,8CAA8C,IAAI,GAAG;AAAA,IACjI;AACA,QAAI,gBAAgB,EAAG,OAAM,IAAI,WAAW,qCAAqC;AACjF,UAAM,gBAAgB,YAAY,aAAa,cAAc;AAE7D,QAAI,gBAAgB,GAAI,OAAM,IAAI,WAAW,QAAQ,aAAa,2EAA2E;AAE7I,SAAK,UAAU,OAAO,OAAO;AAAA,MAC5B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,KAAK,UAAU;AAAA,MAC5B,cAAc,KAAK,WAAW;AAAA,MAC9B,cAAc,KAAK,aAAa;AAAA,MAChC,oBAAoB,KAAK,aAAa;AAAA,IACvC,CAAC;AACD,SAAK,gBAAgB,OAAO,aAAa;AACzC,SAAK,eAAe,KAAK,gBAAgB,OAAO,WAAW;AAC3D,SAAK,kBAAkB,KAAK,eAAe,OAAO,UAAU;AAE5D,SAAK,YAAY,QAAQ,KAAK,YAAY,IAAI,KAAK,QAAQ,aAAa,UAAU;AAClF,SAAK,aAAa,QAAQ,KAAK,cAAc,KAAK,QAAQ,gBAAgB,KAAK,KAAK,KAAK,KAAK,QAAQ,cAAc,WAAW;AAAA,EAChI;AAAA;AAAA,EAGA,IAAW,QAAgB;AAC1B,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,IAAW,cAAsB;AAChC,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,IAAW,SAAoC;AAC9C,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,IAAW,WAAmB;AAC7B,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,IAAW,SAAS,OAAqB;AACxC,SAAK,YAAY,QAAQ,OAAO,KAAK,QAAQ,aAAa,UAAU;AAAA,EACrE;AAAA;AAAA,EAGA,IAAW,YAAoB;AAC9B,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,IAAW,UAAU,OAAqB;AACzC,SAAK,aAAa,QAAQ,OAAO,KAAK,QAAQ,cAAc,WAAW;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASO,SAAS,UAAoC,CAAC,GAAW;AAC/D,UAAM,WAAW,QAAQ,aAAa,SAAY,KAAK,YAAY,QAAQ,QAAQ,UAAU,KAAK,QAAQ,aAAa,UAAU;AACjI,UAAM,YAAY,QAAQ,cAAc,SAAY,KAAK,aAAa,QAAQ,QAAQ,WAAW,KAAK,QAAQ,cAAc,WAAW;AAEvI,QAAI;AACJ,QAAI;AAEJ,QAAI,QAAQ,cAAc,QAAW;AAEpC,kBAAY,QAAQ,QAAQ,WAAW,KAAK,QAAQ,cAAc,WAAW;AAC7E,kBAAY,QAAQ,cAAc,SAAY,KAAK,KAAK,IAAI,OAAO,QAAQ,WAAW,WAAW;AAAA,IAClG,WAAW,QAAQ,cAAc,QAAW;AAC3C,kBAAY,OAAO,QAAQ,WAAW,WAAW;AAEjD,WAAK,QAAQ,SAAS;AACtB,UAAI,YAAY,KAAK,gBAAgB;AAEpC,oBAAY;AAAA,MACb,OAAO;AACN,YAAI,cAAc,KAAK,gBAAgB;AACtC,cAAI,KAAK,cAAc,KAAK,QAAQ,cAAc;AACjD,kBAAM,IAAI,WAAW,6BAA6B,SAAS,SAAS,KAAK,QAAQ,eAAe,EAAE,iBAAiB;AAAA,UACpH;AACA,eAAK;AAAA,QACN,OAAO;AACN,eAAK,YAAY;AAAA,QAClB;AACA,aAAK,iBAAiB;AACtB,oBAAY,KAAK;AAAA,MAClB;AAAA,IACD,OAAO;AACN,kBAAY,KAAK,MAAM;AACvB,kBAAY,KAAK;AAAA,IAClB;AAEA,WAAO,KAAK,SAAS,WAAW,UAAU,WAAW,SAAS;AAAA,EAC/D;AAAA;AAAA,EAGO,eAAe,SAA4C;AACjE,WAAO,KAAK,SAAS,OAAO,EAAE,SAAS;AAAA,EACxC;AAAA;AAAA,EAGO,YAAY,IAA6C;AAC/D,UAAM,QAAQ,KAAK,YAAY,KAAK,EAAE,CAAC;AACvC,UAAM,EAAE,aAAa,cAAc,aAAa,IAAI,KAAK;AACzD,WAAO;AAAA,MACN,IAAI;AAAA,MACJ,YAAY,SAAS,KAAK,mBAAmB,KAAK;AAAA,MAClD,UAAW,SAAS,KAAK,eAAgB;AAAA,MACzC,WAAY,SAAS,KAAK,gBAAiB;AAAA,MAC3C,WAAW,QAAQ;AAAA,MACnB,OAAO,KAAK;AAAA,IACb;AAAA,EACD;AAAA;AAAA,EAGgB,SAAS,CAAC,OAAgD,KAAK,YAAY,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtF,cAAc,IAA6B;AACjD,WAAO,OAAO,KAAK,YAAY,KAAK,EAAE,CAAC,KAAK,KAAK,eAAe,IAAI,KAAK;AAAA,EAC1E;AAAA;AAAA,EAGO,QAAQ,WAA8B;AAC5C,WAAO,KAAK,QAAQ,OAAO,WAAW,WAAW,CAAC,KAAK,KAAK;AAAA,EAC7D;AAAA;AAAA,EAGO,QAAQ,WAA8B;AAC5C,WAAQ,KAAK,QAAQ,OAAO,WAAW,WAAW,CAAC,KAAK,KAAK,kBAAmB,KAAK,OAAO,KAAK,eAAe,CAAC;AAAA,EAClH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAc,QAAQ,GAAoB,GAAgC;AACzE,UAAM,IAAI,KAAK,CAAC;AAChB,UAAM,IAAI,KAAK,CAAC;AAChB,WAAO,MAAM,IAAI,IAAI,IAAI,IAAI,KAAK;AAAA,EACnC;AAAA;AAAA,EAGA,QAAgB;AACf,UAAM,MAAM,KAAK,KAAK;AACtB,QAAI,MAAM,KAAK,gBAAgB;AAC9B,WAAK,iBAAiB;AACtB,WAAK,YAAY;AAAA,IAClB,WAAW,KAAK,YAAY,KAAK,QAAQ,cAAc;AAEtD,WAAK;AAAA,IACN,OAAO;AAEN,WAAK;AACL,WAAK,YAAY;AAAA,IAClB;AACA,WAAO,KAAK;AAAA,EACb;AAAA,EAEA,OAAe;AACd,WAAO,OAAO,KAAK,OAAO,GAAG,SAAS;AAAA,EACvC;AAAA;AAAA,EAGA,QAAQ,WAA2B;AAClC,UAAM,SAAS,YAAY,KAAK;AAChC,QAAI,SAAS,GAAI,OAAM,IAAI,WAAW,aAAa,SAAS,yBAAyB,KAAK,MAAM,GAAG;AACnG,QAAI,SAAS,KAAK,QAAQ,oBAAoB;AAC7C,YAAM,IAAI;AAAA,QACT,aAAa,SAAS,+CAA+C,KAAK,SAAS,KAAK,QAAQ,kBAAkB;AAAA,MACnH;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA,EAEA,SAAS,WAAmB,UAAkB,WAAmB,WAA2B;AAC3F,WAAQ,KAAK,QAAQ,SAAS,KAAK,KAAK,kBAAoB,YAAY,KAAK,eAAiB,aAAa,KAAK,gBAAiB;AAAA,EAClI;AAAA,EAEA,YAAY,IAAoB;AAC/B,QAAI,KAAK,KAAK,KAAK,QAAQ,SAAS,EAAG,OAAM,IAAI,WAAW,GAAG,EAAE,kBAAkB,KAAK,QAAQ,SAAS,OAAO;AAChH,WAAO;AAAA,EACR;AACD;;;AC/WO,IAAM,eAAe;AAMrB,IAAM,eAAe;AAOrB,IAAM,mBAAmB,IAAI,UAAU,YAAY;AAGnD,IAAM,mBAAmB,IAAI,UAAU,YAAY;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snowflake IDs: 64-bit (or 63-bit) integers made of a timestamp, a worker id, a process id and a
|
|
3
|
+
* per-millisecond sequence, so they are unique and sort by creation time.
|
|
4
|
+
*
|
|
5
|
+
* Twitter / Discord layout (the default):
|
|
6
|
+
* ```
|
|
7
|
+
* 64 22 17 12 0
|
|
8
|
+
* 000000111011000111100001101001000101000000 00001 00000 000000000000
|
|
9
|
+
* milliseconds since the epoch worker process increment
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* A drop-in successor to `@sapphire/snowflake` that, on one generator, guarantees unique and
|
|
13
|
+
* increasing IDs: the sequence restarts every millisecond, and when it runs out (more than 4096
|
|
14
|
+
* IDs in a millisecond) or the clock goes back, generation carries on from the last millisecond
|
|
15
|
+
* used instead of repeating or going backwards. Out-of-range input throws instead of being masked.
|
|
16
|
+
*/
|
|
17
|
+
/** A point in time: milliseconds since the Unix epoch, or a Date. */
|
|
18
|
+
type TimeInput = number | bigint | Date;
|
|
19
|
+
/** Something that becomes a non-negative integer. */
|
|
20
|
+
type IntegerInput = number | bigint;
|
|
21
|
+
interface SnowflakeBits {
|
|
22
|
+
/** Bits of the worker id. @default 5 */
|
|
23
|
+
worker?: number;
|
|
24
|
+
/** Bits of the process id. @default 5 */
|
|
25
|
+
process?: number;
|
|
26
|
+
/** Bits of the per-millisecond sequence. @default 12 */
|
|
27
|
+
increment?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Width of the whole id. 64 is the Twitter / Discord layout; 63 keeps every id positive in a
|
|
30
|
+
* signed 64-bit integer, such as PostgreSQL's `bigint` or Java's `long`. The timestamp gets the bits left over.
|
|
31
|
+
* @default 64
|
|
32
|
+
*/
|
|
33
|
+
total?: 63 | 64;
|
|
34
|
+
}
|
|
35
|
+
interface SnowflakeOptions {
|
|
36
|
+
/** Where timestamps count from. Pick one close to today: the timestamp field fills up after 2^timestampBits ms. */
|
|
37
|
+
epoch: TimeInput;
|
|
38
|
+
/** Default worker id for generated ids. Give every generator that runs at the same time its own worker / process pair. @default 0 */
|
|
39
|
+
workerId?: IntegerInput;
|
|
40
|
+
/** Default process id for generated ids. @default 1 */
|
|
41
|
+
processId?: IntegerInput;
|
|
42
|
+
/** Field widths. */
|
|
43
|
+
bits?: SnowflakeBits;
|
|
44
|
+
/** The clock, in milliseconds since the Unix epoch. Replace it in tests. @default Date.now */
|
|
45
|
+
clock?: () => number | bigint;
|
|
46
|
+
}
|
|
47
|
+
interface SnowflakeGenerateOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Timestamp of the id. Without it the clock is used, and ids are guaranteed unique and increasing.
|
|
50
|
+
* With it, the id belongs to that millisecond: it continues the sequence when it is the latest
|
|
51
|
+
* millisecond used, and starts at 0 for an earlier one (which doesn't move the generator back).
|
|
52
|
+
* @default the clock
|
|
53
|
+
*/
|
|
54
|
+
timestamp?: TimeInput;
|
|
55
|
+
/**
|
|
56
|
+
* Use this sequence value as is (0 to 4095 by default), outside the generator's own sequence.
|
|
57
|
+
* Uniqueness is then up to you.
|
|
58
|
+
*/
|
|
59
|
+
increment?: IntegerInput;
|
|
60
|
+
/** Worker id for this id only. */
|
|
61
|
+
workerId?: IntegerInput;
|
|
62
|
+
/** Process id for this id only. */
|
|
63
|
+
processId?: IntegerInput;
|
|
64
|
+
}
|
|
65
|
+
/** Object returned by {@link Snowflake.deconstruct}. */
|
|
66
|
+
interface DeconstructedSnowflake {
|
|
67
|
+
/** The id as a bigint. */
|
|
68
|
+
id: bigint;
|
|
69
|
+
/** Milliseconds since the Unix epoch, when the id was made. */
|
|
70
|
+
timestamp: bigint;
|
|
71
|
+
workerId: bigint;
|
|
72
|
+
processId: bigint;
|
|
73
|
+
increment: bigint;
|
|
74
|
+
/** The epoch the generator counts from. */
|
|
75
|
+
epoch: bigint;
|
|
76
|
+
}
|
|
77
|
+
/** The resolved field widths and limits of a {@link Snowflake}. */
|
|
78
|
+
interface SnowflakeLayout {
|
|
79
|
+
totalBits: number;
|
|
80
|
+
timestampBits: number;
|
|
81
|
+
workerBits: number;
|
|
82
|
+
processBits: number;
|
|
83
|
+
incrementBits: number;
|
|
84
|
+
maxWorkerId: bigint;
|
|
85
|
+
maxProcessId: bigint;
|
|
86
|
+
maxIncrement: bigint;
|
|
87
|
+
/** Largest timestamp offset (ms after the epoch) that still fits. */
|
|
88
|
+
maxTimestampOffset: bigint;
|
|
89
|
+
}
|
|
90
|
+
/** The largest worker id in the default layout. */
|
|
91
|
+
declare const MaximumWorkerId = 31n;
|
|
92
|
+
/** The largest process id in the default layout. */
|
|
93
|
+
declare const MaximumProcessId = 31n;
|
|
94
|
+
/** The largest per-millisecond sequence value in the default layout. */
|
|
95
|
+
declare const MaximumIncrement = 4095n;
|
|
96
|
+
declare class Snowflake {
|
|
97
|
+
#private;
|
|
98
|
+
/**
|
|
99
|
+
* @param options the epoch alone (as `@sapphire/snowflake` takes it), or {@link SnowflakeOptions}
|
|
100
|
+
*/
|
|
101
|
+
constructor(options: TimeInput | SnowflakeOptions);
|
|
102
|
+
/** The epoch, as a bigint. */
|
|
103
|
+
get epoch(): bigint;
|
|
104
|
+
/** The epoch, as a number. */
|
|
105
|
+
get epochNumber(): number;
|
|
106
|
+
/** Field widths and limits. */
|
|
107
|
+
get layout(): Readonly<SnowflakeLayout>;
|
|
108
|
+
/** The worker id used by default. */
|
|
109
|
+
get workerId(): bigint;
|
|
110
|
+
/** Sets the default worker id. Throws a RangeError when it doesn't fit (it used to be silently masked). */
|
|
111
|
+
set workerId(value: IntegerInput);
|
|
112
|
+
/** The process id used by default. */
|
|
113
|
+
get processId(): bigint;
|
|
114
|
+
/** Sets the default process id. Throws a RangeError when it doesn't fit (it used to be silently masked). */
|
|
115
|
+
set processId(value: IntegerInput);
|
|
116
|
+
/**
|
|
117
|
+
* Generates a snowflake.
|
|
118
|
+
*
|
|
119
|
+
* Without options, ids from one generator are unique and strictly increasing: the sequence
|
|
120
|
+
* restarts each millisecond; past its last value, or when the clock goes back, the id takes the
|
|
121
|
+
* next millisecond instead (ids may then run slightly ahead of the clock until it catches up).
|
|
122
|
+
*/
|
|
123
|
+
generate(options?: SnowflakeGenerateOptions): bigint;
|
|
124
|
+
/** {@link generate}, as a decimal string: ready for JSON, which can't hold a bigint. */
|
|
125
|
+
generateString(options?: SnowflakeGenerateOptions): string;
|
|
126
|
+
/** Splits a snowflake into its fields. */
|
|
127
|
+
deconstruct(id: string | bigint): DeconstructedSnowflake;
|
|
128
|
+
/** Alias for {@link deconstruct}, bound to this generator: it still works detached (`const { decode } = snowflake`). */
|
|
129
|
+
readonly decode: (id: string | bigint) => DeconstructedSnowflake;
|
|
130
|
+
/**
|
|
131
|
+
* The timestamp stored in a snowflake, in milliseconds since the Unix epoch. Computed with
|
|
132
|
+
* bigints: going through a floating-point number could be a millisecond off.
|
|
133
|
+
*/
|
|
134
|
+
timestampFrom(id: string | bigint): number;
|
|
135
|
+
/** The smallest id possible at a timestamp: for "ids created from this time on" queries. */
|
|
136
|
+
minIdAt(timestamp: TimeInput): bigint;
|
|
137
|
+
/** The largest id possible at a timestamp: for "ids created up to this time" queries. */
|
|
138
|
+
maxIdAt(timestamp: TimeInput): bigint;
|
|
139
|
+
/**
|
|
140
|
+
* Orders two snowflakes: -1 if `a` is older than `b`, 0 if they are the same, 1 if `a` is newer.
|
|
141
|
+
* Works for any mix of bigints and decimal strings, leading zeros included.
|
|
142
|
+
*/
|
|
143
|
+
static compare(a: string | bigint, b: string | bigint): -1 | 0 | 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Discord's epoch: 2015-01-01T00:00:00.000Z.
|
|
148
|
+
* {@link https://discord.com/developers/docs/reference#snowflakes}
|
|
149
|
+
*/
|
|
150
|
+
declare const DiscordEpoch = 1420070400000n;
|
|
151
|
+
/**
|
|
152
|
+
* Twitter's epoch: 2010-11-04T01:42:54.657Z, from the archived snowflake repository.
|
|
153
|
+
* {@link https://github.com/twitter-archive/snowflake/blob/b3f6a3c6ca8e1b6847baa6ff42bf72201e2c2231/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L25}
|
|
154
|
+
*/
|
|
155
|
+
declare const TwitterEpoch = 1288834974657n;
|
|
156
|
+
/**
|
|
157
|
+
* Shared generator with Discord's epoch, mainly for reading Discord ids. To generate ids of your
|
|
158
|
+
* own, make a `new Snowflake({ epoch: DiscordEpoch, workerId, processId })` per process instead,
|
|
159
|
+
* so concurrent generators never share a worker / process pair.
|
|
160
|
+
*/
|
|
161
|
+
declare const DiscordSnowflake: Snowflake;
|
|
162
|
+
/** Shared generator with Twitter's epoch; same advice as {@link DiscordSnowflake}. */
|
|
163
|
+
declare const TwitterSnowflake: Snowflake;
|
|
164
|
+
|
|
165
|
+
export { type DeconstructedSnowflake, DiscordEpoch, DiscordSnowflake, type IntegerInput, MaximumIncrement, MaximumProcessId, MaximumWorkerId, Snowflake, type SnowflakeBits, type SnowflakeGenerateOptions, type SnowflakeLayout, type SnowflakeOptions, type TimeInput, TwitterEpoch, TwitterSnowflake };
|