// Generated by scripts/api-client.mjs from schemas/openapi-v1.json; do not edit by hand.
// The QuantLab API client (architecture/api-v1.md): one method per operation of the contract.

/** The one error shape of the API. */
export type ErrorBody = {
  /** Human-readable message. */
  error: string;
  /** The HTTP status again. */
  status: number;
  /** Plan problems only: the lab's dictionary key with its parameters, to show the problem in another language. */
  message?: PlanMessage;
};

/** 128 random bits, 32 lowercase hex. */
export type Id = string;

export type Health = {
  status: "ok";
  version: string;
  engine: string;
  /** Runs and series waiting in the shared queue. */
  queued: number;
  /** Runs and series being computed. */
  running: number;
  /** Cores of the one queue and cores in use. */
  pool: {
    cores?: number;
    used?: number;
  };
  /** Run and series jobs kept. */
  stored: number;
  completed: number;
  failed: number;
  limits: {
    queue?: number;
    per_ip_hour?: number;
    max_events?: number;
    max_wall_seconds?: number;
    max_output_mib?: number;
    keep_seconds?: number;
  };
  worlds: {
    stored?: number;
    running?: number;
    queued?: number;
    limits?: {
      max_worlds?: number;
      max_running?: number;
      max_realtime?: number;
      per_ip_hour?: number;
      idle_seconds?: number;
    };
  };
  stories: {
    waiting?: number;
    building?: number;
    seconds_per_hour?: number;
    queue?: number;
  };
  gpu: {
    enabled?: boolean;
    queued?: number;
    /** Unix seconds of the worker's last request. */
    worker_seen?: number | null;
  };
};

export type SeriesInfo = {
  runs: number;
  first_seed: string;
  scenarios: 1 | 2;
  /** Computed by a GPU worker. */
  gpu?: boolean;
  /** GPU asked for but computed on the server's CPUs. */
  cpu_instead?: "small" | "no-worker";
  /** A single run with the observer's chain (chain.json). */
  chain?: boolean;
};

export type Job = {
  id: Id;
  status: "queued" | "running" | "completed" | "partial" | "failed";
  name: string;
  nodes: number;
  horizon_seconds: number;
  /** Unix seconds. */
  submitted_at: number;
  started_at?: number;
  finished_at?: number;
  /** Scenario limits reduced to the server's, as `name = value`. */
  clamped?: Array<string>;
  error?: string;
  series?: SeriesInfo;
};

export type RunQueued = {
  run: Job;
  /** Place in the queue, 1 is next. */
  position: number | null;
};

export type RunStatus = {
  run: Job;
  /** Place in the queue, 1 is next; null once started. */
  position: number | null;
  /** Cores of the shared queue in use. */
  used: number;
  cores: number;
  /** Files to fetch; present for a completed or partial job. */
  artifacts?: Array<string>;
  /** Run result summary (summary.json), runs only. */
  summary?: unknown;
  /** Series report (series.json), series only. */
  report?: unknown;
};

/** Bounded digest of a result: totals, the tail of the block tree, thinned metrics, the latest events. */
export type ResultView = {
  manifest: {
    status?: unknown;
    reason?: string | null;
    profile?: string;
    seed?: string;
    artifacts_complete?: boolean;
    history_blake3?: unknown;
    state_sha256?: unknown;
    configuration_sha256?: unknown;
    wall_elapsed_ms?: number;
    version?: string;
  };
  summary: unknown;
  scenario: unknown;
  /** The last 120 blocks. */
  blocks: Array<unknown>;
  blocks_total: number;
  /** The last 1000 journal records. */
  records: Array<unknown>;
  records_total: number;
  metrics: {
    columns: Array<string>;
    rows: Array<Array<string>>;
    total: number;
  };
};

export type SeriesRequest = {
  /** One TOML scenario, or two compared on shared seeds. */
  scenarios: Array<string>;
  runs: number;
  /** Default: the first scenario's seed. */
  first_seed?: string;
  /** gpu needs a GPU worker enabled on the server (else 400). */
  device?: "cpu" | "gpu";
  /** One run of one scenario on the server's CPU with the observer's chain. */
  chain?: boolean;
};

