Skip to content

Connection and lifecycle

The broker is the one connection your realm holds into FKN, where a realm is one JavaScript execution context such as a window or a worker. This page covers what waits for that connection and what gives up, what a replaced broker does to a call in flight, and how shell updates and busy tokens fit together.

Every call that reaches the broker travels over this connection, and so do the sockets, streams and frames the broker hands back. opfs and the in-memory half of fs never reach the broker. Neither do the root fetch and attachFrame while the extension is exposed. Nothing on this page applies to them, and backends explains which calls take which path.

An error thrown in the broker crosses a realm before you catch it. handling errors gives the rules for matching each family, what survives the hop and which failures are worth a retry. Every message has its row on every error.

Every entry that reaches the broker connects to it once per realm, at import. Every call on that surface waits for the connection.

apiPromise from @fkn/lib/api is that wait, and it never rejects. A frame that never loads, or a worker nobody relayed, leaves it pending for the life of the realm, and every call awaiting it stays pending too. The exact signatures of that entry are in the generated API reference for @fkn/lib/api.

The wait is deliberate: a broker that arrives ten seconds later still serves the call.

In a window realm these calls wait rather than fail: cloud.fetch, cloud.fs, cloud.quota, dns.lookup, account.login, account.logout, account.info, the packages.* calls the broker answers, connect(), promptInstall(), promptRelay(), shell.updateReady() and shell.applyUpdate(). The subscription forms, account.onChange, shell.onUpdate and shell.onUpdateTaken, return at once. So does shell.busyReasons(), which never asks the broker.

Outside a window most of that list answers at once instead, with false, null or a no-op. Only cloud.fetch, cloud.fs, cloud.quota, dns.lookup and the packages.* calls the broker answers keep waiting there. The full list is on what works in a relayed worker.

net and dgram are the exception, and http with them, since every request opens a net.Socket. A socket that never emits connect or error looks like a transport fault.

So connect, listen and bind wait through apiWithin instead, for 8,000 ms at first, then for 1,000 ms per call once the realm’s short deadline has latched. A miss lands on the socket’s error event:

engine.ts
import * as
import net
net
from '@fkn/lib/net'
import {
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
const
const socket: net.Socket
socket
=
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
({
TcpSocketConnectOpts.host?: string | undefined
host
: '67.215.246.10',
TcpSocketConnectOpts.port: number
port
: 6881 })
const socket: net.Socket
socket
.
Stream.Duplex.on<"error">(eventName: "error", listener: (err: Error) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) => {
if (
error: Error
error
instanceof
class BrokerUnreachableError
BrokerUnreachableError
) {
error: BrokerUnreachableError
error
.
Error.message: string
message
// @fkn/lib: no broker connection within 8000ms, so an outbound tcp socket could not be requested
}
})

Inside a worker the page never relayed, this is the whole failure: no connect, no close, only this one error after the deadline. The detail is on the worker nobody relayed. If your own UI has to report a missing broker, use the same race the sockets use, with a what that names the caller in the message:

app.ts
import {
const apiWithin: (what: string) => Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>
apiWithin
,
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
try {
const
const api: {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}
api
= await
function apiWithin(what: string): Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>
apiWithin
('the quota readout')
const {
const remaining: number
remaining
} = await
const api: {
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}
api
.
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
}
cloud
.
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>
quota
()
const remaining: number
remaining
// bytes of the free daily volume left today
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
class BrokerUnreachableError
BrokerUnreachableError
) {
var error: BrokerUnreachableError
error
.
Error.message: string
message
// @fkn/lib: no broker connection within 8000ms, so the quota readout could not be requested
} else {
throw
var error: unknown
error
}
}

The what you pass is the phrase in the message, so the rejection names the caller. That rejection is where a page learns the deadline was missed.

Every other error is rethrown, since a storage or permission failure is not a missing broker. api.cloud.quota() is the raw call. The public cloud.quota() built on it renames remaining to remainingBytes.

cloud.available() does not tell you whether this connection exists. It checks the shape of the realm and answers true in every window and every worker, as what available() means explains.

