Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 13x 13x 13x 13x 13x 13x 13x 13x 13x 15x 13x 65x 85x 15x 15x 9x 9x 8x | /**
* Shape the raw {@link RedisDump} into per-room records.
*
* Input Redis keys (per CLAUDE.md §6.3):
* snapshot-<room> string → Room.snapshot
* log-<room> list → Room.log
* audit-<room> list → Room.audit
* chat-<room> list → Room.chat
* ecell-<room> hash → Room.ecell
* timestamps hash → Room.updatedAt (from field `timestamp-<room>`
* or bare `<room>` — legacy
* wrote both forms; §6.3)
*
* Rooms are identified by the *snapshot* key only. A room with only logs
* but no snapshot is still emitted (snapshot === '') — the legacy server
* did the same when a room was created via POST `/_/…` but not yet saved.
* This is how freshly-created, never-edited rooms show up in dumps.
*/
import type { RedisDump } from './parse-rdb.ts';
export interface Room {
name: string;
snapshot: string;
log: string[];
audit: string[];
chat: string[];
ecell: Record<string, string>;
updatedAt?: number;
}
/**
* Partition the dump into per-room `Room[]`. Pure — deterministic output
* order (rooms sorted alphabetically) so snapshots diff cleanly in tests.
*/
export function extractRooms(dump: RedisDump): Room[] {
const names = new Set<string>();
collectNames(dump.strings.keys(), 'snapshot-', names);
collectNames(dump.lists.keys(), 'log-', names);
collectNames(dump.lists.keys(), 'audit-', names);
collectNames(dump.lists.keys(), 'chat-', names);
collectNames(dump.hashes.keys(), 'ecell-', names);
const timestamps = dump.hashes.get('timestamps') ?? new Map<string, string>();
const rooms: Room[] = [];
for (const name of [...names].sort()) {
rooms.push({
name,
snapshot: dump.strings.get(`snapshot-${name}`) ?? '',
log: dump.lists.get(`log-${name}`) ?? [],
audit: dump.lists.get(`audit-${name}`) ?? [],
chat: dump.lists.get(`chat-${name}`) ?? [],
ecell: Object.fromEntries(dump.hashes.get(`ecell-${name}`) ?? new Map()),
...readTimestamp(timestamps, name),
});
}
return rooms;
}
function collectNames(keys: IterableIterator<string>, prefix: string, out: Set<string>): void {
for (const k of keys) {
if (k.startsWith(prefix)) out.add(k.slice(prefix.length));
}
}
/**
* The legacy `timestamps` hash stores per-room `updated_at` values, but
* with two inconsistent field shapes across historical server versions:
* - `timestamp-<room>` — current format
* - `<room>` — pre-2015 format
* We accept either. If a value isn't a finite integer, we drop it (the
* oracle did the same — `Number('')` → NaN, which broke the /_roomtimes
* sort; we preserve the skip rather than propagate bad data).
*/
function readTimestamp(
timestamps: Map<string, string>,
room: string,
): { updatedAt?: number } {
const raw = timestamps.get(`timestamp-${room}`) ?? timestamps.get(room);
if (raw === undefined) return {};
const n = Number(raw);
if (!Number.isFinite(n)) return {};
return { updatedAt: n };
}
|