Skip to content

Limitations

@fkn/lib gives a browser page sockets, a file system and a way into other sites, and a boundary that wide cannot be hidden completely. This page lists the places where it stays visible to you, one surface at a time, with the reason and the workaround where there is one.

Some of these limits are caught by TypeScript before your code runs. The rest you meet at runtime as a refusal with a message:

app.ts
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('form button').
first: () => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
first
().
click: (options?: PositionOptions | undefined) => Promise<void>
click
()
await
const frame: Frame
frame
.click() // the element members come after the first locator()
Error ts(2339) ― Property 'click' does not exist on type 'Frame'.

The first line is the shape every frame action takes: a chain that starts with locator(). The second line is the compile error for skipping it. Every refusal below links to its row on every error, and every number links to its row on limits and timeouts.

The cloud path never carries the user’s session: cloud.fetch sends the request through the proxy, the FKN service that fetches on the page’s behalf, and forwards no credentials.

The root fetch with credentials: 'include' runs only through the extension and never falls back to the cloud path. A call that needs the user’s cookies is refused when the extension is missing rather than sent anonymously:

app.ts
function setMissingExtensionHandler(handler: MissingExtensionHandler | null): void
setMissingExtensionHandler
(null) // no install card, a missing extension rejects once the exposure wait is over
try {
await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}): Promise<Response>
fetch
('https://example.org/api/me', {
RequestInit.credentials?: RequestCredentials | undefined

A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.

credentials
: 'include',
reason?: string | undefined
reason
: 'Load your profile' })
} catch (
var error: unknown
error
) {
(
var error: unknown
error
as
interface Error
Error
).
Error.message: string
message
// 'The FKN WebExtension is not installed, enabled or not exposed on this page.', and nothing went out
}

The call rejects with the not-installed message and sends nothing.

With the missing extension handler left in place, the same call waits 1 second or 150 ms and then opens the install card, drawn by the broker, the connection your page holds into FKN. Dismissing the card rejects the call with The FKN WebExtension is not installed, enabled or not exposed on this page., as every extension.* call does, see when the extension is missing.

The root fetch chooses its path per call and waits for nothing. Await waitForExtensionExposure() first when the path matters, see how the root exports choose.

On the cloud path a redirect is data: the proxy follows none, and redirect is not forwarded. The response is rebuilt twice on the way back. url is '', redirected is false, and statusText is '' unless the upstream status matched the transport’s, see cloud.fetch().

A Request passed to cloud.fetch has already lost its Cookie, Origin and Referer headers. The browser drops those names when the Request is built. Pass a string URL with init.headers when one of them has to travel, see headers the page cannot set.

The extension path buffers the whole request body, so there is no streaming upload there. redirect: 'manual' on that path answers Response.error(), with status 0 and type 'error'. set-cookie is readable on neither path, because the browser drops it from every Response an app holds.

Of the header names a page cannot normally set, only origin, referer and cookie can be supplied on the extension path. Any other is refused with fetch: refusing to forge request header(s): <names>. A supplied cookie together with credentials: 'include' names two identities, and is refused with fetch: a forged Cookie header and credentials:'include' are different identities…, see request header rules.

fkn.app, fkn.dev, sdbx.app and their subdomains are refused as a target on every path. The root fetch and cloud.fetch answer fetch refuses FKN platform domains (<hostname>) before anything is sent. extension.fetch called directly answers fetch: refusing to target the extension's own pages or FKN platform domains, see platform hosts.

The cloud path reaches public addresses only. The proxy answers 403 egress refused (non-public target) to anything else. A local network target belongs to the extension path, behind its own consent, see local network targets.

cloud.fetch and promptInstall have no deadline. Both wait on the broker connection, and init.signal bounds only the transfer after the call reached the broker. To bound the wait itself, write your own race, shaped like apiWithin from @fkn/lib/api, see connecting.

The relay is the FKN service that holds the real socket at the far end of net and dgram. It carries TCP and UDP and nothing above them. http is HTTP/1.1 in the clear, and there is no https entry at all:

