|
|
@@ -7,9 +7,7 @@ import { eq } from "drizzle-orm"
|
|
|
import { GlobalBus } from "@/bus/global"
|
|
|
import { Bus as ProjectBus } from "@/bus"
|
|
|
import { BusEvent } from "@/bus/bus-event"
|
|
|
-import type { InstanceContext } from "@/project/instance-context"
|
|
|
import { EventSequenceTable, EventTable } from "./event.sql"
|
|
|
-import type { WorkspaceID } from "@/control-plane/schema"
|
|
|
import { EventID } from "./schema"
|
|
|
import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
|
|
|
import type { DeepMutable } from "@opencode-ai/core/schema"
|
|
|
@@ -17,7 +15,7 @@ import { EventV2 } from "@opencode-ai/core/event"
|
|
|
import { serviceUse } from "@/effect/service-use"
|
|
|
import { InstanceState } from "@/effect/instance-state"
|
|
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
|
-import { attachWith } from "@/effect/run-service"
|
|
|
+import { EffectBridge } from "@/effect/bridge"
|
|
|
|
|
|
// Keep `Event["data"]` mutable because projectors mutate the persisted shape
|
|
|
// when writing to the database. Bus payloads (`Properties`) stay readonly —
|
|
|
@@ -51,10 +49,6 @@ export type SerializedEvent<Def extends Definition = Definition> = Event<Def> &
|
|
|
|
|
|
type ProjectorFunc = (db: Database.TxOrDb, data: unknown, event: Event) => void
|
|
|
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
|
|
|
-type PublishContext = {
|
|
|
- instance?: InstanceContext
|
|
|
- workspace?: WorkspaceID
|
|
|
-}
|
|
|
|
|
|
export interface Interface {
|
|
|
readonly run: <Def extends Definition>(
|
|
|
@@ -107,16 +101,14 @@ export const layer = Layer.effect(Service)(
|
|
|
}
|
|
|
|
|
|
const publish = !!options?.publish
|
|
|
- const context = publish
|
|
|
- ? {
|
|
|
- instance: yield* InstanceState.context,
|
|
|
- workspace: yield* InstanceState.workspaceID,
|
|
|
- }
|
|
|
- : undefined
|
|
|
+ // Bridge captures handler-fiber refs (InstanceRef/WorkspaceRef) and the
|
|
|
+ // full Effect context, so the forked publish + GlobalBus emit run with
|
|
|
+ // the right state without a per-call attachWith.
|
|
|
+ const bridge = yield* EffectBridge.make()
|
|
|
process(def, event, {
|
|
|
bus,
|
|
|
+ bridge,
|
|
|
publish,
|
|
|
- context,
|
|
|
ownerID: options?.ownerID,
|
|
|
experimentalWorkspaces: flags.experimentalWorkspaces,
|
|
|
})
|
|
|
@@ -154,12 +146,7 @@ export const layer = Layer.effect(Service)(
|
|
|
}
|
|
|
|
|
|
const { publish = true } = options || {}
|
|
|
- const context = publish
|
|
|
- ? {
|
|
|
- instance: yield* InstanceState.context,
|
|
|
- workspace: yield* InstanceState.workspaceID,
|
|
|
- }
|
|
|
- : undefined
|
|
|
+ const bridge = yield* EffectBridge.make()
|
|
|
|
|
|
// Note that this is an "immediate" transaction which is critical.
|
|
|
// We need to make sure we can safely read and write with nothing
|
|
|
@@ -175,7 +162,7 @@ export const layer = Layer.effect(Service)(
|
|
|
const seq = row?.seq != null ? row.seq + 1 : 0
|
|
|
|
|
|
const event = { id, seq, aggregateID: agg, data }
|
|
|
- process(def, event, { bus, publish, context, experimentalWorkspaces: flags.experimentalWorkspaces })
|
|
|
+ process(def, event, { bus, bridge, publish, experimentalWorkspaces: flags.experimentalWorkspaces })
|
|
|
},
|
|
|
{
|
|
|
behavior: "immediate",
|
|
|
@@ -308,8 +295,8 @@ function process<Def extends Definition>(
|
|
|
event: Event<Def>,
|
|
|
options: {
|
|
|
bus: ProjectBus.Interface
|
|
|
+ bridge: EffectBridge.Shape
|
|
|
publish: boolean
|
|
|
- context?: PublishContext
|
|
|
ownerID?: string
|
|
|
experimentalWorkspaces: boolean
|
|
|
},
|
|
|
@@ -351,37 +338,36 @@ function process<Def extends Definition>(
|
|
|
}
|
|
|
|
|
|
Database.effect(() => {
|
|
|
- if (options?.publish) {
|
|
|
- if (!options.context?.instance) {
|
|
|
- throw new Error("SyncEvent.process: publish requires instance context")
|
|
|
- }
|
|
|
-
|
|
|
- const result = convertEvent(def.type, event.data)
|
|
|
- const publish = (data: unknown) =>
|
|
|
- Effect.runPromise(
|
|
|
- attachWith(options.bus.publish(def, data as Properties<Def>, { id: event.id }), {
|
|
|
- instance: options.context?.instance,
|
|
|
- workspace: options.context?.workspace,
|
|
|
- }),
|
|
|
- )
|
|
|
- if (result instanceof Promise) {
|
|
|
- void result.then(publish)
|
|
|
- } else {
|
|
|
- void publish(result)
|
|
|
- }
|
|
|
-
|
|
|
- GlobalBus.emit("event", {
|
|
|
- directory: options.context.instance.directory,
|
|
|
- project: options.context.instance.project.id,
|
|
|
- workspace: options.context.workspace,
|
|
|
- payload: {
|
|
|
- type: "sync",
|
|
|
- syncEvent: {
|
|
|
- type: versionedType(def.type, def.version),
|
|
|
- ...event,
|
|
|
- },
|
|
|
- },
|
|
|
- })
|
|
|
+ if (!options.publish) return
|
|
|
+ const result = convertEvent(def.type, event.data)
|
|
|
+ // The bridge was built inside the caller's fiber so it already carries
|
|
|
+ // InstanceRef/WorkspaceRef and the full Effect context. Both the bus
|
|
|
+ // publish and the GlobalBus emit run inside the forked Effect so they
|
|
|
+ // share the same instance/workspace lookup.
|
|
|
+ const publish = (data: unknown) =>
|
|
|
+ options.bridge.fork(
|
|
|
+ Effect.gen(function* () {
|
|
|
+ yield* options.bus.publish(def, data as Properties<Def>, { id: event.id })
|
|
|
+ const instance = yield* InstanceState.context
|
|
|
+ const workspace = yield* InstanceState.workspaceID
|
|
|
+ GlobalBus.emit("event", {
|
|
|
+ directory: instance.directory,
|
|
|
+ project: instance.project.id,
|
|
|
+ workspace,
|
|
|
+ payload: {
|
|
|
+ type: "sync",
|
|
|
+ syncEvent: {
|
|
|
+ type: versionedType(def.type, def.version),
|
|
|
+ ...event,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ if (result instanceof Promise) {
|
|
|
+ void result.then(publish)
|
|
|
+ } else {
|
|
|
+ publish(result)
|
|
|
}
|
|
|
})
|
|
|
})
|