The short deadline is a latch. The timer behind the realm’s first bounded wait flips it when it fires, whether or not that wait was answered, and nothing resets it. From then on every connect, listen, bind and apiWithin in the realm waits the short deadline for a broker that is still missing.

The broker document can be replaced under you. Taking an update to the shell, the fkn.app document inside the broker frame, reloads that frame, the hidden fkn.app/api iframe the library mounts. The reloaded document announces itself as a new peer.

The library treats every peer as a new broker epoch, meaning a new generation of the broker, and routes every call through one stable facade to the newest one. The api you awaited earlier still works after the swap.

A call in flight at the moment of the swap cannot be finished by the old document. Instead of hanging, it rejects with FKN: the broker was replaced while this call was pending; retry it. That is a plain Error matched by its prefix, and retrying once is the right answer.

The example retries the catalog read an app is most likely to have in flight:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const REPLACED: "FKN: the broker was replaced"
REPLACED
= 'FKN: the broker was replaced'
const
const readCatalog: () => Promise<string | Buffer<ArrayBufferLike>>
readCatalog
= () =>
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
const fs_d_exports.promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
unlink: (path: import("node:fs").PathLike) => Promise<void>;
rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>;
readdir: (path: import("node:fs").PathLike) => Promise<string[]>;
mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>;
... 4 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
export fs_d_exports.promises
promises
.
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile
('library/catalog.json', 'utf8')
const
const catalog: string | Buffer<ArrayBufferLike>
catalog
= await
const readCatalog: () => Promise<string | Buffer<ArrayBufferLike>>
readCatalog
().
Promise<string | Buffer<ArrayBufferLike>>.catch<string | Buffer<ArrayBufferLike>>(onrejected?: ((reason: any) => string | Buffer<ArrayBufferLike> | PromiseLike<string | Buffer<ArrayBufferLike>>) | null | undefined): Promise<string | Buffer<ArrayBufferLike>>

Attaches a callback for only the rejection of the Promise.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of the callback.

catch
(
error: any
error
=> {
if (
error: any
error
instanceof
var Error: ErrorConstructor
Error
&&
error: Error
error
.
Error.message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
(
const REPLACED: "FKN: the broker was replaced"
REPLACED
)) return
const readCatalog: () => Promise<string | Buffer<ArrayBufferLike>>
readCatalog
() // the second attempt routes to the new broker
throw
error: any
error
})
const catalog: string | Buffer<ArrayBufferLike>
catalog
// the catalog the app saved

The retry is capped at one because the second attempt already routes to the new broker, so a second rejection is some other failure. The message is the only handle: the library creates it as a plain Error with no class of its own. The wider rule is on which failures are worth retrying.

Registrations cross the hop too, and they end with the connection that carried them. The library re-sends each of them on every broker epoch for you, among them account.onChange, shell.onUpdate, the taken signal and the overlay host. An unsubscribe you hold stays valid across a swap, and you never re-register anything yourself. The machinery is on epochs and the facade.

The shell ships on its own schedule, separately from the @fkn/lib in your bundle. Two moments matter, ready and taken, and nothing in the library fires on its own between them.

An update is ready once it has downloaded. It is taken once someone presses update, in this tab or in another. Once it is taken, the new shell controls every page with a broker frame.

shell.updateReady() and shell.onUpdate() are the ready side. shell.applyUpdate() takes the update, and shell.onUpdateTaken() is the taken side:

app.ts
import {
(alias) namespace shell
import shell
shell
} from '@fkn/lib'
const
const button: HTMLButtonElement
button
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<HTMLButtonElement>(selectors: string): HTMLButtonElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
<
interface HTMLButtonElement

The HTMLButtonElement interface provides properties and methods (beyond the regular HTMLElement interface it also has available to it by inheritance) for manipulating button elements.

MDN Reference

HTMLButtonElement
>('#update-fkn')!
const
const ready: boolean
ready
= await
(alias) namespace shell
import shell
shell
.
shell_d_exports.updateReady(): Promise<boolean>
export shell_d_exports.updateReady

Whether a newer FKN shell is downloaded and ready to apply.

updateReady
() // false until a newer shell has downloaded behind the active one
const button: HTMLButtonElement
button
.
HTMLElement.hidden: boolean