export type StoryRequest = {
  /** TOML scenario. */
  input: string;
  seed: string;
  /** Asking tab: its newer request replaces its older waiting ones. */
  tab?: string;
};

export type StoryPending = {
  id: string;
  status: "waiting" | "building";
  position: number | null;
};

export type WorldStatus = "queued" | "running" | "pausing" | "paused" | "completed" | "stopped" | "limited" | "failed";

export type World = {
  id: Id;
  name: string;
  profile: string;
  nodes: number;
  horizon_seconds: number;
  created_at: number;
  updated_at: number;
  status: WorldStatus;
  reason: string | null;
  segments: number;
  /** Files available at GET /worlds/{id}/{file}. */
  files: Array<string>;
  /** Amendments applied. */
  amendments: number;
  /** Amendments waiting to be applied. */
  pending: number;
  /** Real-time world only: world seconds per second. */
  pace?: number;
  /** Latest progress of the computation. */
  progress?: Record<string, unknown>;
  clamped?: Array<string>;
  parent?: Id;
};

export type WorldEnvelope = {
  world: World;
};

export type WorldCreated = {
  world: World;
  /** Secret of the world, shown once: send it as X-QuantLab-World-Key. */
  key: string;
};

export type WorldDeleted = {
  deleted: Id;
};

export type WorldCommand = {
  command: "start" | "pause" | "stop" | "clone" | "delete";
};

export type Amendments = {
  amendments: Array<{
    at_event: number;
    toml: string;
    pending?: true;
  }>;
};

export type WorkerJob = {
  job: null | {
    id: Id;
    runs: number;
    first_seed: string;
    scenarios: Array<string>;
  };
};

export type WorkerResult = {
  /** Series report in the fast series format. */
  series: unknown;
  markdown: string;
};

/** A key of the lab's dictionary (apps/lab/src/i18n) and the values of its {placeholders}: a number, a text, {count} (a number shown with digit grouping), a nested message, or a list of messages joined by commas. */
export type PlanMessage = {
  key: string;
  params?: Record<string, number | string | {
    count: number;
  } | PlanMessage | Array<PlanMessage>>;
};

export type PlanRequest = {
  /** A network library as the lab saves and exports it (schemas/network-library-v1.schema.json), up to 1 MB. */
  library: Record<string, unknown>;
  /** The network's id; without it, the library's first network. */
  network?: string;
  /** The plan's id in that network; without it, the network's first plan. */
  plan?: string;
  /** Amendment only: ids of the plan's cards to add to a world. */
  impacts?: Array<string>;
  /** Amendment only: put before the cards' change ids, to keep them apart from the world's earlier changes (the lab uses a1-, a2-…). */
  prefix?: string;
};

export type CompiledPlan = {
  network: string;
  plan: string;
  /** The scenario TOML, for POST /runs, /worlds or /series. */
  scenario: string;
  /** Why a series of this world cannot run on the fast engine, or null. */
  fast_engine: string | null;
};

export type PlanAmendment = {
  network: string;
  plan: string;
  /** Amendment TOML, for POST /worlds/{id}/amend. */
  amendment: string;
};

export type ScenarioCheck = {
  /** The normalized scenario, as scenario.json of a result. */
  scenario: unknown;
  /** Protocol mode: runs only where the server has the Requant adapter. */
  protocol: boolean;
  engines: {
    /** The main engine takes it: runs and worlds (up to 128 nodes). */
    main: boolean;
    /** The fast engine takes it: series of up to 200 000 nodes. */
    fast: boolean;
    /** Why the fast engine does not, or null. */
    fast_issue: string | null;
  };
};

export type Profiles = {
  profiles: Array<{
    id: string;
    name: string;
    /** Block interval, difficulty rule and window, coin, transaction flow, recommended days, uncles (architecture/network-profiles-v1.md). */
    profile: Record<string, unknown>;
    /** The profile's world over its recommended days, as TOML. */
    scenario: string;
  }>;
};

