perf(ready): stop sending read state twice per session (#2223)

This commit is contained in:
Hampus
2026-08-31 01:04:43 +02:00
committed by GitHub
parent 50a17b6263
commit 21b1e4e719
9 changed files with 100 additions and 57 deletions
+1 -5
View File
@@ -32708,13 +32708,9 @@
"type": "array",
"items": {"$ref": "#/components/schemas/ReadStateResponse"},
"description": "Authoritative read states after applying the acknowledgement"
},
"read_state_proto": {
"type": "string",
"description": "Authoritative read states after applying the acknowledgement, encoded as a base64 protobuf bundle"
}
},
"required": ["read_states", "read_state_proto"]
"required": ["read_states"]
},
"ReadStateResponse": {
"type": "object",
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
import {createTestAccount} from '../auth/tests/AuthTestUtils';
import {createChannel, createGuild} from '../guild/tests/GuildTestUtils';
import {sendMessage} from '../message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '../test/ApiTestHarness';
import {HTTP_STATUS} from '../test/TestConstants';
import {createBuilder} from '../test/TestRequestBuilder';
interface AckResponse {
read_states: Array<{
id: string;
mention_count: number;
last_message_id: string | null;
version: string;
}>;
}
describe('POST /read-states/ack response payload', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
test('returns the authoritative read states without a protobuf bundle', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Read State Guild');
const channel = await createChannel(harness, account.token, guild.id, 'read-state-channel');
const message = await sendMessage(harness, account.token, channel.id, 'hello');
const response = await createBuilder<AckResponse>(harness, account.token)
.post('/read-states/ack')
.body({read_states: [{channel_id: channel.id, message_id: message.id, manual: true, mention_count: 0}]})
.expect(HTTP_STATUS.OK)
.execute();
expect(response.read_states).toHaveLength(1);
expect(response.read_states[0]?.id).toBe(channel.id);
expect(response.read_states[0]?.last_message_id).toBe(message.id);
expect(typeof response.read_states[0]?.version).toBe('string');
expect(Object.hasOwn(response, 'read_state_proto')).toBe(false);
});
});
@@ -7,7 +7,7 @@ import type {
} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
import type {UserID} from '../BrandedTypes';
import {createChannelID, createMessageID} from '../BrandedTypes';
import {encodeReadStatesResponseProto, mapReadStateResponse} from './ReadStateResponseMapper';
import {mapReadStateResponse} from './ReadStateResponseMapper';
import type {ReadStateService} from './ReadStateService';
interface ReadStateAckBulkParams {
@@ -45,7 +45,6 @@ export class ReadStateRequestService {
});
return {
read_states: readStates.map(mapReadStateResponse),
read_state_proto: encodeReadStatesResponseProto(readStates),
};
}
}
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ReadStateResponse} from '@fluxer/schema/src/domains/gateway/GatewaySchemas';
import {encodeReadStateProtoNative} from '@fluxer/schema/src/domains/read_state/ReadStateProtoCodec';
import type {ReadState} from '../models/ReadState';
export function mapReadStateResponse(readState: ReadState): ReadStateResponse {
@@ -13,15 +12,3 @@ export function mapReadStateResponse(readState: ReadState): ReadStateResponse {
version: readState.version.toString(),
};
}
export function encodeReadStatesResponseProto(readStates: ReadonlyArray<ReadState>): string {
return encodeReadStateProtoNative(
readStates.map((readState) => ({
channelId: readState.channelId,
mentionCount: readState.mentionCount,
lastMessageId: readState.lastMessageId,
lastPinTimestamp: readState.lastPinTimestamp,
version: readState.version,
})),
);
}
+1 -4
View File
@@ -85,7 +85,7 @@ import {UserSettings} from '../models/UserSettings';
import type {WebAuthnCredential} from '../models/WebAuthnCredential';
import type {BotAuthService} from '../oauth/BotAuthService';
import {sendApnsPush} from '../push/ApnsPushService';
import {encodeReadStatesResponseProto, mapReadStateResponse} from '../read_state/ReadStateResponseMapper';
import {mapReadStateResponse} from '../read_state/ReadStateResponseMapper';
import type {ReadStateService} from '../read_state/ReadStateService';
import type {IUserRepository} from '../user/IUserRepository';
import {PaymentRepository} from '../user/repositories/PaymentRepository';
@@ -1169,9 +1169,6 @@ export class RpcService {
read_states: timeRpcStepSync(responseBuildSteps, 'map_read_states', () =>
userData.readStates.map(mapReadStateResponse),
),
read_state_proto: timeRpcStepSync(responseBuildSteps, 'encode_read_state_proto', () =>
encodeReadStatesResponseProto(userData.readStates),
),
guilds,
private_channels: privateChannels,
relationships,
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {createChannel, createGuild} from '../../guild/tests/GuildTestUtils';
import {sendMessage} from '../../message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {HTTP_STATUS} from '../../test/TestConstants';
import {createBuilder} from '../../test/TestRequestBuilder';
interface RpcSessionResponse {
type: 'session';
data: {
read_states: Array<{id: string; last_message_id: string | null}>;
};
}
describe('RpcService session read states', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
test('sends read states as JSON only', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Read State Guild');
const channel = await createChannel(harness, account.token, guild.id, 'read-state-channel');
const message = await sendMessage(harness, account.token, channel.id, 'hello');
await createBuilder(harness, account.token)
.post('/read-states/ack')
.body({read_states: [{channel_id: channel.id, message_id: message.id, manual: true, mention_count: 0}]})
.expect(HTTP_STATUS.OK)
.execute();
const response = await createBuilder<RpcSessionResponse>(harness, '')
.post('/test/rpc-session-init')
.body({type: 'session', token: account.token, version: 1, ip: '127.0.0.1'})
.expect(HTTP_STATUS.OK)
.execute();
expect(response.data.read_states.some((readState) => readState.id === channel.id)).toBe(true);
expect(Object.hasOwn(response.data, 'read_state_proto')).toBe(false);
});
});
@@ -249,9 +249,6 @@ export type ReadStateAckRequest = z.infer<typeof ReadStateAckRequest>;
export const ReadStateAckResponse = z.object({
read_states: z.array(ReadStateResponse).describe('Authoritative read states after applying the acknowledgement'),
read_state_proto: z
.string()
.describe('Authoritative read states after applying the acknowledgement, encoded as a base64 protobuf bundle'),
});
export type ReadStateAckResponse = z.infer<typeof ReadStateAckResponse>;
@@ -13,14 +13,6 @@ interface ReadStateProtoEntryInput {
version?: string | null;
}
interface ReadStateProtoNativeEntryInput {
channelId: bigint | string;
mentionCount?: number | null;
lastMessageId?: bigint | string | null;
lastPinTimestamp?: Date | string | null;
version?: bigint | string | null;
}
interface ReadStateProtoEntry {
id: string;
mention_count: number;
@@ -34,24 +26,12 @@ const MAX_UINT32 = 0xffffffff;
const MAX_UINT64 = 0xffffffffffffffffn;
export function encodeReadStateProto(readStates: ReadonlyArray<ReadStateProtoEntryInput>): string {
return encodeReadStateProtoNative(
readStates.map((readState) => ({
channelId: readState.id,
lastMessageId: readState.last_message_id,
mentionCount: readState.mention_count,
lastPinTimestamp: readState.last_pin_timestamp,
version: readState.version,
})),
);
}
export function encodeReadStateProtoNative(readStates: ReadonlyArray<ReadStateProtoNativeEntryInput>): string {
const bundle = create(ReadStateBundleSchema, {
readStates: readStates.map((readState) => ({
channelId: parseUint64(readState.channelId, 'id'),
lastMessageId: optionalUint64(readState.lastMessageId, 'last_message_id'),
mentionCount: normalizeMentionCount(readState.mentionCount),
lastPinTimestamp: optionalTimestamp(readState.lastPinTimestamp),
channelId: parseUint64(readState.id, 'id'),
lastMessageId: optionalUint64(readState.last_message_id, 'last_message_id'),
mentionCount: normalizeMentionCount(readState.mention_count),
lastPinTimestamp: optionalTimestamp(readState.last_pin_timestamp),
version: optionalUint64(readState.version, 'version'),
})),
});
@@ -101,13 +81,13 @@ class ReadStateProtoEncodeError extends Error {
}
}
function optionalUint64(value: bigint | string | null | undefined, field: string): bigint | undefined {
function optionalUint64(value: string | null | undefined, field: string): bigint | undefined {
return value == null ? undefined : parseUint64(value, field);
}
function parseUint64(value: bigint | string, field: string): bigint {
function parseUint64(value: string, field: string): bigint {
try {
const parsed = typeof value === 'bigint' ? value : BigInt(value);
const parsed = BigInt(value);
if (parsed < 0n || parsed > MAX_UINT64) {
throw new Error('out of uint64 range');
}
@@ -119,9 +99,9 @@ function parseUint64(value: bigint | string, field: string): bigint {
}
}
function optionalTimestamp(value: Date | string | null | undefined) {
function optionalTimestamp(value: string | null | undefined) {
if (value == null) return undefined;
const date = value instanceof Date ? value : new Date(value);
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new ReadStateProtoEncodeError('last_pin_timestamp: invalid timestamp');
}
@@ -288,7 +288,6 @@ export const RpcResponseSessionData = z.object({
user_guild_settings: z.array(UserGuildSettingsResponse).describe('Per-guild settings for the user'),
notes: z.record(SnowflakeStringType, z.string()).describe('User notes keyed by user ID'),
read_states: z.array(ReadStateResponse).describe('Read state for each channel'),
read_state_proto: z.string().describe('Read state for each channel, encoded as a base64 protobuf bundle'),
private_channels: z.array(ChannelResponse).describe('List of DM and group DM channels'),
relationships: z.array(RelationshipResponse).describe('User relationships (friends, blocked, etc.)'),
favorite_memes: z.array(FavoriteMemeResponse).describe('List of user favorite memes'),