Bläddra i källkod

refactor: use Effect services instead of async facades in provider, auth, and file (#20480)

Kit Langton 2 veckor sedan
förälder
incheckning
2f405daa98

+ 5 - 5
bun.lock

@@ -612,7 +612,7 @@
   },
   "catalog": {
     "@cloudflare/workers-types": "4.20251008.0",
-    "@effect/platform-node": "4.0.0-beta.42",
+    "@effect/platform-node": "4.0.0-beta.43",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@octokit/rest": "22.0.0",
@@ -636,7 +636,7 @@
     "dompurify": "3.3.1",
     "drizzle-kit": "1.0.0-beta.19-d95b7a4",
     "drizzle-orm": "1.0.0-beta.19-d95b7a4",
-    "effect": "4.0.0-beta.42",
+    "effect": "4.0.0-beta.43",
     "fuzzysort": "3.1.0",
     "hono": "4.10.7",
     "hono-openapi": "1.1.2",
@@ -995,9 +995,9 @@
 
     "@effect/language-service": ["@effect/[email protected]", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
 
-    "@effect/platform-node": ["@effect/[email protected]2", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.42", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42", "ioredis": "^5.7.0" } }, "sha512-kbdRML2FBa4q8U8rZQcnmLKZ5zN/z1bAA7t5D1/UsBHZqJgnfRgu1CP6kaEfb1Nie6YyaWshxTktZQryjvW/Yg=="],
+    "@effect/platform-node": ["@effect/[email protected]3", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="],
 
-    "@effect/platform-node-shared": ["@effect/[email protected]2", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42" } }, "sha512-PC+lxLsrwob3+nBChAPrQq32olCeyApgXBvs1NrRsoArLViNT76T/68CttuCAksCZj5e1bZ1ZibLPel3vUmx2g=="],
+    "@effect/platform-node-shared": ["@effect/[email protected]3", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="],
 
     "@electron/asar": ["@electron/[email protected]", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
 
@@ -2771,7 +2771,7 @@
 
     "ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
 
-    "effect": ["[email protected]2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-c1UrRP+tLzyHb4Fepl8XBDJlLQLkrcMXrRBba441GQRxMbeQ/aIOSFcBwSda1iMJ5l9F0lYc3Bhe33/whrmavQ=="],
+    "effect": ["[email protected]3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
 
     "ejs": ["[email protected]", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
 

+ 2 - 2
package.json

@@ -25,7 +25,7 @@
       "packages/slack"
     ],
     "catalog": {
-      "@effect/platform-node": "4.0.0-beta.42",
+      "@effect/platform-node": "4.0.0-beta.43",
       "@types/bun": "1.3.11",
       "@octokit/rest": "22.0.0",
       "@hono/zod-validator": "0.4.2",
@@ -45,7 +45,7 @@
       "dompurify": "3.3.1",
       "drizzle-kit": "1.0.0-beta.19-d95b7a4",
       "drizzle-orm": "1.0.0-beta.19-d95b7a4",
-      "effect": "4.0.0-beta.42",
+      "effect": "4.0.0-beta.43",
       "ai": "6.0.138",
       "hono": "4.10.7",
       "hono-openapi": "1.1.2",

+ 4 - 4
packages/opencode/src/effect/instance-state.ts

@@ -24,9 +24,9 @@ export namespace InstanceState {
     return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
   }
 
-  export const context = Effect.fnUntraced(function* () {
+  export const context = Effect.gen(function* () {
     return (yield* InstanceRef) ?? Instance.current
-  })()
+  })
 
   export const directory = Effect.map(context, (ctx) => ctx.directory)
 
@@ -37,9 +37,9 @@ export namespace InstanceState {
       const cache = yield* ScopedCache.make<string, A, E, R>({
         capacity: Number.POSITIVE_INFINITY,
         lookup: () =>
-          Effect.fnUntraced(function* () {
+          Effect.gen(function* () {
             return yield* init(yield* context)
-          })(),
+          }),
       })
 
       const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))

+ 52 - 59
packages/opencode/src/file/index.ts

@@ -5,7 +5,6 @@ import { AppFileSystem } from "@/filesystem"
 import { git } from "@/util/git"
 import { Effect, Layer, ServiceMap } from "effect"
 import { formatPatch, structuredPatch } from "diff"
-import fs from "fs"
 import fuzzysort from "fuzzysort"
 import ignore from "ignore"
 import path from "path"
@@ -359,49 +358,46 @@ export namespace File {
         const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global"
         const next: Entry = { files: [], dirs: [] }
 
-        yield* Effect.promise(async () => {
-          if (isGlobalHome) {
-            const dirs = new Set<string>()
-            const protectedNames = Protected.names()
-            const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
-            const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
-            const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
-            const top = await fs.promises
-              .readdir(Instance.directory, { withFileTypes: true })
-              .catch(() => [] as fs.Dirent[])
-
-            for (const entry of top) {
-              if (!entry.isDirectory()) continue
-              if (shouldIgnoreName(entry.name)) continue
-              dirs.add(entry.name + "/")
-
-              const base = path.join(Instance.directory, entry.name)
-              const children = await fs.promises.readdir(base, { withFileTypes: true }).catch(() => [] as fs.Dirent[])
-              for (const child of children) {
-                if (!child.isDirectory()) continue
-                if (shouldIgnoreNested(child.name)) continue
-                dirs.add(entry.name + "/" + child.name + "/")
-              }
+        if (isGlobalHome) {
+          const dirs = new Set<string>()
+          const protectedNames = Protected.names()
+          const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
+          const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
+          const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
+          const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => []))
+
+          for (const entry of top) {
+            if (entry.type !== "directory") continue
+            if (shouldIgnoreName(entry.name)) continue
+            dirs.add(entry.name + "/")
+
+            const base = path.join(Instance.directory, entry.name)
+            const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => []))
+            for (const child of children) {
+              if (child.type !== "directory") continue
+              if (shouldIgnoreNested(child.name)) continue
+              dirs.add(entry.name + "/" + child.name + "/")
             }
+          }
 
-            next.dirs = Array.from(dirs).toSorted()
-          } else {
-            const seen = new Set<string>()
-            for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
-              next.files.push(file)
-              let current = file
-              while (true) {
-                const dir = path.dirname(current)
-                if (dir === ".") break
-                if (dir === current) break
-                current = dir
-                if (seen.has(dir)) continue
-                seen.add(dir)
-                next.dirs.push(dir + "/")
-              }
+          next.dirs = Array.from(dirs).toSorted()
+        } else {
+          const files = yield* Effect.promise(() => Array.fromAsync(Ripgrep.files({ cwd: Instance.directory })))
+          const seen = new Set<string>()
+          for (const file of files) {
+            next.files.push(file)
+            let current = file
+            while (true) {
+              const dir = path.dirname(current)
+              if (dir === ".") break
+              if (dir === current) break
+              current = dir
+              if (seen.has(dir)) continue
+              seen.add(dir)
+              next.dirs.push(dir + "/")
             }
           }
-        })
+        }
 
         const s = yield* InstanceState.get(state)
         s.cache = next
@@ -636,30 +632,27 @@ export namespace File {
         yield* ensure()
         const { cache } = yield* InstanceState.get(state)
 
-        return yield* Effect.promise(async () => {
-          const query = input.query.trim()
-          const limit = input.limit ?? 100
-          const kind = input.type ?? (input.dirs === false ? "file" : "all")
-          log.info("search", { query, kind })
+        const query = input.query.trim()
+        const limit = input.limit ?? 100
+        const kind = input.type ?? (input.dirs === false ? "file" : "all")
+        log.info("search", { query, kind })
 
-          const result = cache
-          const preferHidden = query.startsWith(".") || query.includes("/.")
+        const preferHidden = query.startsWith(".") || query.includes("/.")
 
-          if (!query) {
-            if (kind === "file") return result.files.slice(0, limit)
-            return sortHiddenLast(result.dirs.toSorted(), preferHidden).slice(0, limit)
-          }
+        if (!query) {
+          if (kind === "file") return cache.files.slice(0, limit)
+          return sortHiddenLast(cache.dirs.toSorted(), preferHidden).slice(0, limit)
+        }
 
-          const items =
-            kind === "file" ? result.files : kind === "directory" ? result.dirs : [...result.files, ...result.dirs]
+        const items =
+          kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs]
 
-          const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
-          const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target)
-          const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted
+        const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
+        const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target)
+        const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted
 
-          log.info("search", { query, kind, results: output.length })
-          return output
-        })
+        log.info("search", { query, kind, results: output.length })
+        return output
       })
 
       log.info("init")

