/**
* fixtures の偽制御盤(指示書AY 段階8-2。D10・`CLAUDE.md` §9.5)。
*
* ★relay の同じ経路を通す★: ポーリング → 指示 → 結果(list)/準備完了(export)/チャンク → 完了(send)。
* ルートは「fixtures のときは relay を呼ぶ前に `ensureFixtureAgent()`」だけを書き、それ以外の分岐を持たない。
*
* - `FIXTURE_AGENT_OFFLINE_SERIALS` と fixtures に無いシリアルは何もしない(→ 409 / 404 が本物と同じ経路で出る)
* - ★1シリアル1本★(relay ごとの登録で二重起動を防ぐ。`globalThis` に置き HMR でも二重にしない)
* - 指示ごとの処理は待たずに次のポーリングへ進む(本物と同じく、一覧はエクスポートと並行して返す。D25 ③)
* - ★ディスクに書かない★(zip は base64 の定数から作る)
*/
import { createHash } from "node:crypto";
import { AGENT_POLL_WAIT_SEC } from "@/lib/config/ds-agent";
import { getRelay, type Relay } from "@/lib/ds-agent/relay";
import {
FIXTURE_AGENT_OFFLINE_SERIALS,
FIXTURE_EXPORT_ZIP_BASE64,
fixtureDeviceLogItems,
fixtureDeviceLogTypes,
} from "@/lib/fixtures/ds-device-logs";
import { dsDevices } from "@/lib/fixtures/ds-devices";
import type { DsAgentCommand } from "@/lib/types";
/** 流量制御の経路を通すため、zip を小さく分けて送る(438バイト → 4チャンク) */
const FIXTURE_CHUNK_BYTES = 128;
declare global {
/** relay ごとに、偽制御盤を動かしているシリアル */
var __dwatchFixtureAgents: WeakMap<Relay, Set<string>> | undefined;
}
function running(relay: Relay): Set<string> {
globalThis.__dwatchFixtureAgents ??= new WeakMap();
let set = globalThis.__dwatchFixtureAgents.get(relay);
if (!set) {
set = new Set();
globalThis.__dwatchFixtureAgents.set(relay, set);
}
return set;
}
function fixtureZip(): Uint8Array {
return new Uint8Array(Buffer.from(FIXTURE_EXPORT_ZIP_BASE64, "base64"));
}
const sha256Hex = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
async function handle(relay: Relay, serial: string, command: DsAgentCommand): Promise<void> {
switch (command.kind) {
case "list":
relay.submitListResult(serial, command.command_id, {
types: [...fixtureDeviceLogTypes],
items: fixtureDeviceLogItems(new Date()),
truncated: false,
});
return;
case "export": {
const zip = fixtureZip();
relay.submitPrepared(serial, command.command_id, {
total_bytes: zip.length,
sha256: sha256Hex(zip),
file_count: command.params.file_ids.length,
});
return;
}
case "send": {
const exportId = command.params.export_command_id;
const zip = fixtureZip();
for (let seq = 0, offset = 0; offset < zip.length; seq++, offset += FIXTURE_CHUNK_BYTES) {
const chunk = zip.subarray(offset, offset + FIXTURE_CHUNK_BYTES);
const result = await relay.pushChunk(serial, exportId, seq, chunk, sha256Hex(chunk));
if (result !== "ok") return;
}
relay.completeExport(serial, exportId, zip.length, sha256Hex(zip));
return;
}
}
}
async function loop(relay: Relay, serial: string): Promise<void> {
const signal = new AbortController().signal;
try {
// ★relay が差し替えられたら(テスト)止める★
while (getRelay() === relay) {
const command = await relay.poll(serial, "fixture", AGENT_POLL_WAIT_SEC, signal);
if (command) void handle(relay, serial, command);
}
} finally {
running(relay).delete(serial);
}
}
/**
* fixtures のシリアルに偽制御盤を1本だけ動かす。★最初のポーリングは同期的に始まる★ので、
* 呼んだ直後の `requestList()` / `createExport()` から接続中として扱われる。
*/
export function ensureFixtureAgent(relay: Relay, serial: string): void {
if (FIXTURE_AGENT_OFFLINE_SERIALS.includes(serial)) return;
if (!dsDevices.some((d) => d.serial_number === serial)) return;
const set = running(relay);
if (set.has(serial)) return;
set.add(serial);
void loop(relay, serial);
}