Skip to content

Availability · Modules

10 modules · 45 exports · 1 repository · 8 e2e tests · 1 route

Who imports who · hover a node
Depends onAvailability modulesUsed byLeagueSchedulingtournam…SeriesTeamCount.serverAvailability25 exportsCommitments.server1 exp…FriendSchedule.serverMySchedule.server2 expo…RegistrationAvailability.serverRosterSchedule.serverScheduleWeek6 exportsTournamentDuration.serverTournamentDuration2 exp…VisibleSchedules.serverLoaders8 filesActions1 fileScrimscrimsComponents6 filesRoutines1 fileScripts1 fileOther3 files

Availability

app/features/availability/core/Availability.ts

No overview comment at the top of the file.

Imports
no other modules
Used by
2 loaders1 action7 modules4 components1 routine1 script3 other

Functions & values

function

weekStartsAt

:32

Database timestamp of the Monday 00:00 starting the week date falls in, as seen in timezone.

export function weekStartsAt(date: Date, timezone: string)
Unit tests (2)
function

weekRange

:42

The week date falls in, endsAt being the next Monday 00:00. Not always 7×24h: a week with a DST transition is an hour shorter or longer.

export function weekRange(date: Date, timezone: string): TimeRange
Unit tests (2)
function

isFirstDayOfWeek

:53

Whether date falls on the first day of its week (Monday), as the week is seen in timezone.

export function isFirstDayOfWeek(date: Date, timezone: string)
Unit tests (1)
function

isLastDayOfWeek

:58

Whether date falls on the last day of its week (Sunday), as the week is seen in timezone.

export function isLastDayOfWeek(date: Date, timezone: string)
Unit tests (2)
function

isoWeekNumber

:63

ISO week number of the week the timestamp falls in, as seen in timezone.

export function isoWeekNumber(timestamp: number, timezone: string)
Unit tests (2)
function

isSameWeek

:68

Whether the two week starts name the same week: closer than timezones can set them apart (hours, never days).

export function isSameWeek(weekStart: number, rangeStartsAt: number)
function

localToTimestamp

:76

Database timestamp of date (YYYY-MM-DD) at time (HH:mm) in timezone.

export function localToTimestamp({
	date,
	time,
	timezone,
}: {
	date: string;
	time: string;
	timezone: string;
})
function

dayMinutesToTimestamp

:103

Database timestamp of minutes from midnight of date in timezone. Minutes past 1440 roll into the next day; on a DST day the clock rolls through the change like a hand-entered time would.

export function dayMinutesToTimestamp({
	date,
	minutes,
	timezone,
}: {
	date: string;
	minutes: number;
	timezone: string;
})
Unit tests (3)
Actions/events
function

timestampToDayMinutes

:129

Minutes from midnight of date in timezone, the inverse of {@link dayMinutesToTimestamp}. A timestamp on a later day counts on past 1440, read off the wall clock so a DST change inside the range does not stretch or shrink it.

export function timestampToDayMinutes({
	date,
	timestamp,
	timezone,
}: {
	date: string;
	timestamp: number;
	timezone: string;
})
function

dateInTimezone

:153

YYYY-MM-DD of the timestamp in timezone. A slot's day depends on who is looking, so it is always resolved from the timestamp, never from the day its author entered it on.

export function dateInTimezone(timestamp: number, timezone: string)
Unit tests (2)
function

dateAcrossTimezones

:166

YYYY-MM-DD in to of a day saved as a date in from, mapped through that day's noon in case the viewer has since moved. How day notes find their viewer-local day.

export function dateAcrossTimezones({
	date,
	from,
	to,
}: {
	date: string;
	from: string;
	to: string;
})
function

overlaps

:182

Whether the two ranges share any time at all. Ranges that merely touch do not overlap.

export function overlaps(one: TimeRange, other: TimeRange)
Unit tests (1)
function

normalize

:187

The ranges sorted and merged so that no two overlap or touch. Empty ranges are dropped.

export function normalize(ranges: Array<TimeRange>): Array<TimeRange>
Unit tests (3)
function

