{
  "format": "odb-inspection/1",
  "executed": false,
  "source": {
    "path": "packages/components/src/host.ts",
    "sha256": "b19197f22f2765b9fce38a3fd10a601ab1ec02313536c03f2918bf055f2fb8aa",
    "build": "Provided by @opendevicedb/components/host; no contributor JavaScript is evaluated as a host service.",
    "text": "// Host SDK for the scalar pilot. Contributor WASM executes through an injected worker\n// executor; interface code runs through an injected sandbox renderer, never in this module.\nimport { findComponent, lockComposition, parameterValues, validateComposition, type Registry, type Composition, type RegisteredComponent } from \"./browser.ts\";\n\nexport interface ScalarSample { format: \"odb.scalar/1\"; value: number | null; unit: string; timestamp: number; source: string; session: string; label?: string }\nexport const RECORDED_VALUES = [10, 20, 30, 20, 10];\nexport interface SessionHost {\n  prepareTransform(component: RegisteredComponent, signal: AbortSignal): Promise<{ process(value: number, factor: number): Promise<number>; close(): void }>;\n  prepareInterface(component: RegisteredComponent, node: string, signal: AbortSignal): Promise<{ update(sample: ScalarSample): void; close(): void }>;\n  usbDevice?: { deviceVersionMajor: number };\n  values?: number[];\n  onSample?(sample: ScalarSample): void;\n  onState?(state: \"running\" | \"stopped\" | \"error\", error?: string): void;\n}\n\nexport class ScalarSession {\n  private aborter = new AbortController();\n  private cleanups: (() => void)[] = [];\n  private timer?: ReturnType<typeof setTimeout>;\n  private active = false;\n  private started = false;\n  readonly id = globalThis.crypto.randomUUID();\n  readonly composition: Composition;\n  private registry: Registry;\n  private host: SessionHost;\n  constructor(composition: Composition, registry: Registry, host: SessionHost) {\n    this.registry = registry; this.host = host;\n    this.composition = lockComposition(composition, registry);\n  }\n  async start(): Promise<void> {\n    if (this.started) throw Error(\"A session cannot be restarted; create a new session explicitly\");\n    this.started = true;\n    const signal = this.aborter.signal, c = this.composition;\n    const result = validateComposition(c, this.registry, \"browser\", true);\n    if (!result.valid) throw Error(result.diagnostics.map(d => d.message).join(\"; \"));\n    const nodes = new Map(c.nodes.map(n => [n.id, { ...n, manifest: findComponent(this.registry, n.component, n.version)! }]));\n    const sources = [...nodes.values()].filter(n => n.manifest.runtime?.kind === \"host\");\n    if (sources.length !== 1) throw Error(\"The scalar pilot requires exactly one source\");\n    const source = sources[0];\n    const params = parameterValues(source.manifest, source.parameters);\n    const usb = source.manifest.runtime!.entry === \"usb-descriptors\";\n    if (usb && (!source.manifest.permissions.includes(\"usb.descriptors\") || !this.host.usbDevice)) throw Error(\"Select and authorize a USB device before starting this application\");\n    const values = this.host.values ?? RECORDED_VALUES;\n    if (!Array.isArray(values) || !values.length || values.length > 4096 || values.some(n => !Number.isFinite(n))) throw Error(\"Recorded input must contain 1–4096 finite numbers\");\n    const transforms = new Map<string, Awaited<ReturnType<SessionHost[\"prepareTransform\"]>>>();\n    const interfaces = new Map<string, Awaited<ReturnType<SessionHost[\"prepareInterface\"]>>>();\n    const release = (close: () => void) => { if (signal.aborted) { close(); throw Error(\"Session stopped during preparation\"); } this.cleanups.push(close); };\n    try {\n      // Prepare consumers before producers; do not emit data until all dependencies are ready.\n      for (const id of [...result.order].reverse()) {\n        signal.throwIfAborted();\n        const node = nodes.get(id)!, kind = node.manifest.runtime?.kind;\n        if (kind === \"wasm\") { const handle = await this.host.prepareTransform(node.manifest, signal); release(() => handle.close()); transforms.set(id, handle); }\n        else if (kind === \"web-component\") { const handle = await this.host.prepareInterface(node.manifest, id, signal); release(() => handle.close()); interfaces.set(id, handle); }\n        else if (kind !== \"host\") throw Error(`Unsupported runtime for ${id}`);\n      }\n      signal.throwIfAborted(); this.active = true; this.host.onState?.(\"running\");\n      let index = 0;\n      const tick = async () => {\n        if (!this.active || signal.aborted) return;\n        try {\n          const sample: ScalarSample = { format: \"odb.scalar/1\", value: usb ? this.host.usbDevice!.deviceVersionMajor : values[index++ % values.length],\n            unit: usb ? \"major revision\" : String(params.unit ?? \"count\"), timestamp: Date.now(), source: usb ? \"Selected USB device descriptor\" : \"Recorded fixture replay\", session: this.id };\n          const outputs = new Map<string, ScalarSample>([[source.id, sample]]);\n          for (const id of result.order) {\n            if (id === source.id) continue;\n            signal.throwIfAborted();\n            const edge = c.connections.find(e => e.to.split(\".\")[0] === id);\n            const input = edge ? outputs.get(edge.from.split(\".\")[0]) : undefined;\n            if (!input || input.value === null) continue;\n            const node = nodes.get(id)!;\n            let output = input;\n            if (transforms.has(id)) {\n              const factor = parameterValues(node.manifest, node.parameters).factor;\n              const value = await transforms.get(id)!.process(input.value, Number(factor));\n              signal.throwIfAborted();\n              if (!Number.isFinite(value)) throw Error(`Non-finite result from ${id}`);\n              output = { ...input, value }; outputs.set(id, output);\n            }\n            if (interfaces.has(id)) { interfaces.get(id)!.update({ ...output, label: node.manifest.title }); this.host.onSample?.(output); }\n          }\n          if (this.active) this.timer = setTimeout(tick, Number(params.interval ?? 500));\n        } catch (e) { if (!signal.aborted) { this.stop(); this.host.onState?.(\"error\", (e as Error).message); } }\n      };\n      // One in-flight sample; backpressure pauses production and cannot grow an unbounded queue.\n      void tick();\n    } catch (e) { this.stop(); throw e; }\n  }\n  stop() {\n    if (this.aborter.signal.aborted) return;\n    this.active = false; this.aborter.abort(); if (this.timer) clearTimeout(this.timer);\n    for (const close of this.cleanups.reverse()) { try { close(); } catch { /* release all remaining resources */ } }\n    this.cleanups = []; this.host.onState?.(\"stopped\");\n  }\n}\n"
  },
  "behavior": "Replays 10, 20, 30, 20, 10, or a user-selected array of 1–4096 finite numbers. Backpressure pauses production. Source timestamps and session IDs are assigned by the host; no data is uploaded.",
  "ports": {
    "samples": {
      "direction": "out",
      "kind": "stream",
      "contract": "odb.scalar/1"
    }
  },
  "parameters": {
    "interval": {
      "type": "integer",
      "default": 500,
      "minimum": 50,
      "maximum": 10000
    },
    "unit": {
      "type": "string",
      "default": "count"
    }
  },
  "fixtures": [],
  "evidence": []
}