| author | |
| committer | |
| log | 2fbb0565d7976cbb338f5593a566ff53c6165419 |
| tree | b5d6965f36db07e44e46e17043dca6d164e6eb74 |
| parent | a3dadc04bf38b49bc8476a8e6c59bda7dc81d2e4 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 65 insertions(+), 10 deletions(-)
apps/server/src/workspace/WorkspaceEntries.ts+18-9| ... | ... | @@ -151,20 +151,29 @@ export const make = Effect.gen(function* () { |
| 151 | 151 | if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, normalizedCwd))) { |
| 152 | 152 | return; |
| 153 | 153 | } |
| 154 | const recoverRefreshFailure = ( | |
| 155 | cause: | |
| 156 | | WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed | |
| 157 | | WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut | |
| 158 | | WorkspaceSearchIndex.WorkspaceSearchIndexRefreshFailed, | |
| 159 | ) => | |
| 160 | Effect.gen(function* () { | |
| 161 | yield* Effect.logWarning("Failed to refresh workspace search index", { | |
| 162 | cwd, | |
| 163 | cause, | |
| 164 | }); | |
| 165 | yield* workspaceSearchIndexes.invalidate(normalizedCwd); | |
| 166 | }); | |
| 154 | 167 | yield* Effect.gen(function* () { |
| 155 | 168 | const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; |
| 156 | 169 | yield* searchIndex.refresh(); |
| 157 | 170 | }).pipe( |
| 158 | 171 | Effect.provide(workspaceSearchIndexes.get(normalizedCwd)), |
| 159 | Effect.catch((cause) => | |
| 160 | Effect.gen(function* () { | |
| 161 | yield* Effect.logWarning("Failed to refresh workspace search index", { | |
| 162 | cwd, | |
| 163 | cause, | |
| 164 | }); | |
| 165 | yield* workspaceSearchIndexes.invalidate(normalizedCwd); | |
| 166 | }), | |
| 167 | ), | |
| 172 | Effect.catchTags({ | |
| 173 | WorkspaceSearchIndexCreateFailed: recoverRefreshFailure, | |
| 174 | WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure, | |
| 175 | WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure, | |
| 176 | }), | |
| 168 | 177 | ); |
| 169 | 178 | }, |
| 170 | 179 | ); |
apps/server/src/workspace/WorkspaceSearchIndex.test.ts+31| ... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 | import { FileFinder } from "@ff-labs/fff-node"; |
| 2 | 2 | import { afterEach, expect, it } from "@effect/vitest"; |
| 3 | import * as Cause from "effect/Cause"; | |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Exit from "effect/Exit"; | |
| 4 | 6 | import { vi } from "vite-plus/test"; |
| 5 | 7 | |
| 6 | 8 | import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; |
| ... | ... | @@ -49,6 +51,35 @@ it.effect("keeps returned FileFinder creation diagnostics out of the cause chain |
| 49 | 51 | }), |
| 50 | 52 | ); |
| 51 | 53 | |
| 54 | it.effect("preserves FileFinder destroy failures as structured defects", () => | |
| 55 | Effect.gen(function* () { | |
| 56 | const cause = new Error("native destroy failed"); | |
| 57 | const finder = { | |
| 58 | destroy: vi.fn(() => { | |
| 59 | throw cause; | |
| 60 | }), | |
| 61 | isScanning: vi.fn(() => false), | |
| 62 | } as unknown as FileFinder; | |
| 63 | vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); | |
| 64 | ||
| 65 | const exit = yield* Effect.scoped(WorkspaceSearchIndex.make("/workspace/project")).pipe( | |
| 66 | Effect.exit, | |
| 67 | ); | |
| 68 | ||
| 69 | expect(Exit.isFailure(exit)).toBe(true); | |
| 70 | if (Exit.isFailure(exit)) { | |
| 71 | expect(Cause.hasDies(exit.cause)).toBe(true); | |
| 72 | const error = Cause.squash(exit.cause); | |
| 73 | expect(error).toBeInstanceOf(WorkspaceSearchIndex.WorkspaceSearchIndexDestroyFailed); | |
| 74 | expect(error).toMatchObject({ | |
| 75 | _tag: "WorkspaceSearchIndexDestroyFailed", | |
| 76 | cwd: "/workspace/project", | |
| 77 | cause, | |
| 78 | }); | |
| 79 | } | |
| 80 | }), | |
| 81 | ); | |
| 82 | ||
| 52 | 83 | it.effect("preserves search and refresh failures with operation context", () => |
| 53 | 84 | Effect.scoped( |
| 54 | 85 | Effect.gen(function* () { |
apps/server/src/workspace/WorkspaceSearchIndex.ts+16-1| ... | ... | @@ -71,6 +71,18 @@ export class WorkspaceSearchIndexRefreshFailed extends Schema.TaggedErrorClass<W |
| 71 | 71 | } |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | export class WorkspaceSearchIndexDestroyFailed extends Schema.TaggedErrorClass<WorkspaceSearchIndexDestroyFailed>()( | |
| 75 | "WorkspaceSearchIndexDestroyFailed", | |
| 76 | { | |
| 77 | cwd: Schema.String, | |
| 78 | cause: Schema.Defect(), | |
| 79 | }, | |
| 80 | ) { | |
| 81 | override get message(): string { | |
| 82 | return `Failed to destroy the workspace search index for '${this.cwd}'.`; | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 74 | 86 | export type WorkspaceSearchIndexError = |
| 75 | 87 | | WorkspaceSearchIndexCreateFailed |
| 76 | 88 | | WorkspaceSearchIndexScanTimedOut |
| ... | ... | @@ -201,7 +213,10 @@ const waitForScan = <E>(cwd: string, finder: FileFinder, onFailure: (cause: unkn |
| 201 | 213 | |
| 202 | 214 | export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: string) { |
| 203 | 215 | const finder = yield* Effect.acquireRelease(createFinder(cwd), (finder) => |
| 204 | Effect.sync(() => finder.destroy()), | |
| 216 | Effect.try({ | |
| 217 | try: () => finder.destroy(), | |
| 218 | catch: (cause) => new WorkspaceSearchIndexDestroyFailed({ cwd, cause }), | |
| 219 | }).pipe(Effect.orDie), | |
| 205 | 220 | ); |
| 206 | 221 | yield* waitForScan( |
| 207 | 222 | cwd, |