|
1 | 1 | export * as EventFeed from "./event-feed" |
2 | 2 |
|
3 | 3 | import { EventV2 } from "@opencode-ai/core/event" |
4 | | -import { isOpenCodeEvent, OpenCodeEvent, sessionIDOf } from "@opencode-ai/protocol/groups/event" |
5 | | -import { Cause, Context, Effect, Layer, Queue, Schema, Scope, Stream } from "effect" |
| 4 | +import { EventFeedDiagnostics } from "@opencode-ai/protocol/groups/debug" |
| 5 | +import { |
| 6 | + EventSubscribeQuery, |
| 7 | + isOpenCodeEvent, |
| 8 | + OpenCodeEvent, |
| 9 | + sessionIDOf, |
| 10 | +} from "@opencode-ai/protocol/groups/event" |
| 11 | +import { Cause, Context, Effect, Layer, Option, Queue, Schema, Scope, Stream } from "effect" |
6 | 12 |
|
7 | 13 | export const SubscriberCapacity = 4_096 |
8 | 14 |
|
@@ -38,14 +44,7 @@ export type Interest = { |
38 | 44 | } |
39 | 45 |
|
40 | 46 | /** Content-free counters for leak detection — never includes event payloads. */ |
41 | | -export type Diagnostics = { |
42 | | - readonly active: number |
43 | | - readonly opens: number |
44 | | - readonly closes: number |
45 | | - readonly serializedEvents: number |
46 | | - readonly serializedBytes: number |
47 | | - readonly overflows: number |
48 | | -} |
| 47 | +export type Diagnostics = typeof EventFeedDiagnostics.Type |
49 | 48 |
|
50 | 49 | export interface Interface { |
51 | 50 | readonly subscribe: (interest?: Interest) => Effect.Effect<Stream.Stream<string, Error>, never, Scope.Scope> |
@@ -81,25 +80,59 @@ export function matchesInterest(event: EventV2.Payload, interest?: Interest): bo |
81 | 80 | return true |
82 | 81 | } |
83 | 82 |
|
84 | | -export function interestFromQuery(query: URLSearchParams): Interest | undefined { |
85 | | - const directory = query.get("location[directory]") ?? undefined |
86 | | - const workspace = query.get("location[workspace]") ?? undefined |
87 | | - const sessions = query |
| 83 | +const decodeSubscribeQuery = Schema.decodeUnknownOption(EventSubscribeQuery) |
| 84 | + |
| 85 | +/** Map an already-decoded subscribe query into feed interest (HttpApi `ctx.query`). */ |
| 86 | +export function interestFromSubscribeQuery(query: typeof EventSubscribeQuery.Type): Interest | undefined { |
| 87 | + const sessions = normalizeSessions(query.session) |
| 88 | + if (query.location === undefined && sessions.length === 0) return undefined |
| 89 | + return { |
| 90 | + ...(query.location |
| 91 | + ? { |
| 92 | + location: { |
| 93 | + directory: query.location.directory, |
| 94 | + ...(query.location.workspace !== undefined ? { workspace: query.location.workspace } : {}), |
| 95 | + }, |
| 96 | + } |
| 97 | + : {}), |
| 98 | + ...(sessions.length > 0 ? { sessions } : {}), |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +/** Parse deepObject / repeated `session` search params through `EventSubscribeQuery`. */ |
| 103 | +export function interestFromQuery(params: URLSearchParams): Interest | undefined { |
| 104 | + const decoded = Option.getOrUndefined(decodeSubscribeQuery(subscribeQueryCandidate(params))) |
| 105 | + if (decoded === undefined) return undefined |
| 106 | + return interestFromSubscribeQuery(decoded) |
| 107 | +} |
| 108 | + |
| 109 | +function subscribeQueryCandidate(params: URLSearchParams) { |
| 110 | + const directory = params.get("location[directory]") ?? undefined |
| 111 | + const workspace = params.get("location[workspace]") ?? undefined |
| 112 | + const sessions = params |
88 | 113 | .getAll("session") |
89 | 114 | .flatMap((value) => value.split(",")) |
90 | 115 | .map((value) => value.trim()) |
91 | 116 | .filter((value) => value.length > 0) |
92 | | - // Workspace interest requires a directory; alone it is an invalid location scope. |
93 | | - if (directory === undefined && workspace !== undefined) return undefined |
94 | | - if (directory === undefined && sessions.length === 0) return undefined |
95 | 117 | return { |
96 | | - ...(directory !== undefined |
97 | | - ? { location: { directory, ...(workspace !== undefined ? { workspace } : {}) } } |
| 118 | + ...(directory !== undefined || workspace !== undefined |
| 119 | + ? { |
| 120 | + location: { |
| 121 | + ...(directory !== undefined ? { directory } : {}), |
| 122 | + ...(workspace !== undefined ? { workspace } : {}), |
| 123 | + }, |
| 124 | + } |
98 | 125 | : {}), |
99 | | - ...(sessions.length > 0 ? { sessions } : {}), |
| 126 | + ...(sessions.length === 0 ? {} : { session: sessions.length === 1 ? sessions[0] : sessions }), |
100 | 127 | } |
101 | 128 | } |
102 | 129 |
|
| 130 | +function normalizeSessions(session: typeof EventSubscribeQuery.Type.session): string[] { |
| 131 | + if (session === undefined) return [] |
| 132 | + if (typeof session === "string") return session.trim() === "" ? [] : [session] |
| 133 | + return session.map((value) => value.trim()).filter((value) => value.length > 0) |
| 134 | +} |
| 135 | + |
103 | 136 | type Subscriber = { |
104 | 137 | readonly queue: Queue.Queue<string, Error> |
105 | 138 | readonly interest?: Interest |
|
0 commit comments