Skip to content

Install a package and show its UI

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:

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') // the confirm card, with Install and Not now
if (
const installed: packages.InstalledPackage | null
installed
) {
const installed: packages.InstalledPackage
installed
.
uri: string
uri
// 'npm:@example/subtitles-plugin', the version-free record and the key every later call takes
const installed: 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
const saveEnabled: (uris: string[]) => void
saveEnabled
([...new
var Set: SetConstructor
new <string>(iterable?: Iterable<string> | null | undefined) => Set<string> (+1 overload)
Set
([...
const loadEnabled: () => string[]
loadEnabled
(),
const installed: packages.InstalledPackage
installed
.
uri: string
uri
])]) // persisted only now, once the broker holds the record
}
const installed: 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
const enabled: string[]
enabled
=
const loadEnabled: () => string[]
loadEnabled
() // ['npm:@example/subtitles-plugin'], the uris install() gave back
for (const
const uri: string
uri
of
const enabled: string[]
enabled
) {
try {
await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<unknown>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<unknown>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
(
const uri: string
uri
, {
protocol?: string | undefined

opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'

protocol
: 'example-source@1' }) // resolves from the record alone, no install() on a return visit
} catch (
var error: unknown
error
) {
if ((
var error: unknown
error
as
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
).
code: packages.PackagesErrorCode
code
!== 'not-installed') throw
var error: unknown
error
const saveEnabled: (uris: string[]) => void
saveEnabled
(
const enabled: string[]
enabled
.
Array<string>.filter(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

@parampredicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.

@paramthisArg 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
!==
const uri: 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:

app.ts
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
import type {
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
} from './package'
const
const connection: packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>
connection
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<{
search: (text: string) => Promise<string[]>;
}>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
<
type SourceApi = {
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
const connection: 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.

A connection fails in two shapes, and only one of them fires closed. The first is a rejected connect(), which hands you no closed at all: a 'timeout' after 30 seconds of boot or 30 seconds of handshake, packages.connect: '<uri>' failed to boot: <failure> under 'unavailable', or a refusal with packages.connect: the package refused the connection. After a handshake timeout, packages.connect: the package did not complete the connection, the package’s frame is still alive, so nothing later tells you.

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:

app.ts
const
const connectPackage: (uri: string, attempt?: number) => Promise<packages.PackageConnection<unknown> | null>
connectPackage
= async (
uri: string
uri
: string,
attempt: number
attempt
= 1) => {
try {
const
const connection: packages.PackageConnection<unknown>
connection
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<unknown>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<unknown>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
(
uri: string
uri
, {
protocol?: string | undefined

opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'

protocol
: 'example-source@1' })
const connection: packages.PackageConnection<unknown>
connection
.
closed: Promise<void>

settles when the package side of the connection dies (uninstall, reload, crash) - reconnect by calling connect() again

closed
.
Promise<void>.then<void, never>(onfulfilled?: ((value: void) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<void>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of which ever callback is executed.

then
(() =>
const scheduleReconnect: (uri: string, attempt: number) => void
scheduleReconnect
(
uri: string
uri
, 1)) // the package side died, the one shape closed reports
return
const connection: packages.PackageConnection<unknown>
connection
} catch (
function (local var) error: unknown
error
) {
const {
const code: packages.PackagesErrorCode
code
} =
function (local var) error: unknown
error
as
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
if (
const code: packages.PackagesErrorCode
code
=== 'timeout')
const scheduleReconnect: (uri: string, attempt: number) => void
scheduleReconnect
(
uri: string
uri
,
attempt: number
attempt
+ 1) // no closed exists for a rejected connect, so this retry is yours
else if (
const code: "invalid" | "not-installed" | "unaddressable" | "unavailable" | "denied"
code
=== 'not-installed') return null // install() first, or drop the saved uri
else throw
function (local var) error: unknown
error
// the rest, and a broker replaced mid-call, which carries no code at all
return null
}
}

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
const uri: "npm:@example/subtitles-plugin"
uri
= 'npm:@example/subtitles-plugin'
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
>('#subtitles-slot')! // the border-radius goes on this element
const
const connection: packages.PackageConnection<unknown>
connection
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<unknown>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<unknown>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
(
const uri: "npm:@example/subtitles-plugin"
uri
, {
protocol?: string | undefined

opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'

protocol
: 'example-source@1' }) // show() needs this live connection first
const
const view: packages.PackageView
view
= 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
(
const uri: "npm:@example/subtitles-plugin"
uri
, {
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
}) // the frame sits over the slot, clipped to its corners
const connection: packages.PackageConnection<unknown>
connection
.
closed: Promise<void>

settles when the package side of the connection dies (uninstall, reload, crash) - reconnect by calling connect() again

closed
.
Promise<void>.then<void, never>(onfulfilled?: ((value: void) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<void>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of which ever callback is executed.

then
(() =>
const view: packages.PackageView
view
.
hide: () => void

release this view; equivalent to packages.hide(uri, { element })

hide
()) // the package went away, so the view comes down with it
var document: Document

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

MDN Reference

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

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

MDN Reference

querySelector
('#toggle-sidebar')!.
Element.addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', () =>
const view: packages.PackageView
view
.
refresh: () => void

force a re-measure, e.g. right after a layout change the tracker cannot observe

refresh
()) // a layout change the tracker cannot see

The frame tracks the slot every animation frame and is clipped by every ancestor whose overflow is not visible, so it reads as inline content. A percentage on any corner drops the rounding on all four. A show() before the connect settles is refused with packages.show: '<id>' is not connected by this app - connect() before showing it, see showing a package’s frame.

[Page] Grant allow before mount() navigates

Section titled “[Page] Grant allow before mount() navigates”

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
const iframe: HTMLIFrameElement
iframe
=
var document: Document

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

MDN Reference

document
.
Document.createElement<"iframe">(tagName: "iframe", options?: ElementCreationOptions): HTMLIFrameElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('iframe')
const iframe: HTMLIFrameElement
iframe
.
Element.setAttribute(qualifiedName: string, value: string): void

The setAttribute() method of the Element interface sets the value of an attribute on the specified element.

MDN Reference

setAttribute
('sandbox', 'allow-scripts allow-same-origin') // both tokens, or mount() refuses by name
const iframe: HTMLIFrameElement
iframe
.
Element.setAttribute(qualifiedName: string, value: string): void

The setAttribute() method of the Element interface sets the value of an attribute on the specified element.

MDN Reference

setAttribute
('allow', 'fullscreen; autoplay; cross-origin-isolated') // granted before the navigation, since it cannot be added after
const iframe: HTMLIFrameElement
iframe
.
ElementCSSInlineStyle.style: CSSStyleDeclaration
style
.
CSSStyleDeclaration.cssText: string

The cssText property of the CSSStyleDeclaration interface returns or sets the text of the element's inline style declaration only.

MDN Reference

cssText
= 'width:100%;aspect-ratio:16/9;border:0'
var document: Document

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

MDN Reference

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

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

MDN Reference

querySelector
('#subtitles-panel')!.
ParentNode.append(...nodes: (Node | string)[]): void

Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.

Throws a "HierarchyRequestError" DOMException if the constraints of the node tree are violated.

MDN Reference

append
(
const iframe: HTMLIFrameElement
iframe
) // in the document, in its place, before the mount
type
type PickerApi = {
pick: (title: string) => Promise<string | null>;
}
PickerApi
= {
pick: (title: string) => Promise<string | null>
pick
: (
title: string
title
: string) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string | null> }
const
const mounted: packages.MountedPackage<PickerApi>
mounted
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.mount<PickerApi>(uri: string, options: packages.MountOptions): Promise<packages.MountedPackage<PickerApi>>
export packages_d_exports.mount

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
<
type PickerApi = {
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
const mounted: 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.

MDN Reference

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

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

MDN Reference

querySelector
('#close')!.
Element.addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', () =>
const mounted: packages.MountedPackage<PickerApi>
mounted
.
unmount: () => void

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

mount checks the iframe first and refuses by name, as 'invalid', rather than waiting out the boot. A sandbox attribute without both tokens is refused with packages.mount: the iframe's sandbox attribute must include <tokens>, or the package cannot start. A cross-origin isolated page that does not hand the isolation down is refused with packages.mount: this page is cross-origin isolated, so the iframe's allow attribute must include 'cross-origin-isolated' to hand that down to the package.

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
const uri: "npm:@example/subtitles-plugin"
uri
= 'npm:@example/subtitles-plugin'
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
>('#subtitles-slot')!
await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect<unknown>(uri: string, options?: packages.AppConnectOptions & {
raw?: false;
}): Promise<packages.PackageConnection<unknown>> (+1 overload)
export packages_d_exports.connect

Connect to an installed package. Throws a PackagesError with code 'not-installed' when it is not.

connect
(
const uri: "npm:@example/subtitles-plugin"
uri
, {
protocol?: string | undefined

opaque contract tag delivered to the package's onConnect, e.g. 'stub-source@1'

protocol
: 'example-source@1' })
let
let pending: Promise<packages.PackageView> | undefined
pending
:
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type PackageView = {
hide: () => void;
refresh: () => void;
}
PackageView
> | undefined
const
const open: () => void
open
= () => {
let pending: Promise<packages.PackageView> | undefined
pending
=
(alias) namespace packages
import packages
packages
.
packages_d_exports.show(uri: string, options: packages.ShowOptions): Promise<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
(
const uri: "npm:@example/subtitles-plugin"
uri
, {
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
}) }
const
const close: () => Promise<void>
close
= async () => {
await
let pending: Promise<packages.PackageView> | undefined
pending
?.
Promise<PackageView>.catch<TResult>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined): Promise<packages.PackageView | TResult>

Attaches a callback for only the rejection of the Promise.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of the callback.

catch
(() => {}) // a show() still in flight lands first, so there is a view to take down
let pending: Promise<packages.PackageView> | undefined
pending
=
var undefined
undefined
await
(alias) namespace packages
import packages
packages
.
packages_d_exports.hide(uri: string, options?: packages.ShowOptions): Promise<void>
export packages_d_exports.hide

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.

hide
(
const uri: "npm:@example/subtitles-plugin"
uri
, {
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
}) // stops the tracker, then asks the broker
(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
() // ['package connection (1)'], the package view entry went with the hide
}
const
const remove: () => Promise<void>
remove
= async () => {
await
const close: () => Promise<void>
close
()
await
(alias) namespace packages
import packages
packages
.
packages_d_exports.uninstall(uri: string): Promise<void>
export packages_d_exports.uninstall

Uninstall a package from this app; its frames and connections are torn down.

uninstall
(
const uri: "npm:@example/subtitles-plugin"
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:

  1. packages.connect: '<uri>' did not register a connection handler after 30 seconds means the package never called onConnect. When the package does call it at boot, the usual cause is a failure while its bundle evaluates, which reaches you only as this timeout, see ship a package.
  2. No frame, and packages.show: '<id>' is not connected by this app - connect() before showing it, means show() ran before connect() settled. Await the connection first.
  3. packages.<call>: '<uri>' is not installed by this app means the saved uri has no record: it was never installed, it was uninstalled, or it is not the uri install() gave back.
  4. 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.
  5. packages.mount: the package did not register a connection handler covers a boot failure too, because the package’s failure report is addressed to fkn.app and never reaches an iframe you mounted. Connect the same package with connect() once to read packages.connect: '<uri>' failed to boot: <failure> in the package’s own words.

connect(uri, { raw: true }) skips the handshake and hands back the untouched port with closed, so you can transfer the port into a worker and run attach() there, see attaching in a worker.

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.