---
title: "Event stream — API reference"
description: "The event union a quario render emits, the walk driver, and the token seam every render target consumes."
url: "https://getquario.com/docs/reference/events"
---

# Event stream

What a render emits. Nothing in the stream is markup-escaped, because escaping
belongs to the target at its own edge.

## `ReportEventStream`

```ts title="signature"
interface ReportEventStream {
  (data?: unknown): Generator<ReportEvent, void, undefined>
  readonly names: readonly string[]
  readonly readPaths: ReadPaths
  readonly functions: readonly FunctionSignature[]
  readonly paths: readonly QueryPath[]
}
```

What a target’s `compile` receives.

## `ReportEvent`

```ts title="signature"
type ReportEvent =
  | ReportStartEvent
  | ItemEvent
  | ImageEvent
  | SplitStartEvent
  | SplitEndEvent
  | GroupStartEvent
  | GroupEndEvent
  | TableStartEvent
  | RowEvent
  | TotalRowEvent
  | TableEndEvent
  | ReportEndEvent
```

### `ReportStartEvent`

```ts title="signature"
interface ReportStartEvent {
  type: 'report-start'
  params: Record<string, JsonValue>
  aggregates: Record<string, unknown>
  page?: PageBandRenderers
  columns?: number
  style?: Record<string, unknown>
  marking?: string
  margin?: number | PageMargin
  size?: PaperName
  orientation?: Orientation
  headerHeight?: number
  locale?: string
  currency?: string
  timeZone?: string
}
```

`marking` carries the unlicensed wording when the instance has no valid key.
`margin`, `size` and `orientation` are the document’s paper, present only where
it declared them and in the shape the author wrote. A
target settles what it needs from this event in its handler, before pulling the
stream further.

### `ItemEvent` and `ImageEvent`

```ts title="signature"
interface ItemEvent {
  type: 'item'
  role: ItemRole
  path: string
  tokens: Token[]
  style?: Record<string, unknown>
  run?: Record<string, unknown>
}

interface ImageEvent {
  type: 'image'
  role: ItemRole
  path: string
  bytes: Uint8Array
  format: 'png' | 'jpeg' | 'svg'
  width: number
  height: number
  fit: ImageFit
  alt?: Token[]
  style?: Record<string, unknown>
  run?: Record<string, unknown>
}
```

The engine reads `format`, `width` and `height` off the bytes’ own header, so
no target parses the PNG IHDR, JPEG frame header, or SVG root for itself. Bytes
too short to state a size never reach a target at all: that’s one render error
on every target alike. Past the size, the engine vouches for nothing. Bytes a
target can’t write are that target’s render error, which it mints with
`imageError`.

### `SplitStartEvent` and `SplitEndEvent`

```ts title="signature"
interface SplitStartEvent {
  type: 'split-start'
  role: ItemRole
  path: string
  slots: { width?: number }[]
  style?: Record<string, unknown>
}

interface SplitEndEvent { type: 'split-end' }
```

