fix(projects): scope revision reads and advance heads

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-08-31 22:05:02 -04:00
parent d88016b3a8
commit ef90d63172
6 changed files with 78 additions and 13 deletions

View File

@@ -82,13 +82,13 @@ test("buildProjectHomeFromFetcher scopes startup lookup to the active channel",
extraFilter: { "#buzz-channel": [channelId] },
});
assert.deepEqual(calls[1], {
kinds: [47001],
extraFilter: undefined,
});
assert.deepEqual(calls[2], {
kinds: [30617],
extraFilter: { "#buzz-channel": [channelId] },
});
assert.deepEqual(calls[2], {
kinds: [47001],
extraFilter: { "#a": [`30621:${owner}:relay`] },
});
assert.deepEqual(calls[3], {
kinds: [5],
extraFilter: { "#a": [`30621:${owner}:relay`, repositoryAddress] },
@@ -255,6 +255,31 @@ test("buildProjectsFromFetcher scopes the tombstone fetch to announcement coordi
});
});
test("buildProjectsFromFetcher scopes revisions to announced Project coordinates", async () => {
const OWNER = "a".repeat(64);
const projectEvent = {
id: "p".repeat(64),
kind: 30621,
pubkey: OWNER,
created_at: 200,
content: "",
tags: [["d", "proj"]],
};
const revisionCalls = [];
const fetchExhaustively = async (kinds, extraFilter) => {
if (kinds.includes(30621)) return [projectEvent];
if (kinds.includes(47001)) {
revisionCalls.push(extraFilter);
return [];
}
return [];
};
await buildProjectsFromFetcher(fetchExhaustively);
assert.deepEqual(revisionCalls, [{ "#a": [`30621:${OWNER}:proj`] }]);
});
test("buildProjectsFromFetcher skips the tombstone fetch when there are no announcements", async () => {
let deletionQueried = false;
const fetchExhaustively = async (kinds) => {

View File

@@ -14,7 +14,7 @@ const PROJECT_ENUMERATION_PAGE_SIZE = 500;
// Relays commonly cap filter tag-value lists; chunk `#a` scoping well below
// any such cap.
const TOMBSTONE_COORDINATE_CHUNK_SIZE = 100;
const PROJECT_COORDINATE_CHUNK_SIZE = 100;
/** Additional server-side scoping merged into every enumeration page. */
export type ProjectEventExtraFilter = {
@@ -149,10 +149,10 @@ async function fetchScopedDeletionEvents(
for (
let index = 0;
index < coordinates.length;
index += TOMBSTONE_COORDINATE_CHUNK_SIZE
index += PROJECT_COORDINATE_CHUNK_SIZE
) {
chunks.push(
coordinates.slice(index, index + TOMBSTONE_COORDINATE_CHUNK_SIZE),
coordinates.slice(index, index + PROJECT_COORDINATE_CHUNK_SIZE),
);
}
@@ -162,6 +162,36 @@ async function fetchScopedDeletionEvents(
return pages.flat();
}
async function fetchScopedProjectRevisionEvents(
fetchExhaustively: FetchProjectEventsExhaustively,
projectEvents: RelayEvent[],
): Promise<RelayEvent[]> {
const coordinates = [
...new Set(
projectEvents.flatMap((event) => {
const coordinate = eventCoordinate(event);
return coordinate ? [coordinate] : [];
}),
),
];
const chunks: string[][] = [];
for (
let index = 0;
index < coordinates.length;
index += PROJECT_COORDINATE_CHUNK_SIZE
) {
chunks.push(
coordinates.slice(index, index + PROJECT_COORDINATE_CHUNK_SIZE),
);
}
const pages = await Promise.all(
chunks.map((chunk) =>
fetchExhaustively([KIND_PROJECT_REVISION], { "#a": chunk }),
),
);
return pages.flat();
}
/**
* Core fetch-and-build logic for `fetchProjects`, extracted for testability.
*
@@ -180,12 +210,14 @@ export async function buildProjectsFromFetcher(
viewerPubkey?: string | null;
} = {},
): Promise<Project[]> {
const [projectEvents, projectRevisionEvents, repositoryEvents] =
await Promise.all([
fetchExhaustively([KIND_PROJECT_ANNOUNCEMENT]),
fetchExhaustively([KIND_PROJECT_REVISION]),
fetchExhaustively([KIND_REPO_ANNOUNCEMENT]),
]);
const [projectEvents, repositoryEvents] = await Promise.all([
fetchExhaustively([KIND_PROJECT_ANNOUNCEMENT]),
fetchExhaustively([KIND_REPO_ANNOUNCEMENT]),
]);
const projectRevisionEvents = await fetchScopedProjectRevisionEvents(
fetchExhaustively,
projectEvents,
);
// Tombstones are fetched second (not in parallel) because the `#a` scoping
// needs the announcement coordinates; both announcement kinds are small,

View File

@@ -311,11 +311,13 @@ test("addRepositoryToProject promotes a legacy repository route to a project coo
legacyProject,
attachedRepository,
300,
"f".repeat(64),
);
assert.equal(updated.id, `30621:${PROJECT_OWNER}:sprout`);
assert.equal(updated.legacy, false);
assert.equal(updated.repositories.length, 2);
assert.equal(updated.effectiveRevisionId, "f".repeat(64));
});
test("selectProjectRepository honors a request and falls back to primary", () => {

View File

@@ -629,6 +629,7 @@ export function addRepositoryToProject(
project: Project,
repository: Repository,
createdAt: number,
effectiveRevisionId = project.effectiveRevisionId,
): Project {
const projectAddress = `${KIND_PROJECT_ANNOUNCEMENT}:${project.owner}:${project.dtag}`;
const repositoryAddresses = [
@@ -645,6 +646,7 @@ export function addRepositoryToProject(
...project,
id: projectAddress,
createdAt,
effectiveRevisionId,
legacy: false,
projectAddress,
primaryRepositoryAddress:

View File

@@ -186,6 +186,7 @@ export async function addProjectRepository(
project,
repository,
projectEvent.created_at,
projectEvent.id.toLowerCase(),
),
repository,
};
@@ -306,6 +307,7 @@ export async function addProjectRepository(
project,
repository,
projectEvent.created_at,
projectEvent.id.toLowerCase(),
),
repository,
};

View File

@@ -80,6 +80,7 @@ async function attachProjectRepository({
project,
repository,
projectEvent.created_at,
projectEvent.id.toLowerCase(),
),
repository,
};
@@ -101,6 +102,7 @@ async function attachProjectRepository({
project,
repository,
projectEvent.created_at,
projectEvent.id.toLowerCase(),
),
repository,
};