SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
2.5 KB · 72 lines typescript
Raw Blame History
1import { createWriteStream, mkdirSync } from "node:fs";2import { readFile } from "node:fs/promises";3import { join } from "node:path";4import { gunzipSync, gzipSync } from "node:zlib";5import type { RawObservation } from "@market-atlas/market-model";6import { redactObject, sha256 } from "@market-atlas/connector-sdk";7import { config } from "../config.js";8import { telemetry } from "./telemetry.js";910/**11 * L0 archive: raw payloads stored gzip-compressed, content-addressed, one directory per day12 * and connector. Streaming frames are sampled (MA_RAW_SAMPLE_RATE); polled/bulk payloads are13 * always kept. Secrets are redacted before writing.14 */15export class RawArchive {16  private pending = new Map<string, Buffer>();17  private timer: NodeJS.Timeout | null = null;1819  constructor(private root = join(config.dataDir, "raw")) {}2021  /** Returns a raw_ref (relative path) or null when the frame was not sampled. */22  store(raw: RawObservation, force = false): string | null {23    if (!force && Math.random() > config.rawSampleRate) return null;24    const day = new Date(raw.receivedAt).toISOString().slice(0, 10);25    const body = JSON.stringify({26      connector_id: raw.connectorId,27      source_id: raw.sourceId,28      kind: raw.kind,29      received_at: new Date(raw.receivedAt).toISOString(),30      meta: raw.meta ? redactObject(raw.meta) : undefined,31      payload: redactObject(raw.payload),32    });33    const hash = sha256(body).slice(0, 32);34    const rel = join(day, raw.connectorId, `${hash}.json.gz`);35    if (!this.pending.has(rel)) {36      this.pending.set(rel, gzipSync(body, { level: 6 }));37      this.schedule();38    }39    return rel;40  }4142  private schedule() {43    if (this.timer) return;44    this.timer = setTimeout(() => {45      this.timer = null;46      this.flush();47    }, 500);48  }4950  flush(): void {51    const batch = [...this.pending.entries()];52    this.pending.clear();53    for (const [rel, buf] of batch) {54      const abs = join(this.root, rel);55      mkdirSync(join(abs, ".."), { recursive: true });56      const ws = createWriteStream(abs, { flags: "wx" });57      ws.on("error", () => {}); // already exists → content-addressed, fine58      ws.end(buf);59      telemetry.inc("raw_archived_total");60      telemetry.inc("raw_archived_bytes", buf.length);61    }62  }6364  async read(ref: string): Promise<unknown> {65    if (ref.includes("..")) throw new Error("invalid ref");66    const buf = await readFile(join(this.root, ref));67    return JSON.parse(gunzipSync(buf).toString("utf8"));68  }69}7071export const rawArchive = new RawArchive();72