diff --git a/apps/server/src/workspace/WorkspaceEntries.test.ts b/apps/server/src/workspace/WorkspaceEntries.test.ts index 150ac2c69131a..260bd7ee4bf5c 100644 --- a/apps/server/src/workspace/WorkspaceEntries.test.ts +++ b/apps/server/src/workspace/WorkspaceEntries.test.ts @@ -1,5 +1,6 @@ // @effect-diagnostics nodeBuiltinImport:off import * as NodeServices from "@effect/platform-node/NodeServices"; +import { FileFinder } from "@ff-labs/fff-node"; import { it, afterEach, describe, expect } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -569,6 +570,53 @@ it.layer(layerTest, { excludeTestServices: true })("WorkspaceEntries", (it) => { expect(match.lineContent.slice(range.start, range.end)).toBe("wörld"); }), ); + + it.effect("keeps answering from the live index when a refresh outlasts the scan wait", () => + Effect.gen(function* () { + const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-refresh-" }); + yield* writeTextFile(cwd, "src/notes.ts", "const needle = 1;\n"); + let scansOutlastWait = false; + let finders = 0; + let rescans = 0; + const createFinder = FileFinder.create.bind(FileFinder); + vi.spyOn(FileFinder, "create").mockImplementation((options) => { + const created = createFinder(options); + if (created.ok) { + const finder = created.value; + finders += 1; + const scanFiles = finder.scanFiles.bind(finder); + finder.scanFiles = () => { + rescans += 1; + return scanFiles(); + }; + const waitForIndexReady = finder.waitForIndexReady.bind(finder); + finder.waitForIndexReady = (timeoutMs) => + scansOutlastWait + ? Promise.resolve({ ok: true, value: false }) + : waitForIndexReady(timeoutMs); + } + return created; + }); + const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; + const searchNeedle = workspaceEntries.searchContents({ + cwd, + query: "needle", + limit: 100, + caseSensitive: true, + wholeWord: false, + useRegex: false, + }); + yield* searchNeedle; + + scansOutlastWait = true; + yield* workspaceEntries.refresh(cwd); + const result = yield* searchNeedle; + + expect(result.matches.map((match) => match.path)).toEqual(["src/notes.ts"]); + // The second search ran on the rescanned live finder, not a rebuild. + expect({ finders, rescans }).toEqual({ finders: 1, rescans: 1 }); + }), + ); }); describe("browse", () => { diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index 3ae5d524611a4..1cf77eb51fc62 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -120,10 +120,7 @@ export class WorkspaceSearchIndex extends Context.Service< readonly searchContents: ( input: Omit, ) => Effect.Effect; - readonly refresh: () => Effect.Effect< - void, - WorkspaceSearchIndexRefreshFailed | WorkspaceSearchIndexScanTimedOut - >; + readonly refresh: () => Effect.Effect; } >()("t3/workspace/WorkspaceSearchIndex") {} @@ -428,6 +425,8 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( reason: result.error, }); } + // A rescan that outlasts the wait keeps running, and the finder answers + // from its previous index until it finishes, so the index stays usable. yield* waitForIndexReady( cwd, finder, @@ -437,7 +436,7 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( reason, cause, }), - ); + ).pipe(Effect.catchTags({ WorkspaceSearchIndexScanTimedOut: () => Effect.void })); }); const list: WorkspaceSearchIndex["Service"]["list"] = Effect.fn("WorkspaceSearchIndex.list")(