refactor(bus): migrate BusEvent to Effect Schema (#24040)
This commit is contained in:
@@ -1,15 +1,19 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { ZodType } from "zod"
|
import { Schema } from "effect"
|
||||||
|
import { zodObject } from "@/util/effect-zod"
|
||||||
|
|
||||||
export type Definition = ReturnType<typeof define>
|
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
|
||||||
|
type: Type
|
||||||
|
properties: Properties
|
||||||
|
}
|
||||||
|
|
||||||
const registry = new Map<string, Definition>()
|
const registry = new Map<string, Definition>()
|
||||||
|
|
||||||
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
export function define<Type extends string, Properties extends Schema.Top>(
|
||||||
const result = {
|
type: Type,
|
||||||
type,
|
properties: Properties,
|
||||||
properties,
|
): Definition<Type, Properties> {
|
||||||
}
|
const result = { type, properties }
|
||||||
registry.set(type, result)
|
registry.set(type, result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -21,7 +25,7 @@ export function payloads() {
|
|||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal(type),
|
type: z.literal(type),
|
||||||
properties: def.properties,
|
properties: zodObject(def.properties),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: `Event.${def.type}`,
|
ref: `Event.${def.type}`,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import z from "zod"
|
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
|
||||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema as EffectSchema, Types } from "effect"
|
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { BusEvent } from "./bus-event"
|
import { BusEvent } from "./bus-event"
|
||||||
@@ -9,16 +8,12 @@ import { makeRuntime } from "@/effect/run-service"
|
|||||||
|
|
||||||
const log = Log.create({ service: "bus" })
|
const log = Log.create({ service: "bus" })
|
||||||
|
|
||||||
type BusProperties<D extends BusEvent.Definition = BusEvent.Definition> = D extends {
|
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
|
||||||
effectProperties: infer Properties extends EffectSchema.Top
|
|
||||||
}
|
|
||||||
? Types.DeepMutable<EffectSchema.Schema.Type<Properties>>
|
|
||||||
: z.infer<D["properties"]>
|
|
||||||
|
|
||||||
export const InstanceDisposed = BusEvent.define(
|
export const InstanceDisposed = BusEvent.define(
|
||||||
"server.instance.disposed",
|
"server.instance.disposed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
directory: z.string(),
|
directory: Schema.String,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { SessionID } from "@/session/schema"
|
import { SessionID } from "@/session/schema"
|
||||||
import z from "zod"
|
import { Schema } from "effect"
|
||||||
|
|
||||||
export const TuiEvent = {
|
export const TuiEvent = {
|
||||||
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
|
PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })),
|
||||||
CommandExecute: BusEvent.define(
|
CommandExecute: BusEvent.define(
|
||||||
"tui.command.execute",
|
"tui.command.execute",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
command: z.union([
|
command: Schema.Union([
|
||||||
z.enum([
|
Schema.Literals([
|
||||||
"session.list",
|
"session.list",
|
||||||
"session.new",
|
"session.new",
|
||||||
"session.share",
|
"session.share",
|
||||||
@@ -26,23 +26,23 @@ export const TuiEvent = {
|
|||||||
"prompt.submit",
|
"prompt.submit",
|
||||||
"agent.cycle",
|
"agent.cycle",
|
||||||
]),
|
]),
|
||||||
z.string(),
|
Schema.String,
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
ToastShow: BusEvent.define(
|
ToastShow: BusEvent.define(
|
||||||
"tui.toast.show",
|
"tui.toast.show",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
title: z.string().optional(),
|
title: Schema.optional(Schema.String),
|
||||||
message: z.string(),
|
message: Schema.String,
|
||||||
variant: z.enum(["info", "success", "warning", "error"]),
|
variant: Schema.Literals(["info", "success", "warning", "error"]),
|
||||||
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
|
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
SessionSelect: BusEvent.define(
|
SessionSelect: BusEvent.define(
|
||||||
"tui.session.select",
|
"tui.session.select",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod.describe("Session ID to navigate to"),
|
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { useTheme } from "@tui/context/theme"
|
|||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { SplitBorder } from "../component/border"
|
import { SplitBorder } from "../component/border"
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import z from "zod"
|
import { Schema } from "effect"
|
||||||
import { type TuiEvent } from "../event"
|
import { type TuiEvent } from "../event"
|
||||||
|
|
||||||
export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>
|
export type ToastOptions = Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>
|
||||||
|
|
||||||
export function Toast() {
|
export function Toast() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
|||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import type { InstanceContext } from "@/project/instance"
|
import type { InstanceContext } from "@/project/instance"
|
||||||
import { SessionID, MessageID } from "@/session/schema"
|
import { SessionID, MessageID } from "@/session/schema"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
import { MCP } from "../mcp"
|
import { MCP } from "../mcp"
|
||||||
@@ -18,11 +18,11 @@ type State = {
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Executed: BusEvent.define(
|
Executed: BusEvent.define(
|
||||||
"command.executed",
|
"command.executed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
name: z.string(),
|
name: Schema.String,
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
arguments: z.string(),
|
arguments: Schema.String,
|
||||||
messageID: MessageID.zod,
|
messageID: MessageID,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
|
|||||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||||
import { InstanceRef } from "@/effect/instance-ref"
|
import { InstanceRef } from "@/effect/instance-ref"
|
||||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||||
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
|
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||||
import { ConfigAgent } from "./agent"
|
import { ConfigAgent } from "./agent"
|
||||||
import { ConfigCommand } from "./command"
|
import { ConfigCommand } from "./command"
|
||||||
import { ConfigFormatter } from "./formatter"
|
import { ConfigFormatter } from "./formatter"
|
||||||
@@ -249,26 +249,9 @@ export const Info = Schema.Struct({
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schema.Struct produces readonly types by default, but the service code
|
// Uses the shared `DeepMutable` from `@/util/schema`. See the definition
|
||||||
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
// there for why the local variant is needed over `Types.DeepMutable` from
|
||||||
// readonly recursively so callers get the same mutable shape zod inferred.
|
// effect-smol (the upstream version collapses `unknown` to `{}`).
|
||||||
//
|
|
||||||
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
|
|
||||||
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
|
|
||||||
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
|
|
||||||
// fields like `ConfigPlugin.Options`. The local version gates on
|
|
||||||
// `extends object` so `unknown` passes through.
|
|
||||||
//
|
|
||||||
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
|
|
||||||
// shape (otherwise the general array branch widens it to an array).
|
|
||||||
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
|
||||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
|
||||||
: T extends readonly (infer U)[]
|
|
||||||
? DeepMutable<U>[]
|
|
||||||
: T extends object
|
|
||||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
|
||||||
: T
|
|
||||||
|
|
||||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
||||||
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
||||||
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { Schema } from "effect"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Database, asc, eq, inArray } from "@/storage"
|
import { Database, asc, eq, inArray } from "@/storage"
|
||||||
@@ -25,36 +26,37 @@ import { errorData } from "@/util/error"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { waitEvent } from "./util"
|
import { waitEvent } from "./util"
|
||||||
import { WorkspaceContext } from "./workspace-context"
|
import { WorkspaceContext } from "./workspace-context"
|
||||||
|
import { NonNegativeInt } from "@/util/schema"
|
||||||
|
|
||||||
export const Info = WorkspaceInfo.meta({
|
export const Info = WorkspaceInfo.meta({
|
||||||
ref: "Workspace",
|
ref: "Workspace",
|
||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const ConnectionStatus = z.object({
|
export const ConnectionStatus = Schema.Struct({
|
||||||
workspaceID: WorkspaceID.zod,
|
workspaceID: WorkspaceID,
|
||||||
status: z.enum(["connected", "connecting", "disconnected", "error"]),
|
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
|
||||||
})
|
})
|
||||||
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
|
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
|
||||||
|
|
||||||
const Restore = z.object({
|
const Restore = Schema.Struct({
|
||||||
workspaceID: WorkspaceID.zod,
|
workspaceID: WorkspaceID,
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
total: z.number().int().min(0),
|
total: NonNegativeInt,
|
||||||
step: z.number().int().min(0),
|
step: NonNegativeInt,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Ready: BusEvent.define(
|
Ready: BusEvent.define(
|
||||||
"workspace.ready",
|
"workspace.ready",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
name: z.string(),
|
name: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Failed: BusEvent.define(
|
Failed: BusEvent.define(
|
||||||
"workspace.failed",
|
"workspace.failed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
message: z.string(),
|
message: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Restore: BusEvent.define("workspace.restore", Restore),
|
Restore: BusEvent.define("workspace.restore", Restore),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
|||||||
|
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Effect, Layer, Context, Scope } from "effect"
|
import { Effect, Layer, Context, Schema, Scope } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
import { formatPatch, structuredPatch } from "diff"
|
import { formatPatch, structuredPatch } from "diff"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
@@ -76,8 +76,8 @@ export type Content = z.infer<typeof Content>
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Edited: BusEvent.define(
|
Edited: BusEvent.define(
|
||||||
"file.edited",
|
"file.edited",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
file: z.string(),
|
file: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cause, Effect, Layer, Context } from "effect"
|
import { Cause, Effect, Layer, Context, Schema } from "effect"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
@@ -25,9 +25,9 @@ const SUBSCRIBE_TIMEOUT_MS = 10_000
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: BusEvent.define(
|
Updated: BusEvent.define(
|
||||||
"file.watcher.updated",
|
"file.watcher.updated",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
file: z.string(),
|
file: Schema.String,
|
||||||
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
|
event: Schema.Literals(["add", "change", "unlink"]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { Schema } from "effect"
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { Process } from "@/util"
|
import { Process } from "@/util"
|
||||||
@@ -17,8 +18,8 @@ const log = Log.create({ service: "ide" })
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Installed: BusEvent.define(
|
Installed: BusEvent.define(
|
||||||
"ide.installed",
|
"ide.installed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
ide: z.string(),
|
ide: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ export type ReleaseType = "patch" | "minor" | "major"
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: BusEvent.define(
|
Updated: BusEvent.define(
|
||||||
"installation.updated",
|
"installation.updated",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
version: z.string(),
|
version: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
UpdateAvailable: BusEvent.define(
|
UpdateAvailable: BusEvent.define(
|
||||||
"installation.update-available",
|
"installation.update-available",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
version: z.string(),
|
version: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Log } from "../util"
|
|||||||
import { Process } from "../util"
|
import { Process } from "../util"
|
||||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { Schema } from "effect"
|
||||||
import type * as LSPServer from "./server"
|
import type * as LSPServer from "./server"
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { withTimeout } from "../util/timeout"
|
import { withTimeout } from "../util/timeout"
|
||||||
@@ -41,9 +42,9 @@ export const InitializeError = NamedError.create(
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Diagnostics: BusEvent.define(
|
Diagnostics: BusEvent.define(
|
||||||
"lsp.client.diagnostics",
|
"lsp.client.diagnostics",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
serverID: z.string(),
|
serverID: Schema.String,
|
||||||
path: z.string(),
|
path: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { zod, ZodOverride } from "@/util/effect-zod"
|
|||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: BusEvent.define("lsp.updated", z.object({})),
|
Updated: BusEvent.define("lsp.updated", Schema.Struct({})),
|
||||||
}
|
}
|
||||||
|
|
||||||
const Position = Schema.Struct({
|
const Position = Schema.Struct({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { BusEvent } from "../bus/bus-event"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
@@ -47,16 +47,16 @@ export type Resource = z.infer<typeof Resource>
|
|||||||
|
|
||||||
export const ToolsChanged = BusEvent.define(
|
export const ToolsChanged = BusEvent.define(
|
||||||
"mcp.tools.changed",
|
"mcp.tools.changed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
server: z.string(),
|
server: Schema.String,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const BrowserOpenFailed = BusEvent.define(
|
export const BrowserOpenFailed = BusEvent.define(
|
||||||
"mcp.browser.open.failed",
|
"mcp.browser.open.failed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
mcpName: z.string(),
|
mcpName: Schema.String,
|
||||||
url: z.string(),
|
url: Schema.String,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -73,16 +73,14 @@ export class Approval extends Schema.Class<Approval>("PermissionApproval")({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Asked: BusEvent.define("permission.asked", Request.zod),
|
Asked: BusEvent.define("permission.asked", Request),
|
||||||
Replied: BusEvent.define(
|
Replied: BusEvent.define(
|
||||||
"permission.replied",
|
"permission.replied",
|
||||||
zod(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
sessionID: SessionID,
|
||||||
sessionID: SessionID,
|
requestID: PermissionID,
|
||||||
requestID: PermissionID,
|
reply: Reply,
|
||||||
reply: Reply,
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const Info = Schema.Struct({
|
|||||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: BusEvent.define("project.updated", Info.zod),
|
Updated: BusEvent.define("project.updated", Info),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Row = typeof ProjectTable.$inferSelect
|
type Row = typeof ProjectTable.$inferSelect
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Effect, Layer, Context, Stream, Scope } from "effect"
|
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
|
||||||
import { formatPatch, structuredPatch } from "diff"
|
import { formatPatch, structuredPatch } from "diff"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
@@ -107,8 +107,8 @@ export type Mode = z.infer<typeof Mode>
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
BranchUpdated: BusEvent.define(
|
BranchUpdated: BusEvent.define(
|
||||||
"vcs.branch.updated",
|
"vcs.branch.updated",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
branch: z.string().optional(),
|
branch: Schema.optional(Schema.String),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { Bus } from "@/bus"
|
|||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import type { Proc } from "#pty"
|
import type { Proc } from "#pty"
|
||||||
import z from "zod"
|
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { lazy } from "@opencode-ai/shared/util/lazy"
|
import { lazy } from "@opencode-ai/shared/util/lazy"
|
||||||
import { Shell } from "@/shell/shell"
|
import { Shell } from "@/shell/shell"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { PtyID } from "./schema"
|
import { PtyID } from "./schema"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||||
|
import { zod } from "@/util/effect-zod"
|
||||||
|
import { withStatics } from "@/util/schema"
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
|
|
||||||
const log = Log.create({ service: "pty" })
|
const log = Log.create({ service: "pty" })
|
||||||
@@ -53,47 +54,47 @@ const meta = (cursor: number) => {
|
|||||||
|
|
||||||
const pty = lazy(() => import("#pty"))
|
const pty = lazy(() => import("#pty"))
|
||||||
|
|
||||||
export const Info = z
|
export const Info = Schema.Struct({
|
||||||
.object({
|
id: PtyID,
|
||||||
id: PtyID.zod,
|
title: Schema.String,
|
||||||
title: z.string(),
|
command: Schema.String,
|
||||||
command: z.string(),
|
args: Schema.Array(Schema.String),
|
||||||
args: z.array(z.string()),
|
cwd: Schema.String,
|
||||||
cwd: z.string(),
|
status: Schema.Literals(["running", "exited"]),
|
||||||
status: z.enum(["running", "exited"]),
|
pid: Schema.Number,
|
||||||
pid: z.number(),
|
|
||||||
})
|
|
||||||
.meta({ ref: "Pty" })
|
|
||||||
|
|
||||||
export type Info = z.infer<typeof Info>
|
|
||||||
|
|
||||||
export const CreateInput = z.object({
|
|
||||||
command: z.string().optional(),
|
|
||||||
args: z.array(z.string()).optional(),
|
|
||||||
cwd: z.string().optional(),
|
|
||||||
title: z.string().optional(),
|
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
|
||||||
})
|
})
|
||||||
|
.annotate({ identifier: "Pty" })
|
||||||
|
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
|
||||||
export type CreateInput = z.infer<typeof CreateInput>
|
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||||
|
|
||||||
export const UpdateInput = z.object({
|
export const CreateInput = Schema.Struct({
|
||||||
title: z.string().optional(),
|
command: Schema.optional(Schema.String),
|
||||||
size: z
|
args: Schema.optional(Schema.Array(Schema.String)),
|
||||||
.object({
|
cwd: Schema.optional(Schema.String),
|
||||||
rows: z.number(),
|
title: Schema.optional(Schema.String),
|
||||||
cols: z.number(),
|
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||||
})
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type UpdateInput = z.infer<typeof UpdateInput>
|
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||||
|
|
||||||
|
export const UpdateInput = Schema.Struct({
|
||||||
|
title: Schema.optional(Schema.String),
|
||||||
|
size: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
rows: Schema.Number,
|
||||||
|
cols: Schema.Number,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
|
|
||||||
|
export type UpdateInput = Types.DeepMutable<Schema.Schema.Type<typeof UpdateInput>>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Created: BusEvent.define("pty.created", z.object({ info: Info })),
|
Created: BusEvent.define("pty.created", Schema.Struct({ info: Info })),
|
||||||
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
|
Updated: BusEvent.define("pty.updated", Schema.Struct({ info: Info })),
|
||||||
Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
|
Exited: BusEvent.define("pty.exited", Schema.Struct({ id: PtyID, exitCode: Schema.Number })),
|
||||||
Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
|
Deleted: BusEvent.define("pty.deleted", Schema.Struct({ id: PtyID })),
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
|
|||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Asked: BusEvent.define("question.asked", Request.zod),
|
Asked: BusEvent.define("question.asked", Request),
|
||||||
Replied: BusEvent.define("question.replied", zod(Replied)),
|
Replied: BusEvent.define("question.replied", Replied),
|
||||||
Rejected: BusEvent.define("question.rejected", zod(Rejected)),
|
Rejected: BusEvent.define("question.rejected", Rejected),
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
|
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
|
||||||
@@ -194,7 +194,7 @@ export const layer = Layer.effect(
|
|||||||
yield* bus.publish(Event.Replied, {
|
yield* bus.publish(Event.Replied, {
|
||||||
sessionID: existing.info.sessionID,
|
sessionID: existing.info.sessionID,
|
||||||
requestID: existing.info.id,
|
requestID: existing.info.id,
|
||||||
answers: input.answers,
|
answers: input.answers.map((a) => [...a]),
|
||||||
})
|
})
|
||||||
yield* Deferred.succeed(existing.deferred, input.answers)
|
yield* Deferred.succeed(existing.deferred, input.answers)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import z from "zod"
|
import { Schema } from "effect"
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Connected: BusEvent.define("server.connected", z.object({})),
|
Connected: BusEvent.define("server.connected", Schema.Struct({})),
|
||||||
Disposed: BusEvent.define("global.disposed", z.object({})),
|
Disposed: BusEvent.define("global.disposed", Schema.Struct({})),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describeRoute, resolver, validator } from "hono-openapi"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { listAdaptors } from "@/control-plane/adaptors"
|
import { listAdaptors } from "@/control-plane/adaptors"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
|
import { zodObject } from "@/util/effect-zod"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { errors } from "../../error"
|
import { errors } from "../../error"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
@@ -107,7 +108,7 @@ export const WorkspaceRoutes = lazy(() =>
|
|||||||
description: "Workspace status",
|
description: "Workspace status",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.array(Workspace.ConnectionStatus)),
|
schema: resolver(z.array(zodObject(Workspace.ConnectionStatus))),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono, type Context } from "hono"
|
import { Hono, type Context } from "hono"
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import { streamSSE } from "hono/streaming"
|
import { streamSSE } from "hono/streaming"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
@@ -18,7 +18,7 @@ import { errors } from "../error"
|
|||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
|
export const GlobalDisposedEvent = BusEvent.define("global.disposed", Schema.Struct({}))
|
||||||
|
|
||||||
async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
|
async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
|
||||||
return streamSSE(c, async (stream) => {
|
return streamSSE(c, async (stream) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
description: "List of sessions",
|
description: "List of sessions",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(Pty.Info.array()),
|
schema: resolver(Pty.Info.zod.array()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -46,18 +46,18 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
description: "Created session",
|
description: "Created session",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(Pty.Info),
|
schema: resolver(Pty.Info.zod),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...errors(400),
|
...errors(400),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", Pty.CreateInput),
|
validator("json", Pty.CreateInput.zod),
|
||||||
async (c) =>
|
async (c) =>
|
||||||
jsonRequest("PtyRoutes.create", c, function* () {
|
jsonRequest("PtyRoutes.create", c, function* () {
|
||||||
const pty = yield* Pty.Service
|
const pty = yield* Pty.Service
|
||||||
return yield* pty.create(c.req.valid("json"))
|
return yield* pty.create(c.req.valid("json") as Pty.CreateInput)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -71,7 +71,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
description: "Session info",
|
description: "Session info",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(Pty.Info),
|
schema: resolver(Pty.Info.zod),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -105,7 +105,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
description: "Updated session",
|
description: "Updated session",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(Pty.Info),
|
schema: resolver(Pty.Info.zod),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -113,11 +113,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||||
validator("json", Pty.UpdateInput),
|
validator("json", Pty.UpdateInput.zod),
|
||||||
async (c) =>
|
async (c) =>
|
||||||
jsonRequest("PtyRoutes.update", c, function* () {
|
jsonRequest("PtyRoutes.update", c, function* () {
|
||||||
const pty = yield* Pty.Service
|
const pty = yield* Pty.Service
|
||||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json") as Pty.UpdateInput)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Hono, type Context } from "hono"
|
import { Hono, type Context } from "hono"
|
||||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Session } from "@/session"
|
import { Session } from "@/session"
|
||||||
|
import type { SessionID } from "@/session/schema"
|
||||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||||
|
import { zodObject } from "@/util/effect-zod"
|
||||||
import { AsyncQueue } from "@/util/queue"
|
import { AsyncQueue } from "@/util/queue"
|
||||||
import { errors } from "../../error"
|
import { errors } from "../../error"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
@@ -96,9 +99,9 @@ export const TuiRoutes = lazy(() =>
|
|||||||
...errors(400),
|
...errors(400),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", TuiEvent.PromptAppend.properties),
|
validator("json", zodObject(TuiEvent.PromptAppend.properties)),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
|
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json") as { text: string })
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -305,9 +308,9 @@ export const TuiRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", TuiEvent.ToastShow.properties),
|
validator("json", zodObject(TuiEvent.ToastShow.properties)),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
|
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json") as Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -336,7 +339,7 @@ export const TuiRoutes = lazy(() =>
|
|||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal(def.type),
|
type: z.literal(def.type),
|
||||||
properties: def.properties,
|
properties: zodObject(def.properties),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: `Event.${def.type}`,
|
ref: `Event.${def.type}`,
|
||||||
@@ -345,8 +348,9 @@ export const TuiRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const evt = c.req.valid("json")
|
const evt = c.req.valid("json") as { type: string; properties: Record<string, unknown> }
|
||||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)! as any, evt.properties as any)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -368,9 +372,9 @@ export const TuiRoutes = lazy(() =>
|
|||||||
...errors(400, 404),
|
...errors(400, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", TuiEvent.SessionSelect.properties),
|
validator("json", zodObject(TuiEvent.SessionSelect.properties)),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { sessionID } = c.req.valid("json")
|
const { sessionID } = c.req.valid("json") as { sessionID: SessionID }
|
||||||
await runRequest(
|
await runRequest(
|
||||||
"TuiRoutes.sessionSelect",
|
"TuiRoutes.sessionSelect",
|
||||||
c,
|
c,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Plugin } from "@/plugin"
|
|||||||
import { Config } from "@/config"
|
import { Config } from "@/config"
|
||||||
import { NotFoundError } from "@/storage"
|
import { NotFoundError } from "@/storage"
|
||||||
import { ModelID, ProviderID } from "@/provider/schema"
|
import { ModelID, ProviderID } from "@/provider/schema"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context, Schema } from "effect"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { isOverflow as overflow, usable } from "./overflow"
|
import { isOverflow as overflow, usable } from "./overflow"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
@@ -24,8 +24,8 @@ const log = Log.create({ service: "session.compaction" })
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Compacted: BusEvent.define(
|
Compacted: BusEvent.define(
|
||||||
"session.compacted",
|
"session.compacted",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -617,12 +617,12 @@ export const Event = {
|
|||||||
}),
|
}),
|
||||||
PartDelta: BusEvent.define(
|
PartDelta: BusEvent.define(
|
||||||
"message.part.delta",
|
"message.part.delta",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
messageID: MessageID.zod,
|
messageID: MessageID,
|
||||||
partID: PartID.zod,
|
partID: PartID,
|
||||||
field: z.string(),
|
field: Schema.String,
|
||||||
delta: z.string(),
|
delta: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
PartRemoved: SyncEvent.define({
|
PartRemoved: SyncEvent.define({
|
||||||
|
|||||||
@@ -273,17 +273,18 @@ export const Event = {
|
|||||||
}),
|
}),
|
||||||
Diff: BusEvent.define(
|
Diff: BusEvent.define(
|
||||||
"session.diff",
|
"session.diff",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
diff: Snapshot.FileDiff.zod.array(),
|
diff: Schema.Array(Snapshot.FileDiff),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Error: BusEvent.define(
|
Error: BusEvent.define(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod.optional(),
|
sessionID: Schema.optional(SessionID),
|
||||||
// z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session
|
// Reuses MessageV2.Assistant.fields.error (already Schema.optional) so
|
||||||
error: z.lazy(() => (MessageV2.Assistant.zod as unknown as z.ZodObject<any>).shape.error),
|
// the derived zod keeps the same discriminated-union shape on the bus.
|
||||||
|
error: MessageV2.Assistant.fields.error,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ export type Info = Schema.Schema.Type<typeof Info>
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Status: BusEvent.define(
|
Status: BusEvent.define(
|
||||||
"session.status",
|
"session.status",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
status: Info.zod,
|
status: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
// deprecated
|
// deprecated
|
||||||
Idle: BusEvent.define(
|
Idle: BusEvent.define(
|
||||||
"session.idle",
|
"session.idle",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ export type Info = Schema.Schema.Type<typeof Info>
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: BusEvent.define(
|
Updated: BusEvent.define(
|
||||||
"todo.updated",
|
"todo.updated",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID,
|
||||||
todos: z.array(Info.zod),
|
todos: Schema.Array(Info),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,51 +8,48 @@ import { EventSequenceTable, EventTable } from "./event.sql"
|
|||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { EventID } from "./schema"
|
import { EventID } from "./schema"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Schema as EffectSchema, Types } from "effect"
|
import { Schema as EffectSchema } from "effect"
|
||||||
import { zodObject } from "@/util/effect-zod"
|
import { zodObject } from "@/util/effect-zod"
|
||||||
import { isRecord } from "@/util/record"
|
import type { DeepMutable } from "@/util/schema"
|
||||||
|
|
||||||
|
// Keep `Event["data"]` mutable because projectors mutate the persisted shape
|
||||||
|
// when writing to the database. Bus payloads (`Properties`) stay readonly —
|
||||||
|
// subscribers only read.
|
||||||
|
|
||||||
export type Definition<
|
export type Definition<
|
||||||
|
Type extends string = string,
|
||||||
Schema extends EffectSchema.Top = EffectSchema.Top,
|
Schema extends EffectSchema.Top = EffectSchema.Top,
|
||||||
BusSchema extends EffectSchema.Top = Schema,
|
BusSchema extends EffectSchema.Top = Schema,
|
||||||
> = {
|
> = {
|
||||||
type: string
|
type: Type
|
||||||
version: number
|
version: number
|
||||||
aggregate: string
|
aggregate: string
|
||||||
effectSchema: Schema
|
schema: Schema
|
||||||
effectProperties: BusSchema
|
// Bus event payload schema. Defaults to `schema` unless `busSchema` was
|
||||||
schema: z.ZodObject
|
// passed at definition time (see `session.updated`, whose projector
|
||||||
|
// expands the persisted data to a `{ sessionID, info }` bus payload).
|
||||||
// This is temporary and only exists for compatibility with bus
|
properties: BusSchema
|
||||||
// event definitions
|
|
||||||
properties: z.ZodObject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Event<Def extends Definition = Definition> = {
|
export type Event<Def extends Definition = Definition> = {
|
||||||
id: string
|
id: string
|
||||||
seq: number
|
seq: number
|
||||||
aggregateID: string
|
aggregateID: string
|
||||||
data: Types.DeepMutable<EffectSchema.Schema.Type<Def["effectSchema"]>>
|
data: DeepMutable<EffectSchema.Schema.Type<Def["schema"]>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Properties<Def extends Definition = Definition> = Types.DeepMutable<
|
export type Properties<Def extends Definition = Definition> = EffectSchema.Schema.Type<Def["properties"]>
|
||||||
EffectSchema.Schema.Type<Def["effectProperties"]>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
|
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
|
||||||
|
|
||||||
type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void
|
type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void
|
||||||
|
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
|
||||||
|
|
||||||
export const registry = new Map<string, Definition>()
|
export const registry = new Map<string, Definition>()
|
||||||
let projectors: Map<Definition, ProjectorFunc> | undefined
|
let projectors: Map<Definition, ProjectorFunc> | undefined
|
||||||
const versions = new Map<string, number>()
|
const versions = new Map<string, number>()
|
||||||
let frozen = false
|
let frozen = false
|
||||||
let convertEvent: (type: string, event: Event["data"]) => Promise<unknown> | unknown
|
let convertEvent: ConvertEvent
|
||||||
|
|
||||||
function asRecord(input: unknown) {
|
|
||||||
if (isRecord(input)) return input
|
|
||||||
throw new Error(`SyncEvent.convertEvent must return an object, got: ${JSON.stringify(input)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reset() {
|
export function reset() {
|
||||||
frozen = false
|
frozen = false
|
||||||
@@ -60,7 +57,7 @@ export function reset() {
|
|||||||
convertEvent = (_, data) => data
|
convertEvent = (_, data) => data
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) {
|
export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: ConvertEvent }) {
|
||||||
projectors = new Map(input.projectors)
|
projectors = new Map(input.projectors)
|
||||||
|
|
||||||
// Install all the latest event defs to the bus. We only ever emit
|
// Install all the latest event defs to the bus. We only ever emit
|
||||||
@@ -76,7 +73,7 @@ export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; co
|
|||||||
// Freeze the system so it clearly errors if events are defined
|
// Freeze the system so it clearly errors if events are defined
|
||||||
// after `init` which would cause bugs
|
// after `init` which would cause bugs
|
||||||
frozen = true
|
frozen = true
|
||||||
convertEvent = input.convertEvent || ((_, data) => data)
|
convertEvent = input.convertEvent ?? ((_, data) => data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function versionedType<A extends string>(type: A): A
|
export function versionedType<A extends string>(type: A): A
|
||||||
@@ -96,21 +93,17 @@ export function define<
|
|||||||
aggregate: Agg
|
aggregate: Agg
|
||||||
schema: Schema
|
schema: Schema
|
||||||
busSchema?: BusSchema
|
busSchema?: BusSchema
|
||||||
}): Definition<Schema, BusSchema> {
|
}): Definition<Type, Schema, BusSchema> {
|
||||||
if (frozen) {
|
if (frozen) {
|
||||||
throw new Error("Error defining sync event: sync system has been frozen")
|
throw new Error("Error defining sync event: sync system has been frozen")
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectProperties = (input.busSchema ?? input.schema) as BusSchema
|
|
||||||
|
|
||||||
const def = {
|
const def = {
|
||||||
type: input.type,
|
type: input.type,
|
||||||
version: input.version,
|
version: input.version,
|
||||||
aggregate: input.aggregate,
|
aggregate: input.aggregate,
|
||||||
effectSchema: input.schema,
|
schema: input.schema,
|
||||||
effectProperties,
|
properties: (input.busSchema ?? input.schema) as BusSchema,
|
||||||
schema: zodObject(input.schema),
|
|
||||||
properties: zodObject(effectProperties),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
|
versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
|
||||||
@@ -167,12 +160,11 @@ function process<Def extends Definition>(def: Def, event: Event<Def>, options: {
|
|||||||
Database.effect(() => {
|
Database.effect(() => {
|
||||||
if (options?.publish) {
|
if (options?.publish) {
|
||||||
const result = convertEvent(def.type, event.data)
|
const result = convertEvent(def.type, event.data)
|
||||||
|
const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>)
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
void result.then((data) => {
|
void result.then(publish)
|
||||||
void ProjectBus.publish({ type: def.type, properties: def.properties }, asRecord(data))
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
void ProjectBus.publish({ type: def.type, properties: def.properties }, asRecord(result))
|
void publish(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
@@ -292,7 +284,7 @@ export function payloads() {
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
seq: z.number(),
|
seq: z.number(),
|
||||||
aggregateID: z.literal(def.aggregate),
|
aggregateID: z.literal(def.aggregate),
|
||||||
data: def.schema,
|
data: zodObject(def.schema),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: `SyncEvent.${def.type}`,
|
ref: `SyncEvent.${def.type}`,
|
||||||
|
|||||||
@@ -59,8 +59,17 @@ function walk(ast: SchemaAST.AST): z.ZodTypeAny {
|
|||||||
|
|
||||||
function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||||
const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined
|
const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined
|
||||||
if (override) return override
|
// `description` annotations layer on top of an override so callers can
|
||||||
|
// reuse a shared override schema (e.g. `SessionID`) and still add a
|
||||||
|
// per-field description on the outer wrapper.
|
||||||
|
const base = override ?? bodyWithChecks(ast)
|
||||||
|
const desc = SchemaAST.resolveDescription(ast)
|
||||||
|
const ref = SchemaAST.resolveIdentifier(ast)
|
||||||
|
const described = desc ? base.describe(desc) : base
|
||||||
|
return ref ? described.meta({ ref }) : described
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||||
// Schema.Class wraps its fields in a Declaration AST plus an encoding that
|
// Schema.Class wraps its fields in a Declaration AST plus an encoding that
|
||||||
// constructs the class instance. For the Zod derivation we want the plain
|
// constructs the class instance. For the Zod derivation we want the plain
|
||||||
// field shape (the decoded/consumer view), not the class instance — so
|
// field shape (the decoded/consumer view), not the class instance — so
|
||||||
@@ -74,11 +83,7 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
|||||||
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
||||||
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
||||||
const base = hasTransform ? encoded(ast) : body(ast)
|
const base = hasTransform ? encoded(ast) : body(ast)
|
||||||
const checked = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||||
const desc = SchemaAST.resolveDescription(ast)
|
|
||||||
const ref = SchemaAST.resolveIdentifier(ast)
|
|
||||||
const described = desc ? checked.describe(desc) : checked
|
|
||||||
return ref ? described.meta({ ref }) : described
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the encoded side and apply each link's decode to produce the decoded
|
// Walk the encoded side and apply each link's decode to produce the decoded
|
||||||
|
|||||||
@@ -10,6 +10,34 @@ export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
|
|||||||
*/
|
*/
|
||||||
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
|
||||||
|
* until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands.
|
||||||
|
*
|
||||||
|
* The upstream version falls through `unknown` into `{ -readonly [K in keyof T]: ... }`
|
||||||
|
* where `keyof unknown = never`, so `unknown` collapses to `{}`. This local
|
||||||
|
* version gates the object branch on `extends object` (which `unknown` does
|
||||||
|
* not) so `unknown` passes through untouched.
|
||||||
|
*
|
||||||
|
* Primitive bailout matches upstream — without it, branded strings like
|
||||||
|
* `string & Brand<"SessionID">` fall into the object branch and get their
|
||||||
|
* prototype methods walked.
|
||||||
|
*
|
||||||
|
* Tuple branch preserves readonly tuples (e.g. `ConfigPlugin.Spec`'s
|
||||||
|
* `readonly [string, Options]`); the general array branch would otherwise
|
||||||
|
* widen them to unbounded arrays.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export type DeepMutable<T> = T extends string | number | boolean | bigint | symbol | Function
|
||||||
|
? T
|
||||||
|
: T extends readonly [unknown, ...unknown[]]
|
||||||
|
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||||
|
: T extends readonly (infer U)[]
|
||||||
|
? DeepMutable<U>[]
|
||||||
|
: T extends object
|
||||||
|
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||||
|
: T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach static methods to a schema object. Designed to be used with `.pipe()`:
|
* Attach static methods to a schema object. Designed to be used with `.pipe()`:
|
||||||
*
|
*
|
||||||
@@ -26,13 +54,16 @@ export const withStatics =
|
|||||||
(schema: S): S & M =>
|
(schema: S): S & M =>
|
||||||
Object.assign(schema, methods(schema))
|
Object.assign(schema, methods(schema))
|
||||||
|
|
||||||
declare const NewtypeBrand: unique symbol
|
|
||||||
type NewtypeBrand<Tag extends string> = { readonly [NewtypeBrand]: Tag }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nominal wrapper for scalar types. The class itself is a valid schema —
|
* Nominal wrapper for scalar types. The class itself is a valid schema —
|
||||||
* pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
|
* pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
|
||||||
*
|
*
|
||||||
|
* Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type`
|
||||||
|
* of a field using this newtype resolves to `Self` rather than the underlying
|
||||||
|
* branded phantom. Without that override, passing a class instance to code
|
||||||
|
* typed against `Schema.Schema.Type<FieldSchema>` would require a cast even
|
||||||
|
* though the values are structurally equivalent at runtime.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
|
* class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
|
||||||
* static make(id: string): QuestionID {
|
* static make(id: string): QuestionID {
|
||||||
@@ -44,10 +75,8 @@ type NewtypeBrand<Tag extends string> = { readonly [NewtypeBrand]: Tag }
|
|||||||
*/
|
*/
|
||||||
export function Newtype<Self>() {
|
export function Newtype<Self>() {
|
||||||
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
|
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
|
||||||
type Branded = NewtypeBrand<Tag>
|
|
||||||
|
|
||||||
abstract class Base {
|
abstract class Base {
|
||||||
declare readonly [NewtypeBrand]: Tag
|
declare readonly _newtype: Tag
|
||||||
|
|
||||||
static make(value: Schema.Schema.Type<S>): Self {
|
static make(value: Schema.Schema.Type<S>): Self {
|
||||||
return value as unknown as Self
|
return value as unknown as Self
|
||||||
@@ -56,8 +85,10 @@ export function Newtype<Self>() {
|
|||||||
|
|
||||||
Object.setPrototypeOf(Base, schema)
|
Object.setPrototypeOf(Base, schema)
|
||||||
|
|
||||||
return Base as unknown as (abstract new (_: never) => Branded) & {
|
return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & {
|
||||||
readonly make: (value: Schema.Schema.Type<S>) => Self
|
readonly make: (value: Schema.Schema.Type<S>) => Self
|
||||||
} & Omit<Schema.Opaque<Self, S, {}>, "make">
|
} & Omit<Schema.Opaque<Self, S, {}>, "make" | "~type.make"> & {
|
||||||
|
readonly "~type.make": Self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { errorMessage } from "../util/error"
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
|
import { Effect, Layer, Path, Schema, Scope, Context, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { NodePath } from "@effect/platform-node"
|
import { NodePath } from "@effect/platform-node"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
@@ -26,15 +26,15 @@ const log = Log.create({ service: "worktree" })
|
|||||||
export const Event = {
|
export const Event = {
|
||||||
Ready: BusEvent.define(
|
Ready: BusEvent.define(
|
||||||
"worktree.ready",
|
"worktree.ready",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
name: z.string(),
|
name: Schema.String,
|
||||||
branch: z.string(),
|
branch: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Failed: BusEvent.define(
|
Failed: BusEvent.define(
|
||||||
"worktree.failed",
|
"worktree.failed",
|
||||||
z.object({
|
Schema.Struct({
|
||||||
message: z.string(),
|
message: Schema.String,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Deferred, Effect, Layer, Stream } from "effect"
|
import { Deferred, Effect, Layer, Schema, Stream } from "effect"
|
||||||
import z from "zod"
|
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
@@ -9,8 +8,8 @@ import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const TestEvent = {
|
const TestEvent = {
|
||||||
Ping: BusEvent.define("test.effect.ping", z.object({ value: z.number() })),
|
Ping: BusEvent.define("test.effect.ping", Schema.Struct({ value: Schema.Number })),
|
||||||
Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })),
|
Pong: BusEvent.define("test.effect.pong", Schema.Struct({ message: Schema.String })),
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = CrossSpawnSpawner.defaultLayer
|
const node = CrossSpawnSpawner.defaultLayer
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import z from "zod"
|
import { Schema } from "effect"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
const TestEvent = BusEvent.define("test.integration", z.object({ value: z.number() }))
|
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
||||||
|
|
||||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||||
return Instance.provide({ directory, fn })
|
return Instance.provide({ directory, fn })
|
||||||
@@ -42,7 +42,7 @@ describe("Bus integration: acquireRelease subscriber pattern", () => {
|
|||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
const received: Array<{ type: string; value?: number }> = []
|
const received: Array<{ type: string; value?: number }> = []
|
||||||
|
|
||||||
const OtherEvent = BusEvent.define("test.other", z.object({ value: z.number() }))
|
const OtherEvent = BusEvent.define("test.other", Schema.Struct({ value: Schema.Number }))
|
||||||
|
|
||||||
await withInstance(tmp.path, async () => {
|
await withInstance(tmp.path, async () => {
|
||||||
Bus.subscribeAll((evt) => {
|
Bus.subscribeAll((evt) => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import z from "zod"
|
import { Schema } from "effect"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
const TestEvent = {
|
const TestEvent = {
|
||||||
Ping: BusEvent.define("test.ping", z.object({ value: z.number() })),
|
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
||||||
Pong: BusEvent.define("test.pong", z.object({ message: z.string() })),
|
Pong: BusEvent.define("test.pong", Schema.Struct({ message: Schema.String })),
|
||||||
}
|
}
|
||||||
|
|
||||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||||
|
|||||||
@@ -111,9 +111,12 @@ describe("step-finish token propagation via Bus event", () => {
|
|||||||
mode: "",
|
mode: "",
|
||||||
} as unknown as MessageV2.Info)
|
} as unknown as MessageV2.Info)
|
||||||
|
|
||||||
|
// Bus subscribers receive readonly Schema.Type payloads; `MessageV2.Part`
|
||||||
|
// is the mutable domain type. Cast bridges the two — safe because the
|
||||||
|
// test only reads the value afterwards.
|
||||||
let received: MessageV2.Part | undefined
|
let received: MessageV2.Part | undefined
|
||||||
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, (event) => {
|
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, (event) => {
|
||||||
received = event.properties.part
|
received = event.properties.part as MessageV2.Part
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokens = {
|
const tokens = {
|
||||||
|
|||||||
Reference in New Issue
Block a user