Skip to content

Commit 91e36a5

Browse files
Apply PR #17544: refactor(instance): move scoped services to LayerMap
2 parents 7aeeead + 23ccb80 commit 91e36a5

File tree

15 files changed

+154
-497
lines changed

15 files changed

+154
-497
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const disposers = new Set<(directory: string) => Promise<void>>()
2+
3+
export function registerDisposer(disposer: (directory: string) => Promise<void>) {
4+
disposers.add(disposer)
5+
return () => {
6+
disposers.delete(disposer)
7+
}
8+
}
9+
10+
export async function disposeInstance(directory: string) {
11+
await Promise.allSettled([...disposers].map((disposer) => disposer(directory)))
12+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Effect, Layer, LayerMap, ServiceMap } from "effect"
2+
import { registerDisposer } from "./instance-registry"
3+
import { ProviderAuthService } from "@/provider/auth-service"
4+
import { QuestionService } from "@/question/service"
5+
import { PermissionService } from "@/permission/service"
6+
import { Instance } from "@/project/instance"
7+
import type { Project } from "@/project/project"
8+
9+
export declare namespace InstanceContext {
10+
export interface Shape {
11+
readonly directory: string
12+
readonly project: Project.Info
13+
}
14+
}
15+
16+
export class InstanceContext extends ServiceMap.Service<InstanceContext, InstanceContext.Shape>()(
17+
"opencode/InstanceContext",
18+
) {}
19+
20+
export type InstanceServices = QuestionService | PermissionService | ProviderAuthService
21+
22+
function lookup(directory: string) {
23+
const project = Instance.project
24+
const ctx = Layer.sync(InstanceContext, () => InstanceContext.of({ directory, project }))
25+
return Layer.mergeAll(
26+
Layer.fresh(QuestionService.layer),
27+
Layer.fresh(PermissionService.layer),
28+
Layer.fresh(ProviderAuthService.layer),
29+
).pipe(Layer.provide(ctx))
30+
}
31+
32+
export class Instances extends ServiceMap.Service<Instances, LayerMap.LayerMap<string, InstanceServices>>()(
33+
"opencode/Instances",
34+
) {
35+
static readonly layer = Layer.effect(
36+
Instances,
37+
Effect.gen(function* () {
38+
const layerMap = yield* LayerMap.make(lookup, { idleTimeToLive: Infinity })
39+
const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory)))
40+
yield* Effect.addFinalizer(() => Effect.sync(unregister))
41+
return Instances.of(layerMap)
42+
}),
43+
)
44+
45+
static get(directory: string): Layer.Layer<InstanceServices, never, Instances> {
46+
return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory))))
47+
}
48+
49+
static invalidate(directory: string): Effect.Effect<void, never, Instances> {
50+
return Instances.use((map) => map.invalidate(directory))
51+
}
52+
}
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { Layer, ManagedRuntime } from "effect"
1+
import { Effect, Layer, ManagedRuntime } from "effect"
22
import { AccountService } from "@/account/service"
33
import { AuthService } from "@/auth/service"
4-
import { PermissionService } from "@/permission/service"
5-
import { QuestionService } from "@/question/service"
4+
import { Instances } from "@/effect/instances"
5+
import type { InstanceServices } from "@/effect/instances"
6+
import { Instance } from "@/project/instance"
67

78
export const runtime = ManagedRuntime.make(
8-
Layer.mergeAll(AccountService.defaultLayer, AuthService.defaultLayer, PermissionService.layer, QuestionService.layer),
9+
Layer.mergeAll(AccountService.defaultLayer, Instances.layer).pipe(Layer.provideMerge(AuthService.defaultLayer)),
910
)
11+
12+
export function runPromiseInstance<A, E>(effect: Effect.Effect<A, E, InstanceServices>) {
13+
return runtime.runPromise(effect.pipe(Effect.provide(Instances.get(Instance.directory))))
14+
}

packages/opencode/src/permission/next.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
import { runtime } from "@/effect/runtime"
1+
import { runPromiseInstance } from "@/effect/runtime"
22
import { Config } from "@/config/config"
33
import { fn } from "@/util/fn"
44
import { Wildcard } from "@/util/wildcard"
5-
import { Effect } from "effect"
65
import os from "os"
76
import * as S from "./service"
8-
import type {
9-
Action as ActionType,
10-
PermissionError,
11-
Reply as ReplyType,
12-
Request as RequestType,
13-
Rule as RuleType,
14-
Ruleset as RulesetType,
15-
} from "./service"
167

