Files
rea/src/hopper/protocol.ts
T
morluto 735c10059c refactor(domain): split error taxonomy into thematic modules (#636)
Move each error family out of domain/errors.ts into its own module
(base, core, provider selection/adapter, browser observation,
artifact, evidence, unknown registry, hopper, configuration) and
relocate AnalysisErrorProjection to analysisErrorProjection.
Migrate all 162 importers to the owning modules and delete
errors.ts. No re-export shims; behavior is unchanged.
2026-10-06 05:57:38 +08:00

120 lines
3.1 KiB
TypeScript

import { z } from "zod";
import {
HopperProtocolError,
HopperRemoteError,
} from "../domain/hopperErrors.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { safeParseJson } from "../domain/safeJson.js";
const remoteErrorSchema = z
.object({
code: z.number().int(),
message: z.string(),
type: z
.enum([
"remote",
"authorization",
"invalid_request",
"capability_unavailable",
"bridge_exception",
])
.default("remote"),
})
.strict();
const responseSchema = z.union([
z
.object({
id: z.number().int().nonnegative(),
result: jsonValueSchema,
})
.strict(),
z
.object({
id: z.number().int().nonnegative(),
error: remoteErrorSchema,
})
.strict(),
]);
const eventSchema = z
.object({
id: z.number().int().nonnegative(),
event: z.discriminatedUnion("type", [
z
.object({
type: z.literal("progress"),
phase: z.string().min(1),
completed: z.number().nonnegative().finite(),
total: z.number().nonnegative().finite().nullable(),
message: z.string().min(1),
terminal: z.boolean().optional(),
})
.strict(),
z
.object({
type: z.literal("diagnostic"),
error: remoteErrorSchema,
})
.strict(),
]),
})
.strict()
.superRefine((message, context) => {
if (
message.event.type === "progress" &&
message.event.total !== null &&
message.event.completed > message.event.total
) {
context.addIssue({
code: "custom",
message: "Progress total cannot be less than completed",
path: ["event", "total"],
});
}
});
const messageSchema = z.union([responseSchema, eventSchema]);
export type HopperResponse = z.infer<typeof responseSchema>;
export type HopperBridgeEvent = z.infer<typeof eventSchema>;
export type HopperBridgeMessage = z.infer<typeof messageSchema>;
/** Parse one complete Hopper NDJSON response or event line. */
export const parseBridgeMessageLine = (
line: string,
): Result<HopperBridgeMessage, HopperProtocolError> => {
const decodedResult = safeParseJson(line);
if (!decodedResult.ok)
return err(
new HopperProtocolError("Hopper returned malformed JSON", {
cause: decodedResult.cause,
}),
);
const decoded: unknown = decodedResult.value;
const parsed = messageSchema.safeParse(decoded);
return parsed.success
? ok(parsed.data)
: err(
new HopperProtocolError(
"Hopper returned a message outside the bridge contract",
{ cause: parsed.error },
),
);
};
/** Project a parsed response into its result or expected remote failure. */
export const responseResult = (
response: HopperResponse,
): Result<JsonValue, HopperRemoteError> =>
"error" in response
? err(
new HopperRemoteError(response.error.code, response.error.message, {
diagnosticType: response.error.type,
}),
)
: ok(response.result);