1/**
2 * Creates compact, sortable identifiers for cluster messages and runtime
3 * events.
4 *
5 * A snowflake id is a branded `bigint` built from a millisecond timestamp, a
6 * machine id, and a sequence number for that machine. The parts make generated
7 * ids sortable by time while still being unique for a runner. This module
8 * includes schemas for bigint and string encodings, helpers for creating and
9 * reading ids, and a `Clock`-backed generator service for cluster runtime use.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type * as Brand from "../Brand.ts"
15import { Clock } from "../Clock.ts"
16import * as Context from "../Context.ts"
17import * as DateTime from "../DateTime.ts"
18import * as Effect from "../Effect.ts"
19import { identity } from "../Function.ts"
20import * as Layer from "../Layer.ts"
21import * as Schema from "../Schema.ts"
22import * as SchemaTransformation from "../SchemaTransformation.ts"
23import type { MachineId } from "./MachineId.ts"
24
25/**
26 * Runtime brand identifier for cluster snowflake ids.
27 *
28 * @stability unstable
29 * @category type IDs
30 * @since 4.0.0
31 */
32export const TypeId = "~effect/cluster/Snowflake"
33
34/**
35 * Type-level representation of the cluster snowflake brand identifier.
36 *
37 * @stability unstable
38 * @category type IDs
39 * @since 4.0.0
40 */
41export type TypeId = typeof TypeId
42
43/**
44 * Branded bigint identifier composed from a timestamp, machine id, and per-machine
45 * sequence number.
46 *
47 * @stability unstable
48 * @category models
49 * @since 4.0.0
50 */
51export type Snowflake = Brand.Branded<bigint, TypeId>
52
53/**
54 * Constructs a branded cluster snowflake id from a bigint or bigint-compatible
55 * string.
56 *
57 * @stability unstable
58 * @category constructors
59 * @since 4.0.0
60 */
61export const Snowflake = (input: string | number | bigint): Snowflake =>
62 typeof input === "bigint" ? input as Snowflake : BigInt(input) as Snowflake
63
64/**
65 * Namespace containing support types for snowflake parts and generators.
66 *
67 * @stability unstable
68 * @since 4.0.0
69 */
70export declare namespace Snowflake {
71 /**
72 * Decoded components of a snowflake id: Unix timestamp milliseconds, machine id,
73 * and sequence number.
74 *
75 * @stability unstable
76 * @category models
77 * @since 4.0.0
78 */
79 export interface Parts {
80 readonly timestamp: number
81 readonly machineId: MachineId
82 readonly sequence: number
83 }
84
85 /**
86 * Stateful generator for runner-local snowflake ids, exposing an unsafe
87 * synchronous `nextUnsafe` operation and an effectful machine id setter.
88 *
89 * @stability unstable
90 * @category models
91 * @since 4.0.0
92 */
93 export interface Generator {
94 readonly nextUnsafe: () => Snowflake
95 readonly setMachineId: (machineId: MachineId) => Effect.Effect<void>
96 }
97}
98
99/**
100 * Schema type for snowflake ids represented as branded bigints.
101 *
102 * @stability unstable
103 * @category schemas
104 * @since 4.0.0
105 */
106export interface SnowflakeFromBigInt extends Schema.brand<Schema.BigInt, TypeId> {}
107
108/**
109 * Schema for snowflake ids represented as branded bigints.
110 *
111 * @stability unstable
112 * @category schemas
113 * @since 4.0.0
114 */
115export const SnowflakeFromBigInt: SnowflakeFromBigInt = Schema.BigInt.pipe(Schema.brand(TypeId))
116
117/**
118 * Schema type for snowflake ids decoded from strings into branded bigints.
119 *
120 * @stability unstable
121 * @category schemas
122 * @since 4.0.0
123 */
124export interface SnowflakeFromString extends Schema.decodeTo<SnowflakeFromBigInt, Schema.String> {}
125
126/**
127 * Schema that decodes snowflake ids from strings into branded bigints and encodes
128 * them back to strings.
129 *
130 * @stability unstable
131 * @category schemas
132 * @since 4.0.0
133 */
134export const SnowflakeFromString: SnowflakeFromString = Schema.String.pipe(
135 Schema.decodeTo(SnowflakeFromBigInt, SchemaTransformation.bigintFromString)
136)
137
138/**
139 * Defines the custom snowflake epoch in Unix milliseconds.
140 *
141 * @stability unstable
142 * @category constants
143 * @since 4.0.0
144 */
145export const constEpochMillis: number = Date.UTC(2025, 0, 1)
146
147const sinceUnixEpoch = constEpochMillis - Date.UTC(1970, 0, 1)
148const constBigInt12 = BigInt(12)
149const constBigInt22 = BigInt(22)
150const constBigInt1024 = BigInt(1024)
151const constBigInt4096 = BigInt(4096)
152
153/**
154 * Creates a branded snowflake id from a timestamp, machine id, and sequence number,
155 * using the custom snowflake epoch and 10-bit machine id and 12-bit sequence
156 * fields.
157 *
158 * **When to use**
159 *
160 * Use to pack known timestamp, machine id, and sequence parts into a branded
161 * snowflake id.
162 *
163 * **Gotchas**
164 *
165 * Machine id values are encoded modulo 1024, and sequence values modulo 4096;
166 * values outside those ranges wrap instead of being rejected.
167 *
168 * @see {@link toParts} for the inverse operation that decodes a snowflake id into timestamp, machine id, and sequence parts
169 * @see {@link makeGenerator} for generating ids with Clock-backed timestamp and sequence management
170 *
171 * @stability unstable
172 * @category constructors
173 * @since 4.0.0
174 */
175export const make = (options: {
176 readonly machineId: MachineId
177 readonly sequence: number
178 readonly timestamp: number
179}): Snowflake =>
180 (BigInt(options.timestamp - constEpochMillis) << constBigInt22
181 | (BigInt(options.machineId % 1024) << constBigInt12)
182 | BigInt(options.sequence % 4096)) as Snowflake
183
184/**
185 * Extracts the Unix timestamp in milliseconds from a snowflake id.
186 *
187 * @stability unstable
188 * @category getters
189 * @since 4.0.0
190 */
191export const timestamp = (snowflake: Snowflake): number => Number(snowflake >> constBigInt22) + sinceUnixEpoch
192
193/**
194 * Extracts the timestamp from a snowflake id as a `DateTime.Utc`.
195 *
196 * @stability unstable
197 * @category getters
198 * @since 4.0.0
199 */
200export const dateTime = (snowflake: Snowflake): DateTime.Utc => DateTime.makeUnsafe(timestamp(snowflake))
201
202/**
203 * Extracts the machine id component from a snowflake id.
204 *
205 * @stability unstable
206 * @category getters
207 * @since 4.0.0
208 */
209export const machineId = (snowflake: Snowflake): MachineId =>
210 Number((snowflake >> constBigInt12) % constBigInt1024) as MachineId
211
212/**
213 * Extracts the per-machine sequence component from a snowflake id.
214 *
215 * @stability unstable
216 * @category getters
217 * @since 4.0.0
218 */
219export const sequence = (snowflake: Snowflake): number => Number(snowflake % constBigInt4096)
220
221/**
222 * Decomposes a snowflake id into its timestamp, machine id, and sequence parts.
223 *
224 * @stability unstable
225 * @category converting
226 * @since 4.0.0
227 */
228export const toParts = (snowflake: Snowflake): Snowflake.Parts => ({
229 timestamp: timestamp(snowflake),
230 machineId: machineId(snowflake),
231 sequence: sequence(snowflake)
232})
233
234/**
235 * Creates a stateful snowflake generator using `Clock`.
236 *
237 * **Details**
238 *
239 * The generator starts with a random machine id, never moves generated timestamps
240 * backward, resets the sequence each millisecond, and advances the timestamp when
241 * more than 4096 ids are requested in the same millisecond.
242 *
243 * @stability unstable
244 * @category constructors
245 * @since 4.0.0
246 */
247export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(function*() {
248 let machineId = Math.floor(Math.random() * 1024) as MachineId
249 const clock = yield* Clock
250
251 let sequence = 0
252 let sequenceAt = Math.floor(clock.currentTimeMillisUnsafe())
253
254 return identity<Snowflake.Generator>({
255 setMachineId: (newMachineId) =>
256 Effect.sync(() => {
257 machineId = newMachineId
258 }),
259 nextUnsafe() {
260 let now = Math.floor(clock.currentTimeMillisUnsafe())
261
262 // account for clock drift, only allow time to move forward
263 if (now < sequenceAt) {
264 now = sequenceAt
265 } else if (now > sequenceAt) {
266 // reset sequence if we're in a new millisecond
267 sequence = 0
268 sequenceAt = now
269 } else if (sequence >= 4096) {
270 // if we've hit the max sequence for this millisecond, go to the next
271 // millisecond
272 sequenceAt++
273 sequence = 0
274 }
275
276 return make({
277 machineId,
278 sequence: sequence++,
279 timestamp: sequenceAt
280 })
281 }
282 })
283})
284
285/**
286 * Context service for a stateful snowflake id generator.
287 *
288 * @stability unstable
289 * @category services
290 * @since 4.0.0
291 */
292export class Generator extends Context.Service<
293 Generator,
294 Snowflake.Generator
295>()("effect/cluster/Snowflake/Generator") {}
296
297/**
298 * Layer that provides the default snowflake `Generator` service.
299 *
300 * @stability unstable
301 * @category layers
302 * @since 4.0.0
303 */
304export const layerGenerator: Layer.Layer<Generator> = Layer.effect(Generator)(makeGenerator)