Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function makeHarness(options: HarnessOptions = {}) {
snapshot: Effect.die("unused"),
getShell: () => Effect.die("unused"),
listShells: () => Effect.die("unused"),
listResolvedShells: () => Effect.die("unused"),
}),
Layer.mock(GitWorkflow.GitWorkflowService)({
createWorktree,
Expand Down
52 changes: 52 additions & 0 deletions apps/server/src/project/ProjectService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,3 +803,55 @@ it.effect("rejects an update that waited on the lock while its project was delet
);
}).pipe(Effect.provide(ProjectServiceDependenciesLayer)),
);

it.effect("lists resolved shells for changed roots without requesting enrichment", () =>
Effect.gen(function* () {
const enrichment = yield* ProjectEnrichmentService.ProjectEnrichmentService;
const enrichmentCalls: Array<string> = [];
const record = (method: string, workspaceRoot: string) =>
Effect.sync(() => enrichmentCalls.push(`${method} ${workspaceRoot}`));
const service = yield* ProjectService.make.pipe(
Effect.provideService(ProjectEnrichmentService.ProjectEnrichmentService, {
...enrichment,
peek: (root) => record("peek", root).pipe(Effect.andThen(enrichment.peek(root))),
request: (root) => record("request", root).pipe(Effect.andThen(enrichment.request(root))),
getAvailable: (root) =>
record("getAvailable", root).pipe(Effect.andThen(enrichment.getAvailable(root))),
}),
);
for (const name of ["a", "b", "c"]) {
yield* service.create({
commandId: CommandId.make(`command:project:resolved:${name}`),
projectId: ProjectId.make(`project:resolved-${name}`),
title: name,
workspaceRoot: `/work/resolved-${name}`,
});
}
enrichmentCalls.length = 0;

const identity = {
canonicalKey: "github.com/t3tools/resolved-a",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "git@github.com:t3tools/resolved-a.git",
},
rootPath: "/work/resolved-a",
};
const shells = yield* service.listResolvedShells(
new Map([
["/work/resolved-a", identity],
["/work/resolved-c", null],
]),
);

assert.deepEqual(
shells.map((shell) => [shell.workspaceRoot, shell.repositoryIdentity?.canonicalKey ?? null]),
[
["/work/resolved-a", "github.com/t3tools/resolved-a"],
["/work/resolved-c", null],
],
);
assert.deepEqual(enrichmentCalls, []);
}).pipe(Effect.provide(ProjectServiceDependenciesLayer)),
);
28 changes: 28 additions & 0 deletions apps/server/src/project/ProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ProjectCreatePayload,
type ProjectUpdatePayload,
type ProjectSnapshot,
type RepositoryIdentity,
type ThreadId,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
Expand Down Expand Up @@ -141,6 +142,15 @@ export class ProjectService extends Context.Service<
readonly listShells: (options?: {
readonly projectIds?: ReadonlyArray<ProjectId>;
}) => Effect.Effect<ReadonlyArray<OrchestrationProjectShell>, ProjectOperationError>;
/**
* Active shells for the workspace roots an enrichment refresh resolved,
* carrying those identities. Requests no enrichment: re-enriching here
* would re-request every expired root, and each resolution publishes
* another refresh.
*/
readonly listResolvedShells: (
identities: ReadonlyMap<string, RepositoryIdentity | null>,
) => Effect.Effect<ReadonlyArray<OrchestrationProjectShell>, ProjectOperationError>;
}
>()("t3/project/ProjectService") {}

Expand Down Expand Up @@ -539,6 +549,23 @@ export const make = Effect.gen(function* () {
return yield* Effect.forEach(shells, enrichShell, { concurrency: 16 });
});

const listResolvedShells: ProjectService["Service"]["listResolvedShells"] = Effect.fn(
"ProjectService.listResolvedShells",
)(function* (identities) {
const shells = yield* projects
.listShells()
.pipe(
Effect.mapError(
(cause) => new ProjectOperationError({ operation: "list-projects", cause }),
),
);
return shells.flatMap((shell) =>
identities.has(shell.workspaceRoot)
? [{ ...shell, repositoryIdentity: identities.get(shell.workspaceRoot) ?? null }]
: [],
);
});

const snapshot = Effect.gen(function* () {
const rows = yield* projects
.list()
Expand All @@ -564,6 +591,7 @@ export const make = Effect.gen(function* () {
snapshot,
getShell,
listShells,
listResolvedShells,
});
});

Expand Down
1 change: 1 addition & 0 deletions apps/server/src/relay/AgentAwarenessRelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ const makeTestRelay = Effect.fnUntraced(function* (
snapshot: Effect.succeed({ projects: [] } as never),
getShell: unused,
listShells: unused,
listResolvedShells: unused,
getById: () =>
Effect.succeed(
Option.some({
Expand Down
21 changes: 7 additions & 14 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,23 +948,16 @@ export const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subsc
const enrichmentRefreshes = Stream.fromSubscription(enrichmentChanges).pipe(
Stream.filter((change) => change.repositoryIdentityResolved),
Stream.groupedWithin(64, Duration.millis(25)),
// Build the refresh from the identities the changes carry. Re-enriching
// every project here re-requested each expired root, whose resolution
// published again, so one expiry kept every subscriber reloading every
// project's metadata once a minute.
Stream.mapEffect((changes) =>
Effect.gen(function* () {
const identities = new Map(
Array.from(changes, (change) => [
change.workspaceRoot,
change.enrichment.repositoryIdentity,
]),
);
const snapshotSequence = yield* applicationEvents.latestApplicationSequence;
const changedProjects = (yield* projects.listShells()).flatMap((project) =>
identities.has(project.workspaceRoot)
? [{ ...project, repositoryIdentity: identities.get(project.workspaceRoot) ?? null }]
: [],
const changedProjects = yield* projectService.listResolvedShells(
new Map(
Array.from(changes, (change) => [
change.workspaceRoot,
change.enrichment.repositoryIdentity,
]),
),
);
return shellStreamItemFromEnrichmentRefresh({
snapshot: {
Expand Down
Loading