← Proxy lesson
Lab

Proxy

type GaugeSnapshot = { id: string; station: string; reading: string };
type ReadRequest = { operator: string; id: string };
type ReadResult =
  | { ok: true; snapshot: GaugeSnapshot }
  | { ok: false; error: string };

// The contract the console is given. The collector and the proxy both keep it.
interface StationReader {
  read(request: ReadRequest): ReadResult;
}

class AccessRules {
  private grants = new Map<string, Set<string>>();
  allow(operator: string, id: string) {
    this.grants.set(operator, (this.grants.get(operator) ?? new Set()).add(id));
  }
  revoke(operator: string, id: string) {
    this.grants.get(operator)?.delete(id);
  }
  canRead(operator: string, id: string): boolean {
    return this.grants.get(operator)?.has(id) ?? false;
  }
}

// The resource: historical snapshots. Each call is one collector read.
class GaugeCollector implements StationReader {
  reads = 0;
  constructor(private snapshots: GaugeSnapshot[]) {}
  read(request: ReadRequest): ReadResult {
    this.reads++;
    const snapshot = this.snapshots.find((s) => s.id === request.id);
    if (!snapshot) return { ok: false, error: 'not found' };
    return { ok: true, snapshot };
  }
}

// The stand-in: the same contract, a grant check, then a cache.
class GaugeProxy implements StationReader {
  private cache = new Map<string, GaugeSnapshot>();
  constructor(private origin: StationReader, private rules: AccessRules) {}
  read(request: ReadRequest): ReadResult {
    if (!this.rules.canRead(request.operator, request.id)) {
      return { ok: false, error: 'forbidden' };
    }
    const cached = this.cache.get(request.id);
    if (cached) return { ok: true, snapshot: cached };
    const result = this.origin.read(request);
    if (result.ok) this.cache.set(request.id, result.snapshot);
    return result;
  }
}

const collector = new GaugeCollector([
  { id: 'north-fork', station: 'North Fork', reading: '2.4 m, rising' },
  { id: 'harbor', station: 'Harbor', reading: '1.8 m, steady' },
]);
const rules = new AccessRules();
rules.allow('mira', 'north-fork');
rules.allow('mira', 'harbor');
rules.allow('lee', 'harbor');
// The console only ever sees a StationReader.
const reader: StationReader = new GaugeProxy(collector, rules);

const first = reader.read({ operator: 'mira', id: 'north-fork' });
const warm = reader.read({ operator: 'mira', id: 'north-fork' });
const lee = reader.read({ operator: 'lee', id: 'north-fork' });
rules.revoke('mira', 'north-fork');
const revoked = reader.read({ operator: 'mira', id: 'north-fork' });
const reads = collector.reads;

const describe = (s: GaugeSnapshot) => `${s.station} ${s.reading}`;
const show = (name: string, result: ReadResult) => {
  const shown = result.ok ? describe(result.snapshot) : result.error;
  console.log(`${name}: ${shown}`);
};
show('first', first);
show('warm', warm);
show('lee', lee);
show('revoked', revoked);
console.log(`collector reads: ${reads}`);