All files / src/targets in-memory.ts

100% Statements 23/23
100% Branches 5/5
100% Functions 8/8
100% Lines 23/23

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 87 88 89 90 91 92                                                                            7x   7x   7x         7x       3x 3x 3x       5x 5x       3x 3x       2x 2x       3x 3x       3x 3x 3x       19x 19x 6x 6x   19x      
/**
 * In-process `MigrationTarget`. Recorded writes are available on the
 * public readonly maps so tests can assert the exact migration product
 * without any wrangler or network plumbing.
 *
 * Key layout mirrors CLAUDE.md §10.2 + `@ethercalc/shared/storage-keys`:
 *   doStorage[<room>][<STORAGE_KEYS.snapshot | logKey | auditKey | chatKey | ecellKey>]
 *   d1Rooms[<room>]                               → { updated_at }
 *   kvRoomsExists[<room>]                         → "1"
 * so an assertion can just compare the populated maps against a fixture.
 */
 
import {
  STORAGE_KEYS,
  logKey,
  auditKey,
  chatKey,
  ecellKey,
} from '@ethercalc/shared/storage-keys';
 
import type { MigrationTarget } from '../apply.ts';
 
export interface InMemoryRoomIndexRow {
  updatedAt: number;
}
 
export interface InMemoryTargetOptions {
  /** Clock for `meta:updated_at` writes. Defaults to `Date.now`. */
  now?: () => number;
}
 
/**
 * Implementation of {@link MigrationTarget} that stashes everything in
 * regular JS maps. All methods return resolved Promises so they compose
 * with the async applyRooms() pipeline.
 */
export class InMemoryTarget implements MigrationTarget {
  /** `doStorage.get(room)` → (storageKey → value). */
  public readonly doStorage: Map<string, Map<string, string>> = new Map();
  /** D1 `rooms` mirror. */
  public readonly d1Rooms: Map<string, InMemoryRoomIndexRow> = new Map();
  /** KV `rooms:exists:<room>` → "1". */
  public readonly kvRoomsExists: Map<string, string> = new Map();
 
  private readonly now: () => number;
 
  constructor(opts: InMemoryTargetOptions = {}) {
    this.now = opts.now ?? Date.now;
  }
 
  putSnapshot(room: string, snapshot: string): Promise<void> {
    this.bucket(room).set(STORAGE_KEYS.snapshot, snapshot);
    this.bucket(room).set(STORAGE_KEYS.metaUpdatedAt, String(this.now()));
    return Promise.resolve();
  }
 
  putLog(room: string, seq: number, cmd: string): Promise<void> {
    this.bucket(room).set(logKey(seq), cmd);
    return Promise.resolve();
  }
 
  putAudit(room: string, seq: number, cmd: string): Promise<void> {
    this.bucket(room).set(auditKey(seq), cmd);
    return Promise.resolve();
  }
 
  putChat(room: string, seq: number, msg: string): Promise<void> {
    this.bucket(room).set(chatKey(seq), msg);
    return Promise.resolve();
  }
 
  putEcell(room: string, user: string, cell: string): Promise<void> {
    this.bucket(room).set(ecellKey(user), cell);
    return Promise.resolve();
  }
 
  setRoomIndex(room: string, updatedAt: number): Promise<void> {
    this.d1Rooms.set(room, { updatedAt });
    this.kvRoomsExists.set(room, '1');
    return Promise.resolve();
  }
 
  private bucket(room: string): Map<string, string> {
    let b = this.doStorage.get(room);
    if (b === undefined) {
      b = new Map();
      this.doStorage.set(room, b);
    }
    return b;
  }
}