atagen

malice

public
Star0
Watch0
1bd1ad6a629b4087d33645665833dfe68b495fd8
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/search-model.ts

ba90983cec2eatagen2026-10-25 10:51 UTC1
import { isRecord } from './guards';
ba90983cec2eatagen2026-10-25 10:51 UTC2
import { parseTimelineItem } from './types';
ba90983cec2eatagen2026-10-25 10:51 UTC3
import type { TimelineEvent } from './types';
ba90983cec2eatagen2026-10-25 10:51 UTC4
ba90983cec2eatagen2026-10-25 10:51 UTC5
export type SearchScope = { kind: 'room'; room_id: string } | { kind: 'rooms'; room_ids: string[] } | { kind: 'all' };
ba90983cec2eatagen2026-10-25 10:51 UTC6
ba90983cec2eatagen2026-10-25 10:51 UTC7
export interface ScopeOption {
ba90983cec2eatagen2026-10-25 10:51 UTC8
  label: string;
ba90983cec2eatagen2026-10-25 10:51 UTC9
  scope: SearchScope;
ba90983cec2eatagen2026-10-25 10:51 UTC10
}
ba90983cec2eatagen2026-10-25 10:51 UTC11
ba90983cec2eatagen2026-10-25 10:51 UTC12
export interface SearchHit {
ba90983cec2eatagen2026-10-25 10:51 UTC13
  room_id: string;
ba90983cec2eatagen2026-10-25 10:51 UTC14
  event_id: string;
ba90983cec2eatagen2026-10-25 10:51 UTC15
  timestamp_ms: number;
ba90983cec2eatagen2026-10-25 10:51 UTC16
  event: TimelineEvent;
ba90983cec2eatagen2026-10-25 10:51 UTC17
  ranges: [number, number][];
ba90983cec2eatagen2026-10-25 10:51 UTC18
}
ba90983cec2eatagen2026-10-25 10:51 UTC19
ba90983cec2eatagen2026-10-25 10:51 UTC20
export interface SearchCoverage {
ba90983cec2eatagen2026-10-25 10:51 UTC21
  room_id: string;
ba90983cec2eatagen2026-10-25 10:51 UTC22
  oldest_indexed_ms: number | null;
ba90983cec2eatagen2026-10-25 10:51 UTC23
  reached_start: boolean;
ba90983cec2eatagen2026-10-25 10:51 UTC24
}
ba90983cec2eatagen2026-10-25 10:51 UTC25
ba90983cec2eatagen2026-10-25 10:51 UTC26
export interface SearchPage {
ba90983cec2eatagen2026-10-25 10:51 UTC27
  hits: SearchHit[];
ba90983cec2eatagen2026-10-25 10:51 UTC28
  next: string | null;
ba90983cec2eatagen2026-10-25 10:51 UTC29
  coverage: SearchCoverage[];
ba90983cec2eatagen2026-10-25 10:51 UTC30
}
ba90983cec2eatagen2026-10-25 10:51 UTC31
ba90983cec2eatagen2026-10-25 10:51 UTC32
/** The wire form of a scope, as macltok's externally tagged enum. */
ba90983cec2eatagen2026-10-25 10:51 UTC33
export function scopeWire(scope: SearchScope): unknown {
ba90983cec2eatagen2026-10-25 10:51 UTC34
  if (scope.kind === 'room') return { Room: scope.room_id };
ba90983cec2eatagen2026-10-25 10:51 UTC35
  if (scope.kind === 'rooms') return { Rooms: scope.room_ids };
ba90983cec2eatagen2026-10-25 10:51 UTC36
  return 'All';
ba90983cec2eatagen2026-10-25 10:51 UTC37
}
ba90983cec2eatagen2026-10-25 10:51 UTC38
ba90983cec2eatagen2026-10-25 10:51 UTC39
function parseRanges(raw: unknown): [number, number][] {
ba90983cec2eatagen2026-10-25 10:51 UTC40
  if (!Array.isArray(raw)) return [];
ba90983cec2eatagen2026-10-25 10:51 UTC41
  return raw.flatMap((range) =>
ba90983cec2eatagen2026-10-25 10:51 UTC42
    Array.isArray(range) && typeof range[0] === 'number' && typeof range[1] === 'number' ? [[range[0], range[1]] as [number, number]] : []
ba90983cec2eatagen2026-10-25 10:51 UTC43
  );
ba90983cec2eatagen2026-10-25 10:51 UTC44
}
ba90983cec2eatagen2026-10-25 10:51 UTC45
ba90983cec2eatagen2026-10-25 10:51 UTC46
function parseHit(raw: unknown): SearchHit | null {
ba90983cec2eatagen2026-10-25 10:51 UTC47
  if (!isRecord(raw) || typeof raw['room_id'] !== 'string' || typeof raw['event_id'] !== 'string') return null;
ba90983cec2eatagen2026-10-25 10:51 UTC48
  const item = parseTimelineItem(raw['preview'] ?? raw['event']);
ba90983cec2eatagen2026-10-25 10:51 UTC49
  if (item?.kind !== 'event') return null;
ba90983cec2eatagen2026-10-25 10:51 UTC50
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC51
    room_id: raw['room_id'],
ba90983cec2eatagen2026-10-25 10:51 UTC52
    event_id: raw['event_id'],
ba90983cec2eatagen2026-10-25 10:51 UTC53
    timestamp_ms: typeof raw['timestamp_ms'] === 'number' ? raw['timestamp_ms'] : item.event.timestamp_ms,
ba90983cec2eatagen2026-10-25 10:51 UTC54
    event: item.event,
ba90983cec2eatagen2026-10-25 10:51 UTC55
    ranges: parseRanges(raw['ranges'])
ba90983cec2eatagen2026-10-25 10:51 UTC56
  };
