Skip to content

Scanner ingest · Modules

3 modules · 20 exports · 1 repository · 0 e2e tests · 1 route

Who imports who · hover a node
Depends onScanner ingest modulesUsed byMatches5 exportsScoreboards13 exportsVodMatches2 exportsActions1 fileRepositories1 fileComponents1 fileOther1 fileLoaders1 file

Matches

app/features/scanner-ingest/core/Matches.ts

Pure logic for stored scanner matches: canonical serialization (hashing), same-game identity of two partials, and merging a partial into a stored one.

Imports
no other modules
Used by
1 action1 repository1 module

Functions & values

function

canonicalMatch

:35

Rebuilds a match with a fixed key order so JSON.stringify is stable — the hashing and change-detection representation.

export function canonicalMatch(match: ScannerMatch): ScannerMatch
Unit tests (1)
function

mergeMatches

:117

Merges an ingested partial into the stored match: incoming teams are aligned to the stored orientation (a scoreboard match's teams[0] is the winner, a minimap match's is alpha), then every field fills stored nulls, stored values winning. changed is false when nothing was added.

export function mergeMatches(
	existing: ScannerMatch,
	incoming: ScannerMatch,
): { merged: ScannerMatch; changed: boolean }
Unit tests (6)
function

gameStartedAt

:158

Wall-clock ms of the game's first read (live reads count startsAt in wall-clock seconds), null when it isn't one shortly before its results screen.

export function gameStartedAt(match: ScannerMatch): number | null
Unit tests (2)
Actions/ingest
function

normalizeInGameName

:169

Lowercased, width-normalized in-game name without the #discriminator.

export function normalizeInGameName(name: string): string
Unit tests not about one export (2)

Scoreboards

app/features/scanner-ingest/core/Scoreboards.ts

No overview comment at the top of the file.

Imports
Matches
Used by
1 action1 repository1 component1 other

Types

type

IngestContext

:37

The match context an ingest request was resolved to belong to.

export type IngestContext =
	| { type: "tournament"; tournamentId: number }
	| { type: "sendouq"; groupMatchId: number };
type

IngestableGameTarget

:42

The reported game result an ingested match can link to.

export type IngestableGameTarget =
	| {
			type: "tournament";
			matchGameResultId: number;
			tournamentMatchId: number;
	  }
	| { type: "sendouq"; groupMatchMapId: number; groupMatchId: number };
Actions/ingest
interface

IngestableGame

:51

A game of a tournament or SendouQ match that ingested matches can be linked to.

export interface IngestableGame {
	target: IngestableGameTarget;
	/** 0-based index of the game within its match */
	mapIndex: number;
	mode: ModeShort;
	stageId: StageId;
	/** user ids who played the game on the winning side (a tournament game's active roster, a SendouQ group); only they can link a scan to it */
	winnerUserIds: number[];
	/** user ids who played the game on the losing side, see `winnerUserIds` */
	loserUserIds: number[];
	/** both teams' known in-game names by user id, tells whose seat a POV read marks */
	inGameNameByUserId: Map<number, string>;
	/** timestamp of the game's report: the chronological key and what a scan's play time is measured against */
	playedAt: number;
	/** winner-first row-order names of the game's earliest linked ingest that read enough names to recognize a re-detection, null when none; lets matching skip taken games yet recognize re-detections */
	linkedPlayerNames: string[] | null;
}
interface

IngestableGameWithContext

:70

A candidate game for content resolution, tagged with its context.

export interface IngestableGameWithContext extends IngestableGame {
	context: IngestContext;
}
interface

IngestedScoreboardPlayer

no JSDoc:213
export interface IngestedScoreboardPlayer {
	name: string;
	tournamentTeamId: number | null;
	weaponSplId: MainWeaponId | null;
	ka: number | null;
	d: number | null;
	s: number | null;
	paint: number | null;
	/** [head, clothes, shoes] ability rows gathered from the match's death screens */
	abilities?: AbilityWithUnknown[][];
	/** set via POV attribution of a linked ingested match */
	userId?: number;
}
interface

IngestedScoreboardData

:228

A game's scoreboard derived from its linked ingested matches, the shape match pages render. Derived at read time, not stored.

export interface IngestedScoreboardData {
	/** game scores [winner, loser] (0-100; a knockout's winner is 100) */
	scores: [number | null, number | null];
	/** in scoreboard order: rows 0-3 winning team, rows 4-7 losing team */
	players: IngestedScoreboardPlayer[];
	/** objective-counter progress winner-first (per-team values [winner, loser], `control` 0 = winner), `t` in seconds since the game's first read (the source video is not stored). Absent when no counter was read. */
	objective?: ScannerMatchObjective;
	/** per-player special/death samples, teams winner-first, `t` on the same origin as `objective`. Absent when the icon strip was never read. */
	playerStatus?: ScannerMatchPlayerStatus;
}

