mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
perf(ready): stop sending read state twice per session (#2223)
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user