fix(realtime): redact browser event details

This commit is contained in:
2026-08-13 00:19:24 +00:00
parent de804a6c2a
commit 08ea4d96bb
8 changed files with 570 additions and 327 deletions
@@ -1,85 +1,29 @@
import {
createEmptyRealtimeReadModelChanges,
mergeRealtimeReadModelChanges,
createEmptyRealtimeReadModelInvalidation,
mergeRealtimeReadModelInvalidations,
resolveRealtimeReadModelInvalidation,
type RealtimeReadModelChanges,
type RealtimeReadModelInvalidation,
type RealtimeViewerIdentity,
} from '@sammo-ts/common';
export interface DashboardReadModelIdentity {
generalId: number | null;
cityId: number | null;
nationId: number | null;
}
export interface DashboardRefreshPlan {
context: boolean;
lobby: boolean;
map: boolean;
commands: boolean;
contacts: boolean;
boardAccess: boolean;
reservedTurns: boolean;
records: boolean;
frontStatus: boolean;
}
const contains = (ids: readonly number[], id: number | null): boolean => id !== null && ids.includes(id);
export type DashboardReadModelIdentity = RealtimeViewerIdentity;
export type DashboardRefreshPlan = RealtimeReadModelInvalidation;
export const resolveDashboardRefreshPlan = (
changes: RealtimeReadModelChanges,
identity: DashboardReadModelIdentity
): DashboardRefreshPlan => {
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 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 || lobbyChanged || ownLobbyGeneralChanged,
map: changes.worldChanged || mapEntitiesChanged || ownGeneralMapChanged,
commands: changes.worldChanged || commandEntitiesChanged || ownGeneralChanged,
contacts: changes.contactsChanged,
boardAccess: ownGeneralChanged || ownNationChanged,
reservedTurns: contains(changes.reservedGeneralIds, identity.generalId),
records:
changes.globalRecordsChanged ||
changes.worldHistoryChanged ||
contains(changes.recordGeneralIds, identity.generalId),
// lastTurnTime is intentionally excluded. This slice contains the
// nation notice/vote/presence model and only follows related changes.
frontStatus:
Boolean(changes.frontStatusChanged) ||
frontStatusGeneralChanged ||
ownFrontStatusNationChanged ||
ownFrontStatusActorChanged,
};
};
): DashboardRefreshPlan => resolveRealtimeReadModelInvalidation(changes, identity);
type TimerHandle = ReturnType<typeof setTimeout>;
export interface MergedReadModelRefreshQueue {
request(changes: RealtimeReadModelChanges): void;
request(invalidation: RealtimeReadModelInvalidation): void;
cancelPending(): void;
}
export const createMergedReadModelRefreshQueue = (
refresh: (changes: RealtimeReadModelChanges) => Promise<void>,
refresh: (invalidation: RealtimeReadModelInvalidation) => Promise<void>,
options: {
minIntervalMs?: number;
now?: () => number;
@@ -91,7 +35,7 @@ export const createMergedReadModelRefreshQueue = (
const now = options.now ?? Date.now;
const setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
let pending = createEmptyRealtimeReadModelChanges();
let pending = createEmptyRealtimeReadModelInvalidation();
let hasPending = false;
let running = false;
let timer: TimerHandle | null = null;
@@ -108,7 +52,7 @@ export const createMergedReadModelRefreshQueue = (
return;
}
const next = pending;
pending = createEmptyRealtimeReadModelChanges();
pending = createEmptyRealtimeReadModelInvalidation();
hasPending = false;
running = true;
lastStartedAt = now();
@@ -120,14 +64,14 @@ export const createMergedReadModelRefreshQueue = (
};
return {
request: (changes) => {
pending = hasPending ? mergeRealtimeReadModelChanges(pending, changes) : changes;
request: (invalidation) => {
pending = hasPending ? mergeRealtimeReadModelInvalidations(pending, invalidation) : invalidation;
hasPending = true;
schedule();
},
cancelPending: () => {
hasPending = false;
pending = createEmptyRealtimeReadModelChanges();
pending = createEmptyRealtimeReadModelInvalidation();
if (timer !== null) {
clearTimer(timer);
timer = null;