app.ts
const
const request: http.ClientRequest
request
=
import http
http
.
function get(...args: [options: string | URL | http.ClientRequestArgs, callback?: (res: http.IncomingMessage) => void] | [url: string | URL, options: http.ClientRequestArgs, callback?: (res: http.IncomingMessage) => void]): http.ClientRequest
export get
get
({
protocol: string
protocol
: 'https:',
hostname: string
hostname
: 'example.org',
ClientRequestArgs.path?: string | undefined
path
: '/api/catalog.json' }) // port 443, Host: example.org, and plain HTTP on the wire
const request: http.ClientRequest
request
.
Stream.Writable.on<"error">(eventName: "error", listener: (err: Error) => void): http.ClientRequest (+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', () => {}) // an error with no listener throws, so listen before anything answers
const request: http.ClientRequest
request
.
ClientRequest.protocol: string
protocol
// 'https:', which changed nothing but the default port
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json')
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200, the proxy spoke TLS for you

https: changes only the default port, so a TLS server cannot answer the request. Use cloud.fetch for an HTTPS origin, and keep http for a plaintext service you control, see no https.

A URL string names its port or loses it. The library copies the URL’s port as given, empty when there is none, so http.get('https://example.org/api/catalog.json') sends Host: example.org:0 and dials port 0. Name the port in the URL, or pass the options form as above.

The relay reaches public addresses only. A loopback or wildcard target pairs with a net.Server listening in the same data plane, the shared worker behind the broker that the pages of one origin share. Any other non-public target is refused with webvpn: egress to a non-public address refused. The relay may refuse a port too, and the error message names the reason.

A lost session closes every socket on it with WebVPN session closed, and nothing reconnects. The next connect, listen or bind dials a fresh session, see the transports underneath.

There are no IPC paths and no file descriptors. A string first argument to connect or listen throws FKN WebVPN does not support IPC connections, and an fd in the Socket options throws FKN WebVPN does not support file descriptors. net.connect and createConnection are typed for the options form only, so connect(80, 'example.org') is a compile error, see sockets, HTTP and DNS.

bytesRead, bytesWritten, bufferSize, connecting, pending and readyState on a Socket, and connections, maxConnections and listening on a Server, are declared and never assigned. The address getters throw Socket is not connected before the relay answered, where Node answers undefined. Server.getConnections throws Method not implemented., ref and unref do nothing, setTypeOfService sends nothing, and there is no TCP setTTL.

new dgram.Socket() throws Missing options, so build sockets with createSocket. A send callback means the datagram was handed to the transport, not that it was delivered: the broker drops what it cannot deliver and reports nothing back. The buffer size getters echo your last request, the send queue getters are always 0, and the multicast interface and source-specific memberships do nothing, see UDP.

http.Agent pools nothing, and every request opens its own socket, see one socket per request. req.on('socket') never fires, so read req.socket instead. OutgoingMessage is not a named export. A second writeHead throws Cannot render headers after they are sent to the client, where Node merges the headers.

dns is lookup alone, with no cache and no deadline. A name with no answer resolves undefined, or [] with all: true, rather than rejecting. The socket resolvers turn that into getaddrinfo ENOTFOUND <hostname> on the error event, see dns.lookup().

No code survives the broker hop. Match the socket errors and the relay’s refusals on error.message, see handling errors.

There is no synchronous network, so cloud.fs has no *Sync member, and a synchronous call on it is a compile error. It also leaves out appendFile, exists, mount, flush, remount and pull, see cloud.fs is async only. On the hybrid fs, the synchronous forms read the in-memory layer. mount() fills that layer, and only remount() refreshes it:

app.ts
await
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
mount: () => Promise<void>
mount
() // fills the in-memory layer, synchronous reads work after this
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string
readFileSync
('library/catalog.json', 'utf8')) // this device's copy, even when the account holds a newer one
await
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
pull: (path: string) => Promise<Blob>
pull
('library/catalog.json') // the account's copy, written into OPFS and not into memory
await
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
remount: () => Promise<void>
remount
() // the in-memory layer hydrates again, OPFS first
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
const fs: {
available: () => Promise<boolean>;
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string;
writeFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
appendFileSync: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => void;
existsSync: (path: import("node:fs").PathLike) => boolean;
statSync: (path: import("node:fs").PathLike) => Stats;
... 28 more ...;
onConflict: (next: ConflictResolver) => (() => void);
}
fs
.
readFileSync: (path: import("node:fs").PathLike, options?: ReadOptions) => Buffer | string
readFileSync
('library/catalog.json', 'utf8')) // the account's bytes, now in the sync layer