The HTMLElement property hidden reflects the value of the element's hidden attribute.

MDN Reference

hidden
= !
const ready: boolean
ready
await
(alias) namespace shell
import shell
shell
.
shell_d_exports.onUpdate(callback: () => void): Promise<() => void>
export shell_d_exports.onUpdate

Subscribe to "a newer FKN shell is ready". Fires immediately when an update is already waiting at subscribe time, so a late subscriber misses nothing. Returns an unsubscribe that stays valid across broker replacements.

onUpdate
(() => {
const button: HTMLButtonElement
button
.
HTMLElement.hidden: boolean

The HTMLElement property hidden reflects the value of the element's hidden attribute.

MDN Reference

hidden
= false }) // fires at once when an update is already waiting, so a late subscriber misses nothing
const button: HTMLButtonElement
button
.
HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', async () => {
const
const applied: boolean
applied
= await
(alias) namespace shell
import shell
shell
.
shell_d_exports.applyUpdate(): Promise<boolean>
export shell_d_exports.applyUpdate

Apply a ready update by reloading the FKN frame this page mounted. Resolves true when the reload was taken, false when there is nothing to apply or this caller does not own the frame.

Nothing calls this on the app's behalf. The broker's own header offers the same action to the user; this is the app's way to offer it on its own terms, or to take it at a moment it knows is safe. Check busyReasons() first if the app has no better signal of its own.

applyUpdate
() // true, and the broker frame reloads a tick later
if (!
const applied: boolean
applied
)
const button: HTMLButtonElement
button
.
HTMLElement.hidden: boolean

The HTMLElement property hidden reflects the value of the element's hidden attribute.

MDN Reference

hidden
= true // nothing was ready, or a broker card is open
})
// the default reload, replaced
const
const restoreDefault: () => void
restoreDefault
=
(alias) namespace shell
import shell
shell
.
shell_d_exports.onUpdateTaken(handler: () => void): (() => void)
export shell_d_exports.onUpdateTaken

Handle "the FKN shell was just updated", which arrives in EVERY open page, not only the one where the person pressed the button. Activating a new shell claims every client on the origin, so each page's broker frame learns about it and tells its app.

The default, with no handler registered, is to RELOAD THIS PAGE. That is deliberate: after an update every tab is running the new worker against a document from the old build, and the only way out of that mix is a reload. Doing it for the whole browser at once is what a person means when they press update.

Registering a handler REPLACES that default, and then reloading is entirely the app's business. That is the escape hatch for an app that cannot be interrupted:

shell.onUpdateTaken(() => {
if (downloads.idle()) location.reload()
else banner.show('FKN updated. Reload when you are ready.')
})

Unsubscribing restores the default. Note the difference from onUpdate, which fires when an update is merely READY and nothing has happened yet.

onUpdateTaken
(() => {
if (
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
().
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
=== 0)
var location: Location

The Window.location read-only property returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.reload(): void

The reload() method of the Location interface reloads the current URL, like the Refresh button.

MDN Reference

reload
()
else
const button: HTMLButtonElement
button
.
Element.textContent: string | null
textContent
= 'FKN updated, reload when you are ready'
})
// restoreDefault() on teardown puts the default reload back

applyUpdate() activates the waiting shell, waits up to 3,500 ms for the taken signal and reloads the broker frame. It returns true before the broker frame reloads, so the swap described above follows.

It answers false when nothing is ready, and when a relay, allowance or storage card is open. A caller that is not the page that mounted this frame, such as a package, gets false too. So does a caller outside a window, where it never asks the broker.

With no onUpdateTaken handler, the default is one location.reload() of your page. It runs in every open page with a broker frame, whatever that page’s origin. Taking the update claims every client on fkn.app, and each broker frame then tells its app.

After an update every such page holds a broker document from the old build under the new shell, and a reload is the way out of that mix. A handler replaces the default rather than running before it. A handler that throws still counts as handled, and unsubscribing restores the default.

A busy token is the library’s record of something a broker swap would cut off: an open socket, a streaming response, a relayed worker, a mounted package. shell.busyReasons() lists the tokens this realm holds. Nothing in the library acts on the list: it is evidence for your own decision.

The unregisterSignal you hand relayWorker ends the relay and releases its token, as what the relay does explains. Relaying the engine.ts above from the page reads like this:

app.ts
import {
const relayWorker: (worker: Worker, options?: {
unregisterSignal?: AbortSignal;
originA?: string;
originB?: string;
}) => Promise<void>
relayWorker
,
(alias) namespace shell
import shell
shell
} from '@fkn/lib'
const
const stop: AbortController
stop
= new
var AbortController: new () => AbortController

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

MDN Reference

AbortController
()
const
const engine: Worker
engine
= new
var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => Worker

The Worker interface of the Web Workers API represents a background task that can be created via script, which can send messages back to its creator.

MDN Reference

Worker
(new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
('./engine.ts', import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.url: string
url
), {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
await
function relayWorker(worker: Worker, options?: {
unregisterSignal?: AbortSignal;
originA?: string;
originB?: string;
}): Promise<void>
relayWorker
(
const engine: Worker
engine
, {
unregisterSignal?: AbortSignal | undefined
unregisterSignal
:
const stop: AbortController
stop
.
AbortController.signal: AbortSignal

The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.

MDN Reference

signal
})
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
() // ['relayed worker (1)'], the engine's socket is in the engine's own tally
const stop: AbortController
stop
.
AbortController.abort(reason?: any): void

The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.

MDN Reference

abort
()
const engine: Worker
engine
.
Worker.terminate(): void

The terminate() method of the Worker interface immediately terminates the Worker.

MDN Reference

terminate
()
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
() // []

The engine’s tcp socket never appears here because the tally is per realm. A worker that imports @fkn/lib/net keeps its own tally, and the page cannot see it, so ask the worker when your transfers live there.

An empty list is not a promise that a reload is free. It only says that this realm holds nothing the library knows about.

The entry you just watched come and go is one row of this list. Every holder is listed with what releases it:

ReasonHeld whileReleased by
tcp socket (n)a net.Socket, whether from connect or accepted by a net.Serverdestroy(), or a refused connect
tcp server (n)a net.Server from listenthe first close, or a refused bind
udp socket (n)a dgram.Socket from bindclose(), transport loss, or a refused bind
streamed response (n)a cloud.fetch body not yet fully readthe body ending, erroring or cancelled, or the response being garbage collected
relayed worker (n)a relayWorker registrationits unregisterSignal aborting
attached frame (n)a cloud attachFrame attachmentthe iframe leaving the document, or the page being navigated away from
package connection (n)a packages.connectclosed settling, or a broker epoch change
mounted package (n)a packages.mountunmount()
package viewany live packages.show viewhide()
storage replicationthe hybrid fs draining or offering uploadsthe drain or the offer settling
unsaved filesfs or opfs writes not yet flushedthe flush, 250 ms after a write, flush(), or the page hiding

The first eight are counted, and they appear in the order each kind was first held. The last three are live probes that always follow them, without a count and in a fixed order. Each holder is explained on its own guide: TCP and UDP sockets, fetch(), frames, packages and storage. The machinery is in how it works.

The three broker rows, each with its cause and its fix:

MessageWhat happenedWhat to do
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedBrokerUnreachableError: a connect, listen or bind, or your own apiWithin, waited out its deadline, or the short one once the latch flipped.In a worker, call await relayWorker(worker, { unregisterSignal }) from the page. The detail is on workers. On a page, the broker frame never committed, and the broker frame covers why.
FKN: the broker was replaced while this call was pending; retry itA shell update reloaded the broker frame under a call in flight.Retry once. The pattern is on a replaced broker.
FKN @fkn/lib: relayWorker found no FKN transport in this realmThis realm holds neither a mounted broker frame nor a port from a parent FKN realm, a frame removed from the document included.Import the library on a page that mounts the broker frame and leave the frame in the document. The frame’s lifetime is on the broker frame.

Every other message has its row on every error.

From here, handling errors carries the rules for matching each message, and how it works covers the broker frame, the epochs and the facade underneath all of this.