refactor(game): invalidate field-level read-model projections
This commit is contained in:
@@ -601,6 +601,15 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
generalIds: [7],
|
||||
cityIds: [],
|
||||
nationIds: [],
|
||||
mapGeneralIds: [],
|
||||
mapCityIds: [],
|
||||
mapNationIds: [],
|
||||
frontStatusGeneralIds: [],
|
||||
frontStatusNationIds: [],
|
||||
frontStatusActorIds: [],
|
||||
frontStatusChanged: false,
|
||||
lobbyGeneralIds: [],
|
||||
lobbyChanged: false,
|
||||
reservedGeneralIds: [],
|
||||
recordGeneralIds: [],
|
||||
worldChanged: false,
|
||||
@@ -620,11 +629,12 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
await expect(page.locator('.general-title')).toContainText('부드럽게갱신된장수');
|
||||
const changedOperations = state.operations.slice(operationsBeforeChangedBurst);
|
||||
expect(changedOperations).toEqual(
|
||||
expect.arrayContaining(['general.me', 'world.getMap', 'turns.getCommandTable', 'board.getAccess'])
|
||||
expect.arrayContaining(['general.me', 'turns.getCommandTable', 'board.getAccess'])
|
||||
);
|
||||
expect(changedOperations).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
'lobby.info',
|
||||
'world.getMap',
|
||||
'messages.getRecent',
|
||||
'messages.getContacts',
|
||||
'general.getRecentRecords',
|
||||
@@ -633,6 +643,40 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
])
|
||||
);
|
||||
|
||||
const operationsBeforeSurvey = state.operations.length;
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
|
||||
'readModelChanged',
|
||||
{
|
||||
at: new Date().toISOString(),
|
||||
revision: 42,
|
||||
changes: {
|
||||
generalIds: [],
|
||||
cityIds: [],
|
||||
nationIds: [],
|
||||
mapGeneralIds: [],
|
||||
mapCityIds: [],
|
||||
mapNationIds: [],
|
||||
frontStatusGeneralIds: [],
|
||||
frontStatusNationIds: [],
|
||||
frontStatusActorIds: [],
|
||||
frontStatusChanged: true,
|
||||
lobbyGeneralIds: [],
|
||||
lobbyChanged: false,
|
||||
reservedGeneralIds: [],
|
||||
recordGeneralIds: [],
|
||||
worldChanged: false,
|
||||
globalRecordsChanged: false,
|
||||
worldHistoryChanged: false,
|
||||
contactsChanged: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await expect.poll(() => state.operations.slice(operationsBeforeSurvey), { timeout: 3_000 }).toEqual([
|
||||
'general.getFrontStatus',
|
||||
]);
|
||||
|
||||
const profile = await page.evaluate(() => {
|
||||
const probe = (
|
||||
window as unknown as {
|
||||
@@ -697,6 +741,15 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
generalIds: [7],
|
||||
cityIds: [],
|
||||
nationIds: [],
|
||||
mapGeneralIds: [],
|
||||
mapCityIds: [],
|
||||
mapNationIds: [],
|
||||
frontStatusGeneralIds: [],
|
||||
frontStatusNationIds: [],
|
||||
frontStatusActorIds: [],
|
||||
frontStatusChanged: false,
|
||||
lobbyGeneralIds: [],
|
||||
lobbyChanged: false,
|
||||
reservedGeneralIds: [],
|
||||
recordGeneralIds: [],
|
||||
worldChanged: false,
|
||||
|
||||
@@ -31,16 +31,31 @@ export const resolveDashboardRefreshPlan = (
|
||||
const ownGeneralChanged = contains(changes.generalIds, identity.generalId);
|
||||
const ownCityChanged = contains(changes.cityIds, identity.cityId);
|
||||
const ownNationChanged = contains(changes.nationIds, identity.nationId);
|
||||
const ownFrontStatusNationChanged = contains(
|
||||
changes.frontStatusNationIds ?? changes.nationIds,
|
||||
identity.nationId
|
||||
);
|
||||
const ownGeneralMapChanged = contains(changes.mapGeneralIds ?? changes.generalIds, identity.generalId);
|
||||
const frontStatusGeneralChanged =
|
||||
changes.frontStatusGeneralIds !== undefined
|
||||
? changes.frontStatusGeneralIds.length > 0
|
||||
: changes.contactsChanged;
|
||||
const ownFrontStatusActorChanged = contains(changes.frontStatusActorIds ?? [], identity.generalId);
|
||||
const ownLobbyGeneralChanged = contains(changes.lobbyGeneralIds ?? changes.generalIds, identity.generalId);
|
||||
const lobbyChanged = changes.lobbyChanged ?? changes.contactsChanged;
|
||||
const entityContextChanged = ownGeneralChanged || ownCityChanged || ownNationChanged;
|
||||
const worldEntitiesChanged = changes.cityIds.length > 0 || changes.nationIds.length > 0;
|
||||
const mapEntitiesChanged =
|
||||
(changes.mapCityIds ?? changes.cityIds).length > 0 ||
|
||||
(changes.mapNationIds ?? changes.nationIds).length > 0;
|
||||
const commandEntitiesChanged = changes.cityIds.length > 0 || changes.nationIds.length > 0;
|
||||
|
||||
return {
|
||||
context: entityContextChanged,
|
||||
lobby: changes.worldChanged || changes.contactsChanged,
|
||||
map: changes.worldChanged || worldEntitiesChanged || ownGeneralChanged,
|
||||
commands: changes.worldChanged || worldEntitiesChanged || ownGeneralChanged,
|
||||
lobby: changes.worldChanged || lobbyChanged || ownLobbyGeneralChanged,
|
||||
map: changes.worldChanged || mapEntitiesChanged || ownGeneralMapChanged,
|
||||
commands: changes.worldChanged || commandEntitiesChanged || ownGeneralChanged,
|
||||
contacts: changes.contactsChanged,
|
||||
boardAccess: entityContextChanged,
|
||||
boardAccess: ownGeneralChanged || ownNationChanged,
|
||||
reservedTurns: contains(changes.reservedGeneralIds, identity.generalId),
|
||||
records:
|
||||
changes.globalRecordsChanged ||
|
||||
@@ -48,7 +63,11 @@ export const resolveDashboardRefreshPlan = (
|
||||
contains(changes.recordGeneralIds, identity.generalId),
|
||||
// lastTurnTime is intentionally excluded. This slice contains the
|
||||
// nation notice/vote/presence model and only follows related changes.
|
||||
frontStatus: changes.contactsChanged || ownNationChanged,
|
||||
frontStatus:
|
||||
Boolean(changes.frontStatusChanged) ||
|
||||
frontStatusGeneralChanged ||
|
||||
ownFrontStatusNationChanged ||
|
||||
ownFrontStatusActorChanged,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ void test('selects only the read models affected by the current identity', () =>
|
||||
assert.deepEqual(resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 }), {
|
||||
context: true,
|
||||
lobby: false,
|
||||
map: true,
|
||||
map: false,
|
||||
commands: true,
|
||||
contacts: false,
|
||||
boardAccess: true,
|
||||
@@ -48,6 +48,87 @@ void test('selects only the read models affected by the current identity', () =>
|
||||
});
|
||||
});
|
||||
|
||||
void test('refreshes the map only for map-projection changes', () => {
|
||||
const changes = {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [7],
|
||||
mapGeneralIds: [7],
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 }).map,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
void test('keeps conservative map behavior for rolling-deploy payloads without projections', () => {
|
||||
const changes = {
|
||||
generalIds: [7],
|
||||
cityIds: [],
|
||||
nationIds: [],
|
||||
reservedGeneralIds: [],
|
||||
recordGeneralIds: [],
|
||||
worldChanged: false,
|
||||
globalRecordsChanged: false,
|
||||
worldHistoryChanged: false,
|
||||
contactsChanged: false,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 }).map,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
void test('does not refresh front status for contact-only permission changes', () => {
|
||||
const changes = {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [9],
|
||||
contactsChanged: true,
|
||||
frontStatusGeneralIds: [],
|
||||
};
|
||||
const plan = resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 });
|
||||
|
||||
assert.equal(plan.contacts, true);
|
||||
assert.equal(plan.lobby, false);
|
||||
assert.equal(plan.frontStatus, false);
|
||||
});
|
||||
|
||||
void test('refreshes only front status for a global survey projection change', () => {
|
||||
const changes = {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
frontStatusChanged: true,
|
||||
};
|
||||
|
||||
assert.deepEqual(resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 }), {
|
||||
context: false,
|
||||
lobby: false,
|
||||
map: false,
|
||||
commands: false,
|
||||
contacts: false,
|
||||
boardAccess: false,
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: true,
|
||||
});
|
||||
});
|
||||
|
||||
void test('targets a submitted survey projection to its own general', () => {
|
||||
const changes = {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
frontStatusActorIds: [7],
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
resolveDashboardRefreshPlan(changes, { generalId: 7, cityId: 3, nationId: 2 }).frontStatus,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
resolveDashboardRefreshPlan(changes, { generalId: 8, cityId: 3, nationId: 2 }).frontStatus,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
void test('merges burst payloads without losing entity ids and starts at most once per interval', async () => {
|
||||
let nowMs = 0;
|
||||
let nextTimerId = 1;
|
||||
|
||||
Reference in New Issue
Block a user