A relayed worker is a Worker whose @fkn/lib calls travel over the broker, the connection your page holds into FKN. This page covers what relayWorker does and its options, the busy token it holds, what a worker looks like when nobody relayed it, and which calls work inside one.
A realm is one JavaScript execution context, such as a window or a worker. A worker is a realm of its own, and it cannot open a broker connection by itself, so the page relays its calls. The shape is two files: an engine that opens the sockets and a page that relays it. run sockets in a worker is the recipe for building one:
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
Destroy the stream. Optionally emit an 'error' event, and emit a 'close' event (unless emitClose is set to false). After this call, the readable
stream will release any internal resources and subsequent calls to push() will be ignored.
Once destroy() has been called any further calls will be a no-op and no
further errors except from _destroy() may be emitted as 'error'.
Implementors should not override this method, but instead implement readable._destroy().
@since ― v8.0.0
@param ― error Error which will be passed as payload in 'error' event
destroy() // ends the socket and releases its busy token
var self:Window&typeof globalThis
The Window.self read-only property returns the window itself, as a WindowProxy.
The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
The writable.write() method writes some data to the stream, and calls the
supplied callback once the data has been fully handled. If an error
occurs, the callback will be called with the error as its
first argument. The callback is called asynchronously and before 'error' is
emitted.
The return value is true if the internal buffer is less than the highWaterMark configured when the stream was created after admitting chunk.
If false is returned, further attempts to write data to the stream should
stop until the 'drain' event is emitted.
While a stream is not draining, calls to write() will buffer chunk, and
return false. Once all currently buffered chunks are drained (accepted for
delivery by the operating system), the 'drain' event will be emitted.
Once write() returns false, do not write more chunks
until the 'drain' event is emitted. While calling write() on a stream that
is not draining is allowed, Node.js will buffer all written chunks until
maximum memory usage occurs, at which point it will abort unconditionally.
Even before it aborts, high memory usage will cause poor garbage collector
performance and high RSS (which is not typically released back to the system,
even after the memory is no longer required). Since TCP sockets may never
drain if the remote peer does not read the data, writing a socket that is
not draining may lead to a remotely exploitable vulnerability.
Writing data while the stream is not draining is particularly
problematic for a Transform, because the Transform streams are paused
by default until they are piped or a 'data' or 'readable' event handler
is added.
If the data to be written can be generated or fetched on demand, it is
recommended to encapsulate the logic into a Readable and use
pipe
. However, if calling write() is preferred, it is
possible to respect backpressure and avoid memory issues using the 'drain' event:
functionwrite(data, cb) {
if (!stream.write(data)) {
stream.once('drain', cb);
} else {
process.nextTick(cb);
}
}
// Wait for cb to be called before doing any other write.
write('hello', () => {
console.log('Write completed, do more writes now.');
});
A Writable stream in object mode will always ignore the encoding argument.
Writes data to the stream, with an explicit encoding for string data.
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.
The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
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.
The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
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.
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 engine imports the library exactly as your page does. On a page, @fkn/lib mounts the broker frame, a hidden fkn.app iframe, and talks to the broker through it. Inside a worker there is no document to mount that frame in, so the library’s transport is { receive: self, emit: self }, connected to nothing until the page relays it. Every call the worker makes after that travels over the page’s connection, as how it works explains.
relayWorker forwards the fkn-api channel both ways, between this realm’s broker transport and the worker, so both ends have to exist. It is async, and its two refusals arrive as rejections of the promise it returns. Await it, so that a refusal reaches your code as a rejection you can catch.
Once the relay is registered and until unregisterSignal aborts, it holds the busy token relayed worker. A busy token is one of the reasons a realm reports itself busy, and shell.busyReasons() lists the ones held. Aborting the signal releases the token before abort() returns:
app.ts
const
conststop: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.
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)']
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() // [], released before abort() returned
Neither reading counts anything the engine holds. The tally is per realm, as connection and lifecycle explains.
An AbortSignal that ends the relay and releases the busy token when aborted. A signal that was already aborted when you call relayWorker releases the token at once.
originA
https://fkn.app, the origin baked into the published build
The targetOrigin used when posting into the broker frame.
originB
'*'
The targetOrigin handed to osra when posting into the worker.
unregisterSignal is the only way to end a relay. Keep the controller next to the worker and abort it when you terminate the worker. Otherwise the token stays held for the life of the page.
originA and originB apply only on the window pair of a mounted broker frame. A realm nested under a broker document reaches it over a granted MessagePort, which has no origin to compare against, so both options are ignored there.
originB restricts nothing even on the window pair, because a worker does not check the targetOrigin it is handed. Only originA decides where a post is delivered.
relayWorker resolves once the relay is registered, not once the worker has connected to the broker. The worker’s first call still waits for the broker. That wait is bounded by 8 seconds for a socket and unbounded for everything else, as connection and lifecycle explains.
A worker that opens a socket before the page relays it does not fail at once. It waits: apiPromise, the library’s promise of a broker connection, stays pending until the page relays the worker, and for the life of the worker if nobody does. Its transport is connected to nothing, so every call parks. Nothing rejects, and no socket emits connect or listening.
Only the deadline on connect, listen and bind turns that wait into something you can see. Those three give up with a BrokerUnreachableError on the socket’s or the server’s error event after 8 seconds, then after 1 second once a deadline was missed:
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.
For UDP sockets, causes the dgram.Socket to listen for datagram
messages on a named port and optional address. If port is not
specified or is 0, the operating system will attempt to bind to a
random port. If address is not specified, the operating system will
attempt to listen on all addresses. Once binding is complete, a 'listening' event is emitted and the optional callback function is
called.
Specifying both a 'listening' event listener and passing a callback to the socket.bind() method is not harmful but not very
useful.
A bound datagram socket keeps the Node.js process running to receive
datagram messages.
If binding fails, an 'error' event is generated. In rare case (e.g.
attempting to bind with a closed socket), an Error may be thrown.
Example of a UDP server listening on port 41234:
import dgram from'node:dgram';
constserver= dgram.createSocket('udp4');
server.on('error', (err) => {
console.error(`server error:\n${err.stack}`);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
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.
lookup('example.org') // never settles here, nothing bounds it
Both sockets fail together, because both waits began before either deadline was missed. lookup is the contrast. It has no deadline, and neither do cloud.fetch and cloud.fs, so all three stay pending forever.
The order matters. An engine that resolves a name before it connects never reaches the socket call that would have told you.
fs and opfs are the exception. Both write OPFS with no broker at all, and opfs never asks for one. An fs write reaches OPFS first, then waits on the cloud availability probe before it resolves.
write OPFS. fs waits on the availability probe before a write resolves, replicates to the account over the relay, and keeps no queue in a worker because the queue lives in localStorage, see sync and conflicts
work through the relayed broker. The three entry calls bound the wait for it with the broker deadline, so available() is false in a worker nobody relayed and create and join reject rooms: rooms are unavailable, see rooms
fetch('https://example.org/api/catalog.json') // relayed through the page
(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() // ['streamed response (1)'], this worker's own tally
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@param ― value A JavaScript value, usually an object or array, to be converted.
@param ― replacer A function that transforms the results.
@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
@throws ― {TypeError} If a circular reference or a BigInt value is found.
stringify(
constcatalog:any
catalog)) // the cloud-only file system, written from the worker
(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.
The fetch and the write need nothing from the page beyond the relay, while every prompt answers at once with nothing to show. A worker that fetches and nothing more can import @fkn/lib/cloud/fetch and skip the socket shims. entry points lists what each entry reaches.
The worker’s tally is the one that knows about its transfers, so ask the worker before you reload. The tally is explained on connection and lifecycle.
Prompts, consent and frames stay page work. Ask the user from the page, drive frames from the page, and hand the worker what it needs. The detail is on permissions and consent and frames.
Close the worker’s sockets from inside the worker, then abort the signal, then terminate the worker, in that order. terminate() ends the worker at once, so a close posted just before it may never run. The engine above destroys its socket on close and answers closed, and the page waits for that answer before it goes on:
app.ts
const
constclosed:Promise<void>
closed=new
var Promise:PromiseConstructor
new <void>(executor: (resolve: (value:void|PromiseLike<void>) =>void, reject: (reason?:any) =>void) =>void) =>Promise<void>
Creates a new Promise.
@param ― executor A callback used to initialize the promise. This callback is passed two arguments:
a resolve callback used to resolve the promise with a value or the result of another promise,
and a reject callback used to reject the promise with a provided reason or error.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
abort() // the relay ends, and its token goes with it
(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() // []
constengine:Worker
engine.
Worker.terminate(): void
The terminate() method of the Worker interface immediately terminates the Worker.
terminate() // nothing is left for the worker to hold
run sockets in a worker is the recipe for the whole worker, from build to teardown. Nothing in the library acts on a token: it is evidence for your own reload decision, as connection and lifecycle explains.
This realm holds neither a mounted broker frame nor a port from a parent FKN realm. A broker frame removed from the document counts as missing, see how it works.
A packages.* call ran with neither window nor self. A worker has self, so in a worker nobody relayed the same call waits instead, see the worker nobody relayed.