subtract

:208

What is left of ranges once every busy block is cut out: a commitment always wins over what was reported.

export function subtract(
	ranges: Array<TimeRange>,
	busy: Array<TimeRange>,
): Array<TimeRange>
Unit tests (4)
function

clip

:238

The parts of the ranges inside window, sorted and merged. Keeps one week's view from picking up the next week's windows.

export function clip(
	ranges: Array<TimeRange>,
	window: TimeRange,
): Array<TimeRange>
Unit tests (3)
function

splitByDayTracks

:251

The ranges cut up into the day tracks they render on, what runs past a track's end continuing on the next day's.

export function splitByDayTracks(
	ranges: Array<TimeRange>,
	timezone: string,
): Array<TimeRange>
function

availabilityInWindow

:282

How one person's schedule relates to an event's window. A busy block overlapping it wins over anything reported (committed elsewhere, schedule known or not). Otherwise the slots cover the window (available, ranges as reported), part of it (partial, overlap clipped to the window), miss it (unavailable) or do not exist (unknown).

export function availabilityInWindow({
	reported,
	slots,
	busy,
	window,
}: {
	reported: boolean;
	slots: Array<TimeRange>;
	busy: Array<BusyBlock>;
	window: TimeRange;
}): WindowAvailability
Unit tests (10)
function

playableWindows

:318

Spans where minPlayers members (FULL) or one fewer (ONE_SHORT) are free throughout. Windows shorter than minDurationMinutes are dropped, as is a ONE_SHORT window containing a FULL one.

export function playableWindows({
	members,
	minPlayers = AVAILABILITY.DEFAULT_MIN_PLAYERS,
	minDurationMinutes = AVAILABILITY.MIN_WINDOW_MINUTES,
}: {
	members: Array<MemberAvailability>;
	minPlayers?: number;
	minDurationMinutes?: number;
}): Array<PlayableWindow>
Unit tests (8)
function

availabilitySegments

:365

Splits the members' availability at every start/end into spans, each with the members free throughout. Spans nobody is free in come out with an empty userIds.

export function availabilitySegments(members: Array<MemberAvailability>)
Unit tests (4)
function

minutesToTime

:454

HH:mm at the given minutes from midnight; minutes past 24h wrap, so a range crossing midnight ends at e.g. 02:00.

export function minutesToTime(minutes: number)
Unit tests (1)
function

dayRangeFromTimes

:466

Editor day range of the given times. An end before the start crosses midnight; an end equal to the start is an empty range (dropped by {@link mergedDayRanges}).

export function dayRangeFromTimes(start: string, end: string): DayTimeRange
Unit tests (3)
function

mergedDayRanges

:477

One day track's ranges sorted and merged so that no two overlap or touch. Empty ranges are dropped.

export function mergedDayRanges(
	ranges: Array<DayTimeRange>,
): Array<DayTimeRange>
Unit tests (2)
function

paintedRange

:502

