Просмотр исходного кода

fix(server): emit fixed workspace fence headers (#26443)

Kit Langton 4 месяцев назад
Родитель
Сommit
8e9550d90d

+ 20 - 0
packages/opencode/src/server/routes/instance/httpapi/middleware/fence.ts

@@ -0,0 +1,20 @@
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Effect } from "effect"
+import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
+import * as Fence from "@/server/shared/fence"
+
+const ignoredMethods = new Set(["GET", "HEAD", "OPTIONS"])
+
+export const fenceLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
+  Effect.gen(function* () {
+    const request = yield* HttpServerRequest.HttpServerRequest
+    if (!Flag.OPENCODE_WORKSPACE_ID || ignoredMethods.has(request.method)) return yield* effect
+
+    const previous = Fence.load()
+    const response = yield* effect
+    const current = Fence.diff(previous, Fence.load())
+    if (Object.keys(current).length === 0) return response
+
+    return HttpServerResponse.setHeader(response, Fence.HEADER, JSON.stringify(current))
+  }),
+).layer

+ 2 - 0
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -82,6 +82,7 @@ import { disposeMiddleware } from "./lifecycle"
 import { memoMap } from "@opencode-ai/core/effect/memo-map"
 import * as ServerBackend from "@/server/backend"
 import { errorLayer } from "./middleware/error"
+import { fenceLayer } from "./middleware/fence"
 
 export const context = Context.makeUnsafe<unknown>(new Map())
 
@@ -173,6 +174,7 @@ export function createRoutes(corsOptions?: CorsOptions) {
   return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, docRoute, uiRoute).pipe(
     Layer.provide([
       errorLayer,
+      fenceLayer,
       cors(corsOptions),
       runtime,
       Account.defaultLayer,

+ 25 - 0
packages/opencode/test/server/httpapi-instance.test.ts

@@ -4,8 +4,11 @@ import { describe, expect } from "bun:test"
 import { Config, Effect, FileSystem, Layer, Path } from "effect"
 import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
 import * as Socket from "effect/unstable/socket/Socket"
+import { WorkspaceID } from "../../src/control-plane/schema"
 import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
+import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
 import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
+import { HEADER as FenceHeader } from "../../src/server/shared/fence"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
@@ -65,6 +68,28 @@ describe("instance HttpApi", () => {
     }),
   )
 
+  it.live("emits a sync fence header for fixed-workspace mutations", () =>
+    Effect.gen(function* () {
+      const originalWorkspaceID = Flag.OPENCODE_WORKSPACE_ID
+      Flag.OPENCODE_WORKSPACE_ID = WorkspaceID.ascending()
+      yield* Effect.addFinalizer(() =>
+        Effect.sync(() => {
+          Flag.OPENCODE_WORKSPACE_ID = originalWorkspaceID
+        }),
+      )
+
+      const dir = yield* tmpdirScoped({ git: true })
+      const response = yield* HttpClientRequest.post(SessionPaths.create).pipe(
+        directoryHeader(dir),
+        HttpClientRequest.bodyJson({ title: "fenced" }),
+        Effect.flatMap(HttpClient.execute),
+      )
+
+      expect(response.status).toBe(200)
+      expect(JSON.parse(response.headers[FenceHeader] ?? "{}")).not.toEqual({})
+    }),
+  )
+
   it.live("serves path and VCS read endpoints", () =>
     Effect.gen(function* () {
       const dir = yield* tmpdirScoped({ git: true })