export type Presets = {
  presets: Array<{
    id: string;
    /** Its file in scenarios/. */
    file: string;
    name: string | null;
    description: string | null;
    /** Protocol mode (Requant): server only. */
    protocol: boolean;
    /** TOML. */
    scenario: string;
  }>;
};

export type ChannelSummary = {
  id: string;
  name: string;
  /** The network profile (GET /profiles). */
  profile: string;
  /** World seconds per wall second. */
  pace: number;
  /** The season's number: by default wall time over six hours since the Unix epoch; an admin's restart starts the next one at once. Each season the world starts anew. */
  season: number;
  started_ms: number;
  ends_ms: number;
  /** The server's clock when it answered. */
  server_ms: number;
  /** The channel's world time: wall time since the season's start times the pace. */
  now_us: string;
  /** Impacts in the season's plan: a new one means a new story. */
  version: number;
  /** Addresses that asked for the channel in the last 30 seconds, and its bots. */
  viewers: number;
  /** Viewers the server keeps: they watch and step in as people would, through the same limits; counted in viewers. */
  bots: number;
  /** The crowd's channel: a network with many bots stepping in, to see what a large audience does to it. */
  crowd: boolean;
};

export type Channel = {
  id: string;
  name: string;
  /** The network profile (GET /profiles). */
  profile: string;
  /** World seconds per wall second. */
  pace: number;
  /** The season's number: by default wall time over six hours since the Unix epoch; an admin's restart starts the next one at once. Each season the world starts anew. */
  season: number;
  started_ms: number;
  ends_ms: number;
  /** The server's clock when it answered. */
  server_ms: number;
  /** The channel's world time: wall time since the season's start times the pace. */
  now_us: string;
  /** Impacts in the season's plan: a new one means a new story. */
  version: number;
  /** Addresses that asked for the channel in the last 30 seconds, and its bots. */
  viewers: number;
  /** The season's seed, for POST /stories. */
  seed: string;
  /** The season's scenario (TOML) with the viewers' impacts composed: the network's state with all of them going on at once; for POST /stories. */
  input: string;
  /** The channel's network in the planner's library format. */
  network: Record<string, unknown>;
  /** The season's plan in the planner's library format: its impacts are the viewers'. */
  plan: Record<string, unknown>;
  /** The impacts viewers may add and their limits. */
  limits: Record<string, unknown>;
  /** Viewers the server keeps: they watch and step in as people would, through the same limits; counted in viewers. */
  bots: number;
  /** The crowd's channel: a network with many bots stepping in, to see what a large audience does to it. */
  crowd: boolean;
  /** The asking address's name on air: two words and a number, the same for the same address. */
  you: string;
  /** Who watches now: the addresses of the last 30 seconds by their names, then the bots. */
  audience: Array<ChannelViewer>;
  /** Who took each impact of the plan, in its order: the names on air (empty for an impact from before the names). */
  authors: Array<string>;
  /** Who supported each impact of the plan, in its order: the names on air. */
  supporters: Array<Array<string>>;
  /** When each support of each impact was given, in the order of `supporters`: world seconds of the season. A rewound broadcast counts only the support given by its moment. */
  supported: Array<Array<number>>;
  charges: ChannelCharges;
  /** When each impact of the plan was asked for, in its order: world seconds of the season. A rewound broadcast shows only what was asked by its moment, the queue included. */
  asked: Array<number>;
};

export type Channels = {
  channels: Array<ChannelSummary>;
  server_ms: number;
};

/** An impact, or a support of one (kind support with its id). */
export type ChannelImpactRequest = {
  kind: "partition" | "offline" | "latency" | "selfish" | "surge" | "loss" | "support";
  /** partition: one side (by default about half the mining power, each next split another way, so overlapping ones cut more parts); offline: one to six nodes; selfish and surge: one pool's node. */
  nodes?: Array<number>;
  /** latency: milliseconds on every link, 1000 to 30000, the largest going on applies; surge: the pool's power times 2 to 5 (3 by default), surges going on multiply up to ten times; loss: percent of messages lost, 1 to 50, the largest going on applies. */
  value?: number;
  /** support: the id of another's impact going on or to come (plan.impacts); it lasts support_s longer, up to longest_s, once from a name. */
  id?: string;
};

