feat(plugin): add v2 effect host (#33111)
This commit is contained in:
parent
7a9337da8a
commit
c780d7cee7
139 changed files with 3740 additions and 1943 deletions
|
|
@ -1,112 +1,128 @@
|
|||
export * as State from "./state"
|
||||
|
||||
import { Effect, Scope, Semaphore } from "effect"
|
||||
import type { Draft, Objectish } from "immer"
|
||||
import { Context, Effect, Scope, Semaphore } from "effect"
|
||||
|
||||
/**
|
||||
* A replayable transform applied to an editor during rebuild.
|
||||
* A replayable transform applied to a draft during rebuild.
|
||||
*
|
||||
* Transforms are intentionally synchronous and mutation-shaped: domain editors
|
||||
* hide the draft representation while preserving concise plugin/config code.
|
||||
* Domain drafts expose readable and writable state while preserving concise
|
||||
* plugin/config code. Transforms may perform Effects before returning.
|
||||
*/
|
||||
export type Transform<Editor> = (editor: Editor) => void
|
||||
export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
|
||||
type TransformCallback<DraftApi> = (draft: DraftApi) => Effect.Effect<void> | void
|
||||
export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
|
||||
|
||||
export interface Options<State extends Objectish, Editor> {
|
||||
export interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type Transform<DraftApi> = (
|
||||
transform: TransformCallback<DraftApi>,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
export type Rebuild = () => Effect.Effect<void>
|
||||
|
||||
export interface Transformable<DraftApi> {
|
||||
readonly transform: Transform<DraftApi>
|
||||
readonly rebuild: Rebuild
|
||||
}
|
||||
|
||||
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const current = yield* CurrentBatch
|
||||
if (current) return yield* effect
|
||||
const rebuilds = new Set<Rebuild>()
|
||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
|
||||
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export interface Options<State, DraftApi> {
|
||||
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
||||
readonly initial: () => State
|
||||
/** Wraps the mutable draft in a domain-specific editor. */
|
||||
readonly editor: MakeEditor<State, Editor>
|
||||
/**
|
||||
* Completes every committed edit.
|
||||
*
|
||||
* For rebuilds, this runs after all active transforms have been replayed and
|
||||
* before the rebuilt state becomes visible. For direct updates, this runs
|
||||
* after the current state has already been edited. The optional reason is
|
||||
* caller-defined metadata for exceptional update origins.
|
||||
*/
|
||||
readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
|
||||
/** Wraps mutable state in a domain-specific draft API. */
|
||||
readonly draft: MakeDraft<State, DraftApi>
|
||||
/** Runs after all active transforms and before the rebuilt state becomes visible. */
|
||||
readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Interface<State extends Objectish, Editor> {
|
||||
export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
|
||||
readonly get: () => State
|
||||
/**
|
||||
* Registers a scoped transform slot and returns the slot updater.
|
||||
*
|
||||
* Acquiring the slot has no visible effect until the returned updater is
|
||||
* called. Each updater call replaces that slot's transform, then rebuilds the
|
||||
* materialized state from `initial()` by replaying all active transforms in
|
||||
* registration order. Closing the owning Scope removes the slot and rebuilds.
|
||||
* Registers and applies a scoped transform. Closing the owning Scope removes
|
||||
* the transform and rebuilds the materialized state.
|
||||
*/
|
||||
readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
|
||||
/** Registers and applies a replayable transform in the current Scope. */
|
||||
readonly update: (update: Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
||||
/**
|
||||
* Mutates the current materialized state directly, once.
|
||||
*
|
||||
* This is not replayable transform state: a later rebuild starts again
|
||||
* from `initial()` plus active transforms, so direct edits must be reserved
|
||||
* for current-state adjustments that are intentionally outside the transform
|
||||
* fold.
|
||||
*/
|
||||
readonly mutate: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
|
||||
export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
|
||||
let state = options.initial()
|
||||
let transforms: { update: Transform<Editor> }[] = []
|
||||
let transforms: { run: TransformCallback<DraftApi> }[] = []
|
||||
const semaphore = Semaphore.makeUnsafe(1)
|
||||
|
||||
const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
|
||||
const api = options.editor(next as Draft<State>)
|
||||
if (options.finalize) yield* options.finalize(api, reason)
|
||||
const commit = Effect.fn("State.commit")(function* (next: State) {
|
||||
const api = options.draft(next)
|
||||
if (options.finalize) yield* options.finalize(api)
|
||||
state = next
|
||||
})
|
||||
|
||||
const rebuild = Effect.fnUntraced(function* () {
|
||||
const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
|
||||
Effect.suspend(() => {
|
||||
const result = transform(draft)
|
||||
return Effect.isEffect(result) ? Effect.asVoid(result).pipe(Effect.orDie) : Effect.void
|
||||
})
|
||||
|
||||
const materialize = Effect.fnUntraced(function* () {
|
||||
const next = options.initial()
|
||||
const api = options.editor(next as Draft<State>)
|
||||
for (const transform of transforms)
|
||||
yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
|
||||
const api = options.draft(next)
|
||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
|
||||
yield* commit(next)
|
||||
})
|
||||
|
||||
const result: Interface<State, Editor> = {
|
||||
const rebuild = () => semaphore.withPermit(materialize())
|
||||
|
||||
const result: Interface<State, DraftApi> = {
|
||||
get: () => state,
|
||||
transform: Effect.fn("State.transform")(function* () {
|
||||
transform: Effect.fn("State.transform")(function* (update) {
|
||||
const scope = yield* Scope.Scope
|
||||
return yield* Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const transform = { update: (_editor: Editor) => {} }
|
||||
transforms = [...transforms, transform]
|
||||
yield* Scope.addFinalizer(
|
||||
scope,
|
||||
const transform = { run: update }
|
||||
let active = true
|
||||
const dispose = Effect.uninterruptible(
|
||||
semaphore.withPermit(
|
||||
Effect.sync(() => {
|
||||
Effect.suspend(() => {
|
||||
if (!active) return Effect.void
|
||||
active = false
|
||||
transforms = transforms.filter((item) => item !== transform)
|
||||
}).pipe(Effect.andThen(rebuild())),
|
||||
return Effect.gen(function* () {
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) {
|
||||
batch.add(rebuild)
|
||||
return
|
||||
}
|
||||
yield* materialize()
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
return (update: Transform<Editor>) =>
|
||||
Effect.uninterruptible(
|
||||
semaphore.withPermit(
|
||||
Effect.sync(() => {
|
||||
transform.update = update
|
||||
}).pipe(Effect.andThen(rebuild())),
|
||||
),
|
||||
)
|
||||
yield* semaphore.withPermit(
|
||||
Effect.sync(() => {
|
||||
transforms = [...transforms, transform]
|
||||
}),
|
||||
)
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) batch.add(rebuild)
|
||||
else yield* rebuild()
|
||||
return { dispose }
|
||||
}),
|
||||
)
|
||||
}),
|
||||
update: Effect.fn("State.update")(function* (update) {
|
||||
const transform = yield* result.transform()
|
||||
yield* transform(update)
|
||||
}),
|
||||
mutate: Effect.fn("State.mutate")(function* (update, reason) {
|
||||
const api = options.editor(state as Draft<State>)
|
||||
yield* update(api)
|
||||
if (options.finalize) yield* options.finalize(api, reason)
|
||||
}, semaphore.withPermit),
|
||||
rebuild,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue