A package is an npm module that FKN loads on a sandbox origin of its own. Your app can install one, connect to it and show its UI without hosting any of its code. This page covers both sides of the contract: the host app (the app that installs and connects to a package) and the package that answers with onConnect.
The package side imports from @fkn/lib/packages. An app can use the same path or the packages namespace on the root. The examples call the two files app.ts and package.ts. The two sides talk over a MessagePort with a typed remote on each end.
The broker is the connection your app holds into FKN, reached through the broker frame, a hidden fkn.app iframe the library mounts. Every call the broker answers waits for it with no deadline (see connection and lifecycle). The steps between an app and a package are bounded, and the numbers are under timeouts and error codes.
type and id must match [a-z0-9][a-z0-9-]{0,31}, or the query is refused as 'invalid'. A result’s version comes from the search index, which can lag a publish by hours, so install resolves the version again from the registry.
packages.pick(query, options?) runs the same search behind a broker card, where the person selects and installs in one step:
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map(
result: packages.PackageResult
result=>
result: packages.PackageResult
result.
uri: string
normalized version-free uri, e.g. 'npm:@banou/stub-plugin-foo'
uri) // ['npm:@example/subtitles-plugin'], already installed, or [] when the person cancels
The card is headed Add packages. A title replaces that heading, rendered as text and cut to 120 characters, and multiple allows more than one selection. The resolved list holds only the packages the broker installed. A selection it could not resolve is left out, and the broker frame logs a warning in its own console.
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.
uri// 'npm:@example/subtitles-plugin', the version-free record
constinstalled: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
constinstalled:packages.InstalledPackage|null
installed// null when the person pressed Not now
The uri is npm:<name> with an optional @<version>, and options.version overrides it. Without either, the broker pins the package’s latest dist-tag. Only an exact version is accepted. A range such as ^1.2 is refused as 'invalid', and so is a version the registry does not know.
Unless you pass noConfirm: true, the broker asks “Install <name>@<version>?” with Install and Not now, and Not now resolves null. With noConfirm a notice, Package installed, shows for 6 seconds instead. A package this app already holds at the version you ask for, or when you ask for no version, comes back as the existing record with no card.
Two package prompts cannot be open at once. pick() and a confirming install() share one exclusive card, and a second call while it is open rejects with packages: another package prompt is already open, under 'unavailable'. Restoring a saved list with Promise.all therefore installs one package and rejects the rest.
Install one at a time, or pass noConfirm: true, whose notice sits outside the card. Keep the uri the record gives back: every other call takes the version-free form, and the pin lives in version. There is no update call, so install again with a version to move the pin.
Uninstall a package from this app; its frames and connections are torn down.
uninstall('npm:@example/subtitles-plugin') // resolves, an unknown uri is not an error
Records are per installing app, so another app’s packages are invisible to yours. list() answers [] when the broker cannot identify the caller.
uninstall deletes the record, hides every view this app holds of the package and releases this app from its frame. The frame goes away only once the last app holding it lets go, and that is when every connection’s closed settles. The resolved promise is the signal that your uninstall took effect, never closed.
packages.connect(uri, options?) boots the package in a hidden frame the broker owns and hands you a connection to it. The type argument is the payload the package exposes, so import it from the package’s own file:
log('the package went away, connect() again to resume'))
Every function on that payload becomes an async one, and plain data stays itself, through osra’s Remote<T> (see TypeScript). Beside remote, the connection carries closed and port. closed settles when the package side dies (an uninstall, a reload, a crash). port is the raw MessagePort under remote, for your own messages.
Four options shape the connection. protocol is an opaque tag, cut to 64 characters and delivered to the package’s onConnect as info.protocol. It is the only versioning the connection has, so name your contract in it.
payload is what the package sees as its own remote. signal disconnects when aborted, which settles the package’s closed. raw: true hands back the untouched port with no handshake, so payload and signal do nothing beside it. Pass them to attach instead (see attaching in a worker).
The broker keeps one hidden frame per package and version, shared by every app connected to it. Connecting takes two stages. The package has to call onConnect within 30 seconds, and the handshake over the port then has to complete within another 30 seconds. A stage that runs out rejects as 'timeout'.
After a handshake timeout the frame is still alive, so closed does not settle for it and the retry is yours.
Every rejection the packages surface itself produces is a PackagesError, an Error with a code, and @fkn/lib/packages exports the type. Narrow on the code rather than on the class:
error// a broker replaced mid-call lands here with no code at all
}
One rejection carries no code. Taking an update to the shell, the FKN surface that can update and reload the page, reloads the broker frame. A call in flight at that moment rejects with a plain FKN: the broker was replaced while this call was pending; retry it. code reads undefined there, and the else throw error above passes it on, so retry once (see a replaced broker).
Serve connections from apps that installed this package. The first argument is called once per
incoming connection with the connection info and returns the payload exposed to that app (its
remote). The latest registration receives new connections; existing connections are unaffected.
Serve connections from apps that installed this package. The first argument is called once per
incoming connection with the connection info and returns the payload exposed to that app (its
remote). The latest registration receives new connections; existing connections are unaffected.
}) // unsubscribe() stops new connections, the open ones keep running
createPayload runs once per incoming connection with that connection’s info. It may be async, and it returns what the app sees as its remote. A throw refuses the connection: the app rejects as 'unavailable' with your error’s message, and the package logs a warning. The optional handler then receives the same info plus remote, closed and port, the mirror of the app’s connection.
The info names the two ends:
Field
What it holds
from
The connecting app: its page origin, or its package uri when it is itself a package.
This package’s version-free uri, as the app’s install record has it.
name
This package’s npm name.
version
The exact version this frame runs, encoded into its origin.
The whole info object is asserted by the document embedding the package: the broker under connect, and the app itself under mount. from and the rest are routing information, never proof of who is calling.
The first onConnect call announces the package as ready, so call it at boot. A package that never calls it never announces, and every app connecting to it times out with packages.connect: '<uri>' did not register a connection handler. Ports that arrive before the first registration are queued for it, and the latest registration receives new connections.
search and pick find a package by the keywords in its package.json: fkn, fkn-type:<type> for the type an app queries, and fkn-<type>--<id> for that app’s id. Publish it on npm with those three.
Its main is a self-contained browser ES module, because the loader injects it as a module script and resolves nothing. Import narrow subpaths such as @fkn/lib/packages rather than the root.
The root pulls in the socket surface, which needs Node stream shims a plain bundler configuration does not supply. A build from the root fails until they are added (see install). The recipe ship a package covers publishing.
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.
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.
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.
The frame tracks the placeholder every animation frame, clipped by every ancestor whose overflow is not visible, so it reads as inline content while it renders in FKN’s overlay. A still page pushes nothing. refresh() forces a re-measure after a layout change the tracker cannot see.
The frame follows the placeholder’s corner radii when every corner is a single pixel value. A percentage on any corner, or an elliptical radius written with a slash such as border-radius: 10px / 20px, drops the rounding on all four corners, so border-radius: 50% shows square corners.
show needs a live connect() by this app. An install record alone answers packages.show: '<id>' is not connected by this app - connect() before showing it, under 'not-installed'. With rect instead of element nothing tracks: refresh() re-reads the same object, so mutate it in place or call show() again. Passing neither is 'invalid', and a package that tries to show its own frame is 'denied'.
view.hide() and packages.hide(uri, { element }) release one view. packages.hide(uri) with no element releases every view this app holds of the package. The connection is untouched, so showing it again is cheap.
On the package side, isVisible() and onVisibilityChange(handler) say whether any app is showing the frame:
package.ts
import {
constisVisible: () =>boolean
True while a host app is showing this package's frame. Packages start hidden.
Observe whether a host app is showing this package's frame, so it can render its UI only while on
screen. The handler is called immediately with the current state, and on every change after.
Observe whether a host app is showing this package's frame, so it can render its UI only while on
screen. The handler is called immediately with the current state, and on every change after.
visible }) // called at once with the current state, then on every change
The broker sends true when the first app shows the frame, false when the last one hides it, and true again when the package’s document reloads while an app is still showing it. Under mount the app sends true right after the port, since a frame in your layout is on screen by construction. A handler that throws is swallowed.
packages.mount(uri, { iframe }) loads the package into an iframe you created instead of a frame the broker positions, so it lays out, scrolls and goes fullscreen with the rest of your page:
app.ts
const
constiframe:HTMLIFrameElement
iframe=
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
Load a package into an iframe of YOUR OWN and connect to it, instead of positioning a frame the broker
owns and clipping the overlay to it the way show() does.
You pass the iframe, the same way cloud.attachFrame takes one. It lays out, scrolls, stacks and
fullscreens with the rest of your page, there is no geometry to translate, and every attribute on it
stays exactly as you set it: mount reads sandbox and allow to check the package can boot, then
writes nothing but src. The package still gets its own origin and its own broker connection.
Grant capabilities through the iframe's own allow, before calling this. Permissions policy is read
at navigation and is not inherited, so it cannot be added afterwards on the frame handed back.
Needs a package built against this version of the lib: an older one only accepts a port from fkn.app.
mount<
typePlayerApi= {
play: (url:string) =>Promise<boolean>;
}
PlayerApi>('npm:@example/player-plugin', {
iframe: HTMLIFrameElement
the iframe the package is loaded into. YOU create it and YOU own it: its attributes, its styles and
its place in your layout are yours, and mount only navigates it.
Set allow yourself for anything the package needs delegated, e.g. allow="fullscreen; autoplay".
Permissions policy is not inherited, so a capability this frame is not granted cannot be recovered
by the package or by anything it nests inside itself.
iframe,
protocol?: string |undefined
opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'
protocol: 'example-player@1' }) // needs an install record, no connect() before it
await
constmounted:packages.MountedPackage<PlayerApi>
mounted.
remote: {
play: (url:string) =>Promise<boolean>;
}
the package's exposed payload
remote.
play: (url:string) =>Promise<boolean>
play('https://cdn.example.org/clip.mp4') // true, the package started playing
var document:Document
window.document returns a reference to the document contained in the window.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
blank the frame and settle closed; the element stays in your layout because it is yours
unmount()) // blanks the frame and settles closed, the element stays
The iframe is yours. mount writes src and nothing else, so its attributes, styles and place in your layout are what you set. It checks them first and refuses by name, as 'invalid', rather than failing at the boot deadline:
the iframe is in the document, since a detached frame never navigates
a sandbox attribute, if present, includes allow-scripts and allow-same-origin, since the package registers a service worker
on a cross-origin isolated page, allow includes cross-origin-isolated, to hand it down
Grant everything else the package needs, such as fullscreen or autoplay, through allow before mounting. Permissions policy is read at navigation and cannot be added afterwards. The result is a connection plus frame, the element you passed, and an idempotent unmount() that never removes it.
mount needs an install record, and answers packages.mount: '<uri>' is not installed by this app without one. Unlike show it needs no connect() first: the mount is the connection. It requires a package built against a recent @fkn/lib, because an older one only accepts a port from fkn.app.
Mounting has the same two stages and the same errors as connect. The package sees from, uri and version from the broker’s record rather than the string you typed. A mount that fails either stage blanks your iframe and settles closed before it rejects, unlike connect, so a retry mounts again.
A boot failure under mount arrives as 'timeout', with the same message as a missing handler, rather than in the package’s own words. The package’s failure report is addressed to fkn.app and never reaches an iframe you mounted yourself.
Handoff)) // closed stays on the main thread, so the engine is told
The page hands the port to the worker, and closed stays behind on the main thread. The worker takes the port and runs the app’s end of the connection there:
The MessagePort interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
appVersion: '1.2.0' }) // the app's end of the connection, inside the worker
await
constsource: {
search: (text:string) =>Promise<string[]>;
}
source.
search: (text:string) =>Promise<string[]>
search('naruto') // ['result for naruto'], answered by the package
})
attach needs no window. Its options.signal ends the handshake before the 30 second guard does. It has no closed to watch, so a package that dies before the handshake reads as 'timeout'.
A realm is one JavaScript execution context, such as a window or a worker. The library gates a packages call on whether the realm has a window only for the calls that touch the DOM. onConnect, isVisible and onVisibilityChange are no-ops without a window, and show, hide and mount need one. The calls the broker answers, search, pick, install, list, uninstall and connect, are not gated on the realm by the library, and neither is attach.
Everything above rides on a handful of postMessage shapes, which you meet when you use port directly:
Message
Direction
What it says
fkn-packages-ready
package to its embedder
The package called onConnect, hand it ports.
fkn-packages-failed
the package’s frame to fkn.app
The package failed to boot, with a message. Never reaches an iframe you mounted yourself.
fkn-packages-port
embedder to package
One connection: from, protocol, uri, name, version and a port.
fkn-packages-visibility
embedder to package
visible: true or false.
fkn-packages-close
embedder to package
Every connection on this document is over.
fkn-packages-nack
package to app, over the port
The connection was refused before the handshake, with a message.
fkn-packages-disconnect
app to package, over the port
The app’s signal aborted.
The typed remote on each side is an osra connection over the same port, under the key fkn-packages-connection. When you read port yourself, give your messages a type of your own and return early on anything else. The osra envelopes and the two control messages then pass by untouched.
A malformed uri, query token or version range, a show with neither element nor rect, or a mount iframe the checks refused. The message names it.
'not-installed'
The uri has no record for this app, or was released while connecting, or show was called before connect.
'unaddressable'
npm:<name>@<version> does not fit the sandbox origin label, which holds roughly 40 characters of it. Use a shorter package name.
'timeout'
The package did not announce within 30 seconds, or did not complete the handshake within 30 seconds.
'unavailable'
The registry did not answer, another prompt was open, the package failed to boot, refused or closed the connection, your signal aborted, the record could not be written, the caller is not identified yet, or the realm has no transport.
'denied'
The platform disabled this package, or a package tried to show its own frame.
The two clocks are the boot wait, 30 seconds for fkn-packages-ready in connect and mount, and the handshake wait, 30 seconds in connect, mount and attach. A worst case connect() is therefore a minute, so an app that shows progress should race its own deadline.
A connection, a view and a mount each hold a busy token, one of the reasons a realm reports itself busy. shell.busyReasons() lists them as package connection (1), package view and mounted package (1) (see busy tokens). The exact signatures live in the generated reference for @fkn/lib/packages.