Functions & values

const

PLAYED_AT_TOLERANCE_MS

:34

Max distance between a scan's play time and a game's report for them to be the same game: headroom for a slow reporter and the client's clock, still far inside the gap between two plays of one map. Outside every candidate the scan stays unlinked (re-sendable) rather than silently putting strangers on a match page.

export const PLAYED_AT_TOLERANCE_MS = 30 * 60 * 1000;
function

contextKey

:81

Stable grouping/equality key for an {@link IngestContext}.

export function contextKey(context: IngestContext): string
Actions/ingest
function

resolveContext

:92

Resolves which context (tournament or SendouQ match) a request's matches belong to from content alone: the POV user's reported games are grouped by context and each is scored by how many matches matchedGames aligns with its games.

export function resolveContext({
	matches,
	games,
	povUserId,
}: {
	matches: ScannerMatch[];
	games: IngestableGameWithContext[];
	povUserId: number | null;
}): IngestContext | null
Unit tests (5)
Actions/ingest
function

matchedGames

:141

Decides which game result each ingested match links to.

Only matches with a known winner, two full teams and a play time qualify (minimap-only and VoD reads never link). Matches and games are walked chronologically: each match takes the unassigned game of the same mode+stage reported nearest it that the sender played on the POV seat's side, none beyond PLAYED_AT_TOLERANCE_MS.

A scan only links to a game its sender played: one without a POV seat (cast footage) or sent by someone outside the game's active rosters never does. Other lobbies, unreadable mode/stage and duplicate detections are skipped.

Matches arrive over many requests, so games already linked are skipped — unless the incoming match is a re-detection of the linked one, which lands on the same game so re-sends stay idempotent and another POV's scan joins it.

export function matchedGames({
	matches,
	games,
	povUserId,
}: {
	matches: ScannerMatch[];
	games: IngestableGame[];
	/** the sender, who is the POV player of the request's non-cast matches */
	povUserId: number | null;
}): MatchedGame[]
Unit tests (22)
function

povWeaponId

:186

The weapon the sender played in a linked game, read off the scan's POV seat. Null unless the sender played on the seat's side and the seat's name isn't another roster member's: a teammate's recording marks someone else's seat.

export function povWeaponId({
	match,
	game,
	povUserId,
}: {
	match: ScannerMatch;
	game: IngestableGame;
	povUserId: number;
}): MainWeaponId | null
Unit tests (3)
function

deriveScoreboardData

:245

Derives a game's scoreboard from its linked ingests: earliest link is the base, later ones enrich it field-wise (Matches.mergeMatches), projected winner-first, with every linked POV seat attributing its row to the POV user. winnerTeamId/loserTeamId are the result's sides (tournament team or SendouQ group ids) stamped onto the rows.

export function deriveScoreboardData({
	linked,
	winnerTeamId,
	loserTeamId,
}: {
	/** in link order, earliest first */
	linked: Array<{ data: ScannerMatch; povUserId: number | null }>;
	winnerTeamId: number;
	loserTeamId: number | null;
}): IngestedScoreboardData | null
Unit tests (18)
function

recognizablePlayerNames

:295

A linked match's players winner-first in row order (unread names as ""), the game's linkedPlayerNames. Null when it can't form a view or read too few names to ever recognize a re-detection: such a read must not lock the game against every other POV's scan.

export function recognizablePlayerNames(match: ScannerMatch): string[] | null
Unit tests (3)

Not shown, only used by tests: MatchedGame

VodMatches

app/features/scanner-ingest/core/VodMatches.ts

No overview comment at the top of the file.

Imports
no other modules
Used by
1 loader

Types

interface

PrefillVodMatch

no JSDoc:8
export interface PrefillVodMatch {
	startsAt: number;
	mode: ModeShort | null;
	stageId: StageId | null;
	weapons: (MainWeaponId | null)[];
	/** the POV player's weapon, when the scan identified their seat */
	povWeapon: MainWeaponId | null;
}
Loaders/vods/new

Functions & values

function

prefillVodMatches

:18

Turns a scanner VoD scan's per-match rows (sendou ids, validated by ingestVodPrefillSchema) into /vods/new prefill data; only renames fields.

export function prefillVodMatches(
	matches: IngestVodMatchInput[],
): PrefillVodMatch[]
Unit tests (4)
Loaders/vods/new