Range painted by dragging an empty part of a day track from anchor to cursor (minutes from midnight): snapped to the entry step, at least one step long, inside the track, start kept before midnight (a paint anchored past it grows leftwards from the day's last step). Null when the anchor is inside a wall (a commitment); a paint may still extend across one.

export function paintedRange({
	anchor,
	cursor,
	walls,
	trackStart,
	trackEnd,
}: TrackWindowArgs & {
	anchor: number;
	cursor: number;
	/** Blocks a paint cannot start on, i.e. the day's commitments. */
	walls: Array<DayTimeRange>;
}): DayTimeRange | null
Unit tests (6)
function

movedRange

:537

range moved by delta minutes, snapped to the entry step, stopped at the track edges, start kept before midnight.

export function movedRange({
	range,
	delta,
	trackStart,
	trackEnd,
}: TrackWindowArgs & {
	range: DayTimeRange;
	delta: number;
}): DayTimeRange
Unit tests (3)
function

resizedRange

:558

range with one edge dragged to cursor: snapped, at least one step long, inside the track, start kept before midnight.

export function resizedRange({
	range,
	edge,
	cursor,
	trackStart,
	trackEnd,
}: TrackWindowArgs & {
	range: DayTimeRange;
	edge: "start" | "end";
	cursor: number;
}): DayTimeRange
Unit tests (4)
Unit tests not about one export (3)

Not shown, only used by tests: timeInTimezone, snapMinutes, timeToMinutes

Commitments

serverapp/features/availability/core/Commitments.server.ts

No overview comment at the top of the file.

Imports
LeagueSchedulingSeriesTeamCount.serverAvailabilityTournamentDurationTournamentDuration.server
Used by
2 modules

Functions & values

function

busyBlocksByUserIds

:24

Busy blocks of the users within the window, keyed by user id, sorted by start (effective availability = reported − busy). From tournament registrations (start + estimated duration, {@link TournamentDuration.estimateSeconds}), accepted scrims (start + assumed length) and team events (actual span). A league registration is not a block, its sets are: each one agreed to be played blocks {@link LeagueScheduling.busyBlock}. excludeTournamentId leaves one tournament out, for "busy elsewhere" views of that tournament. Busy blocks are part of the schedule, so callers pass only ids {@link AvailabilityRepository.findScheduleVisibleUserIds} handed back.

export async function busyBlocksByUserIds({
	userIds,
	startsAt,
	endsAt,
	excludeTournamentId,
}: {
	userIds: Array<number>;
	startsAt: number;
	endsAt: number;
	excludeTournamentId?: number;
}): Promise<Map<number, Array<BusyBlock>>>
Unit tests (10)

FriendSchedule

serverapp/features/availability/core/FriendSchedule.server.ts

No overview comment at the top of the file.

Imports
AvailabilityScheduleWeekVisibleSchedules.server
Used by
1 loader

Functions & values

function

findByUserIds

:14

Reportable weeks keyed by user id, free time only (commitments subtracted). Users who reported neither week, and those not sharing their schedule with the viewer, are left out; the friends page sorts and shows its calendar icon by the missing key.

export async function findByUserIds({
	userIds,
	timezone,
	viewerId,
}: {
	userIds: Array<number>;
	timezone: string;
	viewerId: number;
}): Promise<Map<number, Array<ScheduleWeekView>>>
Unit tests (5)
Loaders/friends

MySchedule

serverapp/features/availability/core/MySchedule.server.ts

No overview comment at the top of the file.

Imports
AvailabilityCommitments.serverScheduleWeek
Used by
1 loader1 component

Types

type

MyScheduleData

no JSDoc:12
export type MyScheduleData = SerializeFrom<
	Awaited<ReturnType<typeof myScheduleData>>
>;

Functions & values

function

myScheduleData

:20

The user's reported current and next week in their timezone as the editor's wall-clock representation, plus the week before for the "Copy last week" prefill.

export async function myScheduleData(userId: number)
Unit tests (5)
Loaders/events

RegistrationAvailability

serverapp/features/availability/core/RegistrationAvailability.server.ts

No overview comment at the top of the file.

Imports
AvailabilityTournamentDuration.serverVisibleSchedules.server
Used by
1 loader1 component

Types

type

RegistrationAvailability

no JSDoc:10
export type RegistrationAvailability = Awaited<
	ReturnType<typeof registrationAvailability>
>;

Functions & values

function

registrationAvailability

:19

Availability of the users for the tournament's estimated window (start to {@link estimatedEndsAt}); its own registrations don't count as busy. Past the reportable horizon every schedule would be unknown, so the result is only when the event's week opens up.

export async function registrationAvailability({
	tournament,
	userIds,
	timezone,
	viewerId,
}: {
	tournament: {
		id: number;
		name: string;
		organizationId: number | null;
		startsAt: number;
		minMembersPerTeam: number;
		bracketTypes: Array<Tables["TournamentStage"]["type"]>;
		teamCount: number;
	};
	userIds: Array<number>;
	timezone: string;
	viewerId: number;
})
Unit tests (4)

RosterSchedule

serverapp/features/availability/core/RosterSchedule.server.ts

No overview comment at the top of the file.

Imports
AvailabilityScheduleWeekVisibleSchedules.server
Used by
2 loaders1 component

Types

type

RosterScheduleData

no JSDoc:16
export type RosterScheduleData = SerializeFrom<
	Awaited<ReturnType<typeof rosterScheduleData>>
>;

Functions & values

function

rosterScheduleData

:26

Effective availability of the users over the reportable horizon as viewer-local weeks and days. Which users form a roster is only known in the browser (scrim post form's team select, pick-up search), so members come out one by one and {@link Availability.playableWindows} merges the picked ones client side.

export async function rosterScheduleData({
	userIds,
	timezone,
	viewerId,
}: {
	userIds: Array<number>;
	timezone: string;
	viewerId: number;
})
Unit tests (5)
function

windowSchedules

:120

What the users' schedules say about each window: reported ranges inside it, overriding commitments and whether the week was filled in at all. Resolved in one go so a page with many windows (scrim fit indicators) reads the schedules once. Windows past the horizon are left out.

export async function windowSchedules({
	windows,
	userIds,
	viewerId,
}: {
	windows: Array<TimeRange & { id: number }>;
	userIds: Array<number>;
	/** Null when logged out, which the scrims page is viewable as. */
	viewerId: number | null;
})
Unit tests (5)
Loaders/scrims

ScheduleWeek

app/features/availability/core/ScheduleWeek.ts

No overview comment at the top of the file.

Imports
Availability
Used by
1 loader3 modules

Types

interface

ScheduleWeekDay

:8

One day of a schedule week, as the viewer's timezone places it.

export interface ScheduleWeekDay {
	/** `YYYY-MM-DD` in the viewer's timezone */
	date: string;
	noonAt: number;
}
Only used inside the module
interface

ReportedWeek

:15

A week of reported availability, in the shape the repository returns it.

export interface ReportedWeek {
	userId: number;
	weekStartsAt: number;
	timezone: string;
	slots: Array<TimeRange>;
	dayNotes: Array<{ date: string; text: string }>;
}
interface

MemberWeek

:24

One member's week as the read-only schedule surfaces render it.

export interface MemberWeek {
	userId: number;
	/** Whether they filled the week in at all. */
	reported: boolean;
	days: Array<{
		/** Free to play: commitments cut out. What "when can we play" views read. */
		ranges: Array<TimeRange>;
		/** What they filled in, commitments left in place. What views showing the commitments beside it read. */
		reportedRanges: Array<TimeRange>;
		busy: Array<BusyBlock>;
	}>;
	notes: Array<{ dayIndex: number; text: string }>;
}
Only used inside the module

Functions & values

function

days

:39

The seven days a week is laid out on in the viewer's timezone, Monday first.

export function days(
	range: TimeRange,
	timezone: string,
): Array<ScheduleWeekDay>
function

weekNumber

:51

The week's ISO number, as its heading names it.

export function weekNumber(range: TimeRange, timezone: string)
function

memberRow

:61

One member's week bucketed into the viewer's days: free time both with commitments cut out and as they reported it, the commitments and their notes. Slots land on the viewer-local day track they start on, wherever the author's week put them, adjacent weeks' spillover included, and what runs past the end of that track continues on the next day.

export function memberRow({
	userId,
	days: weekDays,
	timezone,
	reportedWeeks,
	range,
	busy,
}: {
	userId: number;
	days: Array<ScheduleWeekDay>;
	timezone: string;
	reportedWeeks: Array<ReportedWeek>;
	range: TimeRange;
	busy: Array<BusyBlock>;
}): MemberWeek
Unit tests (5)

TournamentDuration

serverapp/features/availability/core/TournamentDuration.server.ts

No overview comment at the top of the file.

Imports
SeriesTeamCount.serverTournamentDuration
Used by
1 loader2 modules

Functions & values

function

estimatedEndsAt

:19

Start plus {@link TournamentDuration.estimateSeconds} sized by the expected team count. Every view showing or blocking out a tournament's window goes through this so they agree.

export async function estimatedEndsAt(tournament: EstimatedTournament)
function

estimatedEndsAtWith

:24

{@link estimatedEndsAt} for callers estimating many tournaments off one resolved lookup.

export function estimatedEndsAtWith(
	tournament: EstimatedTournament,
	expectedTeamCount: (tournament: EstimatedTournament) => number,
)

TournamentDuration

app/features/availability/core/TournamentDuration.ts

No overview comment at the top of the file.

Imports
no other modules
Used by
2 modules

Functions & values

const

MAX_ESTIMATE_SECONDS

:13

The largest value {@link estimateSeconds} can return, for widening fetch windows.

export const MAX_ESTIMATE_SECONDS = LARGE_FOUR_VS_FOUR_HOURS * HOUR_SECONDS;
function

estimateSeconds

:52

Estimated length of a tournament in seconds, used to block its players' availability from the event's start. Only for a tournament played in one sitting, the numbers being measured over whole events.

The actual length is not in the data model, so this is a constant table measured from the production database (August 2026): 3222 finalized tournaments, duration = scheduled start → last reported game result, leagues and test tournaments excluded. Hours:

| case | n | p25 | med | p75 | p90 | | --------------------------- | ---- | --- | --- | --- | --- | | 1v1 | 273 | 1.5 | 2.0 | 2.5 | 3.1 | | 2v2 | 282 | 1.9 | 2.3 | 2.7 | 3.0 | | 3v3 | 30 | 1.6 | 2.1 | 2.5 | 2.7 | | 4v4 | 2593 | 2.6 | 3.2 | 3.8 | 4.3 | | single elim only (any size) | 129 | 0.8 | 1.3 | 1.7 | 2.1 | | 4v4, 32+ teams | 309 | 3.4 | 3.7 | 4.2 | 4.5 |

What the data showed:

- Team size and team count are the strong predictors. Format mostly proxies team count (round robin → elim and swiss events are the bigger ones); the one format that stands out on its own is a lone single elimination bracket, roughly half the length of everything else. - Team count raises duration (4v4 medians: <8 teams 2.2, 8–15 3.1, 16–31 3.7, 32–63 3.7, 64+ 4.2) but at estimate time the registered count is only a lower bound of the final count, so it only ever raises the estimate above the size default, never lowers it. Callers pass what the event is *expected* to draw, see SeriesTeamCount.lookup. - SZ-only vs multi-mode map pools made no meaningful difference (medians 3.4 vs 3.2), so modes are not a dimension.

The estimates sit at ≈p75 of their case: slightly generous, because a block that runs a bit long beats showing a player free while they are still playing. 84.7% of 4v4 tournaments end within their window.

export function estimateSeconds({
	minMembersPerTeam,
	bracketTypes,
	teamCount,
}: {
	minMembersPerTeam: number;
	bracketTypes: Array<Tables["TournamentStage"]["type"]>;
	/** Teams the tournament is expected to draw, not necessarily the registered count. */
	teamCount: number;
})
Unit tests (2)

VisibleSchedules

serverapp/features/availability/core/VisibleSchedules.server.ts

No overview comment at the top of the file.

Imports
Commitments.server
Used by
2 loaders3 modules

Functions & values

function

findByUserIds

:13

Reported weeks and busy blocks overlapping the window, of those of userIds who share their schedule with the viewer. Every read of other users' schedules goes through here so the visibility rule is applied in one place; a user left out looks like one who never filled the week in. excludeTournamentId keeps that tournament's own registrations from counting as busy. bypassVisibility reads every user's schedule: for rosters whose membership itself implies sharing (a league set's own tournament team).

export async function findByUserIds({
	userIds,
	viewerId,
	startsAt,
	endsAt,
	excludeTournamentId,
	bypassVisibility = false,
}: TimeRange & {
	userIds: Array<number>;
	viewerId: number;
	excludeTournamentId?: number;
	bypassVisibility?: boolean;
})