atagen

malice

public
Star0
Watch0
9090eb8cdf5f6e853fd8a08b68b8d5fcc31b062a
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';
249e439c27e5atagen2026-10-25 13:26 UTC2
import { parseTimelineEvent } 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
249e439c27e5atagen2026-10-25 13:26 UTC20
export type SearchBackfill = 'Waiting' | 'Running' | 'Paused' | 'Complete' | 'Disabled' | { Failed: string };
249e439c27e5atagen2026-10-25 13:26 UTC21
249e439c27e5atagen2026-10-25 13:26 UTC22
export interface RoomSearchCoverage {
ba90983cec2eatagen2026-10-25 10:51 UTC23
  room_id: string;
ba90983cec2eatagen2026-10-25 10:51 UTC24
  oldest_indexed_ms: number | null;
249e439c27e5atagen2026-10-25 13:26 UTC25
  start_reached: boolean;
249e439c27e5atagen2026-10-25 13:26 UTC26
  backfill: SearchBackfill;
249e439c27e5atagen2026-10-25 13:26 UTC27
}
249e439c27e5atagen2026-10-25 13:26 UTC28
249e439c27e5atagen2026-10-25 13:26 UTC29
export interface SearchCoverage {
249e439c27e5atagen2026-10-25 13:26 UTC30
  rooms: RoomSearchCoverage[];
249e439c27e5atagen2026-10-25 13:26 UTC31
  persistent: boolean;
249e439c27e5atagen2026-10-25 13:26 UTC32
  undecrypted: number;
ba90983cec2eatagen2026-10-25 10:51 UTC33
}
ba90983cec2eatagen2026-10-25 10:51 UTC34
ba90983cec2eatagen2026-10-25 10:51 UTC35
export interface SearchPage {
ba90983cec2eatagen2026-10-25 10:51 UTC36
  hits: SearchHit[];
ba90983cec2eatagen2026-10-25 10:51 UTC37
  next: string | null;
249e439c27e5atagen2026-10-25 13:26 UTC38
  coverage: SearchCoverage;
249e439c27e5atagen2026-10-25 13:26 UTC39
}
249e439c27e5atagen2026-10-25 13:26 UTC40
249e439c27e5atagen2026-10-25 13:26 UTC41
export interface SearchSession {
249e439c27e5atagen2026-10-25 13:26 UTC42
  search_id: string;
249e439c27e5atagen2026-10-25 13:26 UTC43
  page: SearchPage;
ba90983cec2eatagen2026-10-25 10:51 UTC44
}
ba90983cec2eatagen2026-10-25 10:51 UTC45
ba90983cec2eatagen2026-10-25 10:51 UTC46
/** The wire form of a scope, as macltok's externally tagged enum. */
ba90983cec2eatagen2026-10-25 10:51 UTC47
export function scopeWire(scope: SearchScope): unknown {
ba90983cec2eatagen2026-10-25 10:51 UTC48
  if (scope.kind === 'room') return { Room: scope.room_id };
ba90983cec2eatagen2026-10-25 10:51 UTC49
  if (scope.kind === 'rooms') return { Rooms: scope.room_ids };
ba90983cec2eatagen2026-10-25 10:51 UTC50
  return 'All';
ba90983cec2eatagen2026-10-25 10:51 UTC51
}
ba90983cec2eatagen2026-10-25 10:51 UTC52
249e439c27e5atagen2026-10-25 13:26 UTC53
/** macltok's matches are utf-8 byte ranges, so they're mapped back to string offsets against the body. */
249e439c27e5atagen2026-10-25 13:26 UTC54
function parseRanges(raw: unknown, body: string): [number, number][] {
ba90983cec2eatagen2026-10-25 10:51 UTC55
  if (!Array.isArray(raw)) return [];
249e439c27e5atagen2026-10-25 13:26 UTC56
  const bytes = new TextEncoder().encode(body);
249e439c27e5atagen2026-10-25 13:26 UTC57
  const decoder = new TextDecoder();
249e439c27e5atagen2026-10-25 13:26 UTC58
  const offset = (byte: number) => decoder.decode(bytes.subarray(0, byte)).length;
ba90983cec2eatagen2026-10-25 10:51 UTC59
  return raw.flatMap((range) =>
249e439c27e5atagen2026-10-25 13:26 UTC60
    isRecord(range) && typeof range['start'] === 'number' && typeof range['end'] === 'number'
249e439c27e5atagen2026-10-25 13:26 UTC61
      ? [[offset(range['start']), offset(range['end'])] as [number, number]]
249e439c27e5atagen2026-10-25 13:26 UTC62
      : []
ba90983cec2eatagen2026-10-25 10:51 UTC63
  );
ba90983cec2eatagen2026-10-25 10:51 UTC64
}
ba90983cec2eatagen2026-10-25 10:51 UTC65
249e439c27e5atagen2026-10-25 13:26 UTC66
function hitBody(event: TimelineEvent): string {
249e439c27e5atagen2026-10-25 13:26 UTC67
  const content = event.content;
249e439c27e5atagen2026-10-25 13:26 UTC68
  if (content.kind === 'Message') return content.message.body;
249e439c27e5atagen2026-10-25 13:26 UTC69
  return '';
249e439c27e5atagen2026-10-25 13:26 UTC70
}
249e439c27e5atagen2026-10-25 13:26 UTC71
ba90983cec2eatagen2026-10-25 10:51 UTC72
function parseHit(raw: unknown): SearchHit | null {
249e439c27e5atagen2026-10-25 13:26 UTC73
  if (!isRecord(raw) || typeof raw['room_id'] !== 'string' || typeof raw['event_id'] !== 'string' || !isRecord(raw['event'])) return null;
249e439c27e5atagen2026-10-25 13:26 UTC74
  const event = parseTimelineEvent(raw['event']);
ba90983cec2eatagen2026-10-25 10:51 UTC75
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC76
    room_id: raw['room_id'],
ba90983cec2eatagen2026-10-25 10:51 UTC77
    event_id: raw['event_id'],
249e439c27e5atagen2026-10-25 13:26 UTC78
    timestamp_ms: typeof raw['timestamp_ms'] === 'number' ? raw['timestamp_ms'] : event.timestamp_ms,
249e439c27e5atagen2026-10-25 13:26 UTC79
    event,
249e439c27e5atagen2026-10-25 13:26 UTC80
    ranges: parseRanges(raw['matches'], hitBody(event))
ba90983cec2eatagen2026-10-25 10:51 UTC81
  };