A [grid](https://getquario.com/docs/reference/report-schema.md) reaches the stream as one split per
row, and `path` then names the row. A target that draws splits draws a grid
with no change.

### `GroupStartEvent` and `GroupEndEvent`

```ts title="signature"
interface GroupStartEvent {
  type: 'group-start'
  name: string
  path: string
  depth: number
  key: unknown
  aggregates: Record<string, unknown>
  break?: 'page'
  breakAfter?: 'page'
  reset?: 'page'
  columns?: number
  label?: string
  collapsed?: true
}

interface GroupEndEvent {
  type: 'group-end'
  name: string
  depth: number
  hollow?: true
}
```

The stream states the two page edges, never the four positions a group’s
`break` names. `break` is present where a page turns before the instance, and
`breakAfter` where one turns after it.

`label` is the group’s `label` resolved for this instance. It’s absent where
the group declares none, so a target deriving a name of its own keeps deriving
it.

`collapsed` is present, and only ever `true`, when the group asks that the
instance open shut. A target with somewhere to put a row a reader can open
again honours it. A target without such a place reads the stream through
`collapse()` instead.

`hollow` is present, and only ever `true`, when the instance’s bands emitted
nothing between its brackets. Such an instance occupies nothing, turns no page
by opening, and earns no outline entry. An instance holding nothing but hollow
instances is hollow too.

### `TableStartEvent`, `RowEvent`, `TotalRowEvent`, `TableEndEvent`

```ts title="signature"
interface TableStartEvent {
  type: 'table-start'
  path: string
  header: { cells: EventCell[]; style?: Record<string, unknown> }
  columns: { width?: number }[]
}

interface RowEvent {
  type: 'row'
  cells: EventCell[]
  style?: Record<string, unknown>
  run?: Record<string, unknown>
}

interface TotalRowEvent {
  type: 'total-row'
  cells: EventCell[]
  style?: Record<string, unknown>
}

interface TableEndEvent { type: 'table-end' }
```

A row’s `style` still arrives on the row, and a cell’s on the cell. The box is
the exception. The engine resolves it onto the cells before any target sees
them.

## Cells and tokens

### `EventCell`

```ts title="signature"
interface EventCell {
  tokens: Token[]
  style?: Record<string, unknown>
  span?: number
  path?: string
}
```

`span` is present only when a cell covers more than one column.

### `Token`

```ts title="signature"
interface TokenStyle { style?: Record<string, unknown> }
type PageField = 'page.number' | 'page.total'
type LiteralToken = SjabloonLiteralToken & TokenStyle
type ValueToken = SjabloonValueToken & TokenStyle & { field?: PageField }
type Token = LiteralToken | ValueToken
```

A cell arrives as tokens rather than as a string. A cell whose template is one
interpolation is a single value token holding the value itself. That’s how a
spreadsheet target writes a real numeric cell while a markup target writes
escaped text. Join them with `text()`, or take the typed value with `typed()`.

A token may carry `style`, the resolved declarations for that stretch of the
cell. That’s the cell’s own with its run’s layered over them, already
composed. It’s present only where it differs from the cell’s, so a consumer
that never reads it stays correct. `styledRuns(tokens)` groups a cell back into
its runs.

A value token carries `field` where its interpolation is exactly
`{{ page.number }}` or `{{ page.total }}`. The value still holds the number this
render saw. `field` names which page value the token stands for, so a target
whose own document format numbers pages can write a live field there instead.
Anything computed from a page value (`{{ page.number + 1 }}`) is an ordinary
value and carries no `field`. Write the bare token wherever a live number
matters. A target that paginates itself, or not at all, ignores the key.

## Page bands

```ts title="signature"
interface PageInfo { number: number; total: number }

type PageBandEvent = ItemEvent | ImageEvent | SplitStartEvent | SplitEndEvent

interface PageBandRenderers {
  header?: (page: PageInfo) => PageBandEvent[]
  footer?: (page: PageInfo) => PageBandEvent[]
}
```

The engine doesn’t walk page bands. A paginating target calls the renderers from
`report-start` once it knows the page numbers. An unpaginated target ignores
them.

## `ItemRole`

```ts title="signature"
type ItemRole =
  | 'report-header'
  | 'empty'
  | 'group-header'
  | 'detail'
  | 'group-footer'
  | 'report-footer'
  | 'page-header'
  | 'page-footer'
```

`isReportBand(role)` answers whether a role names one of the report’s own bands
rather than a group instance’s.

## Driving the stream

```ts title="signature"
type WalkEvent = ReportEvent | SplitEvent

type WalkHandlers = {
  [K in WalkEvent['type']]?: (event: Extract<WalkEvent, { type: K }>) => void
}

function walk(events: Iterable<WalkEvent>, handlers: WalkHandlers): Promise<void>
function splits(events: Iterable<ReportEvent>): Generator<WalkEvent, void>
function breathe(): Promise<void>
```

A missing handler ignores that event. Handlers are synchronous, and `walk`
doesn’t await a promise one returns. A target running a loop of its own calls `breathe`
between batches so a long report never blocks the host.

`splits` folds each split bracket into one `split` event carrying the opening
event’s fields and its slot events under `items`. Everything else passes
through unchanged and lazily. Read `walk(splits(events), handlers)` and register
a `split` handler rather than a `split-start` and `split-end` pair. The stream
itself doesn’t change.

```ts title="signature"
interface SplitEvent extends Omit<SplitStartEvent, 'type'> {
  type: 'split'
  items: (ItemEvent | ImageEvent)[]
}
```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Event stream","item":"https://getquario.com/docs/reference/events"}]}
```