ba90983cec2eatagen2026-10-25 10:51 UTC57
}
ba90983cec2eatagen2026-10-25 10:51 UTC58
ba90983cec2eatagen2026-10-25 10:51 UTC59
function parseCoverage(raw: unknown): SearchCoverage | null {
ba90983cec2eatagen2026-10-25 10:51 UTC60
  if (!isRecord(raw) || typeof raw['room_id'] !== 'string') return null;
ba90983cec2eatagen2026-10-25 10:51 UTC61
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC62
    room_id: raw['room_id'],
ba90983cec2eatagen2026-10-25 10:51 UTC63
    oldest_indexed_ms: typeof raw['oldest_indexed_ms'] === 'number' ? raw['oldest_indexed_ms'] : null,
ba90983cec2eatagen2026-10-25 10:51 UTC64
    reached_start: raw['reached_start'] === true
ba90983cec2eatagen2026-10-25 10:51 UTC65
  };
ba90983cec2eatagen2026-10-25 10:51 UTC66
}
ba90983cec2eatagen2026-10-25 10:51 UTC67
ba90983cec2eatagen2026-10-25 10:51 UTC68
export function parseSearchPage(raw: unknown): SearchPage | null {
ba90983cec2eatagen2026-10-25 10:51 UTC69
  if (!isRecord(raw) || !Array.isArray(raw['hits'])) return null;
ba90983cec2eatagen2026-10-25 10:51 UTC70
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC71
    hits: raw['hits'].flatMap((hit) => parseHit(hit) ?? []),
ba90983cec2eatagen2026-10-25 10:51 UTC72
    next: typeof raw['next'] === 'string' ? raw['next'] : null,
ba90983cec2eatagen2026-10-25 10:51 UTC73
    coverage: Array.isArray(raw['coverage']) ? raw['coverage'].flatMap((c) => parseCoverage(c) ?? []) : []
ba90983cec2eatagen2026-10-25 10:51 UTC74
  };
ba90983cec2eatagen2026-10-25 10:51 UTC75
}
ba90983cec2eatagen2026-10-25 10:51 UTC76
ba90983cec2eatagen2026-10-25 10:51 UTC77
/** Hits grouped by room, keeping the order in which each room first appears. */
ba90983cec2eatagen2026-10-25 10:51 UTC78
export function groupByRoom(hits: SearchHit[]): { room_id: string; hits: SearchHit[] }[] {
ba90983cec2eatagen2026-10-25 10:51 UTC79
  const groups = new Map<string, SearchHit[]>();
ba90983cec2eatagen2026-10-25 10:51 UTC80
  for (const hit of hits) {
ba90983cec2eatagen2026-10-25 10:51 UTC81
    const list = groups.get(hit.room_id);
ba90983cec2eatagen2026-10-25 10:51 UTC82
    if (list) list.push(hit);
ba90983cec2eatagen2026-10-25 10:51 UTC83
    else groups.set(hit.room_id, [hit]);
ba90983cec2eatagen2026-10-25 10:51 UTC84
  }
ba90983cec2eatagen2026-10-25 10:51 UTC85
  return [...groups].map(([room_id, list]) => ({ room_id, hits: list }));
ba90983cec2eatagen2026-10-25 10:51 UTC86
}