You can let a user add a third-party package to your app and draw its interface inside your own layout. This page takes that from packages.install() to a frame placed over a placeholder or loaded into an iframe you own, and names the two rules no signature shows: install before you persist, and watch both ways a connection fails.
The end state is a package the user chose, installed, connected, and drawing inside a frame your app positioned. What the recipe costs:
A package is an npm module that FKN loads on a sandbox origin of its own, and your app is the host app, the one that installs and connects to it. Every call below goes through the broker, the connection your app holds into FKN. Every step carries the [Page] badge, defined on recipes, because show, hide and mount place a frame in your document and need a window realm, a JavaScript execution context with a window. The example is a media library adding npm:@example/subtitles-plugin, a package that draws a subtitle picker.
packages.install(uri) asks the person to confirm and records the package for your app, pinned to one version. Save the package to your own list only once that call resolves:
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') // the confirm card, with Install and Not now
if (
constinstalled:packages.InstalledPackage|null
installed) {
constinstalled:packages.InstalledPackage
installed.
uri: string
uri// 'npm:@example/subtitles-plugin', the version-free record and the key every later call takes
constinstalled:packages.InstalledPackage
installed.
version: string |null
null when the handler addresses code directly and has no version to pin, e.g. a dev server
version// '1.4.2', the pin
constsaveEnabled: (uris:string[]) =>void
saveEnabled([...new
var Set:SetConstructor
new <string>(iterable?:Iterable<string> |null|undefined) =>Set<string> (+1 overload)
Set([...
constloadEnabled: () =>string[]
loadEnabled(),
constinstalled:packages.InstalledPackage
installed.
uri: string
uri])]) // persisted only now, once the broker holds the record
}
constinstalled:packages.InstalledPackage|null
installed// null when the person pressed Not now, so nothing reached the list
The card asks Install <name>@<version>? with Install and Not now, and Not now resolves null. The broker refuses to connect a package your app never installed, with packages.connect: '<uri>' is not installed by this app under 'not-installed'. A uri written to your list before the install would be retried on every visit and never connect.
Two package prompts cannot be open at once. A second install() while the card is open rejects with packages: another package prompt is already open under 'unavailable', so install one at a time, or pass noConfirm: true to replace the card with a notice, see installing.
The record’s uri is version-free: npm:@example/subtitles-plugin@1.4.2 installs as npm:@example/subtitles-plugin, and the pin lives in version. Every later call takes that form, so the string the person typed is not the key. The record lives in the broker and is kept between visits, so a return visit connects from the saved uri with no card:
app.ts
const
constenabled:string[]
enabled=
constloadEnabled: () =>string[]
loadEnabled() // ['npm:@example/subtitles-plugin'], the uris install() gave back
Returns the elements of an array that meet the condition specified in a callback function.
@param ― predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
filter(
saved: string
saved=>
saved: string
saved!==
consturi:string
uri)) // no record for this uri, so drop it rather than retry it forever
}
}
A 'not-installed' rejection here means the record is gone: the uri was uninstalled, or it was never the one install() gave back. Dropping it is the answer, since a retry cannot create a record. A 'denied' with packages.connect: '<uri>' has been disabled by the platform is final too. The other codes are on packages.
packages.connect(uri, options?) boots the package in a hidden frame the broker owns and hands you a connection with a typed remote. Import the type from the package’s own file and name your contract in protocol:
Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.
connect<
typeSourceApi= {
search: (text:string) =>Promise<string[]>;
}
SourceApi>('npm:@example/subtitles-plugin', {
protocol?: string |undefined
opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'
protocol: 'example-source@1', // reaches the package as info.protocol, the only versioning the connection has
payload?: unknown
exposed to the package as ITS remote
payload: {
appVersion: string
appVersion: '1.2.0' } // what the package sees as its own remote
})
await
constconnection:packages.PackageConnection<{
search: (text:string) =>Promise<string[]>;
}>
connection.
remote: {
search: (text:string) =>Promise<string[]>;
}
the package's exposed payload
remote.
search: (text:string) =>Promise<string[]>
search('Sintel') // ['result for Sintel'], answered by the package
Every function on the payload becomes an async one through Remote<T>, and the connection carries closed and the raw port beside remote, see connecting to a package.
The second shape is a connection that dies after it resolved, through an uninstall, a reload or a crash, and that one settles closed. Schedule the retry from both places:
code==='not-installed') returnnull// install() first, or drop the saved uri
elsethrow
function (localvar) error: unknown
error// the rest, and a broker replaced mid-call, which carries no code at all
returnnull
}
}
A handler that only awaits closed never sees the first shape. The throw passes on a broker replaced mid-call, whose rejection has no code, see handling errors. A worst case connect() is a minute, so an app that shows progress puts its own timer on it, see limits and timeouts.
Two calls draw the package. packages.show(uri, { element }) keeps the frame in the broker’s own document and renders it in FKN’s overlay over your page, aligned to a placeholder you keep in your layout. packages.mount(uri, { iframe }) loads the package into an iframe you created, so it lays out, scrolls and goes fullscreen with the rest of your page.
show suits a modal picker, where nothing of yours has to sit above the package, since it cannot layer the frame under your own controls. mount suits a panel inside a page with its own chrome, or a player. show needs a live connect() by your app first. mount needs the install record and no connect(), because the mount is the connection.
For show, the frame follows the placeholder’s own computed border-radius when every corner is a single pixel value, so the radius belongs on the slot itself and not on a wrapper around it:
app.ts
const
consturi:"npm:@example/subtitles-plugin"
uri='npm:@example/subtitles-plugin'
const
constslot:HTMLElement
slot=
var document:Document
window.document returns a reference to the document contained in the window.
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.
element:
constslot:HTMLElement
slot }) // the frame sits over the slot, clipped to its corners
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
mount writes the iframe’s src and nothing else, so the attributes are yours to set, and they have to be set before the call. Permissions policy is read when the frame navigates and is not inherited, so a capability missing from allow cannot be added afterwards, by you or by the package:
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<
typePickerApi= {
pick: (title:string) =>Promise<string|null>;
}
PickerApi>('npm:@example/subtitles-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-source@1' }) // needs the install record, and no connect() before it
await
constmounted:packages.MountedPackage<PickerApi>
mounted.
remote: {
pick: (title:string) =>Promise<string |null>;
}
the package's exposed payload
remote.
pick: (title:string) =>Promise<string |null>
pick('Sintel') // a subtitle url or null, the package's answer
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.
A mount that fails either stage blanks your iframe and settles closed before it rejects, and the element stays where you put it. Keep the iframe in the layout and cover it with your message, so the next attempt mounts into the same element. mount needs a package built against a recent @fkn/lib, see mounting into your own iframe.
show() resolves once the broker has placed the frame. A teardown that runs before then finds no view to hide, and the frame comes up with nothing left to take it down. Keep the pending promise and wait for it before hiding:
app.ts
const
consturi:"npm:@example/subtitles-plugin"
uri='npm:@example/subtitles-plugin'
const
constslot:HTMLElement
slot=
var document:Document
window.document returns a reference to the document contained in the window.
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.
element:
constslot:HTMLElement
slot }) }
const
constclose: () =>Promise<void>
close=async () => {
await
let pending:Promise<packages.PackageView> |undefined
Hide a package's frame again, the counterpart to show(). Pass the same element to release only
the view bound to it; with no element every view of this package is released. The connection is
untouched, so the package can be shown again.
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:
constslot:HTMLElement
slot }) // stops the tracker, then asks the broker
(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() // ['package connection (1)'], the package view entry went with the hide
uri) // the record is gone and this app let go of the frame, closed settles once the last holder does
}
hide stops the tracker before it asks the broker, so a scroll cannot re-show the frame on the way down, and the connection is untouched. uninstall deletes the record and releases your app from the frame. closed settles once the last app holding that frame lets go, so the resolved uninstall() is the signal that yours took effect, see installing.
The package’s frame draws where the placeholder is, or inside your iframe, and a call on remote returns the package’s answer. When it does not, take these in order:
A mount refused as 'invalid' names the attribute: the iframe outside the document, a sandbox without both tokens, or an isolated page that did not hand cross-origin-isolated down. Fix the element and mount again, since nothing was navigated.
packages.pick(query, options?) runs a search over npm behind a broker card and installs the selection in one step, and its results carry the same version-free uri, see finding packages.