refactor: move the server-reset clock out of daily.ts
Ticks are about to stop being its only caller: the countdown needs the same grid, and daily.ts already imports time.ts, so asking for it the other way would close a cycle. time.ts is where it belongs anyway — it has held REGION_RESET_UTC_OFFSET all along, and serverOffsetUtc is that table's accessor. Nothing changes shape; `shift` is exported as `resetShiftMs` because dayKey now reads it across a module boundary. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c2fe249d83
commit
00c3f5e2da
+3
-64
@@ -1,7 +1,6 @@
|
|||||||
import { GAMES } from "./games.ts";
|
|
||||||
import type { LaneId } from "./custom.ts";
|
import type { LaneId } from "./custom.ts";
|
||||||
import type { GachaEvent, GameId, Region } from "./schema.ts";
|
import type { GachaEvent, GameId, Region } from "./schema.ts";
|
||||||
import { DAY, HOUR, REGION_RESET_UTC_OFFSET } from "./time.ts";
|
import { DAY, resetShiftMs } from "./time.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events you have to come back to every day.
|
* Events you have to come back to every day.
|
||||||
@@ -17,13 +16,6 @@ import { DAY, HOUR, REGION_RESET_UTC_OFFSET } from "./time.ts";
|
|||||||
* against a fixed instant.
|
* against a fixed instant.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Gacha servers roll the day at 04:00 local server time, not midnight — a
|
|
||||||
* player finishing at 02:00 is still on the previous day's dailies. Getting
|
|
||||||
* this wrong ticks the wrong box for four hours every night.
|
|
||||||
*/
|
|
||||||
export const RESET_HOUR_LOCAL = 4;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A 180-day event is already a parse error (see AGENTS.md § Domain rules), so
|
* A 180-day event is already a parse error (see AGENTS.md § Domain rules), so
|
||||||
* this only ever fires on data that is wrong; it exists so a bad end date
|
* this only ever fires on data that is wrong; it exists so a bad end date
|
||||||
@@ -115,59 +107,6 @@ export function dailyOverride(
|
|||||||
return desired === detected ? undefined : desired;
|
return desired === detected ? undefined : desired;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The UTC offset of the server clock a reader's day rolls on.
|
|
||||||
*
|
|
||||||
* The reader's region is always the question; a game can just answer it
|
|
||||||
* differently. Most run a server per region and take the default. One that
|
|
||||||
* serves two regions off a single machine lists the regions that differ in
|
|
||||||
* `resetOffsets` — Endfield's European players sit on the Americas server, so
|
|
||||||
* `europe` resolves to UTC-5 there and to UTC+1 everywhere else.
|
|
||||||
*
|
|
||||||
* A blanket per-game offset would be the wrong shape: it would drag the regions
|
|
||||||
* that *do* have their own server onto somebody else's clock, which is a
|
|
||||||
* different bug in the same place.
|
|
||||||
*/
|
|
||||||
export function serverOffsetUtc(region: Region, game?: LaneId): number {
|
|
||||||
// A lane the reader invented (PRD F13) has no server map to know about, and
|
|
||||||
// neither does an id that has outlived its game, so both take the regional
|
|
||||||
// default rather than being looked up and crashing.
|
|
||||||
const override =
|
|
||||||
game === undefined ? undefined : GAMES[game as GameId]?.resetOffsets?.[region];
|
|
||||||
return override ?? REGION_RESET_UTC_OFFSET[region];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hour of its own server day a game rolls over on.
|
|
||||||
*
|
|
||||||
* Almost always `RESET_HOUR_LOCAL`. A game that resets on a different hour says
|
|
||||||
* so in `resetHourLocal` (`games.ts`) — Reverse: 1999 rolls at 05:00 — and that
|
|
||||||
* cannot be folded into `serverOffsetUtc`: shifting a game's stated offset to
|
|
||||||
* land the right reset instant would misreport the server clock to everything
|
|
||||||
* else that asks for it.
|
|
||||||
*
|
|
||||||
* A lane the reader invented has no server to know about and takes the default,
|
|
||||||
* for the same reason `serverOffsetUtc` does.
|
|
||||||
*/
|
|
||||||
export function resetHourFor(game?: LaneId): number {
|
|
||||||
const override =
|
|
||||||
game === undefined ? undefined : GAMES[game as GameId]?.resetHourLocal;
|
|
||||||
return override ?? RESET_HOUR_LOCAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Offset from UTC midnight to this game's reset instant.
|
|
||||||
*
|
|
||||||
* Everything downstream of this is a **localStorage key**. Moving the reset
|
|
||||||
* hour, a region offset, or a game's own override re-labels the game-day some
|
|
||||||
* already-logged ticks fall in — at most by one day, and never by deleting one,
|
|
||||||
* but it is still the reader's streak moving under them. Treat a change here as
|
|
||||||
* a data change, not a constant.
|
|
||||||
*/
|
|
||||||
function shift(region: Region, game?: LaneId): number {
|
|
||||||
return serverOffsetUtc(region, game) * HOUR - resetHourFor(game) * HOUR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which game-day an instant falls in, as `YYYY-MM-DD`.
|
* Which game-day an instant falls in, as `YYYY-MM-DD`.
|
||||||
*
|
*
|
||||||
@@ -179,12 +118,12 @@ function shift(region: Region, game?: LaneId): number {
|
|||||||
* that reads or writes a tick should pass it.
|
* that reads or writes a tick should pass it.
|
||||||
*/
|
*/
|
||||||
export function dayKey(ms: number, region: Region, game?: LaneId): string {
|
export function dayKey(ms: number, region: Region, game?: LaneId): string {
|
||||||
return new Date(ms + shift(region, game)).toISOString().slice(0, 10);
|
return new Date(ms + resetShiftMs(region, game)).toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The next reset instant strictly after `ms`. */
|
/** The next reset instant strictly after `ms`. */
|
||||||
export function nextResetMs(ms: number, region: Region, game?: LaneId): number {
|
export function nextResetMs(ms: number, region: Region, game?: LaneId): number {
|
||||||
const s = shift(region, game);
|
const s = resetShiftMs(region, game);
|
||||||
return Math.floor((ms + s) / DAY) * DAY + DAY - s;
|
return Math.floor((ms + s) / DAY) * DAY + DAY - s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-2
@@ -1,5 +1,6 @@
|
|||||||
import type { DisplayEvent } from "./custom.ts";
|
import type { DisplayEvent, LaneId } from "./custom.ts";
|
||||||
import type { Region } from "./schema.ts";
|
import { GAMES } from "./games.ts";
|
||||||
|
import type { GameId, Region } from "./schema.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time is this product's entire subject, so the vocabulary lives in one place:
|
* Time is this product's entire subject, so the vocabulary lives in one place:
|
||||||
@@ -20,6 +21,66 @@ export const REGION_RESET_UTC_OFFSET: Record<Region, number> = {
|
|||||||
europe: 1,
|
europe: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gacha servers roll the day at 04:00 local server time, not midnight — a
|
||||||
|
* player finishing at 02:00 is still on the previous day's dailies. Getting
|
||||||
|
* this wrong ticks the wrong box for four hours every night.
|
||||||
|
*/
|
||||||
|
export const RESET_HOUR_LOCAL = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UTC offset of the server clock a reader's day rolls on.
|
||||||
|
*
|
||||||
|
* The reader's region is always the question; a game can just answer it
|
||||||
|
* differently. Most run a server per region and take the default. One that
|
||||||
|
* serves two regions off a single machine lists the regions that differ in
|
||||||
|
* `resetOffsets` — Endfield's European players sit on the Americas server, so
|
||||||
|
* `europe` resolves to UTC-5 there and to UTC+1 everywhere else.
|
||||||
|
*
|
||||||
|
* A blanket per-game offset would be the wrong shape: it would drag the regions
|
||||||
|
* that *do* have their own server onto somebody else's clock, which is a
|
||||||
|
* different bug in the same place.
|
||||||
|
*/
|
||||||
|
export function serverOffsetUtc(region: Region, game?: LaneId): number {
|
||||||
|
// A lane the reader invented (PRD F13) has no server map to know about, and
|
||||||
|
// neither does an id that has outlived its game, so both take the regional
|
||||||
|
// default rather than being looked up and crashing.
|
||||||
|
const override =
|
||||||
|
game === undefined ? undefined : GAMES[game as GameId]?.resetOffsets?.[region];
|
||||||
|
return override ?? REGION_RESET_UTC_OFFSET[region];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hour of its own server day a game rolls over on.
|
||||||
|
*
|
||||||
|
* Almost always `RESET_HOUR_LOCAL`. A game that resets on a different hour says
|
||||||
|
* so in `resetHourLocal` (`games.ts`) — Reverse: 1999 rolls at 05:00 — and that
|
||||||
|
* cannot be folded into `serverOffsetUtc`: shifting a game's stated offset to
|
||||||
|
* land the right reset instant would misreport the server clock to everything
|
||||||
|
* else that asks for it.
|
||||||
|
*
|
||||||
|
* A lane the reader invented has no server to know about and takes the default,
|
||||||
|
* for the same reason `serverOffsetUtc` does.
|
||||||
|
*/
|
||||||
|
export function resetHourFor(game?: LaneId): number {
|
||||||
|
const override =
|
||||||
|
game === undefined ? undefined : GAMES[game as GameId]?.resetHourLocal;
|
||||||
|
return override ?? RESET_HOUR_LOCAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset from UTC midnight to this game's reset instant.
|
||||||
|
*
|
||||||
|
* `dayKey` and everything downstream of it is a **localStorage key**. Moving the
|
||||||
|
* reset hour, a region offset, or a game's own override re-labels the game-day
|
||||||
|
* some already-logged ticks fall in — at most by one day, and never by deleting
|
||||||
|
* one, but it is still the reader's streak moving under them. Treat a change
|
||||||
|
* here as a data change, not a constant.
|
||||||
|
*/
|
||||||
|
export function resetShiftMs(region: Region, game?: LaneId): number {
|
||||||
|
return serverOffsetUtc(region, game) * HOUR - resetHourFor(game) * HOUR;
|
||||||
|
}
|
||||||
|
|
||||||
export function guessRegion(
|
export function guessRegion(
|
||||||
timeZoneOffsetMinutes: number = -new Date().getTimezoneOffset(),
|
timeZoneOffsetMinutes: number = -new Date().getTimezoneOffset(),
|
||||||
): Region {
|
): Region {
|
||||||
|
|||||||
+6
-3
@@ -8,12 +8,15 @@ import {
|
|||||||
isDaily,
|
isDaily,
|
||||||
msUntilReset,
|
msUntilReset,
|
||||||
nextResetMs,
|
nextResetMs,
|
||||||
resetHourFor,
|
|
||||||
RESET_HOUR_LOCAL,
|
|
||||||
resolveDaily,
|
resolveDaily,
|
||||||
streakOf,
|
streakOf,
|
||||||
} from "../src/shared/daily.ts";
|
} from "../src/shared/daily.ts";
|
||||||
import { DAY, HOUR } from "../src/shared/time.ts";
|
import {
|
||||||
|
DAY,
|
||||||
|
HOUR,
|
||||||
|
resetHourFor,
|
||||||
|
RESET_HOUR_LOCAL,
|
||||||
|
} from "../src/shared/time.ts";
|
||||||
|
|
||||||
const at = (iso: string) => Date.parse(iso);
|
const at = (iso: string) => Date.parse(iso);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user