ba90983cec2eatagen2026-10-25 10:51 UTC82
}
ba90983cec2eatagen2026-10-25 10:51 UTC83
249e439c27e5atagen2026-10-25 13:26 UTC84
function parseBackfill(raw: unknown): SearchBackfill {
249e439c27e5atagen2026-10-25 13:26 UTC85
  if (raw === 'Waiting' || raw === 'Running' || raw === 'Paused' || raw === 'Complete' || raw === 'Disabled') return raw;
249e439c27e5atagen2026-10-25 13:26 UTC86
  if (isRecord(raw) && isRecord(raw['Failed']) && typeof raw['Failed']['message'] === 'string') return { Failed: raw['Failed']['message'] };
249e439c27e5atagen2026-10-25 13:26 UTC87
  return 'Waiting';
249e439c27e5atagen2026-10-25 13:26 UTC88
}
249e439c27e5atagen2026-10-25 13:26 UTC89
249e439c27e5atagen2026-10-25 13:26 UTC90
function parseRoomCoverage(raw: unknown): RoomSearchCoverage | null {
ba90983cec2eatagen2026-10-25 10:51 UTC91
  if (!isRecord(raw) || typeof raw['room_id'] !== 'string') return null;
ba90983cec2eatagen2026-10-25 10:51 UTC92
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC93
    room_id: raw['room_id'],
ba90983cec2eatagen2026-10-25 10:51 UTC94
    oldest_indexed_ms: typeof raw['oldest_indexed_ms'] === 'number' ? raw['oldest_indexed_ms'] : null,
249e439c27e5atagen2026-10-25 13:26 UTC95
    start_reached: raw['start_reached'] === true,
249e439c27e5atagen2026-10-25 13:26 UTC96
    backfill: parseBackfill(raw['backfill'])
249e439c27e5atagen2026-10-25 13:26 UTC97
  };
