Unify live session streaming and evidence fetching

- Route live terminal data through a shared live session socket
- Fetch missing evidence for alerts and classifier hits
- Add live type definitions and storage/API tests
This commit is contained in:
dirtydishes 2026-04-27 13:14:10 -04:00
parent 824b7f2fa0
commit d30513119a
10 changed files with 1923 additions and 258 deletions

View file

@ -1,2 +1,3 @@
export * from "./events";
export * from "./live";
export * from "./sp500";

182
packages/types/src/live.ts Normal file
View file

@ -0,0 +1,182 @@
import { z } from "zod";
import {
AlertEventSchema,
ClassifierHitEventSchema,
EquityCandleSchema,
EquityPrintJoinSchema,
EquityPrintSchema,
FlowPacketSchema,
InferredDarkEventSchema,
OptionNBBOSchema,
OptionPrintSchema
} from "./events";
export const CursorSchema = z.object({
ts: z.number().int().nonnegative(),
seq: z.number().int().nonnegative()
});
export type Cursor = z.infer<typeof CursorSchema>;
export const LiveGenericChannelSchema = z.enum([
"options",
"nbbo",
"equities",
"equity-joins",
"flow",
"classifier-hits",
"alerts",
"inferred-dark"
]);
export const LiveChannelSchema = z.enum([
"options",
"nbbo",
"equities",
"equity-joins",
"flow",
"classifier-hits",
"alerts",
"inferred-dark",
"equity-candles",
"equity-overlay"
]);
export type LiveChannel = z.infer<typeof LiveChannelSchema>;
export type LiveGenericChannel = z.infer<typeof LiveGenericChannelSchema>;
export const LiveSubscriptionSchema = z.discriminatedUnion("channel", [
z.object({
channel: LiveGenericChannelSchema
}),
z.object({
channel: z.literal("equity-candles"),
underlying_id: z.string().min(1),
interval_ms: z.number().int().positive()
}),
z.object({
channel: z.literal("equity-overlay"),
underlying_id: z.string().min(1)
})
]);
export type LiveSubscription = z.infer<typeof LiveSubscriptionSchema>;
const livePayloadSchemas = {
options: OptionPrintSchema,
nbbo: OptionNBBOSchema,
equities: EquityPrintSchema,
"equity-joins": EquityPrintJoinSchema,
flow: FlowPacketSchema,
"classifier-hits": ClassifierHitEventSchema,
alerts: AlertEventSchema,
"inferred-dark": InferredDarkEventSchema,
"equity-candles": EquityCandleSchema,
"equity-overlay": EquityPrintSchema
} as const;
export const FeedSnapshotSchema = z.object({
subscription: LiveSubscriptionSchema,
items: z.array(z.unknown()),
watermark: CursorSchema.nullable(),
next_before: CursorSchema.nullable()
});
export type FeedSnapshot<T> = {
subscription: LiveSubscription;
items: T[];
watermark: Cursor | null;
next_before: Cursor | null;
};
export const LiveSubscribeMessageSchema = z.object({
op: z.literal("subscribe"),
subscriptions: z.array(LiveSubscriptionSchema).min(1)
});
export type LiveSubscribeMessage = z.infer<typeof LiveSubscribeMessageSchema>;
export const LiveUnsubscribeMessageSchema = z.object({
op: z.literal("unsubscribe"),
subscriptions: z.array(LiveSubscriptionSchema).min(1)
});
export type LiveUnsubscribeMessage = z.infer<typeof LiveUnsubscribeMessageSchema>;
export const LivePingMessageSchema = z.object({
op: z.literal("ping")
});
export type LivePingMessage = z.infer<typeof LivePingMessageSchema>;
export const LiveClientMessageSchema = z.discriminatedUnion("op", [
LiveSubscribeMessageSchema,
LiveUnsubscribeMessageSchema,
LivePingMessageSchema
]);
export type LiveClientMessage = z.infer<typeof LiveClientMessageSchema>;
export const LiveReadyMessageSchema = z.object({
op: z.literal("ready")
});
export type LiveReadyMessage = z.infer<typeof LiveReadyMessageSchema>;
export const LiveSnapshotMessageSchema = z.object({
op: z.literal("snapshot"),
snapshot: FeedSnapshotSchema
});
export type LiveSnapshotMessage = z.infer<typeof LiveSnapshotMessageSchema>;
export const LiveEventMessageSchema = z.object({
op: z.literal("event"),
subscription: LiveSubscriptionSchema,
item: z.unknown(),
watermark: CursorSchema.nullable()
});
export type LiveEventMessage = z.infer<typeof LiveEventMessageSchema>;
export const LiveHeartbeatMessageSchema = z.object({
op: z.literal("heartbeat"),
ts: z.number().int().nonnegative()
});
export type LiveHeartbeatMessage = z.infer<typeof LiveHeartbeatMessageSchema>;
export const LiveErrorMessageSchema = z.object({
op: z.literal("error"),
message: z.string().min(1)
});
export type LiveErrorMessage = z.infer<typeof LiveErrorMessageSchema>;
export const LiveServerMessageSchema = z.discriminatedUnion("op", [
LiveReadyMessageSchema,
LiveSnapshotMessageSchema,
LiveEventMessageSchema,
LiveHeartbeatMessageSchema,
LiveErrorMessageSchema
]);
export type LiveServerMessage = z.infer<typeof LiveServerMessageSchema>;
export const getSubscriptionKey = (subscription: LiveSubscription): string => {
switch (subscription.channel) {
case "equity-candles":
return `${subscription.channel}|${subscription.underlying_id}|${subscription.interval_ms}`;
case "equity-overlay":
return `${subscription.channel}|${subscription.underlying_id}`;
default:
return subscription.channel;
}
};
export const parseLivePayload = (
channel: LiveChannel,
item: unknown
): z.infer<(typeof livePayloadSchemas)[typeof channel]> => {
return livePayloadSchemas[channel].parse(item);
};