Files
DrSmoothl 299fa85a74 fix(onebot): match group invitations to the correct request
Report distinct invitations separately and approve only the request that belongs to the account and inviter. Reject ambiguous approvals and resolve the sender of invitations from outside the contact list.

Refs #612
2026-10-31 18:28:05 +08:00

200 lines
6.3 KiB
TypeScript

// Receive-chain micro-benchmark (RoadMap #5).
//
// Measured paths:
// 1. `parseMsgPush` — pure decode + decoder dispatch (no SQLite, no
// event emit). The protobuf workload only.
// 2. `IncomingPacketPipeline.process` — full receive pipeline:
// decode + decoder + handleSideEffects (identity-service SQLite
// writes) + event emit through BridgeEventBus.
//
// Run via: pnpm -F @snowluma/protocol bench
//
// Output is `tinybench` ops/sec — vitest's built-in `bench()` is just a
// thin wrapper. Run before AND after a perf commit; the delta is what
// matters (RoadMap #5 commit messages quote the % shift).
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { bench, describe } from 'vitest';
import { protobuf_encode } from '@snowluma/proton';
import type { PacketInfo } from '@snowluma/common/protocol-types';
import type { PushMsg } from '@snowluma/proto-defs/message';
import type { GroupChange } from '@snowluma/proto-defs/notify';
import { parseMsgPush, MSG_PUSH_CMD } from '../src/msg-push/index';
import { IdentityService } from '../src/identity-service';
import { BridgeEventBus } from '../src/event-bus';
import { GroupInviteTracker } from '../src/group-invite-tracker';
import { IncomingPacketPipeline } from '../src/packet-pipeline';
const SELF_UIN = '10001';
const GROUP_ID = 123456789;
const SENDER_UIN = 22222;
const SENDER_UID = 'u_sender';
// ─── Synthetic packet builders ───
function makeGroupTextPacket(): PacketInfo {
const body = protobuf_encode<PushMsg>({
message: {
responseHead: { fromUin: SENDER_UIN, fromUid: SENDER_UID },
contentHead: {
msgType: 82,
subType: 1,
sequence: 1234,
timestamp: 1710000000,
msgId: 999999,
},
body: {
richText: {
elems: [{ text: { str: 'hello world from bench' } }],
},
},
},
status: 0,
grpInfo: { groupUin: GROUP_ID },
} as any);
return {
pid: 1,
uin: SELF_UIN,
serviceCmd: MSG_PUSH_CMD,
seqId: 1,
retCode: 0,
fromClient: false,
body,
};
}
function makeGroupMixedPacket(): PacketInfo {
const body = protobuf_encode<PushMsg>({
message: {
responseHead: { fromUin: SENDER_UIN, fromUid: SENDER_UID },
contentHead: {
msgType: 82,
subType: 1,
sequence: 1235,
timestamp: 1710000000,
msgId: 999998,
},
body: {
richText: {
elems: [
{ text: { str: '@some_user ' } },
{ face: { index: 76 } },
{ text: { str: 'hello ' } },
{ srcMsg: { origSeqs: [4242] } },
{ text: { str: ' world' } },
],
},
},
},
status: 0,
grpInfo: { groupUin: GROUP_ID },
} as any);
return {
pid: 1,
uin: SELF_UIN,
serviceCmd: MSG_PUSH_CMD,
seqId: 2,
retCode: 0,
fromClient: false,
body,
};
}
function makeGroupJoinPacket(): PacketInfo {
const content = protobuf_encode<GroupChange>({
groupUin: GROUP_ID,
memberUid: 'u_newcomer',
});
const body = protobuf_encode<PushMsg>({
message: {
responseHead: { fromUin: GROUP_ID },
contentHead: { msgType: 33, timestamp: 1710000000 },
body: { msgContent: content },
},
status: 0,
} as any);
return {
pid: 1,
uin: SELF_UIN,
serviceCmd: MSG_PUSH_CMD,
seqId: 3,
retCode: 0,
fromClient: false,
body,
};
}
// ─── Setup helpers ───
// In-memory identity (no SQLite writes) — used for the "pure decode"
// bench to isolate proton + decoder cost from DB cost.
function makeIdentity(): IdentityService {
return IdentityService.memory(SELF_UIN);
}
// SQLite-backed identity in a temp file — used for the full-pipeline
// bench so we capture per-event identity writes. Temp dir is cleaned
// implicitly on process exit (OS tmp policy); we don't need to unlink
// because each `bench()` recreates it.
function makeIdentityWithDb(): IdentityService {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'snowluma-bench-recv-'));
return new IdentityService(SELF_UIN, path.join(dir, 'identity.db'));
}
function makePipeline(identity: IdentityService): IncomingPacketPipeline {
const events = new BridgeEventBus();
events.on('group_message', () => {
/* drain */
});
events.on('group_member_join', () => {
/* drain */
});
const pipeline = new IncomingPacketPipeline({
identity,
events,
fetchGroupList: async () => {},
fetchGroupMemberList: async () => {},
resolveGroupJoinRequest: async () => null,
groupInvites: new GroupInviteTracker(),
});
pipeline.registerCmd(MSG_PUSH_CMD, parseMsgPush);
return pipeline;
}
// ─── Benches ───
const groupText = makeGroupTextPacket();
const groupMixed = makeGroupMixedPacket();
const groupJoin = makeGroupJoinPacket();
describe('parseMsgPush — pure decode + dispatch (no SQLite, no emit)', () => {
const identityPure = makeIdentity();
bench('group_message (text only)', () => {
parseMsgPush(groupText, identityPure);
});
bench('group_message (5 mixed elements)', () => {
parseMsgPush(groupMixed, identityPure);
});
bench('group_member_join', () => {
parseMsgPush(groupJoin, identityPure);
});
});
describe('IncomingPacketPipeline.process — full pipeline (with SQLite identity writes)', () => {
const pipelineText = makePipeline(makeIdentityWithDb());
const pipelineMixed = makePipeline(makeIdentityWithDb());
const pipelineJoin = makePipeline(makeIdentityWithDb());
bench('group_message (text only)', () => {
pipelineText.process(groupText);
});
bench('group_message (5 mixed elements)', () => {
pipelineMixed.process(groupMixed);
});
bench('group_member_join', () => {
pipelineJoin.process(groupJoin);
});
});