import { z } from 'zod'; import { RpcCancelSchema, RpcErrorSchema, RpcRequestSchema, RpcResponseSchema, type RpcCancel, type RpcEnvelope, type RpcError, type RpcRequest, type RpcResponse, } from '../schemas/envelope.js'; import { KIND_PREFIX } from './version.js'; /** Tagged classification of any incoming envelope. */ export type ClassifiedEnvelope = | { kind: 'request'; envelope: RpcRequest } | { kind: 'response'; envelope: RpcResponse } | { kind: 'error'; envelope: RpcError } | { kind: 'cancel'; envelope: RpcCancel }; const RpcAnySchema = z.union([ RpcRequestSchema, RpcResponseSchema, RpcErrorSchema, RpcCancelSchema, ]); /** Encode an envelope to JSON plaintext for `Shade.send`. */ export function encodeEnvelope(env: RpcEnvelope): string { return JSON.stringify(env); } /** * Quick-detection: does this plaintext look like an `@shade/files` envelope? * Used by `ShadeFileRpcChannel` to skip non-files messages cheaply. */ export function looksLikeFileEnvelope(plaintext: string): boolean { return plaintext.includes(KIND_PREFIX); } /** * Parse and classify an incoming plaintext. Returns null on any malformed * input — the channel ignores those silently (could be a different * protocol on the same Shade.send pipe). */ export function tryParseEnvelope(plaintext: string): ClassifiedEnvelope | null { let raw: unknown; try { raw = JSON.parse(plaintext); } catch { return null; } const result = RpcAnySchema.safeParse(raw); if (!result.success) return null; return classify(result.data); } export function classify(env: RpcEnvelope): ClassifiedEnvelope { if (env.kind === 'shade.fs.cancel/v1') { return { kind: 'cancel', envelope: env as RpcCancel }; } if (env.kind === 'shade.fs.error/v1') { return { kind: 'error', envelope: env as RpcError }; } if (env.kind.endsWith('.response')) { return { kind: 'response', envelope: env as RpcResponse }; } return { kind: 'request', envelope: env as RpcRequest }; }