export type ChannelImpact = {
  /** The plan's new card: it starts the channel's lead (15 wall seconds by default) after the channel's time and lasts its length (90 wall seconds, a split 180, by default); impacts overlap and add up. */
  impact: Record<string, unknown>;
  channel: Channel;
};

export type ChannelNow = {
  id: string;
  name: string;
  /** The network profile (GET /profiles). */
  profile: string;
  /** World seconds per wall second. */
  pace: number;
  /** The season's number: by default wall time over six hours since the Unix epoch; an admin's restart starts the next one at once. Each season the world starts anew. */
  season: number;
  started_ms: number;
  ends_ms: number;
  /** The server's clock when it answered. */
  server_ms: number;
  /** The channel's world time: wall time since the season's start times the pace. */
  now_us: string;
  /** Impacts in the season's plan: a new one means a new story. */
  version: number;
  /** Addresses that asked for the channel in the last 30 seconds, and its bots. */
  viewers: number;
  /** Viewers the server keeps: they watch and step in as people would, through the same limits; counted in viewers. */
  bots: number;
  /** The crowd's channel: a network with many bots stepping in, to see what a large audience does to it. */
  crowd: boolean;
  /** The asking address's name on air: two words and a number, the same for the same address. */
  you: string;
  /** Who watches now: the addresses of the last 30 seconds by their names, then the bots. */
  audience: Array<ChannelViewer>;
  charges: ChannelCharges;
};

export type ChannelViewer = {
  nick: string;
  bot: boolean;
};

export type ChannelSettings = {
  /** World seconds per wall second; a new pace starts a new season. */
  pace: number;
  /** Wall seconds from an impact's request to its start. */
  lead_s: number;
  /** Wall seconds an impact lasts. */
  length_s: number;
  /** Wall seconds a split lasts. */
  split_s: number;
  /** Wall seconds between an address's impacts (0 by default: the charges pace them). */
  gap_s: number;
  /** An address's impacts an hour. */
  per_hour: number;
  /** The channel's impacts going on at once, of all kinds. */
  going: number;
  /** The channel's impacts in a season. */
  per_season: number;
  bots: number;
  /** Wall seconds between a bot's impacts, drawn evenly from this range. */
  bot_gap_s: Array<number>;
  /** Impacts of each kind going on at once, in the order partition, offline, latency, selfish, surge, loss; selfish and surge count per pool. An impact with no place waits in the queue. */
  slots: Array<number>;
  /** Impacts waiting for a place past their lead, at most. */
  queue: number;
  /** Wall seconds a storm lasts at most: an impact joins the impacts going on only while their storm with it stays this short, and a support grows an impact no further than its storm's bound (no less than length_s and split_s). */
  storm_s: number;
  /** Wall seconds of calm between storms: an impact joins the impacts going on, or starts this long after they are over (and before the next). */
  calm_s: number;
  /** Wall seconds a viewer's support adds to an impact. */
  support_s: number;
  /** Wall seconds an impact grows to at most by supports (no less than length_s and split_s). */
  longest_s: number;
  /** A viewer's charges on the channel: each impact and support costs some. */
  charges: number;
  /** Wall seconds for one charge to come back. */
  charge_s: number;
  /** Charges each costs, in the order partition, offline, latency, selfish, surge, loss, support; no more than charges. */
  costs: Array<number>;
};

/** Some of the settings: the rest stay as they are. */
export type ChannelSettingsChange = {
  /** World seconds per wall second; a new pace starts a new season. */
  pace?: number;
  /** Wall seconds from an impact's request to its start. */
  lead_s?: number;
  /** Wall seconds an impact lasts. */
  length_s?: number;
  /** Wall seconds a split lasts. */
  split_s?: number;
  /** Wall seconds between an address's impacts (0 by default: the charges pace them). */
  gap_s?: number;
  /** An address's impacts an hour. */
  per_hour?: number;
  /** The channel's impacts going on at once, of all kinds. */
  going?: number;
  /** The channel's impacts in a season. */
  per_season?: number;
  bots?: number;
  /** Wall seconds between a bot's impacts, drawn evenly from this range. */
  bot_gap_s?: Array<number>;
  /** Impacts of each kind going on at once, in the order partition, offline, latency, selfish, surge, loss; selfish and surge count per pool. An impact with no place waits in the queue. */
  slots?: Array<number>;
  /** Impacts waiting for a place past their lead, at most. */
  queue?: number;
  /** Wall seconds a storm lasts at most: an impact joins the impacts going on only while their storm with it stays this short, and a support grows an impact no further than its storm's bound (no less than length_s and split_s). */
  storm_s?: number;
  /** Wall seconds of calm between storms: an impact joins the impacts going on, or starts this long after they are over (and before the next). */
  calm_s?: number;
  /** Wall seconds a viewer's support adds to an impact. */
  support_s?: number;
  /** Wall seconds an impact grows to at most by supports (no less than length_s and split_s). */
  longest_s?: number;
  /** A viewer's charges on the channel: each impact and support costs some. */
  charges?: number;
  /** Wall seconds for one charge to come back. */
  charge_s?: number;
  /** Charges each costs, in the order partition, offline, latency, selfish, surge, loss, support; no more than charges. */
  costs?: Array<number>;
};

export type AdminChannel = {
  id: string;
  name: string;
  /** The network profile (GET /profiles). */
  profile: string;
  /** World seconds per wall second. */
  pace: number;
  /** The season's number: by default wall time over six hours since the Unix epoch; an admin's restart starts the next one at once. Each season the world starts anew. */
  season: number;
  started_ms: number;
  ends_ms: number;
  /** The server's clock when it answered. */
  server_ms: number;
  /** The channel's world time: wall time since the season's start times the pace. */
  now_us: string;
  /** Impacts in the season's plan: a new one means a new story. */
  version: number;
  /** Addresses that asked for the channel in the last 30 seconds, and its bots. */
  viewers: number;
  /** Viewers the server keeps: they watch and step in as people would, through the same limits; counted in viewers. */
  bots: number;
  /** The crowd's channel: a network with many bots stepping in, to see what a large audience does to it. */
  crowd: boolean;
  settings: ChannelSettings;
  defaults: ChannelSettings;
  /** The addresses that watch, by their names, and how long ago each asked. */
  people: Array<{
    nick: string;
    seen_ms: number;
  }>;
  /** The network now: impacts going on, its parts, nodes off and the largest delay. */
  network: {
    going: number;
    parts: number;
    off: number;
    latency_ms: number | null;
  };
  /** The season's impacts: all, those yet to start, by kind, and who took the most. */
  impacts: {
    total: number;
    ahead: number;
    kinds: Record<string, number>;
    authors: Array<{
      nick: string;
      count: number;
    }>;
  };
  /** Whether the channel runs on a network of its own (POST /admin/channels/{id}/network) rather than its profile's. */
  own_network: boolean;
};

export type AdminChannels = {
  channels: Array<AdminChannel>;
  server_ms: number;
};

export type AdminChannelSet = {
  id: string;
  settings: ChannelSettings;
  /** Whether the channel runs on a network of its own (POST /admin/channels/{id}/network) rather than its profile's. */
  own_network: boolean;
  season: number;
  started_ms: number;
};

/** The asking address's charges on the channel. */
export type ChannelCharges = {
  /** Charges now, a part of one coming back counted (two decimals). */
  have: number;
  most: number;
  /** Wall seconds until the next whole charge; null when full. */
  next_s: number | null;
};

/** One Server-Sent Event of an events stream: its name and its data, JSON as text. */
export type ServerEvent = { event: string; data: string };

/** A non-2xx answer: the API's one error shape. */
export class QuantLabError extends Error {
  readonly status: number;
  readonly body: ErrorBody | undefined;
  constructor(status: number, body: ErrorBody | undefined, message: string) {
    super(message);
    this.status = status;
    this.body = body;
  }
}

export type Options = {
  /** Default: the public service, https://requant.network/lab/api/v1. */
  baseUrl?: string;
  /** An API key (qlk_…): the key's quotas instead of the address's. */
  apiKey?: string;
  /** Extra headers on every request. */
  headers?: Record<string, string>;
  fetch?: typeof fetch;
};

type Init = { method: string; type?: string; body?: string; worldKey?: string };

export class QuantLab {
  readonly baseUrl: string;
  private readonly options: Options;
  constructor(options: Options = {}) {
    this.options = options;
    this.baseUrl = (options.baseUrl ?? "https://requant.network/lab/api/v1").replace(/\/$/, "");
  }

  private headers(init: Pick<Init, "type" | "worldKey">): Record<string, string> {
    const headers: Record<string, string> = { ...this.options.headers };
    if (this.options.apiKey) headers.authorization = `Bearer ${this.options.apiKey}`;
    if (init.type) headers["content-type"] = init.type;
    if (init.worldKey) headers["x-quantlab-world-key"] = init.worldKey;
    return headers;
  }

  private async request(path: string, init: Init): Promise<Response> {
    const response = await (this.options.fetch ?? fetch)(this.baseUrl + path, { method: init.method, body: init.body, headers: this.headers(init) });
    if (!response.ok) {
      const text = await response.text();
      let body: ErrorBody | undefined;
      try { body = JSON.parse(text); } catch { body = undefined; }
      throw new QuantLabError(response.status, body, body?.error ?? (text || response.statusText));
    }
    return response;
  }

  /** The events of a stream until the server closes it (a finished job, a stopped world). */
  private async *events(path: string, worldKey?: string): AsyncGenerator<ServerEvent> {
    const response = await this.request(path, { method: "GET", worldKey });
    const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
    let buffer = "";
    // leaving early (a break in for await) closes the connection
    try {
      for (;;) {
        const { value, done } = await reader.read();
        if (done) return;
        buffer += value;
        let end;
        while ((end = buffer.indexOf("\n\n")) >= 0) {
          const block = buffer.slice(0, end);
          buffer = buffer.slice(end + 2);
          let event = "message";
          const data: string[] = [];
          for (const line of block.split("\n")) {
            if (line.startsWith("event:")) event = line.slice(6).trim();
            else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
          }
          if (data.length) yield { event, data: data.join("\n") };
        }
      }
    } finally {
      await reader.cancel().catch(() => undefined);
    }
  }

  /** GET /openapi.json: This contract (OpenAPI 3.1). */
  async getOpenapi(): Promise<Record<string, unknown>> {
    return (await this.request(`/openapi.json`, { method: "GET" })).json();
  }

  /** GET /health: Service state for monitoring: queue, counts, limits. */
  async getHealth(): Promise<Health> {
    return (await this.request(`/health`, { method: "GET" })).json();
  }

  /** POST /scenarios/check: Check a scenario and return it normalized, with the engines that take it. */
  async checkScenario(text: string): Promise<ScenarioCheck> {
    return (await this.request(`/scenarios/check`, { method: "POST", type: "application/toml", body: text })).json();
  }

  /** POST /plans/compile: A library's plan as a scenario, as the lab's planner builds it. */
  async compilePlan(body: PlanRequest): Promise<CompiledPlan> {
    return (await this.request(`/plans/compile`, { method: "POST", type: "application/json", body: JSON.stringify(body) })).json();
  }

  /** POST /plans/amendment: Some of a plan's cards as an amendment to a world. */
  async compileAmendment(body: PlanRequest): Promise<PlanAmendment> {
    return (await this.request(`/plans/amendment`, { method: "POST", type: "application/json", body: JSON.stringify(body) })).json();
  }

  /** GET /profiles: Real networks scaled for a demo, each with its world as a scenario. */
  async listProfiles(): Promise<Profiles> {
    return (await this.request(`/profiles`, { method: "GET" })).json();
  }

  /** GET /presets: The reference scenarios. */
  async listPresets(): Promise<Presets> {
    return (await this.request(`/presets`, { method: "GET" })).json();
  }

  /** POST /runs: Queue a run of a TOML scenario. */
  async submitRun(text: string): Promise<RunQueued> {
    return (await this.request(`/runs`, { method: "POST", type: "application/toml", body: text })).json();
  }

