refactor(state): namespace InstanceState, use Map for pending

- Convert InstanceState to namespace export pattern
- Change pending OAuth state from Record to Map for type-safe lookups
This commit is contained in:
Kit Langton
2026-03-12 20:31:15 -04:00
parent 395a9580bd
commit c45fbb3d45
4 changed files with 41 additions and 39 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { State } from "./state"
import { iife } from "@/util/iife"
import { GlobalBus } from "@/bus/global"
import { Filesystem } from "@/util/filesystem"
import * as InstanceState from "@/util/instance-state"
import { InstanceState } from "@/util/instance-state"
interface Context {
directory: string
@@ -5,7 +5,7 @@ import { filter, fromEntries, map, pipe } from "remeda"
import type { AuthOuathResult } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/util/error"
import * as Auth from "@/auth/service"
import * as InstanceState from "@/util/instance-state"
import { InstanceState } from "@/util/instance-state"
import { ProviderID } from "./schema"
import z from "zod"
@@ -78,7 +78,7 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
map((x) => [x.auth!.provider, x.auth!] as const),
fromEntries(),
)
return { methods, pending: {} as Record<string, AuthOuathResult> }
return { methods, pending: new Map<string, AuthOuathResult>() }
}),
})
@@ -97,7 +97,7 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
const result = yield* Effect.promise(() => method.authorize())
const s = yield* InstanceState.get(state)
s.pending[input.providerID] = result
s.pending.set(input.providerID, result)
return {
url: result.url,
method: result.method,
@@ -110,7 +110,7 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
method: number
code?: string
}) {
const match = (yield* InstanceState.get(state)).pending[input.providerID]
const match = (yield* InstanceState.get(state)).pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
if (match.method === "code" && !input.code)
+34 -32
View File
@@ -8,41 +8,43 @@ type Task = (key: string) => Effect.Effect<void>
const tasks = new Set<Task>()
export interface InstanceState<A, E = never, R = never> {
readonly [TypeId]: typeof TypeId
readonly cache: ScopedCache.ScopedCache<string, A, E, R>
}
export namespace InstanceState {
export interface State<A, E = never, R = never> {
readonly [TypeId]: typeof TypeId
readonly cache: ScopedCache.ScopedCache<string, A, E, R>
}
export const make = <A, E = never, R = never>(input: {
lookup: (key: string) => Effect.Effect<A, E, R>
release?: (value: A, key: string) => Effect.Effect<void>
}): Effect.Effect<InstanceState<A, E, R>, never, R | Scope.Scope> =>
Effect.gen(function* () {
const cache = yield* ScopedCache.make<string, A, E, R>({
capacity: Number.POSITIVE_INFINITY,
lookup: (key) =>
Effect.acquireRelease(input.lookup(key), (value) => (input.release ? input.release(value, key) : Effect.void)),
export const make = <A, E = never, R = never>(input: {
lookup: (key: string) => Effect.Effect<A, E, R>
release?: (value: A, key: string) => Effect.Effect<void>
}): Effect.Effect<State<A, E, R>, never, R | Scope.Scope> =>
Effect.gen(function* () {
const cache = yield* ScopedCache.make<string, A, E, R>({
capacity: Number.POSITIVE_INFINITY,
lookup: (key) =>
Effect.acquireRelease(input.lookup(key), (value) => (input.release ? input.release(value, key) : Effect.void)),
})
const task: Task = (key) => ScopedCache.invalidate(cache, key)
tasks.add(task)
yield* Effect.addFinalizer(() => Effect.sync(() => void tasks.delete(task)))
return {
[TypeId]: TypeId,
cache,
}
})
const task: Task = (key) => ScopedCache.invalidate(cache, key)
tasks.add(task)
yield* Effect.addFinalizer(() => Effect.sync(() => void tasks.delete(task)))
export const get = <A, E, R>(self: State<A, E, R>) => ScopedCache.get(self.cache, Instance.directory)
return {
[TypeId]: TypeId,
cache,
}
})
export const has = <A, E, R>(self: State<A, E, R>) => ScopedCache.has(self.cache, Instance.directory)
export const get = <A, E, R>(self: InstanceState<A, E, R>) => ScopedCache.get(self.cache, Instance.directory)
export const invalidate = <A, E, R>(self: State<A, E, R>) =>
ScopedCache.invalidate(self.cache, Instance.directory)
export const has = <A, E, R>(self: InstanceState<A, E, R>) => ScopedCache.has(self.cache, Instance.directory)
export const invalidate = <A, E, R>(self: InstanceState<A, E, R>) =>
ScopedCache.invalidate(self.cache, Instance.directory)
export const dispose = (key: string) =>
Effect.all(
[...tasks].map((task) => task(key)),
{ concurrency: "unbounded" },
)
export const dispose = (key: string) =>
Effect.all(
[...tasks].map((task) => task(key)),
{ concurrency: "unbounded" },
)
}
@@ -2,10 +2,10 @@ import { afterEach, expect, test } from "bun:test"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import * as InstanceState from "../../src/util/instance-state"
import { InstanceState } from "../../src/util/instance-state"
import { tmpdir } from "../fixture/fixture"
async function access<A, E>(state: InstanceState.InstanceState<A, E>, dir: string) {
async function access<A, E>(state: InstanceState.State<A, E>, dir: string) {
return Instance.provide({
directory: dir,
fn: () => Effect.runPromise(InstanceState.get(state)),