249e439c27e5atagen2026-10-25 13:26 UTC98
}
249e439c27e5atagen2026-10-25 13:26 UTC99
249e439c27e5atagen2026-10-25 13:26 UTC100
function parseCoverage(raw: unknown): SearchCoverage {
249e439c27e5atagen2026-10-25 13:26 UTC101
  if (!isRecord(raw)) return { rooms: [], persistent: false, undecrypted: 0 };
249e439c27e5atagen2026-10-25 13:26 UTC102
  return {
249e439c27e5atagen2026-10-25 13:26 UTC103
    rooms: Array.isArray(raw['rooms']) ? raw['rooms'].flatMap((room) => parseRoomCoverage(room) ?? []) : [],
249e439c27e5atagen2026-10-25 13:26 UTC104
    persistent: raw['persistent'] === true,
249e439c27e5atagen2026-10-25 13:26 UTC105
    undecrypted: typeof raw['undecrypted'] === 'number' ? raw['undecrypted'] : 0
ba90983cec2eatagen2026-10-25 10:51 UTC106
  };
ba90983cec2eatagen2026-10-25 10:51 UTC107
}
ba90983cec2eatagen2026-10-25 10:51 UTC108
ba90983cec2eatagen2026-10-25 10:51 UTC109
export function parseSearchPage(raw: unknown): SearchPage | null {
ba90983cec2eatagen2026-10-25 10:51 UTC110
  if (!isRecord(raw) || !Array.isArray(raw['hits'])) return null;
ba90983cec2eatagen2026-10-25 10:51 UTC111
  return {
ba90983cec2eatagen2026-10-25 10:51 UTC112
    hits: raw['hits'].flatMap((hit) => parseHit(hit) ?? []),
249e439c27e5atagen2026-10-25 13:26 UTC113
    next: typeof raw['next_cursor'] === 'string' ? raw['next_cursor'] : null,
249e439c27e5atagen2026-10-25 13:26 UTC114
    coverage: parseCoverage(raw['coverage'])
ba90983cec2eatagen2026-10-25 10:51 UTC115
  };
ba90983cec2eatagen2026-10-25 10:51 UTC116
}
ba90983cec2eatagen2026-10-25 10:51 UTC117
249e439c27e5atagen2026-10-25 13:26 UTC118
export function parseSearchSession(raw: unknown): SearchSession | null {
249e439c27e5atagen2026-10-25 13:26 UTC119
  if (!isRecord(raw) || typeof raw['search_id'] !== 'string') return null;
249e439c27e5atagen2026-10-25 13:26 UTC120
  const page = parseSearchPage(raw['page']);
249e439c27e5atagen2026-10-25 13:26 UTC121
  return page ? { search_id: raw['search_id'], page } : null;
249e439c27e5atagen2026-10-25 13:26 UTC122
}
249e439c27e5atagen2026-10-25 13:26 UTC123
ba90983cec2eatagen2026-10-25 10:51 UTC124
/** Hits grouped by room, keeping the order in which each room first appears. */
ba90983cec2eatagen2026-10-25 10:51 UTC125
export function groupByRoom(hits: SearchHit[]): { room_id: string; hits: SearchHit[] }[] {
ba90983cec2eatagen2026-10-25 10:51 UTC126
  const groups = new Map<string, SearchHit[]>();
ba90983cec2eatagen2026-10-25 10:51 UTC127
  for (const hit of hits) {
ba90983cec2eatagen2026-10-25 10:51 UTC128
    const list = groups.get(hit.room_id);
ba90983cec2eatagen2026-10-25 10:51 UTC129
    if (list) list.push(hit);
ba90983cec2eatagen2026-10-25 10:51 UTC130
    else groups.set(hit.room_id, [hit]);
ba90983cec2eatagen2026-10-25 10:51 UTC131
  }
ba90983cec2eatagen2026-10-25 10:51 UTC132
  return [...groups].map(([room_id, list]) => ({ room_id, hits: list }));
ba90983cec2eatagen2026-10-25 10:51 UTC133
}