The first read answers the old bytes. The account is the FKN identity a person carries between sites, and a local copy always wins on read, because fs asks the account only when OPFS has nothing at the path. pull writes OPFS alone, so another device’s write stays invisible until pull() plus remount(), see the read rule.

mount() loads the whole scope into memory, and a path that vanished stays there until the page reloads. An incomplete mount is re-run by the next asynchronous call after 5 seconds.

None of fs, cloud.fs or opfs carries every Node fs member. The ones all three leave out are listed on what is covered. withFileTypes is ignored rather than refused: the callback readdir drops its options, so you get names rather than Dirents.

A Blob is not accepted as write data. The node-style writes throw The "data" argument must be of type string, Buffer, TypedArray, or DataView for it. Every read is typed Buffer | string whatever encoding you pass, so coerce with String() before JSON.parse.

Directories live in memory only, so an empty directory does not survive a reload. A delete does not reach another device, because there are no tombstones, see deletes.

flush() cannot fail: it catches every backing failure and resolves. The account copy is the replicated copy of a file in the account, and only cloud.fs.writeFile resolves after the account copy is durable. A hybrid write resolves once OPFS has it and replicates in the background.

The account holds objects rather than a tree. cloud.fs.rename is a read, a write and a delete, and it is not atomic. mkdir there does nothing, rmdir is rm, and stat and readdir list the whole scope on every call.

The library sends a cloud.fs path as given on readFile, writeFile, unlink and a non-recursive rm, so write it relative. fs and opfs normalise their paths for you, see scope and paths.

A cloud write while the api is unreachable is refused with storage: api unreachable rather than guessed, because a write needs the api for the key state. An export or import of the account’s data belongs to fkn.app and is not an app’s to run.

The library rebuilds a StorageNotFoundError for a read or a write of a missing path, and not for unlink. On a delete of a missing path, test the message rather than isNotFound().

A worker realm has no localStorage, so the hybrid’s pending write queues do not persist there and no timer is armed for the drain, the background process that replicates queued writes to the account. OPFS itself works in a worker, see what works in a relayed worker.

An attached frame, the Frame that attachFrame returns, reports what you asked of it and never where the embedded page went on its own. A backend is the place a call runs: the cloud, the extension or the desktop. A frame runs on the first two, and url() is the URL the app last passed to attachFrame or goto() on either of them. A locator action is a synthetic event rather than a real pointer:

