Snowflake
Discord API가 ID를 반환하는 곳이라면, GameVox도 Discord의 64비트 Snowflake 형식으로 반환합니다. 비트 구성, 정렬 순서, 타임스탬프 계산이 모두 같으므로 기존 Snowflake 파싱 코드가 그대로 동작합니다.
형식
Discord와 동일합니다. 42비트 타임스탬프 + 5비트 워커 + 5비트 프로세스 + 12비트 증가값을 부호 없는 64비트 정수 안에 빅엔디언으로 담습니다.
┌─ 42 bits ─────────────────┬─ 5 ─┬─ 5 ─┬─ 12 ────────┐
│ GameVox 에포크 이후 ms │ wkr │ prc │ increment │
└───────────────────────────┴─────┴─────┴─────────────┘
63 22 21 17 16 12 11 0 각 구간은 Discord와 일치합니다.
- 타임스탬프(42비트) — GameVox 에포크 이후의 밀리초. 아래를 참고하세요.
- 워커 ID(5비트) — 시작 시 Redis 원자적 카운터로, 실행 중인 api 태스크마다 배정됩니다.
- 프로세스 ID(5비트) — 워커와 짝을 이뤄 고유한 발급 풀을 만듭니다.
- 증가값(12비트) — (워커, 프로세스)별 밀리초당 일련번호.
에포크
GameVox는 Discord와 같은 에포크를 씁니다: 1420070400000(2015-01-01 00:00:00 UTC). Discord용으로 작성한 Snowflake 파서는 상수를 바꾸지 않고도 GameVox ID에서 동작합니다.
const SNOWFLAKE_EPOCH = 1420070400000n; 와이어 포맷
REST / 게이트웨이의 모든 Snowflake는 숫자가 아니라 JSON 문자열로 직렬화됩니다. JavaScript는 Number.MAX_SAFE_INTEGER(253-1)를 넘으면 정밀도를 잃는데, Snowflake는 이를 흔히 넘습니다. 계산이 필요하면 항상 BigInt로 파싱하고, 그 밖에는 문자열로 보관하세요.
// 올바른 예
const id = BigInt(message.id);
// 잘못된 예 — 정밀도가 조용히 손실됩니다
const id = Number(message.id); 타임스탬프 추출
JavaScript
function snowflakeToDate(snowflake) {
const EPOCH = 1420070400000n; // Discord와 동일
const ms = Number((BigInt(snowflake) >> 22n) + EPOCH);
return new Date(ms);
}
console.log(snowflakeToDate('182955831900192769'));
// → Date 객체 — 이 Snowflake가 생성된 시각 Python
from datetime import datetime, timezone
SNOWFLAKE_EPOCH = 1420070400000
def snowflake_to_dt(snowflake):
ms = (int(snowflake) >> 22) + SNOWFLAKE_EPOCH
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc) Java
long SNOWFLAKE_EPOCH = 1420070400000L;
long ms = (Long.parseUnsignedLong(snowflake) >>> 22) + SNOWFLAKE_EPOCH;
Instant when = Instant.ofEpochMilli(ms); 정렬 순서
타임스탬프가 상위 비트에 있기 때문에, 두 Snowflake를 문자열로 사전순 비교하면 시간순으로 정렬됩니다. Discord와 같습니다. 메시지 엔드포인트의 before / after / around 페이지네이션이 별도 타임스탬프 필드 없이 동작하는 이유입니다.
// 메시지 배열에서, 오래된 것 → 최신 순:
messages.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); 기본 데이터와 별칭 테이블
내부적으로 GameVox의 기본 객체(서버, 채널, 사용자, 메시지 등)는 UUIDv4를 씁니다. 봇 호환 계층은 객체가 처음 봇에 노출될 때 안정적인 Snowflake를 발급하고, 그 짝을 snowflake_aliases 테이블에 저장합니다. 이후 그 객체를 참조할 때는 REST든 게이트웨이든 항상 같은 Snowflake를 씁니다.
메시지의 경우 메시지의 created_at을 원본 타임스탬프로 삼아 Snowflake를 소급 생성하므로, 봇이 채널에 처음 들어와도 과거 메시지가 올바르게 정렬됩니다. 그 밖에는 첫 노출 시점에 NOW()로 지연 생성되므로, Snowflake는 봇이 그 객체를 본 시점을 나타내며 반드시 생성 시점은 아닙니다.
샤딩 해시
Discord의 표준 샤드 공식을 그대로 쓸 수 있습니다.
shardId = (BigInt(guildId) >> 22n) % BigInt(numShards); 서버 2,500개 미만인 봇은 단일 샤드([0, 1])에서 동작하므로 신경 쓸 필요가 없습니다. 더 큰 봇도 상위 42비트가 무작위가 아니라 시간에 따라 단조 증가하기 때문에, Discord와 비슷하게 고른 해시 분포를 얻습니다.
주의할 점
- Snowflake를 숫자 리터럴과
==로 비교하지 마세요. 와이어에서는 문자열입니다. 문자열과 비교하거나 양쪽을BigInt로 변환하세요. - 42비트 타임스탬프는 에포크로부터 약 139년 뒤에 한 바퀴 돕니다. GameVox의 Snowflake는 2165년까지 유효합니다.
- 서버 / 채널 / 역할의 생성 시각을 Snowflake로 추정하지 마세요. 봇 플랫폼보다 먼저 있던 기본 객체는 생성 시점이 아니라 봇에 처음 노출된 시점에 Snowflake를 받았습니다. 메시지만 예외입니다(
created_at으로 소급 생성).