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

pull/19453/merge
Kit Langton 2026-04-01 12:13:13 -04:00 committed by GitHub
parent a9c85b7c27
commit 2f405daa98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 86 additions and 89 deletions

View File

@ -612,7 +612,7 @@
}, },
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@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", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@ -636,7 +636,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "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", "fuzzysort": "3.1.0",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@ -995,9 +995,9 @@
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.42", "", { "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/platform-node@4.0.0-beta.43", "", { "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/platform-node-shared@4.0.0-beta.42", "", { "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/platform-node-shared@4.0.0-beta.43", "", { "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/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "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": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.42", "", { "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": ["effect@4.0.0-beta.43", "", { "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": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],

View File

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

View File

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

View File

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

View File

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

View File

@ -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, Service,
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service const config = yield* Config.Service
const auth = yield* Auth.Service const auth = yield* Auth.Service
const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(() => const state = yield* InstanceState.make<State>(() =>
Effect.gen(function* () { 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) { for (const plugin of plugins) {
if (!plugin.auth) continue if (!plugin.auth) continue
const providerID = ProviderID.make(plugin.auth.provider) 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) const { runPromise } = makeRuntime(Service, defaultLayer)