> For the complete documentation index, see [llms.txt](https://docs.edgegap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.edgegap.com/web-games/matchmaking.md).

# Matchmaking

Add matchmaking to your browser game with Edgegap's optional JavaScript SDK, ready to extend and customize as your multiplayer project grows.

This SDK is an optional starter kit for JavaScript and TypeScript users, which can be extended and customized later.

## 💡 Features

{% columns %}
{% column %}

* Complete Examples
* Promises and Async Loops
* Tickets, Groups, Teams
  {% endcolumn %}

{% column width="33.33333333333333%" %}

* Match Variables
* Type Definitions (TypeScript)
* Backfill Automation
  {% endcolumn %}

{% column width="33.33333333333333%" %}

* Browser, Node.js, Bun, Deno
* Zero Dependencies
* Automated Retry
  {% endcolumn %}
  {% endcolumns %}

## ✔️ Preparation

JavaScript SDK contains optional integration utilities for Deployments and Matchmaking. This package officially supports all evergreen browsers, Node.js 22 and newer, Bun, and Deno.

{% hint style="info" %}
**The latest version of JavaScript SDK is `1.1.0`**. All examples in this documentation are up to date.
{% endhint %}

{% hint style="success" %}
This package is provided 100% free of charge, under Terms and Conditions of Free Tier.
{% endhint %}

#### Requirements

<details>

<summary>Allow your game's web origins in Matchmaker configuration</summary>

Browsers only call your Matchmaker from pages whose origin is listed in `allowed_cors_origins` of your Matchmaker configuration. Entries are matched against the full origin, including the scheme and port:

* `https://*.my-game.com` matches every subdomain over HTTPS,
* `localhost` matches nothing, list local dev servers in full, for example `http://localhost:5173` .

</details>

<details>

<summary>Enable TLS Upgrade for your game server's WebSocket port</summary>

Set your [App Version port](/learn/orchestration/application-and-versions.md#port-mapping) protocol to `WS` and enable **TLS Upgrade**, so browsers can connect with secure WebSockets (`wss://`).

</details>

#### Installation

Install the package from npm using your favorite package manager:

{% tabs %}
{% tab title="npm" %}
{% code collapsedlinecount="10" %}

```bash
npm install @edgegap/sdk
```

{% endcode %}
{% endtab %}

{% tab title="pnpm" %}
{% code collapsedlinecount="10" %}

```bash
pnpm add @edgegap/sdk
```

{% endcode %}
{% endtab %}

{% tab title="yarn" %}
{% code collapsedlinecount="10" %}

```bash
yarn add @edgegap/sdk
```

{% endcode %}
{% endtab %}

{% tab title="bun" %}
{% code collapsedlinecount="10" %}

```bash
bun add @edgegap/sdk
```

{% endcode %}
{% endtab %}

{% tab title="deno" %}
{% code collapsedlinecount="10" %}

```bash
deno add npm:@edgegap/sdk
```

{% endcode %}
{% endtab %}

{% tab title="CDN (no bundler)" %}
Load the browser build in HTML games without a bundler. Everything is exposed under the `Edgegap` global, with Matchmaking under `Edgegap.matchmaking`:

{% code collapsedlinecount="10" %}

```html
<script src="https://cdn.jsdelivr.net/npm/@edgegap/sdk@1.1.0/dist/browser/edgegap.iife.js"></script>
```

{% endcode %}

Or import it as an ES module:

{% code collapsedlinecount="10" %}

```html
<script type="module">
  import { matchmaking, getConnectionUrl } from 'https://cdn.jsdelivr.net/npm/@edgegap/sdk@1.1.0/dist/browser/edgegap.js'
</script>
```

{% endcode %}
{% endtab %}
{% endtabs %}

The package has two entry points:

* `@edgegap/sdk/matchmaking` :
  * `GroupAgent`, `ServerAgent`, `MatchEnvironment`, `MatchmakingError`, `Api` .
* `@edgegap/sdk` - shared utilities:
  * `getConnectionUrl`, `DeploymentEnvironment` .
  * `Observable`, `Logger`, misc. HTTP helpers.

#### Verified Sources

This package is only published on npm as [`@edgegap/sdk`](https://www.npmjs.com/package/@edgegap/sdk), from source code on [GitHub](https://github.com/edgegap/sdk-js-sessions-matchmaker-server-browser). CDNs like jsDelivr and unpkg mirror the npm package. Do not trust unverified sources, packages with similar names outside the `@edgegap` scope are not affiliated with Edgegap!

{% hint style="success" %}
**Every release is published by GitHub Actions with** [**npm provenance**](https://docs.npmjs.com/generating-provenance-statements), linking the package to the commit that built it. Verify your installed packages with `npm audit signatures` .
{% endhint %}

{% hint style="info" %}
When loading from CDN, pin exact version with [Subresource Integrity](https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity) to refuse modified versions.
{% endhint %}

### Update Package

Update to the latest version with your package manager:

{% code collapsedlinecount="10" %}

```bash
npm install @edgegap/sdk@latest
```

{% endcode %}

When loading from a CDN, change the version in the URL and regenerate your integrity hash.

{% hint style="info" %}
Some releases may contain breaking changes. This will be indicated by a new MAJOR version.
{% endhint %}

## 🍀 Getting Started

This guide assumes basic knowledge of [Matchmaking](/learn/matchmaking.md) concepts and a running Matchmaker.

{% hint style="success" %}
**We strongly recommend cloning our** [**Three.js Minigames**](#three.js-minigames) **sample** to follow along code as you read this document.
{% endhint %}

### Overview

Our SDK utilizes heavily the [Observer](https://refactoring.guru/design-patterns/observer) programming pattern, and lets you consume changes with callbacks, [promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise), or [async iteration](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for-await...of) - whichever fits your game loop.

{% hint style="info" %}
This package integrates [Matchmaking](/learn/matchmaking.md) and [Deployments](/learn/orchestration/deployments.md) utilities. You may freely reuse any code for your own customized forks and integrations.
{% endhint %}

This package includes:

* Service-Specific Utilities:
  * [#group-agent](#group-agent "mention") - a complete browser client integration to reuse/extend.
  * [#server-agent](#server-agent "mention") - a complete game server backfill integration to reuse/extend.
  * `MatchEnvironment` - typed [Matchmaking Variables](/learn/matchmaking/matchmaker-in-depth.md#injected-environment-variables) for your game server.
  * `Api` - endpoint definitions, error handling, and retry automations.
* Service-Specific Types - request and response types matching Matchmaking API JSON exactly.
* Shared Utilities - connection URLs, deployment variables, logging, HTTP, observables, etc...

### Group Agent

**Ticket management, host retrieval, and beacon discovery** are performed by Group Agent.

Once instantiated, agent's **handler (your game code) must initialize the agent** and optionally provide:

* `onMonitorUpdate` callback - observe service health changes.
* `onGroupUpdate` callback - observe and react to group and host assignment changes.

Once initialized, this agent will automatically provide validations and hook up logging observers, finishing with a single call to the monitoring API endpoint to indicate service health.

Agent's handler is expected to take control and call agent functions from this point:

* `beacons` to retrieve a list of available [Ping Beacons](/learn/orchestration/ping-beacons.md).
* `measureBeaconsRoundTripTime` to provide ping measurements against a given set of beacons.
* `createGroup` for the lobby leader to create a joinable group, which friends can be invited to.
* `joinGroup` to join an existing group using the group ID sent through 3rd party lobby/backend.
* `setReady` to mark group owner and members as ready and begin searching for matches.
* `nextAssignment` to wait for the host assignment once the group starts searching.
* `resumeMatchmaking` to load a saved group and continue searching in case the page reloaded.
* `stopMatchmaking` to delete the ticket (if not matched) and abandon queue.
* `dispose` to clear all timers, abort requests, and remove subscribers.

Here are the three different concurrency patterns demonstrated, choose whichever you prefer:

{% tabs %}
{% tab title="Promise" %}
{% code collapsedlinecount="10" %}

```javascript
import { getConnectionUrl } from '@edgegap/sdk'
import { GroupAgent, MatchmakingError } from '@edgegap/sdk/matchmaking'

const agent = new GroupAgent({ baseUrl: '<matchmaker url>', authToken: '<matchmaker auth token>' })
agent.initialize()

const { beacons } = await agent.beacons()
const latencies = await agent.measureBeaconsRoundTripTime(beacons)

agent.createGroup({ profile: 'simple-example', attributes: { beacons: latencies }, is_ready: true }, true)

try {
  const assignment = await agent.nextAssignment()
  connect(getConnectionUrl(assignment, 'gameport'), agent.group.current.ticket_id)
} catch (error) {
  if (!(error instanceof MatchmakingError)) throw error
  showRetryButton(error.message)
}
```

{% endcode %}
{% endtab %}

{% tab title="Async Loop" %}
{% code collapsedlinecount="10" %}

```javascript
import { getConnectionUrl, ObservableActionType } from '@edgegap/sdk'
import { GroupAgent } from '@edgegap/sdk/matchmaking'

const agent = new GroupAgent({ baseUrl: '<matchmaker url>', authToken: '<matchmaker auth token>' })
agent.initialize()

const { beacons } = await agent.beacons()
const latencies = await agent.measureBeaconsRoundTripTime(beacons)

agent.createGroup({ profile: 'simple-example', attributes: { beacons: latencies }, is_ready: true }, true)

for await (const { type, message, event, current } of agent.group) {
  if (type === ObservableActionType.Error) {
    showRetryButton(message)
    break
  }
  if (event.code !== 'updated') continue
  showStatus(current.status)
  if (current.status === 'CANCELLED') {
    showRetryButton(message)
    break
  }
  if (current.status !== 'HOST_ASSIGNED') continue
  connect(getConnectionUrl(current.assignment, 'gameport'), current.ticket_id)
  break
}
```

{% endcode %}
{% endtab %}

{% tab title="Callbacks" %}
{% code collapsedlinecount="10" %}

```javascript
import { getConnectionUrl, ObservableActionType } from '@edgegap/sdk'
import { GroupAgent } from '@edgegap/sdk/matchmaking'

const agent = new GroupAgent({ baseUrl: '<matchmaker url>', authToken: '<matchmaker auth token>' })
agent.initialize(
  (_monitor, _type, message) => console.log('matchmaker', message),
  (group, type, message, event) => {
    if (type === ObservableActionType.Error) return showRetryButton(message)
    if (event.code !== 'updated') return
    showStatus(group.current.status)
    if (group.current.status === 'CANCELLED') return showRetryButton(message)
    if (group.current.status === 'HOST_ASSIGNED')
      connect(getConnectionUrl(group.current.assignment, 'gameport'), group.current.ticket_id)
  },
)

const { beacons } = await agent.beacons()
const latencies = await agent.measureBeaconsRoundTripTime(beacons)

agent.createGroup({ profile: 'simple-example', attributes: { beacons: latencies }, is_ready: true }, true)
```

{% endcode %}
{% endtab %}
{% endtabs %}

`profile` and `attributes` must match a profile in your Matchmaker configuration, `gameport` is the port name from your App Version. `getConnectionUrl` returns a `wss://` URL for WebSocket ports.

{% hint style="warning" %}
**Browsers can't send the ICMP, UDP, or TCP pings that Edgegap beacons answer**, so `measureBeaconsRoundTripTime` reports `0` for every beacon by default. Pass your own `ping` implementation in agent options to measure latency another way.
{% endhint %}

When a new player connection is established, player is expected to send their ticket ID to the game server using your netcode, to correlate connections with [Matchmaking Variables](/learn/matchmaking/matchmaker-in-depth.md#injected-environment-variables). Your game server may be still initializing at this time, so **players must retry connection several times**:

{% code collapsedlinecount="10" %}

```javascript
const connect = (url, ticketId, attempts = 10) => {
  const socket = new WebSocket(url)
  let opened = false
  socket.onopen = () => {
    opened = true
    socket.send(JSON.stringify({ ticketId }))
  }
  socket.onclose = () => !opened && attempts > 1 && setTimeout(() => connect(url, ticketId, attempts - 1), 1000)
}
```

{% endcode %}

{% hint style="success" %}
Save connection details in client or game backend to reconnect in case of an unexpected crash. See [#resume-after-reload](#resume-after-reload "mention").
{% endhint %}

### Server Agent

Server Agent performs **backfill creation, detecting ticket assignment, detecting backfill abandonment and removal. It also tracks and re-creates backfills which expired, had the connection grace period elapsed, or were abandoned - to reach the desired team size**.

Server Agent keeps track of currently connected players and automatically includes their tickets in new backfills to ensure that backfilled players will match using the configured profile's rules.

Once instantiated, agent's **handler (your server code) must initialize the agent** and provide:

* `tickets` - players already matched to this server, from [Matchmaking Variables](/learn/matchmaking/matchmaker-in-depth.md#injected-environment-variables).
* `onMonitorUpdate` callback - observe service health changes.
* `onBackfillUpdate` callback - observe and react to backfill changes.

Once initialized, this agent will automatically provide validations and hook up logging observers, finishing with a single call to the monitoring API endpoint to indicate service health.

Server handler is expected to take control and call agent functions from this point:

* `addBackfills` to create backfills until assigned players and open backfills reach target team size.
* `playerConnected` to prevent replacing initial or backfilled players who never connected.
  * Returns the player's ticket, or `null` if the ticket isn't assigned to this server - reject such connections.
* `abandonPlayer` to signal that a connected player left the server and needs to be replaced.
* `abandonAllBackfills` to immediately stop backfilling server and delete backfills.
  * This method should be called before stopping your server. Players who are already assigned at this time need to rejoin matchmaking since the server is shutting down.
  * Assigned backfills at the time of this call can still connect within configured grace period.
* `abandonBackfill` to forcefully abandon a specific backfill (custom handler implementations).
* `dispose` to clear timers, abort requests, and remove subscribers.

{% code collapsedlinecount="10" %}

```javascript
import { WebSocketServer } from 'ws'
import { DeploymentEnvironment } from '@edgegap/sdk'
import { MatchEnvironment, ServerAgent } from '@edgegap/sdk/matchmaking'

const deployment = DeploymentEnvironment.fromEnv(process.env)
const match = MatchEnvironment.fromEnv(process.env)

const agent = new ServerAgent({
  baseUrl: process.env.MATCHMAKER_URL,
  authToken: process.env.MATCHMAKER_TOKEN,
  profile: match.matchProfile,
  targetTeamSize: 4,
  attributes: {
    assignment: {
      request_id: deployment.requestId,
      fqdn: deployment.fqdn,
      public_ip: deployment.publicIp,
      ports: deployment.portMapping,
      location: deployment.location,
    },
  },
})

agent.initialize(
  match.tickets,
  (_monitor, _type, message) => console.log('matchmaker', message),
  (_backfills, _type, _message, event) => {
    if (event.code === 'assigned') console.log(`ticket ${event.ticketId} is joining`)
  },
)
agent.addBackfills()

const wss = new WebSocketServer({ port: 7777 })

wss.on('connection', (socket) => {
  socket.once('message', (data) => {
    const { ticketId } = JSON.parse(data.toString())
    if (!agent.playerConnected(ticketId)) return socket.close(4001, 'unknown ticket')
    socket.once('close', () => agent.abandonPlayer(ticketId))
  })
})

process.once('SIGTERM', () => agent.abandonAllBackfills(() => process.exit(0)))
```

{% endcode %}

{% hint style="danger" %}
**Load server tokens from environment variables**, for example using [App Version Variables](/learn/orchestration/application-and-versions.md#injected-variables). Tokens your game servers use must never be included in a browser bundle.
{% endhint %}

### Match Variables

Read [Matchmaking Variables](/learn/matchmaking/matchmaker-in-depth.md#injected-environment-variables) in your game server to verify connecting players and assign teams, without calling any API:

{% code collapsedlinecount="10" %}

```javascript
import { WebSocketServer } from 'ws'
import { MatchEnvironment } from '@edgegap/sdk/matchmaking'

const match = MatchEnvironment.fromEnv(process.env)
const wss = new WebSocketServer({ port: 7777 })

wss.on('connection', (socket) => {
  socket.once('message', (data) => {
    const { ticketId } = JSON.parse(data.toString())
    const ticket = match.tickets[ticketId]
    if (!ticket) return socket.close(4001, 'unknown ticket')
    console.log(`player joined team ${ticket.team_id} in match ${match.matchId}`)
  })
})
```

{% endcode %}

`match` holds `matchProfile`, `matchId`, `tickets` (by ticket ID), `groups`, `teams`, and the matched `equality` and `intersection` values. Parsing never throws, so the same server runs locally.

Players joining through backfill aren't listed in `match.tickets`, so servers using Server Agent verify connecting players with `agent.playerConnected` instead.

## 🧪 Samples

Get started with samples including a complete functioning integration for both server and client.

### [Three.js Minigames](#three.js-minigames)

Includes five browser games with a full player lifecycle implementation - groups, host assignment retrieval, and connection retries. Demonstrates how to read injected match variables on server side, assign players to teams from a 2-team profile, and stop deployments when the match ends.

Add a new matchmaking profile and game server to expand this example to fit your game easily.

### Group Up

Join matchmaking queue with a group of friends, requiring all players to confirm before starting search. The group owner creates a group and shares its ID through your lobby or backend:

{% code collapsedlinecount="10" %}

```javascript
const attributes = { beacons: latencies }

// on the group owner's device
agent.group.subscribe((group, _type, _message, event) => {
  if (event.code === 'created') shareWithFriends(group.current.group_id)
})
agent.createGroup({ profile: 'simple-example', attributes, is_ready: false }, true)

// on each friend's device, once they receive the group ID
agent.joinGroup({ profile: 'simple-example', attributes, is_ready: false }, groupId, true)

// on every device, when the player is ready
agent.setReady(true)
const assignment = await agent.nextAssignment()
```

{% endcode %}

### Resume After Reload

Save the group when it changes, and resume searching (or reconnect) when the page loads again:

{% code collapsedlinecount="10" %}

```javascript
agent.initialize(undefined, (group) => {
  if (group.current) localStorage.setItem('edgegap-group', JSON.stringify(group.current))
  else if (group.previous?.status !== 'HOST_ASSIGNED') localStorage.removeItem('edgegap-group')
})

const saved = JSON.parse(localStorage.getItem('edgegap-group'))
if (saved?.status === 'HOST_ASSIGNED') {
  connect(getConnectionUrl(saved.assignment, 'gameport'), saved.ticket_id)
} else if (saved) {
  agent.resumeMatchmaking(saved)
  connect(getConnectionUrl(await agent.nextAssignment(), 'gameport'), saved.ticket_id)
}
```

{% endcode %}

Remove the saved group with `localStorage.removeItem('edgegap-group')` once the match ends, so players don't reconnect to a finished match.

### Backfill

Reuse server capacity and add players to ongoing deployments. Set target team size and wait for players to be added. Includes automated connection grace period and recreating expired backfills, see [#server-agent](#server-agent "mention").

Customize your server handler for multi-team or asymmetric team compositions.

## ⚙️ Customization

This SDK is meant to be extended and modified, though some modifications may be risky:

✅ Handler - safely hook up UI observers and perform minor additions or modifications,

⚠️ Agent - fork the source code and modify player lifecycle management at your own risk,

⚠️ API - write your own integration from scratch, using `Api` and cherry-picked utilities.

Handlers may observe any events emitted by Server and Group agents as described below.

{% hint style="warning" %}
Make sure to get familiar with [Matchmaking In-Depth](/learn/matchmaking/matchmaker-in-depth.md) concepts before making customizations.
{% endhint %}

{% hint style="info" %}
If you need help, [please reach out to us over Discord](https://discord.gg/MmJf8fWjnt). For live games support see our [ticketing system](https://edgegap.atlassian.net/servicedesk/customer/portal/3).
{% endhint %}

### Client Events

Group Agent emits typed events for handlers to observe and consume.

{% hint style="success" %}
Read state by accessing `.current` and `.previous` of any observable, and **branch on `event.code`** since event messages may change between versions. 🔴 `Error` events also store the full error message in `.error` .
{% endhint %}

Preview events emitted by observable `monitor` :

<table data-full-width="true"><thead><tr><th width="125">Action Type</th><th width="200">Event Code</th><th width="300">Event Parameters</th><th>Description</th></tr></thead><tbody><tr><td>🟢 <code>Update</code></td><td><code>healthy</code></td><td>-</td><td>All systems OK.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>unhealthy</code></td><td>-</td><td>Unexpected issue.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>monitor_failed</code></td><td><code>detail</code></td><td>Misconfiguration or unexpected issue.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>beacons_failed</code></td><td><code>detail</code></td><td>Unexpected issue.</td></tr></tbody></table>

Preview events emitted by observable `group` :

<table data-full-width="true"><thead><tr><th width="125">Action Type</th><th width="200">Event Code</th><th width="300">Event Parameters</th><th>Description</th></tr></thead><tbody><tr><td>🟢 <code>Update</code></td><td><code>created</code></td><td>-</td><td>Group created OK.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>create_failed</code></td><td><code>detail</code></td><td>Failed to create group.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>joined</code></td><td>-</td><td>Group joined OK.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>join_failed</code></td><td><code>detail</code></td><td>Failed to join group.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>resumed</code></td><td>-</td><td>Group resumed OK.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>not_found</code></td><td><code>detail?</code></td><td>Not a member of group, or group expired.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>conflict</code></td><td>-</td><td>Please abandon current group before trying to create/join a new group, or pass <code>abandon: true</code>.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>member_updated</code></td><td><code>isReady</code></td><td>Member updated with new Ready value.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>member_update_failed</code></td><td>-</td><td>Can't unready after the group started searching for match.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>update_failed</code></td><td><code>detail</code></td><td>Failed to update group member.</td></tr><tr><td>🔵 <code>Log</code></td><td><code>polling</code></td><td><code>attempt</code>, <code>maxAttempts</code></td><td>Agent initiated polling group status. Emitted only with <code>logPollingUpdates: true</code>.</td></tr><tr><td>🔵 <code>Log</code></td><td><code>polling_stopped</code></td><td>-</td><td>Agent stopped polling group status. Emitted only with <code>logPollingUpdates: true</code>.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>polling_failed</code></td><td><code>reason: 'max_retries'</code>, <code>detail</code></td><td>Agent exhausted maximum consecutive polling retries. Check service status.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>polling_failed</code></td><td><code>reason: 'error'</code>, <code>detail</code></td><td>Agent received a non-retryable error when polling. Check service status.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>updated</code></td><td><code>status</code></td><td>Detected group status change while polling.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>abandoned</code></td><td>-</td><td>Deleted ticket OK.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>abandon_failed</code></td><td><code>reason: 'not_found'</code></td><td>Agent couldn't find the group to delete, possibly expired.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>abandon_failed</code></td><td><code>reason: 'match_found'</code></td><td>Agent couldn't delete a matched group. Disable abandonment or <a data-mention href="/learn/matchmaking/matchmaker-in-depth.md#backfill-match">In-Depth Look</a> to replace the player.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>abandon_failed</code></td><td><code>reason: 'error'</code>, <code>detail</code></td><td>Failed to delete group or membership.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>restart_skipped</code></td><td>-</td><td>Group already matched, follow-up action after abandonment was skipped.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>removed</code></td><td>-</td><td>Group finished or expired, local reference deleted.</td></tr></tbody></table>

### Server Events

Server Agent emits typed events for handlers to observe and consume. Server Agent is only needed for [#backfill](#backfill "mention").

{% hint style="info" %}
Currently assigned tickets can be read from Server Agent property `assignments` at any time, and connection times from `connectedAt` .
{% endhint %}

{% hint style="success" %}
Read state by accessing `.current` and `.previous` of any observable, and **branch on `event.code`** since event messages may change between versions. 🔴 `Error` events also store the full error message in `.error` .
{% endhint %}

Preview events emitted by observable `monitor` :

<table data-full-width="true"><thead><tr><th width="125">Action Type</th><th width="200">Event Code</th><th width="300">Event Parameters</th><th>Description</th></tr></thead><tbody><tr><td>🟢 <code>Update</code></td><td><code>healthy</code></td><td>-</td><td>All systems OK.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>unhealthy</code></td><td>-</td><td>Unexpected issue.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>monitor_failed</code></td><td><code>detail</code></td><td>Misconfiguration or unexpected issue.</td></tr></tbody></table>

Preview events emitted by observable `backfills` :

<table data-full-width="true"><thead><tr><th width="125">Action Type</th><th width="200">Event Code</th><th width="300">Event Parameters</th><th>Description</th></tr></thead><tbody><tr><td>🟢 <code>Update</code></td><td><code>created</code></td><td><code>backfillId</code></td><td>Backfill created OK.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>create_failed</code></td><td><code>detail</code></td><td>Unexpected issue with backfill. Check service status.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>capacity_reached</code></td><td>-</td><td>Server is full, no backfill created.</td></tr><tr><td>🔵 <code>Log</code></td><td><code>polling</code></td><td><code>attempt</code>, <code>maxAttempts</code></td><td>Agent initiated polling backfills. Emitted only with <code>logPollingUpdates: true</code>.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>assigned</code></td><td><code>backfillId</code>, <code>ticketId</code></td><td>Assigned ticket to backfill OK.</td></tr><tr><td>🔵 <code>Log</code></td><td><code>polling_stopped</code></td><td>-</td><td>Agent stopped polling, no backfills are left to poll.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>polling_failed</code></td><td><code>backfillId</code>, <code>reason: 'not_found'</code></td><td>Not found, removing reference. Backfill most likely expired.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>polling_failed</code></td><td><code>backfillId</code>, <code>reason: 'max_retries'</code></td><td>Exhausted retries, abandoning all backfills.</td></tr><tr><td>🔴 <code>Error</code></td><td><code>polling_failed</code></td><td><code>backfillId</code>, <code>reason: 'error'</code>, <code>detail</code></td><td>Unexpected issue, abandoning and replacing backfill.</td></tr><tr><td>🔵 <code>Log</code></td><td><code>abandoned</code></td><td><code>backfillId</code></td><td>Backfill abandoned, removing reference.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>abandon_failed</code></td><td><code>backfillId</code>, <code>reason: 'not_found'</code></td><td>Backfill not found, likely expired or duplicate call.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>abandon_failed</code></td><td><code>backfillId</code>, <code>reason: 'error'</code>, <code>detail</code></td><td>Abandonment failed, removing reference.</td></tr><tr><td>🟢 <code>Update</code></td><td><code>removed</code></td><td><code>backfillId</code></td><td>Backfill reference removed. Backfill will be replaced automatically if not stopped.</td></tr><tr><td>🟡 <code>Warn</code></td><td><code>remove_failed</code></td><td><code>backfillId</code></td><td>Backfill reference not found, likely duplicate call.</td></tr></tbody></table>