app.ts
await
const frame: Frame
frame
.
function goto(url: string, options?: GotoOptions): Promise<void>
goto
('https://example.org/')
const frame: Frame
frame
.
function url(): string
url
() // 'https://example.org/', whatever the page navigated to since
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('form button').
first: () => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
first
().
click: (options?: PositionOptions | undefined) => Promise<void>
click
() // a synthetic click at the element's centre
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('form').
getByRole: (role: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
getByRole
('button').
count: (_options?: LocatorOptions | undefined) => Promise<number>
count
() // 0, only an explicit role attribute matches
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('.controls').
getByText: (text: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
getByText
('Play').
click: (options?: PositionOptions | undefined) => Promise<void>
click
() // rejects after the locator timeout with Strict mode violation: locator resolved to 2 elements

The first two lines are the whole of url(). On the extension backend, a frame that left the origins you declared refuses every call with frame: this frame no longer holds the document the app attached it to. That is a terminal error, and isLocatorDenied matches it, see goto() and url().

The click in the middle is synthetic: the pointer and mouse events land at the centre of the element, isTrusted is false, and nothing scrolls or takes focus.

The last two lines show the selectors, which are narrower than Playwright’s. getByRole(role) matches an explicit role attribute only. getByText(text) keeps ancestors as well, so a container matches along with its control.

Strict mode refuses the pair with Strict mode violation: locator resolved to <n> elements. The dispatch loop retries for the full 30 seconds before that last error is thrown. Target a control by tag or id instead, see selectors.

A locator chain, a Locator built by chaining selectors, has no waitFor, press, type, selectOption, check, screenshot, evaluate, innerText, boundingBox, getByLabel, getByPlaceholder or last(), and no exact or hasText filter. isVisible() checks styles and the box only, so opacity, clipping and occlusion are not considered. videoElement() answers only for a <video>, see video.

addStyleTag strips url(), @import, @font-face and the other image functions and at-rules. CSS the parser rejects injects nothing, silently. noSanitize: true skips that sanitiser. A permission key is one entry in the extension’s permission model, and on the extension backend noSanitize costs the key media.appearU, which prompts the user, see styling the page.

frame.fetch is not streamed: the whole body arrives in one ArrayBuffer, and there is no signal.

The two backends differ where the machinery does, and the consent sheet in the table is the prompt the extension shows before an action that needs the user’s permission:

MemberExtension backendCloud backend
lockdownsupportedrefused at attach
videoElement()supportedregistered at every hop and refused nowhere, not measured
a locator actionone sheet per key and scopenone, every action runs unprompted
frame.fetchthe sheet, with Once, Session and Alwaysone card, Allow for this session or Not now

The render proxy is the cloud frame backend. The library refuses a frame.fetch on an attachment that shares the render proxy’s cookie jar, with frame.fetch on the cloud backend needs its own session: attach with syncCookies: false. It also refuses a target outside the declared hosts and the visited origins.

A request that passes those two checks asks the broker for consent. What the render proxy answers after the card is its own, so read the error rather than assuming the path is open, see fetching as the frame.

On the extension backend, lockdown needs domains or a later goto(). syncCookies: true copies nothing without domains or a goto(), so a preloaded iframe attached bare starts logged out. A blank iframe has no child document to answer until a goto(). The cloud backend refuses an iframe that is already attached, see what is refused.

A consent refusal arrives as an Error named PermissionDeniedError, reading Permission denied: <key> (<scope>). Neither isLocatorDenied nor isTerminalError matches it, so compare error.name.

permissions.request with no scope stores '', which covers no locator chain. A sheet your own UI covers is dismissed as a deny once, after three occlusion misses. No member reads or revokes a grant, see the activity log.

A timeout throws the last attempt’s error, which is No elements found for a missing element. A missing selector and a slow page therefore look the same, see the locator timeout.

Detaching the iframe ends a cloud attachment for good, and every later call throws cloud.attachFrame: the attached iframe left the document or was reloaded; attach a fresh iframe.

A package is an npm module that FKN loads on a sandbox origin of its own. A record of one is not a connection to it:

app.ts
const
const installed: packages.InstalledPackage | null
installed
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.install(uri: string, options?: packages.InstallOptions): Promise<packages.InstalledPackage | null>
export packages_d_exports.install

Install a package for this app behind an FKN-rendered confirm, or with { noConfirm: true } for a notice instead of a prompt. Resolves null when the user declines.

install
('npm:@example/subtitles-plugin@1.4.2')
const installed: packages.InstalledPackage | null
installed
?.
uri: string | undefined
uri
// 'npm:@example/subtitles-plugin', version-free
const installed: packages.InstalledPackage | null
installed
?.
version: string | null | undefined

null when the handler addresses code directly and has no version to pin, e.g. a dev server

version
// '1.4.2', the pin
const
const slot: HTMLElement
slot
=
var document: Document

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

MDN Reference

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

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

MDN Reference

querySelector
<
interface HTMLElement

The HTMLElement interface represents any HTML element.

MDN Reference

HTMLElement
>('#player-slot')!
await
(alias) namespace packages
import packages
packages
.
packages_d_exports.show(uri: string, options: packages.ShowOptions): Promise<packages.PackageView>
export packages_d_exports.show

Show an installed, connected package's frame over this page, aligned to element (or an explicit rect). The package renders its own UI there; the app keeps the space in its own layout. Take it back down with hide() on the returned view, or with packages.hide(uri). Throws a PackagesError with code 'not-installed' when the package has not been connected by this app.

show
('npm:@example/subtitles-plugin', {
element?: HTMLElement | undefined

A placeholder the package frame is aligned to for as long as the view lives. The frame tracks its rect every animation frame, is clipped by its scrolling ancestors, and follows its border-radius, so it reads as inline content even though it renders in FKN's overlay.

element
:
const slot: HTMLElement
slot
}) // rejects with code 'not-installed' until connect() ran

The record comes back without a version, with the pin beside it. show() needs a live connect() by this app, see showing a package’s frame. search and pick query npm and nowhere else, cut free text to 128 characters, and offer no pagination beyond size. pick returns only the packages that installed.

The broker derives who is calling from the browser-set origin of the connection, never from a string you pass. There is no update or repin member. install returns the existing record without a prompt, so install again with a version to change the pin.

show() cannot layer or z-order the frame, and cannot receive events from it. With rect alone it tracks nothing, and it drops a percentage corner radius. A package cannot place its own frame: the broker answers packages.show: a package cannot place its own frame.

mount() writes only src, never your sandbox or allow. A sandbox that is too tight is refused by name up front, and allow cannot be added after navigation. A boot failure under mount() arrives as a 'timeout', packages.mount: the package did not register a connection handler, and not as the package’s own message. That message never reaches an iframe you mounted yourself, see mounting into your own iframe.

closed is the only reconnect signal, and it does not settle after a failed handshake. A retry is yours to schedule. onConnect in a top-level window does nothing, accepts a port from window.parent only, and cannot run in a worker, where attach() is the worker’s half, see answering from the package and attaching in a worker.

info.from and info.protocol are asserted by the embedder and are not proof of who is calling. The protocol tag, cut to 64 characters, is the only versioning a connection has.

A package’s account.info() and login() act on the connection of the host app, the app that installed and connected to the package. Its quota(), cloud.fs and cloud.fetch meter and store under the package’s own scope. A package cannot applyUpdate(): the broker answers false to a nested caller.

The worst case for one connect() is a minute: 30 seconds to boot plus 30 for the handshake. An app that shows progress runs its own race against that deadline, see timeouts and error codes.

PackagesError.code survives only because the library rebuilds the error in your realm from the broker’s structured refusal. Nothing else crosses the hop with a code, see handling errors.

A room is a realtime channel several browsers join from an invite the app shares. What the platform relays is ciphertext, and what it keeps is nothing:

app.ts
const
const room: rooms.Room
room
= await
import rooms
rooms
.
function create(options?: rooms.CreateOptions): Promise<rooms.Room>
export create

Open a room and become its owner. Share room.invite to let anyone else in.

create
()
const room: rooms.Room
room
.
key: string

the room key, base64url. The server never sees it. Anyone holding it and the id can join.

key
// minted in this browser, and never sent to the platform
await
const room: rooms.Room
room
.
members: () => Promise<rooms.RoomMember[]>
members
() // the members present now, and nobody who was here before you joined

A joiner sees nothing sent before it arrived, and there is nothing to replay: the platform holds no ciphertext once it has relayed it. A platform deploy snapshots every room and brings it back on the first rejoin, and a hard restart of the platform does not, so every room it held ends. The service runs in one region, and every message crosses it.

Rooms are global by invite and not scoped per app, so two apps holding one invite share one room. There is no list of your rooms: lose the invite and the room is unreachable, and an account keeps that slot against its cap until the room empties.

A guest is blocked by the tab and by the network, so closing the tab and changing network makes a new visitor, and the network half also reaches a bystander on the same address. A guest owner’s identity lives in the tab: close it and nobody can grant or revoke in that room again, although members already holding remove or block keep acting. Ownership never moves, and a member id never changes silently: a rejoin that would seat you as someone else reports the room closed instead, see reconnecting.

The platform cannot read a message and keeps no record of a room once it ends, so moderation is the owner’s and it happens live. Rooms are cloud only: every other capability can run against your own machine, and a rendezvous between strangers cannot. Room bytes reach no meter, so a premium account gets the same rooms as a free one, see limits.

The library needs a window to draw in and a broker to talk to. A realm, one JavaScript execution context such as a window or a worker, loses a known set of members when it lacks one of them. In a worker the page relayed with await relayWorker(worker, { unregisterSignal }), everything that needs only the broker works, and what needs a document does not:

engine.ts
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.available(): boolean
export cloud_d_exports.available
available
() // true, a realm shape check and never a connection check
await
function connect(): Promise<boolean>
connect
() // false, there is no window to draw the card in
await
function promptInstall(reason?: string): Promise<boolean>
promptInstall
() // false
(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
() // [], this worker's own tally, which the page never sees

busyReasons() still answers, with this realm’s own tally. The table sorts the rest:

RealmWorksDoes not
a windoweverything
a worker the page relayedthe sockets, the cloud calls, fs and opfs, the broker-routed packages.* and rooms.* calls, busyReasons()anything that draws, asks, or attaches a frame
a worker nobody relayedopfs, the hybrid fs on its local half, the local helpersnet.connect, server.listen, socket.bind, and everything else that reaches the broker
Nodethe type-only entries and the local helpersapiPromise never settles

A few of these deserve more detail than the table can carry.

The broker frame is the hidden fkn.app/api iframe the library talks to FKN through. In a window, importing any entry other than opfs, opfs/promises, react, messages, contract, wire, attach-policy and desktop mounts it as soon as the module is evaluated, see entry points. The mount needs document.body, so import the library from a module script or after the body exists.

A frame you created yourself is adopted only when its src matches byte for byte, ?coi=1 included on a cross-origin-isolated page. It is taken as built, so give it allow="cross-origin-isolated" there yourself, see the broker frame.

The same frame is the overlay that draws the cards. Its styles on iframe[title="FKN"] belong to the library, and an ancestor with transform, filter, opacity below 1, visibility: hidden or display: none breaks the clip. The library reports a card it cannot show to the console once per kind and enforces nothing, see the overlay projector.

In a relayed worker, attachFrame needs a window realm on either backend, and cloud.attachFrame throws cloud.attachFrame needs a window realm when called directly. The root fetch with credentials: 'include' rejects with fetch with credentials needs the FKN extension, which only exists in window realms. The prompts, account.*, the packages.* members that touch the document and every shell.* member except busyReasons answer false or null, or do nothing, see what works in a relayed worker.

In a worker nobody relayed, net.connect, server.listen and socket.bind are the only calls that give up with an error. They fail on the error event with a BrokerUnreachableError after 8 seconds and then 1 second per call. The storage availability probe is bounded too, at 8 seconds and then 1 second, so a hybrid fs call settles on its local half once the probe answers 'unknown'. Everything else that reaches the broker waits for as long as the worker lives.

cloud.available() and hasTransport are true in any window or worker, connected or not, and false in Node. Neither is a health check, see what available() means.

desktop.* is a placeholder. available() answers false and fs.available() resolves false. Every other member throws The FKN desktop app is not connected, desktop.<name> is unavailable synchronously, so a try catches it and a .catch() on the call does not.

An error crossing the broker keeps only name, message, stack and cause. Every error class the library hands you was created in your own realm. The rest is matched by name or message, see handling errors.

A pinned version of the library keeps working against a newer broker, because the capability logic lives in the broker and the library passes calls through. Against an older broker it works because the facade answers a property read against the connected broker, so every member that arrived later is probed before it is called, see version compatibility.

From here, limits and timeouts carries the numbers above, and how it works explains the machinery behind most of these.