ethercalc
Version:
Multi-User Spreadsheet Server — TypeScript rewrite (Cloudflare fullstack)
76 lines (67 loc) • 2.68 kB
text/typescript
/**
* Durable Object storage key schema.
*
* Each `RoomDO` holds one spreadsheet room plus its append-only logs. All
* keys live under the DO's own storage — key collisions across rooms are
* impossible because each DO has its own isolated storage (the legacy
* `snapshot-<room>` / `log-<room>` Redis namespacing is replaced by the DO
* namespacing itself, see AGENTS.md §3.3 and §10.2).
*
* List-like data (log / audit / chat) is stored as individual keys with a
* zero-padded sequence number suffix so `storage.list({ prefix })` returns
* entries in insertion order.
*/
export const STORAGE_KEYS = {
/**
* String — SocialCalc save format, for rooms whose snapshot fits
* under the DO-storage 128 KiB per-value ceiling. Large snapshots
* use the chunked layout below instead.
*/
snapshot: 'snapshot',
/**
* Object `{ chunks: number }` — present iff the snapshot is split
* across `snapshot:chunk:<i>` keys. Absent for small snapshots.
*/
snapshotMeta: 'snapshot:meta',
/** Prefix for chunked snapshot parts: `snapshot:chunk:<padSeq(i)>`. */
snapshotChunkPrefix: 'snapshot:chunk:',
/** Number — ms since epoch. Updated on every snapshot write. */
metaUpdatedAt: 'meta:updated_at',
/** Prefix for command log entries (folded into snapshot periodically). */
logPrefix: 'log:',
/** Prefix for audit log entries (never truncated). */
auditPrefix: 'audit:',
/** Prefix for chat messages. */
chatPrefix: 'chat:',
/** Prefix for per-user ecell tracking. Key: `ecell:<user>` → cell coord. */
ecellPrefix: 'ecell:',
} as const;
/** Width chosen so every reasonable sequence sorts lexicographically. */
export const SEQ_PAD_WIDTH = 16;
export function padSeq(n: number): string {
if (!Number.isInteger(n) || n < 0) {
throw new RangeError(`padSeq requires a non-negative integer, got ${n}`);
}
return n.toString().padStart(SEQ_PAD_WIDTH, '0');
}
export function logKey(n: number): string {
return STORAGE_KEYS.logPrefix + padSeq(n);
}
export function auditKey(n: number): string {
return STORAGE_KEYS.auditPrefix + padSeq(n);
}
export function chatKey(n: number): string {
return STORAGE_KEYS.chatPrefix + padSeq(n);
}
export function ecellKey(user: string): string {
if (!user) throw new RangeError('ecellKey requires a non-empty user');
return STORAGE_KEYS.ecellPrefix + user;
}
/**
* Chunked-snapshot chunk key: `snapshot:chunk:<padSeq(i)>`. Zero-padded
* so `storage.list({prefix: STORAGE_KEYS.snapshotChunkPrefix})` returns
* chunks in order without a sort step.
*/
export function snapshotChunkKey(i: number): string {
return STORAGE_KEYS.snapshotChunkPrefix + padSeq(i);
}