Preserve empty traffic periods
This commit is contained in:
@@ -93,6 +93,7 @@ export const upsertGeneralAccess = async (
|
|||||||
worldStateId: number;
|
worldStateId: number;
|
||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
|
tickSeconds: number;
|
||||||
generalId: number;
|
generalId: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
@@ -105,8 +106,45 @@ export const upsertGeneralAccess = async (
|
|||||||
throw new Error('Traffic access persistence requires transaction support.');
|
throw new Error('Traffic access persistence requires transaction support.');
|
||||||
}
|
}
|
||||||
await db.$transaction(async (transaction) => {
|
await db.$transaction(async (transaction) => {
|
||||||
|
const periodKey = input.year * 12 + input.month - 1;
|
||||||
const periodRows = await transaction.$queryRaw<Array<{ id: number }>>(
|
const periodRows = await transaction.$queryRaw<Array<{ id: number }>>(
|
||||||
GamePrisma.sql`
|
GamePrisma.sql`
|
||||||
|
WITH latest_period AS (
|
||||||
|
SELECT MAX(year * 12 + month - 1)::INTEGER AS period_key
|
||||||
|
FROM traffic_period
|
||||||
|
WHERE world_state_id = ${input.worldStateId}
|
||||||
|
),
|
||||||
|
missing_periods AS (
|
||||||
|
INSERT INTO traffic_period (
|
||||||
|
world_state_id,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
started_at,
|
||||||
|
last_refresh,
|
||||||
|
refresh,
|
||||||
|
online
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
${input.worldStateId},
|
||||||
|
(missing_key / 12)::INTEGER,
|
||||||
|
(missing_key % 12 + 1)::INTEGER,
|
||||||
|
CAST(${input.periodStartedAt} AS TIMESTAMP) - (
|
||||||
|
(${periodKey} - missing_key) * ${input.tickSeconds}
|
||||||
|
) * INTERVAL '1 second',
|
||||||
|
CAST(${input.periodStartedAt} AS TIMESTAMP) - (
|
||||||
|
(${periodKey} - missing_key - 1) * ${input.tickSeconds}
|
||||||
|
) * INTERVAL '1 second',
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
FROM latest_period
|
||||||
|
CROSS JOIN LATERAL generate_series(
|
||||||
|
latest_period.period_key + 1,
|
||||||
|
${periodKey} - 1
|
||||||
|
) AS missing_key
|
||||||
|
WHERE latest_period.period_key IS NOT NULL
|
||||||
|
ON CONFLICT (world_state_id, year, month) DO NOTHING
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
INSERT INTO traffic_period (
|
INSERT INTO traffic_period (
|
||||||
world_state_id,
|
world_state_id,
|
||||||
year,
|
year,
|
||||||
@@ -265,6 +303,7 @@ export const recordGeneralAccess = async (
|
|||||||
worldStateId: worldState.id,
|
worldStateId: worldState.id,
|
||||||
year: worldState.currentYear,
|
year: worldState.currentYear,
|
||||||
month: worldState.currentMonth,
|
month: worldState.currentMonth,
|
||||||
|
tickSeconds: worldState.tickSeconds,
|
||||||
generalId: general.id,
|
generalId: general.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
weight,
|
weight,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ integration('general access tracking persistence', () => {
|
|||||||
worldStateId,
|
worldStateId,
|
||||||
year: 185,
|
year: 185,
|
||||||
month: 3,
|
month: 3,
|
||||||
|
tickSeconds: 600,
|
||||||
generalId,
|
generalId,
|
||||||
userId: 'access-user-a',
|
userId: 'access-user-a',
|
||||||
now: new Date('2026-07-26T03:05:00.000Z'),
|
now: new Date('2026-07-26T03:05:00.000Z'),
|
||||||
@@ -100,6 +101,7 @@ integration('general access tracking persistence', () => {
|
|||||||
worldStateId,
|
worldStateId,
|
||||||
year: 185,
|
year: 185,
|
||||||
month: 4,
|
month: 4,
|
||||||
|
tickSeconds: 600,
|
||||||
generalId,
|
generalId,
|
||||||
userId: 'access-user-b',
|
userId: 'access-user-b',
|
||||||
now: new Date('2026-07-26T03:15:00.000Z'),
|
now: new Date('2026-07-26T03:15:00.000Z'),
|
||||||
@@ -120,6 +122,7 @@ integration('general access tracking persistence', () => {
|
|||||||
worldStateId,
|
worldStateId,
|
||||||
year: 185,
|
year: 185,
|
||||||
month: 4,
|
month: 4,
|
||||||
|
tickSeconds: 600,
|
||||||
generalId: secondGeneralId,
|
generalId: secondGeneralId,
|
||||||
userId: 'access-user-c',
|
userId: 'access-user-c',
|
||||||
now: new Date('2026-07-26T03:15:01.000Z'),
|
now: new Date('2026-07-26T03:15:01.000Z'),
|
||||||
@@ -154,6 +157,35 @@ integration('general access tracking persistence', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await upsertGeneralAccess(db, {
|
||||||
|
worldStateId,
|
||||||
|
year: 185,
|
||||||
|
month: 6,
|
||||||
|
tickSeconds: 600,
|
||||||
|
generalId,
|
||||||
|
userId: 'access-user-b',
|
||||||
|
now: new Date('2026-07-26T03:35:00.000Z'),
|
||||||
|
periodStartedAt: new Date('2026-07-26T03:30:00.000Z'),
|
||||||
|
scoreStartedAt: new Date('2026-07-26T03:30:00.000Z'),
|
||||||
|
weight: 1,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
db.trafficPeriod.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
worldStateId_year_month: {
|
||||||
|
worldStateId,
|
||||||
|
year: 185,
|
||||||
|
month: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
startedAt: new Date('2026-07-26T03:20:00.000Z'),
|
||||||
|
lastRefresh: new Date('2026-07-26T03:30:00.000Z'),
|
||||||
|
refresh: 0,
|
||||||
|
online: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rolls back the period and member rows when the legacy access update fails', async () => {
|
it('rolls back the period and member rows when the legacy access update fails', async () => {
|
||||||
@@ -174,6 +206,7 @@ integration('general access tracking persistence', () => {
|
|||||||
worldStateId,
|
worldStateId,
|
||||||
year: 186,
|
year: 186,
|
||||||
month: 1,
|
month: 1,
|
||||||
|
tickSeconds: 600,
|
||||||
generalId: rollbackGeneralId,
|
generalId: rollbackGeneralId,
|
||||||
userId: 'rollback-user',
|
userId: 'rollback-user',
|
||||||
now: new Date('2026-07-26T03:20:00.000Z'),
|
now: new Date('2026-07-26T03:20:00.000Z'),
|
||||||
|
|||||||
@@ -80,8 +80,15 @@ describe('general access tracking', () => {
|
|||||||
|
|
||||||
const periodStatement = queryRaw.mock.calls[0]![0] as { sql: string; values: unknown[] };
|
const periodStatement = queryRaw.mock.calls[0]![0] as { sql: string; values: unknown[] };
|
||||||
expect(periodStatement.sql).toContain('INSERT INTO traffic_period');
|
expect(periodStatement.sql).toContain('INSERT INTO traffic_period');
|
||||||
|
expect(periodStatement.sql).toContain('generate_series');
|
||||||
expect(periodStatement.sql).toContain('ON CONFLICT (world_state_id, year, month) DO UPDATE');
|
expect(periodStatement.sql).toContain('ON CONFLICT (world_state_id, year, month) DO UPDATE');
|
||||||
expect(periodStatement.values).toEqual([3, 185, 4, new Date('2026-07-26T03:00:00.000Z'), now, 2]);
|
expect(periodStatement.values).toContain(3);
|
||||||
|
expect(periodStatement.values).toContain(185);
|
||||||
|
expect(periodStatement.values).toContain(4);
|
||||||
|
expect(periodStatement.values).toContain(600);
|
||||||
|
expect(periodStatement.values).toContain(2);
|
||||||
|
expect(periodStatement.values).toContain(now);
|
||||||
|
expect(periodStatement.values).toContainEqual(new Date('2026-07-26T03:00:00.000Z'));
|
||||||
|
|
||||||
const memberStatement = executeRaw.mock.calls[0]![0] as { sql: string; values: unknown[] };
|
const memberStatement = executeRaw.mock.calls[0]![0] as { sql: string; values: unknown[] };
|
||||||
expect(memberStatement.sql).toContain('INSERT INTO traffic_period_general');
|
expect(memberStatement.sql).toContain('INSERT INTO traffic_period_general');
|
||||||
|
|||||||
Reference in New Issue
Block a user