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:
parent
824b7f2fa0
commit
d30513119a
10 changed files with 1923 additions and 258 deletions
123
services/api/tests/live.test.ts
Normal file
123
services/api/tests/live.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { describe, expect, it } from "bun:test";
|
||||
import type { ClickHouseClient } from "@islandflow/storage";
|
||||
import { LiveStateManager } from "../src/live";
|
||||
|
||||
const makeClickHouse = (): ClickHouseClient =>
|
||||
({
|
||||
exec: async () => {},
|
||||
insert: async () => {},
|
||||
ping: async () => ({ success: true }),
|
||||
close: async () => {},
|
||||
query: async () => ({
|
||||
async json<T>() {
|
||||
return [] as T;
|
||||
}
|
||||
})
|
||||
}) as ClickHouseClient;
|
||||
|
||||
const makeRedis = () => {
|
||||
const lists = new Map<string, string[]>();
|
||||
const hashes = new Map<string, Map<string, string>>();
|
||||
|
||||
return {
|
||||
isOpen: true,
|
||||
async lRange(key: string, start: number, stop: number) {
|
||||
return (lists.get(key) ?? []).slice(start, stop + 1);
|
||||
},
|
||||
async lPush(key: string, value: string) {
|
||||
const next = lists.get(key) ?? [];
|
||||
next.unshift(value);
|
||||
lists.set(key, next);
|
||||
return next.length;
|
||||
},
|
||||
async lTrim(key: string, start: number, stop: number) {
|
||||
const next = lists.get(key) ?? [];
|
||||
lists.set(key, start > stop ? [] : next.slice(start, stop + 1));
|
||||
return "OK";
|
||||
},
|
||||
async hGet(key: string, field: string) {
|
||||
return hashes.get(key)?.get(field) ?? null;
|
||||
},
|
||||
async hSet(key: string, field: string, value: string) {
|
||||
const hash = hashes.get(key) ?? new Map<string, string>();
|
||||
hash.set(field, value);
|
||||
hashes.set(key, hash);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe("LiveStateManager", () => {
|
||||
it("hydrates snapshots from redis generic windows", async () => {
|
||||
const redis = makeRedis();
|
||||
await redis.lPush(
|
||||
"live:flow",
|
||||
JSON.stringify({
|
||||
source_ts: 100,
|
||||
ingest_ts: 101,
|
||||
seq: 1,
|
||||
trace_id: "flow-1",
|
||||
id: "flow-1",
|
||||
members: ["a"],
|
||||
features: {},
|
||||
join_quality: {}
|
||||
})
|
||||
);
|
||||
await redis.hSet("live:cursors", "flow", JSON.stringify({ ts: 100, seq: 1 }));
|
||||
|
||||
const manager = new LiveStateManager(makeClickHouse(), redis as never);
|
||||
await manager.hydrate();
|
||||
const snapshot = await manager.getSnapshot({ channel: "flow" });
|
||||
|
||||
expect(snapshot.items).toHaveLength(1);
|
||||
expect(snapshot.watermark).toEqual({ ts: 100, seq: 1 });
|
||||
expect(snapshot.next_before).toEqual({ ts: 100, seq: 1 });
|
||||
});
|
||||
|
||||
it("persists parameterized candle and overlay caches on ingest", async () => {
|
||||
const redis = makeRedis();
|
||||
const manager = new LiveStateManager(makeClickHouse(), redis as never);
|
||||
await manager.ingest("equity-candles", {
|
||||
source_ts: 100,
|
||||
ingest_ts: 101,
|
||||
seq: 1,
|
||||
trace_id: "candle:SPY:60000:100",
|
||||
ts: 100,
|
||||
interval_ms: 60000,
|
||||
underlying_id: "SPY",
|
||||
open: 1,
|
||||
high: 2,
|
||||
low: 1,
|
||||
close: 2,
|
||||
volume: 10,
|
||||
trade_count: 1
|
||||
});
|
||||
await manager.ingest("equity-overlay", {
|
||||
source_ts: 110,
|
||||
ingest_ts: 111,
|
||||
seq: 2,
|
||||
trace_id: "eq-1",
|
||||
ts: 110,
|
||||
underlying_id: "SPY",
|
||||
price: 10,
|
||||
size: 5,
|
||||
exchange: "X",
|
||||
offExchangeFlag: true
|
||||
});
|
||||
|
||||
const candleSnapshot = await manager.getSnapshot({
|
||||
channel: "equity-candles",
|
||||
underlying_id: "SPY",
|
||||
interval_ms: 60000
|
||||
});
|
||||
const overlaySnapshot = await manager.getSnapshot({
|
||||
channel: "equity-overlay",
|
||||
underlying_id: "SPY"
|
||||
});
|
||||
|
||||
expect(candleSnapshot.items).toHaveLength(1);
|
||||
expect(overlaySnapshot.items).toHaveLength(1);
|
||||
expect(candleSnapshot.watermark).toEqual({ ts: 100, seq: 1 });
|
||||
expect(overlaySnapshot.watermark).toEqual({ ts: 110, seq: 2 });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue