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.
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:
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.
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:
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:
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.
readCatalog() // the second attempt routes to the new broker
throw
error: any
error
})
constcatalog: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.
The HTMLButtonElement interface provides properties and methods (beyond the regular HTMLElement interface it also has available to it by inheritance) for manipulating button elements.
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(() => {
constbutton:HTMLButtonElement
button.
HTMLElement.hidden: boolean
The HTMLElement property hidden reflects the value of the element's hidden attribute.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
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 (!
constapplied:boolean
applied)
constbutton:HTMLButtonElement
button.
HTMLElement.hidden: boolean
The HTMLElement property hidden reflects the value of the element's hidden attribute.
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) namespaceshell
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.
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:
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.
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
conststop:AbortController
stop.
AbortController.abort(reason?: any): void
The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.
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:
the 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.
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.
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.