mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
Backfill task markers by thread reference
This commit is contained in:
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
collectAgentConversationMarkerReferenceIds,
|
||||
collectAuxEventIdsForDeletionBackfill,
|
||||
collectMessageIdsForAuxBackfill,
|
||||
mergeAuxEventsWithDeletionBackfill,
|
||||
@@ -59,6 +60,45 @@ test("returns empty for a window of only auxiliary events", () => {
|
||||
assert.deepEqual(collectMessageIdsForAuxBackfill(events), []);
|
||||
});
|
||||
|
||||
test("collects task marker references from loaded timeline replies", () => {
|
||||
const rootId = hex("1");
|
||||
const parentId = hex("2");
|
||||
const replyId = hex("3");
|
||||
const events = [
|
||||
event(replyId, 9, {
|
||||
tags: [
|
||||
["h", CHANNEL_ID],
|
||||
["e", rootId, "", "root"],
|
||||
["e", parentId, "", "reply"],
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(collectAgentConversationMarkerReferenceIds(events), [
|
||||
replyId,
|
||||
rootId,
|
||||
parentId,
|
||||
]);
|
||||
});
|
||||
|
||||
test("skips non-content events when collecting task marker references", () => {
|
||||
const messageId = hex("1");
|
||||
const markerId = hex("2");
|
||||
const events = [
|
||||
event(messageId, 9),
|
||||
event(markerId, 40004, {
|
||||
tags: [
|
||||
["h", CHANNEL_ID],
|
||||
["e", messageId, "", "root"],
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(collectAgentConversationMarkerReferenceIds(events), [
|
||||
messageId,
|
||||
]);
|
||||
});
|
||||
|
||||
test("collects reaction and edit ids for deletion-marker backfill", () => {
|
||||
const events = [
|
||||
event(hex("1"), 9),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getThreadReference } from "@/features/messages/lib/threading";
|
||||
import {
|
||||
channelMessagesKey,
|
||||
sortMessages,
|
||||
} from "@/features/messages/lib/messageQueryKeys";
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { buildChannelStructuralAuxFilter } from "@/shared/api/relayChannelFilters";
|
||||
import {
|
||||
buildChannelAgentConversationMarkerFilter,
|
||||
buildChannelStructuralAuxFilter,
|
||||
} from "@/shared/api/relayChannelFilters";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import {
|
||||
CHANNEL_TIMELINE_CONTENT_KINDS,
|
||||
@@ -17,6 +21,10 @@ const TIMELINE_CONTENT_KINDS: ReadonlySet<number> = new Set(
|
||||
CHANNEL_TIMELINE_CONTENT_KINDS,
|
||||
);
|
||||
|
||||
function isTimelineContentEvent(event: RelayEvent) {
|
||||
return TIMELINE_CONTENT_KINDS.has(event.kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the ids of the visible content messages from a freshly-fetched
|
||||
* history window. Auxiliary events (reactions, edits, deletions) are then
|
||||
@@ -26,9 +34,30 @@ const TIMELINE_CONTENT_KINDS: ReadonlySet<number> = new Set(
|
||||
export function collectMessageIdsForAuxBackfill(
|
||||
historyEvents: RelayEvent[],
|
||||
): string[] {
|
||||
return historyEvents
|
||||
.filter((event) => TIMELINE_CONTENT_KINDS.has(event.kind))
|
||||
.map((event) => event.id);
|
||||
return historyEvents.filter(isTimelineContentEvent).map((event) => event.id);
|
||||
}
|
||||
|
||||
export function collectAgentConversationMarkerReferenceIds(
|
||||
historyEvents: RelayEvent[],
|
||||
): string[] {
|
||||
const referenceIds = new Set<string>();
|
||||
|
||||
for (const event of historyEvents) {
|
||||
if (!isTimelineContentEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
referenceIds.add(event.id);
|
||||
const { parentId, rootId } = getThreadReference(event.tags);
|
||||
if (rootId) {
|
||||
referenceIds.add(rootId);
|
||||
}
|
||||
if (parentId) {
|
||||
referenceIds.add(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
return [...referenceIds];
|
||||
}
|
||||
|
||||
export function collectAuxEventIdsForDeletionBackfill(
|
||||
@@ -86,18 +115,31 @@ export async function backfillAuxForMessages(
|
||||
historyEvents: RelayEvent[],
|
||||
): Promise<void> {
|
||||
const messageIds = collectMessageIdsForAuxBackfill(historyEvents);
|
||||
if (messageIds.length === 0) {
|
||||
const markerReferenceIds =
|
||||
collectAgentConversationMarkerReferenceIds(historyEvents);
|
||||
if (messageIds.length === 0 && markerReferenceIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = channelMessagesKey(channelId);
|
||||
const cachedEvents = queryClient.getQueryData<RelayEvent[]>(cacheKey) ?? [];
|
||||
const auxEvents = await relayClient.fetchAuxEventsByReference(
|
||||
channelId,
|
||||
messageIds,
|
||||
buildChannelStructuralAuxFilter,
|
||||
);
|
||||
const [auxEvents, markerEvents] = await Promise.all([
|
||||
messageIds.length > 0
|
||||
? relayClient.fetchAuxEventsByReference(
|
||||
channelId,
|
||||
messageIds,
|
||||
buildChannelStructuralAuxFilter,
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
markerReferenceIds.length > 0
|
||||
? relayClient.fetchAuxEventsByReference(
|
||||
channelId,
|
||||
markerReferenceIds,
|
||||
buildChannelAgentConversationMarkerFilter,
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
const mergedAuxEvents = await mergeAuxEventsWithDeletionBackfill({
|
||||
channelId,
|
||||
cachedEvents,
|
||||
@@ -105,16 +147,17 @@ export async function backfillAuxForMessages(
|
||||
fetchAuxEventsForMessages: (...args) =>
|
||||
relayClient.fetchAuxDeletionEventsForAuxEvents(...args),
|
||||
});
|
||||
if (mergedAuxEvents.length === 0) {
|
||||
const eventsToMerge = [...markerEvents, ...mergedAuxEvents];
|
||||
if (eventsToMerge.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryClient.setQueryData<RelayEvent[]>(cacheKey, (current = []) =>
|
||||
sortMessages([...current, ...mergedAuxEvents]),
|
||||
sortMessages([...current, ...eventsToMerge]),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to backfill auxiliary events for channel",
|
||||
"Failed to backfill timeline reference events for channel",
|
||||
channelId,
|
||||
error,
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildChannelAgentConversationMarkerFilter,
|
||||
buildChannelAuxDeletionFilter,
|
||||
buildChannelAuxFilter,
|
||||
buildChannelReactionAuxFilter,
|
||||
@@ -43,3 +44,10 @@ test("buildChannelStructuralAuxFilter excludes reactions", () => {
|
||||
assert.deepEqual(filter["#e"], IDS);
|
||||
assert.equal("#h" in filter, false);
|
||||
});
|
||||
|
||||
test("buildChannelAgentConversationMarkerFilter scopes task markers by channel and references", () => {
|
||||
const filter = buildChannelAgentConversationMarkerFilter(CHANNEL, IDS);
|
||||
assert.deepEqual(filter.kinds, [40004, 40010]);
|
||||
assert.deepEqual(filter["#h"], [CHANNEL]);
|
||||
assert.deepEqual(filter["#e"], IDS);
|
||||
});
|
||||
|
||||
@@ -97,6 +97,18 @@ export function buildChannelStructuralAuxFilter(
|
||||
]);
|
||||
}
|
||||
|
||||
export function buildChannelAgentConversationMarkerFilter(
|
||||
channelId: string,
|
||||
referencedEventIds: string[],
|
||||
): RelaySubscriptionFilter {
|
||||
return {
|
||||
kinds: [...CHANNEL_TIMELINE_STATE_KINDS],
|
||||
"#h": [channelId],
|
||||
"#e": referencedEventIds,
|
||||
limit: MAX_HISTORICAL_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactions-only filter for the message rows the GUI is currently rendering.
|
||||
* Keep this separate from structural aux backfill so the slow kind:5 deletion
|
||||
|
||||
Reference in New Issue
Block a user