  /** GET /runs/{id}: Status of a run; a finished one carries its summary and file list. */
  async getRun(id: string): Promise<RunStatus> {
    return (await this.request(`/runs/${encodeURIComponent(String(id))}`, { method: "GET" })).json();
  }

  /** GET /runs/{id}/events: Stream of status changes of a run (Server-Sent Events). */
  streamRunEvents(id: string): AsyncGenerator<ServerEvent> {
    return this.events(`/runs/${encodeURIComponent(String(id))}/events`);
  }

  /** GET /runs/{id}/{file}: A result file of a finished run, or the bounded digest view.json. */
  async getRunFile(id: string, file: string): Promise<Response> {
    return this.request(`/runs/${encodeURIComponent(String(id))}/${encodeURIComponent(String(file))}`, { method: "GET" });
  }

  /** POST /series: Queue a series of up to 1 000 000 runs on the fast engine. */
  async submitSeries(body: SeriesRequest): Promise<RunQueued> {
    return (await this.request(`/series`, { method: "POST", type: "application/json", body: JSON.stringify(body) })).json();
  }

  /** GET /series/{id}: Status of a series; a finished one carries its report. */
  async getSeries(id: string): Promise<RunStatus> {
    return (await this.request(`/series/${encodeURIComponent(String(id))}`, { method: "GET" })).json();
  }

  /** GET /series/{id}/events: Stream of status changes of a series (Server-Sent Events). */
  streamSeriesEvents(id: string): AsyncGenerator<ServerEvent> {
    return this.events(`/series/${encodeURIComponent(String(id))}/events`);
  }

  /** GET /series/{id}/{file}: A result file of a finished series. */
  async getSeriesFile(id: string, file: string): Promise<Response> {
    return this.request(`/series/${encodeURIComponent(String(id))}/${encodeURIComponent(String(file))}`, { method: "GET" });
  }

  /** POST /stories: The story of a world for the map: from the cache, built within the request, or a place in the queue. */
  async requestStory(body: StoryRequest): Promise<Uint8Array | StoryPending> {
    const response = await this.request(`/stories`, { method: "POST", type: "application/json", body: JSON.stringify(body) });
    return response.headers.get("content-type")?.startsWith("application/json") ? response.json() : new Uint8Array(await response.arrayBuffer());
  }

  /** GET /stories/{id}: The story once built, otherwise its place or its error. */
  async getStory(id: string): Promise<Uint8Array | StoryPending> {
    const response = await this.request(`/stories/${encodeURIComponent(String(id))}`, { method: "GET" });
    return response.headers.get("content-type")?.startsWith("application/json") ? response.json() : new Uint8Array(await response.arrayBuffer());
  }

  /** GET /stories/{id}/slices/{index}: A window of a big story: a story of its own from one keyframe to a later one (roadmap И.3). */
  async getStorySlice(id: string, index: number): Promise<Uint8Array> {
    return new Uint8Array(await (await this.request(`/stories/${encodeURIComponent(String(id))}/slices/${encodeURIComponent(String(index))}`, { method: "GET" })).arrayBuffer());
  }

  /** GET /channels: The live channels at this moment. */
  async listChannels(): Promise<Channels> {
    return (await this.request(`/channels`, { method: "GET" })).json();
  }

  /** GET /channels/{id}: A channel at this moment: its clock, its season's plan and who took each impact, who watches, and the scenario to build its story. */
  async getChannel(id: string): Promise<Channel> {
    return (await this.request(`/channels/${encodeURIComponent(String(id))}`, { method: "GET" })).json();
  }

  /** POST /channels/{id}/impacts: A viewer's impact joins the channel's plan from its time plus a lead: a split, nodes off, a delay, a pool hiding its blocks, a pool's surge, lost messages; impacts going on at once add up. */
  async addChannelImpact(id: string, body: ChannelImpactRequest): Promise<ChannelImpact> {
    return (await this.request(`/channels/${encodeURIComponent(String(id))}/impacts`, { method: "POST", type: "application/json", body: JSON.stringify(body) })).json();
  }

