Module: DiscordStore::Snowflake

Defined in:
lib/discord_store/snowflake.rb

Overview

Discord snowflake IDs, treated as what they actually are: a k-sortable primary key with a millisecond timestamp baked into the high bits.

63                                    22     17     12            0
+--------------------------------------+------+------+------------+
| milliseconds since 2015-01-01 (42b)  | wrkr | proc | incr (12b) |
+--------------------------------------+------+------+------------+

Two properties matter for using Discord as a store:

  1. IDs issued later sort after IDs issued earlier, so the natural message order in a channel is insertion order, and no separate sequence column is needed. The log sequence number is free.

  2. Because the timestamp is recoverable, a wall-clock range maps onto an ID range. Discord's +before+/+after+ pagination parameters take snowflakes, so "every record written between 09:00 and 10:00" is a server-side range scan rather than a client-side filter over the whole channel.

Constant Summary collapse

EPOCH_MS =

Discord's epoch: 2015-01-01T00:00:00Z, in milliseconds.

1_420_070_400_000
TIMESTAMP_SHIFT =
22
WORKER_SHIFT =
17
PROCESS_SHIFT =
12
WORKER_MASK =
0x1F
PROCESS_MASK =
0x1F
INCREMENT_MASK =
0xFFF
MAX_TIMESTAMP_MS =

The largest value the 42-bit timestamp field can hold.

(1 << 42) - 1

Class Method Summary collapse

Class Method Details

.at(id) ⇒ Time

Returns UTC time at which id was issued.

Parameters:

  • id (Integer, String)

Returns:

  • (Time) —

    UTC time at which id was issued



49
50
51
# File 'lib/discord_store/snowflake.rb', line 49

def at(id)
  Time.at(timestamp_ms(id) / 1000.0).utc
end

.from_time(time) ⇒ Integer

Build a synthetic snowflake for the given time, with all low bits zeroed.

The result is not a real message ID and will never collide with one, but it sorts exactly where a real ID issued at that instant would sort. That makes it the correct bound for +before+/+after+ pagination when you want a time range rather than a message range.

Parameters:

  • time (Time, Integer) —

    a Time, or milliseconds since the Unix epoch

Returns:

  • (Integer)

Raises:

  • (ArgumentError) —

    if the time predates Discord or overflows 42 bits



78
79
80
81
82
83
84
85
86
# File 'lib/discord_store/snowflake.rb', line 78

def from_time(time)
  ms = time.is_a?(Time) ? (time.to_f * 1000).floor : Integer(time)
  offset = ms - EPOCH_MS

  raise ArgumentError, "time predates the Discord epoch (2015-01-01)" if offset.negative?
  raise ArgumentError, "time overflows the 42-bit snowflake timestamp" if offset > MAX_TIMESTAMP_MS

  offset << TIMESTAMP_SHIFT
end

.from_time_inclusive(time) ⇒ Integer

The same instant as from_time, but with every low bit set, so that it sorts after any real ID issued during that millisecond. Use this as an inclusive upper bound.

Parameters:

  • time (Time, Integer)

Returns:

  • (Integer)


94
95
96
# File 'lib/discord_store/snowflake.rb', line 94

def from_time_inclusive(time)
  from_time(time) | ((1 << TIMESTAMP_SHIFT) - 1)
end

.parts(id) ⇒ Hash

The worker/process/increment fields. Present for completeness and for debugging odd ordering; nothing in this library depends on them.

Parameters:

  • id (Integer, String)

Returns:

  • (Hash)


58
59
60
61
62
63
64
65
66
# File 'lib/discord_store/snowflake.rb', line 58

def parts(id)
  id = Integer(id)
  {
    timestamp_ms: timestamp_ms(id),
    worker: (id >> WORKER_SHIFT) & WORKER_MASK,
    process: (id >> PROCESS_SHIFT) & PROCESS_MASK,
    increment: id & INCREMENT_MASK
  }
end

.timestamp_ms(id) ⇒ Integer

Milliseconds since the Unix epoch at which id was issued.

Parameters:

  • id (Integer, String)

Returns:

  • (Integer)


43
44
45
# File 'lib/discord_store/snowflake.rb', line 43

def timestamp_ms(id)
  (Integer(id) >> TIMESTAMP_SHIFT) + EPOCH_MS
end

.valid?(value) ⇒ Boolean

Whether value could plausibly be a snowflake this library issued or read.

Parameters:

  • value (Object)

Returns:

  • (Boolean)


102
103
104
105
106
107
# File 'lib/discord_store/snowflake.rb', line 102

def valid?(value)
  id = Integer(value)
  id.positive? && id < (1 << 64) && timestamp_ms(id) >= EPOCH_MS
rescue ArgumentError, TypeError
  false
end