+ 17 - 16
packages/opencode/src/provider/auth.ts

@@ -111,26 +111,25 @@ export namespace ProviderAuth {
 
   export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ProviderAuth") {}
 
-  export const layer = Layer.effect(
+  export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
       const auth = yield* Auth.Service
+      const plugin = yield* Plugin.Service
       const state = yield* InstanceState.make<State>(
-        Effect.fn("ProviderAuth.state")(() =>
-          Effect.promise(async () => {
-            const plugins = await Plugin.list()
-            return {
-              hooks: Record.fromEntries(
-                Arr.filterMap(plugins, (x) =>
-                  x.auth?.provider !== undefined
-                    ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
-                    : Result.failVoid,
-                ),
+        Effect.fn("ProviderAuth.state")(function* () {
+          const plugins = yield* plugin.list()
+          return {
+            hooks: Record.fromEntries(
+              Arr.filterMap(plugins, (x) =>
+                x.auth?.provider !== undefined
+                  ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
+                  : Result.failVoid,
               ),
-              pending: new Map<ProviderID, AuthOAuthResult>(),
-            }
-          }),
-        ),
+            ),
+            pending: new Map<ProviderID, AuthOAuthResult>(),
+          }
+        }),
       )
 
       const methods = Effect.fn("ProviderAuth.methods")(function* () {
@@ -230,7 +229,9 @@ export namespace ProviderAuth {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(Auth.defaultLayer))
+  export const defaultLayer = Layer.suspend(() =>
+    layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
+  )
 
   const { runPromise } = makeRuntime(Service, defaultLayer)
 

+ 6 - 3
packages/opencode/src/provider/provider.ts

@@ -961,11 +961,12 @@ export namespace Provider {
     }
   }
 
-  const layer: Layer.Layer<Service, never, Config.Service | Auth.Service> = Layer.effect(
+  const layer: Layer.Layer<Service, never, Config.Service | Auth.Service | Plugin.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
       const config = yield* Config.Service
       const auth = yield* Auth.Service
+      const plugin = yield* Plugin.Service
 
       const state = yield* InstanceState.make<State>(() =>
         Effect.gen(function* () {
@@ -1128,7 +1129,7 @@ export namespace Provider {
             }
           }
 
-          const plugins = yield* Effect.promise(() => Plugin.list())
+          const plugins = yield* plugin.list()
           for (const plugin of plugins) {
             if (!plugin.auth) continue
             const providerID = ProviderID.make(plugin.auth.provider)
@@ -1541,7 +1542,9 @@ export namespace Provider {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer))
+  export const defaultLayer = Layer.suspend(() =>
+    layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
+  )
 
   const { runPromise } = makeRuntime(Service, defaultLayer)