178
export namespace PermissionNext {
189
function expand(pattern: string): string {
@@ -23,20 +14,16 @@ export namespace PermissionNext {
2314
return pattern
2415
}
2516

26-
function runPromise<A>(f: (service: S.PermissionService.Api) => Effect.Effect<A, PermissionError>) {
27-
return runtime.runPromise(S.PermissionService.use(f))
28-
}
29-
3017
export const Action = S.Action
31-
export type Action = ActionType
18+
export type Action = S.Action
3219
export const Rule = S.Rule
33-
export type Rule = RuleType
20+
export type Rule = S.Rule
3421
export const Ruleset = S.Ruleset
35-
export type Ruleset = RulesetType
22+
export type Ruleset = S.Ruleset
3623
export const Request = S.Request
37-
export type Request = RequestType
24+
export type Request = S.Request
3825
export const Reply = S.Reply
39-
export type Reply = ReplyType
26+
export type Reply = S.Reply
4027
export const Approval = S.Approval
4128
export const Event = S.Event
4229
export const Service = S.PermissionService
@@ -66,12 +53,16 @@ export namespace PermissionNext {
6653
return rulesets.flat()
6754
}
6855

69-
export const ask = fn(S.AskInput, async (input) => runPromise((service) => service.ask(input)))
56+
export const ask = fn(S.AskInput, async (input) =>
57+
runPromiseInstance(S.PermissionService.use((service) => service.ask(input))),
58+
)
7059

71-
export const reply = fn(S.ReplyInput, async (input) => runPromise((service) => service.reply(input)))
60+
export const reply = fn(S.ReplyInput, async (input) =>
61+
runPromiseInstance(S.PermissionService.use((service) => service.reply(input))),
62+
)
7263

7364
export async function list() {
74-
return runPromise((service) => service.list())
65+
return runPromiseInstance(S.PermissionService.use((service) => service.list()))
7566
}
7667

7768
export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule {

packages/opencode/src/permission/service.ts

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Bus } from "@/bus"
22
import { BusEvent } from "@/bus/bus-event"
3-
import { Instance } from "@/project/instance"
3+
import { InstanceContext } from "@/effect/instances"
44
import { ProjectID } from "@/project/schema"
55
import { MessageID, SessionID } from "@/session/schema"
66
import { PermissionTable } from "@/session/session.sql"
77
import { Database, eq } from "@/storage/db"
8-
import { InstanceState } from "@/util/instance-state"
98
import { Log } from "@/util/log"
109
import { Wildcard } from "@/util/wildcard"
1110
import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
@@ -104,11 +103,6 @@ interface PendingEntry {
104103
deferred: Deferred.Deferred<void, RejectedError | CorrectedError>
105104
}
106105

107-
type State = {
108-
pending: Map<PermissionID, PendingEntry>
109-
approved: Ruleset
110-
}
111-
112106
export const AskInput = Request.partial({ id: true }).extend({
113107
ruleset: Ruleset,
114108
})
@@ -133,36 +127,30 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
133127
static readonly layer = Layer.effect(
134128
PermissionService,
135129
Effect.gen(function* () {
136-
const instanceState = yield* InstanceState.make<State>(() =>
137-
Effect.sync(() => {
138-
const row = Database.use((db) =>
139-
db.select().from(PermissionTable).where(eq(PermissionTable.project_id, Instance.project.id)).get(),
140-
)
141-
return {
142-
pending: new Map<PermissionID, PendingEntry>(),
143-
approved: row?.data ?? [],
144-
}
145-
}),
130+
const { project } = yield* InstanceContext
131+
const row = Database.use((db) =>
132+
db.select().from(PermissionTable).where(eq(PermissionTable.project_id, project.id)).get(),
146133
)
134+
const pending = new Map<PermissionID, PendingEntry>()
135+
const approved: Ruleset = row?.data ?? []
147136

148137
const ask = Effect.fn("PermissionService.ask")(function* (input: z.infer<typeof AskInput>) {
149-
const state = yield* InstanceState.get(instanceState)
150138
const { ruleset, ...request } = input
151-
let pending = false
139+
let needsAsk = false
152140

153141
for (const pattern of request.patterns) {
154-
const rule = evaluate(request.permission, pattern, ruleset, state.approved)
142+
const rule = evaluate(request.permission, pattern, ruleset, approved)
155143
log.info("evaluated", { permission: request.permission, pattern, action: rule })
156144
if (rule.action === "deny") {
157145
return yield* new DeniedError({
158146
ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
159147
})
160148
}
161149
if (rule.action === "allow") continue
162-
pending = true
150+
needsAsk = true
163151
}
164152

165-
if (!pending) return
153+
if (!needsAsk) return
166154

167155
const id = request.id ?? PermissionID.ascending()
168156
const info: Request = {
@@ -172,22 +160,21 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
172160
log.info("asking", { id, permission: info.permission, patterns: info.patterns })
173161

174162
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
175-
state.pending.set(id, { info, deferred })
163+
pending.set(id, { info, deferred })
176164
void Bus.publish(Event.Asked, info)
177165
return yield* Effect.ensuring(
178166
Deferred.await(deferred),
179167
Effect.sync(() => {
180-
state.pending.delete(id)
168+
pending.delete(id)
181169
}),
182170
)
183171
})
184172

185173
const reply = Effect.fn("PermissionService.reply")(function* (input: z.infer<typeof ReplyInput>) {
186-
const state = yield* InstanceState.get(instanceState)
187-
const existing = state.pending.get(input.requestID)
174+
const existing = pending.get(input.requestID)
188175
if (!existing) return
189176

190-
state.pending.delete(input.requestID)
177+
pending.delete(input.requestID)
191178
void Bus.publish(Event.Replied, {
192179
sessionID: existing.info.sessionID,
193180
requestID: existing.info.id,
@@ -200,9 +187,9 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
200187
input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
201188
)
202189

203-
for (const [id, item] of state.pending.entries()) {
190+
for (const [id, item] of pending.entries()) {
204191
if (item.info.sessionID !== existing.info.sessionID) continue
205-
state.pending.delete(id)
192+
pending.delete(id)
206193
void Bus.publish(Event.Replied, {
207194
sessionID: item.info.sessionID,
208195
requestID: item.info.id,
@@ -217,20 +204,20 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
217204
if (input.reply === "once") return
218205

219206
for (const pattern of existing.info.always) {
220-
state.approved.push({
207+
approved.push({
221208
permission: existing.info.permission,
222209
pattern,
223210
action: "allow",
224211
})
225212
}
226213

227-
for (const [id, item] of state.pending.entries()) {
214+
for (const [id, item] of pending.entries()) {
228215
if (item.info.sessionID !== existing.info.sessionID) continue
229216
const ok = item.info.patterns.every(
230-
(pattern) => evaluate(item.info.permission, pattern, state.approved).action === "allow",
217+
(pattern) => evaluate(item.info.permission, pattern, approved).action === "allow",
231218
)
232219
if (!ok) continue
233-
state.pending.delete(id)
220+
pending.delete(id)
234221
void Bus.publish(Event.Replied, {
235222
sessionID: item.info.sessionID,
236223
requestID: item.info.id,
@@ -246,8 +233,7 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
246233
})
247234

248235
const list = Effect.fn("PermissionService.list")(function* () {
249-
const state = yield* InstanceState.get(instanceState)
250-
return Array.from(state.pending.values(), (item) => item.info)
236+
return Array.from(pending.values(), (item) => item.info)
251237
})
252238

253239
return PermissionService.of({ ask, reply, list })

packages/opencode/src/project/instance.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { Effect } from "effect"
21
import { Log } from "@/util/log"
32
import { Context } from "../util/context"
43
import { Project } from "./project"
54
import { State } from "./state"
65
import { iife } from "@/util/iife"
76
import { GlobalBus } from "@/bus/global"
87
import { Filesystem } from "@/util/filesystem"
9-
import { InstanceState } from "@/util/instance-state"
8+
import { disposeInstance } from "@/effect/instance-registry"
109

1110
interface Context {
1211
directory: string
@@ -108,17 +107,18 @@ export const Instance = {
108107
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
109108
const directory = Filesystem.resolve(input.directory)
110109
Log.Default.info("reloading instance", { directory })
111-
await Promise.all([State.dispose(directory), Effect.runPromise(InstanceState.dispose(directory))])
110+
await Promise.all([State.dispose(directory), disposeInstance(directory)])
112111
cache.delete(directory)
113112
const next = track(directory, boot({ ...input, directory }))
114113
emit(directory)
115114
return await next
116115
},
117116
async dispose() {
118-
Log.Default.info("disposing instance", { directory: Instance.directory })
119-
await Promise.all([State.dispose(Instance.directory), Effect.runPromise(InstanceState.dispose(Instance.directory))])
120-
cache.delete(Instance.directory)
121-
emit(Instance.directory)
117+
const directory = Instance.directory
118+
Log.Default.info("disposing instance", { directory })
119+
await Promise.all([State.dispose(directory), disposeInstance(directory)])
120+
cache.delete(directory)
121+
emit(directory)
122122
},
123123
async disposeAll() {
124124
if (disposal.all) return disposal.all

0 commit comments

Comments
 (0)