  /** POST /worlds: Create a world from a TOML scenario; it starts paused. */
  async createWorld(text: string): Promise<WorldCreated> {
    return (await this.request(`/worlds`, { method: "POST", type: "application/toml", body: text })).json();
  }

  /** POST /worlds/realtime/{pace}: Create a real-time world: no horizon, its clock runs at `pace` world seconds per second. */
  async createRealtimeWorld(pace: number, text: string): Promise<WorldCreated> {
    return (await this.request(`/worlds/realtime/${encodeURIComponent(String(pace))}`, { method: "POST", type: "application/toml", body: text })).json();
  }

  /** GET /worlds/{id}: State of a world. */
  async getWorld(id: string, worldKey: string): Promise<WorldEnvelope> {
    return (await this.request(`/worlds/${encodeURIComponent(String(id))}`, { method: "GET", worldKey })).json();
  }

  /** GET /worlds/{id}/events: Stream of a world's state and live frames (Server-Sent Events). */
  streamWorldEvents(id: string, worldKey: string): AsyncGenerator<ServerEvent> {
    return this.events(`/worlds/${encodeURIComponent(String(id))}/events`, worldKey);
  }

  /** POST /worlds/{id}/commands: start (also continue), pause, stop, clone or delete a world. */
  async commandWorld(id: string, worldKey: string, body: WorldCommand): Promise<WorldEnvelope | WorldDeleted | WorldCreated> {
    return (await this.request(`/worlds/${encodeURIComponent(String(id))}/commands`, { method: "POST", type: "application/json", body: JSON.stringify(body), worldKey })).json();
  }

  /** POST /worlds/{id}/amend: Add an amendment (TOML) to a paused world, or to a running real-time one. */
  async amendWorld(id: string, worldKey: string, text: string): Promise<WorldEnvelope> {
    return (await this.request(`/worlds/${encodeURIComponent(String(id))}/amend`, { method: "POST", type: "application/toml", body: text, worldKey })).json();
  }

  /** GET /worlds/{id}/{file}: A file of a world: scenario, snapshot, amendments, live frame, digest or result files. */
  async getWorldFile(id: string, file: string, worldKey: string): Promise<Response> {
    return this.request(`/worlds/${encodeURIComponent(String(id))}/${encodeURIComponent(String(file))}`, { method: "GET", worldKey });
  }

  /** GET /channels/{id}/now: A channel's clock and audience at this moment, without its plan: a tab asks for this often and for the whole channel when its version changes. */
  async getChannelNow(id: string): Promise<ChannelNow> {
    return (await this.request(`/channels/${encodeURIComponent(String(id))}/now`, { method: "GET" })).json();
  }

  /** GET /admin/channels: Every channel at once: its settings, who watches, its network now and its season's impacts. */
  async adminChannels(): Promise<AdminChannels> {
    return (await this.request(`/admin/channels`, { method: "GET" })).json();
  }

  /** PATCH /admin/channels/{id}: Changes some of a channel's settings; a new pace starts a new season. */
  async adminSetChannel(id: string, body: ChannelSettingsChange): Promise<AdminChannelSet> {
    return (await this.request(`/admin/channels/${encodeURIComponent(String(id))}`, { method: "PATCH", type: "application/json", body: JSON.stringify(body) })).json();
  }

  /** POST /admin/channels/{id}/restart: Starts a channel's next season now: a new world with no impacts. */
  async adminRestartChannel(id: string): Promise<AdminChannelSet> {
    return (await this.request(`/admin/channels/${encodeURIComponent(String(id))}/restart`, { method: "POST" })).json();
  }

  /** POST /admin/channels/{id}/network: Gives a channel a network of its own (the planner's network settings: nodes, pools, miners, latency, difficulty), or null for its profile's; its next season starts now on it. */
  async adminSetChannelNetwork(id: string, body: Record<string, unknown> | null): Promise<AdminChannelSet> {
    return (await this.request(`/admin/channels/${encodeURIComponent(String(id))}/network`, { method: "POST", type: "application/json", body: JSON.stringify(body) })).json();
  }
}
