Skip to content

Packages

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.

packages.search(query) asks the npm registry for packages that carry FKN’s keywords:

app.ts
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
const
const results: packages.PackageResult[]
results
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.search(query: packages.PackageQuery): Promise<packages.PackageResult[]>
export packages_d_exports.search

Search npm for FKN packages, e.g. search({ type: 'plugin', id: 'stub' }).

search
({
type: string

package kind, e.g. 'plugin' - becomes the keyword fkn-type:<type>

type
: 'plugin',
id?: string | undefined

host app scope, e.g. 'stub' - becomes the keyword fkn-<type>--<id>

id
: 'example',
text?: string | undefined

free text mixed into the registry query

text
: 'subtitles' })
const results: packages.PackageResult[]
results
[0]?.
uri: string

normalized version-free uri, e.g. 'npm:@banou/stub-plugin-foo'

uri
// 'npm:@example/subtitles-plugin', the version-free uri
const results: packages.PackageResult[]
results
[0]?.
installed: boolean

installed by the calling app

installed
// true when this app already installed it

The query maps onto npm keywords, and only type is required:

FieldWhat it does
typeThe package kind, the keyword fkn-type:<type>.
idYour app’s scope, the keyword fkn-<type>--<id>. Optional.
originThe package source, and 'npm' is the only one. Optional.
textFree text in the registry query, cut to 128 characters. Optional.
sizeThe result count, 36 by default, clamped to 1 to 100. Optional.

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:

app.ts
const
const picked: packages.PackageResult[]
picked
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.pick(query: packages.PackageQuery, options?: packages.PickOptions): Promise<packages.PackageResult[]>
export packages_d_exports.pick

FKN-rendered picker over the same search; resolves the user's selection, already installed. [] on cancel.

pick
({
type: string

package kind, e.g. 'plugin' - becomes the keyword fkn-type:<type>

type
: 'plugin',
id?: string | undefined

host app scope, e.g. 'stub' - becomes the keyword fkn-<type>--<id>

id
: 'example' }, {
title?: string | undefined

untrusted, rendered as text in the picker header

title
: 'Add a source',
multiple?: boolean | undefined
multiple
: true })
const picked: packages.PackageResult[]
picked
.
Array<PackageResult>.map<string>(callbackfn: (value: packages.PackageResult, index: number, array: packages.PackageResult[]) => string, thisArg?: any): string[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@paramthisArg 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.

packages.install(uri, options?) records a package for this app, pinned to a version:

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', the version-free record
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 installed: 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.

packages.list() returns this app’s records, and packages.uninstall(uri) removes one:

app.ts
for (const
const pkg: packages.InstalledPackage
pkg
of await
(alias) namespace packages
import packages
packages
.
packages_d_exports.list(): Promise<packages.InstalledPackage[]>
export packages_d_exports.list

The packages installed by this app.

list
()) {
const pkg: packages.InstalledPackage
pkg
.
uri: string
uri
// 'npm:@example/subtitles-plugin'
const pkg: packages.InstalledPackage
pkg
.
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 pkg: packages.InstalledPackage
pkg
.
installedAt: number
installedAt
// a millisecond epoch
}
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
('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:

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',
payload?: unknown

exposed to the package as ITS remote

payload
: {
appVersion: string
appVersion
: '1.2.0' }
})
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
('naruto') // ['result for naruto'], answered by the package
const connection: packages.PackageConnection<{
search: (text: string) => Promise<string[]>;
}>
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
(() =>
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

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:

app.ts
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
import type {
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
} from '@fkn/lib/packages'
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
('npm:@example/subtitles-plugin')
} catch (
var error: unknown
error
) {
const {
const code: packages.PackagesErrorCode
code
,
const message: string
message
} =
var error: unknown
error
as
type PackagesError = Error & {
code: packages.PackagesErrorCode;
}
PackagesError
if (
const code: packages.PackagesErrorCode
code
=== 'not-installed')
var console: Console
console
.
Console.warn(...data: any[]): void

The console.warn() static method outputs a warning message to the console at the 'warning' log level.

MDN Reference

warn
('install it first:',
const message: string
message
)
else if (
const code: "invalid" | "unaddressable" | "timeout" | "unavailable" | "denied"
code
=== 'timeout')
var console: Console
console
.
Console.warn(...data: any[]): void

The console.warn() static method outputs a warning message to the console at the 'warning' log level.

MDN Reference

warn
('the package never answered:',
const message: string
message
)
else throw
var error: unknown
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).

onConnect(createPayload, handler?) is how a package serves the apps that connect to it:

package.ts
import {
const onConnect: <T = unknown>(createPayload: ConnectPayload, handler?: (connection: IncomingConnection<T>) => void) => {
unsubscribe: () => void;
}

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.

onConnect
} from '@fkn/lib/packages'
type
type HostApi = {
appVersion: string;
}
HostApi
= {
appVersion: string
appVersion
: string }
const
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
= {
search: (text: string) => Promise<string[]>
search
: async (
text: string
text
: string) => [`result for ${
text: string
text
}`] }
export type
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
= typeof
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
const {
const unsubscribe: () => void
unsubscribe
} =
onConnect<HostApi>(createPayload: ConnectPayload, handler?: ((connection: IncomingConnection<HostApi>) => void) | undefined): {
unsubscribe: () => void;
}

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.

onConnect
<
type HostApi = {
appVersion: string;
}
HostApi
>(
info: IncomingConnectionInfo
info
=> {
if (
info: IncomingConnectionInfo
info
.
protocol: string | null

the contract tag the app passed to connect(), e.g. 'stub-source@1'

protocol
!== 'example-source@1') throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`unsupported protocol ${
info: IncomingConnectionInfo
info
.
protocol: string | null

the contract tag the app passed to connect(), e.g. 'stub-source@1'

protocol
}`)
return
const payload: {
search: (text: string) => Promise<string[]>;
}
payload
},
connection: IncomingConnection<HostApi>
connection
=> {
connection: IncomingConnection<HostApi>
connection
.
from: string

the connecting app's identity: its package uri when it runs on a sandbox origin, else its page origin

from
// 'https://example.org', the connecting app
connection: IncomingConnection<HostApi>
connection
.
remote: {
appVersion: string;
}

the package's exposed payload

remote
.
appVersion: string
appVersion
// '1.2.0', the app's payload, plain data so no await
connection: IncomingConnection<HostApi>
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
(() =>
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('the app disconnected'))
}) // 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:

FieldWhat it holds
fromThe connecting app: its page origin, or its package uri when it is itself a package.
protocolThe tag the app passed to connect(), or null.
uriThis package’s version-free uri, as the app’s install record has it.
nameThis package’s npm name.
versionThe 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.

A package’s account.info() and account.login() act on the host app’s connection. Its cloud.quota(), cloud.fs and cloud.fetch meter and store under the package’s own scope (see account and quota).

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.

packages.show(uri, { element }) places the package’s frame over your page, aligned to a placeholder you keep in your own layout:

app.ts
const
const uri: "npm:@example/player-plugin"
uri
= 'npm:@example/player-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
>('#player-slot')!
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/player-plugin"
uri
, {
protocol?: string | undefined

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

protocol
: 'example-player@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/player-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 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
())
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 view: packages.PackageView
view
.
hide: () => void

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

hide
())

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 {
const isVisible: () => boolean

True while a host app is showing this package's frame. Packages start hidden.

isVisible
,
const onVisibilityChange: (handler: (visible: boolean) => void) => {
unsubscribe: () => void;
}

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.

onVisibilityChange
} from '@fkn/lib/packages'
function isVisible(): boolean

True while a host app is showing this package's frame. Packages start hidden.

isVisible
() // false, a package starts hidden
function onVisibilityChange(handler: (visible: boolean) => void): {
unsubscribe: () => void;
}

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.

onVisibilityChange
(
visible: boolean
visible
=> {
var document: Document

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

MDN Reference

document
.
Document.body: HTMLElement

The Document.body property represents the null if no such element exists.

MDN Reference

body
.
HTMLElement.hidden: boolean

The HTMLElement property hidden reflects the value of the element's hidden attribute.

MDN Reference

hidden
= !
visible: boolean
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
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')
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')
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
('#player')!.
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
)
type
type PlayerApi = {
play: (url: string) => Promise<boolean>;
}
PlayerApi
= {
play: (url: string) => Promise<boolean>
play
: (
url: string
url
: string) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<boolean> }
const
const mounted: packages.MountedPackage<PlayerApi>
mounted
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.mount<PlayerApi>(uri: string, options: packages.MountOptions): Promise<packages.MountedPackage<PlayerApi>>
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 PlayerApi = {
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
const mounted: 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.

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<PlayerApi>
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

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.

raw: true skips the handshake and gives you the untouched port. attach(port, payload?, options?) runs the app’s end of it wherever the port ends up:

app.ts
import type {
type Handoff = {
type: "package-port";
port: MessagePort;
} | {
type: "package-gone";
}
Handoff
} from './engine'
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
const
const engine: Worker
engine
= new
var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => Worker

The Worker interface of the Web Workers API represents a background task that can be created via script, which can send messages back to its creator.

MDN Reference

Worker
(new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
('./engine.ts', import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.url: string
url
), {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
const {
const port: MessagePort

the raw channel under remote, for direct messaging (osra envelopes ride it too - filter by your own message shape)

port
,
const closed: Promise<void>

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

closed
} = await
(alias) namespace packages
import packages
packages
.
packages_d_exports.connect(uri: string, options: packages.AppConnectOptions & {
raw: true;
}): Promise<packages.RawPackageConnection> (+1 overload)
export packages_d_exports.connect

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

connect
('npm:@example/subtitles-plugin', {
raw: true

skip the osra handshake and hand back the untouched port - e.g. to transfer it into a worker and attach() there

raw
: true,
protocol?: string | undefined

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

protocol
: 'example-source@1' }) // the port before any handshake, and closed
const engine: Worker
engine
.
Worker.postMessage(message: any, transfer: Transferable[]): void (+1 overload)

The postMessage() method of the Worker interface sends a message to the worker.

MDN Reference

postMessage
({
type: "package-port"
type
: 'package-port',
port: MessagePort
port
} satisfies
type Handoff = {
type: "package-port";
port: MessagePort;
} | {
type: "package-gone";
}
Handoff
, [
const port: MessagePort

the raw channel under remote, for direct messaging (osra envelopes ride it too - filter by your own message shape)

port
])
const 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 engine: Worker
engine
.
Worker.postMessage(message: any, options?: StructuredSerializeOptions): void (+1 overload)

The postMessage() method of the Worker interface sends a message to the worker.

MDN Reference

postMessage
({
type: "package-gone"
type
: 'package-gone' } satisfies
type Handoff = {
type: "package-port";
port: MessagePort;
} | {
type: "package-gone";
}
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:

engine.ts
import {
const attach: <T = unknown>(port: MessagePort, payload?: unknown, options?: {
signal?: AbortSignal;
}) => Promise<Remote<T>>

Run this end of an already-brokered connection port, e.g. after transferring it into a worker.

attach
} from '@fkn/lib/packages'
import type {
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
} from './package'
export type
type Handoff = {
type: "package-port";
port: MessagePort;
} | {
type: "package-gone";
}
Handoff
= {
type: "package-port"
type
: 'package-port',
port: MessagePort
port
:
interface MessagePort

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.

MDN Reference

MessagePort
} | {
type: "package-gone"
type
: 'package-gone' }
var self: Window & typeof globalThis

The Window.self read-only property returns the window itself, as a WindowProxy.

MDN Reference

self
.
addEventListener<"message">(type: "message", listener: (this: Window, ev: MessageEvent<any>) => any, 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
('message', async (
event: MessageEvent<Handoff>
event
:
interface MessageEvent<T = any>

The MessageEvent interface represents a message received by a target object.

MDN Reference

MessageEvent
<
type Handoff = {
type: "package-port";
port: MessagePort;
} | {
type: "package-gone";
}
Handoff
>) => {
if (
event: MessageEvent<Handoff>
event
.
MessageEvent<Handoff>.data: Handoff

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
.
type: "package-port" | "package-gone"
type
=== 'package-gone') return // the connection is over, the next port starts a new one
const
const source: {
search: (text: string) => Promise<string[]>;
}
source
= await
attach<{
search: (text: string) => Promise<string[]>;
}>(port: MessagePort, payload?: unknown, options?: {
signal?: AbortSignal;
}): Promise<{
search: (text: string) => Promise<string[]>;
}>

Run this end of an already-brokered connection port, e.g. after transferring it into a worker.

attach
<
type SourceApi = {
search: (text: string) => Promise<string[]>;
}
SourceApi
>(
event: MessageEvent<Handoff>
event
.
MessageEvent<Handoff>.data: {
type: "package-port";
port: MessagePort;
}

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
.
port: MessagePort
port
, {
appVersion: string
appVersion
: '1.2.0' }) // the app's end of the connection, inside the worker
await
const source: {
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.

In a worker those calls wait for the broker like every other call, so relay the worker from the page first (see workers). A realm with neither window nor self rejects them at once with packages: no FKN transport in this realm - use relayWorker to bridge workers, under 'unavailable'.

Everything above rides on a handful of postMessage shapes, which you meet when you use port directly:

MessageDirectionWhat it says
fkn-packages-readypackage to its embedderThe package called onConnect, hand it ports.
fkn-packages-failedthe package’s frame to fkn.appThe package failed to boot, with a message. Never reaches an iframe you mounted yourself.
fkn-packages-portembedder to packageOne connection: from, protocol, uri, name, version and a port.
fkn-packages-visibilityembedder to packagevisible: true or false.
fkn-packages-closeembedder to packageEvery connection on this document is over.
fkn-packages-nackpackage to app, over the portThe connection was refused before the handshake, with a message.
fkn-packages-disconnectapp to package, over the portThe 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 PackagesError carries one of six codes. The one plain Error with none is FKN: the broker was replaced while this call was pending; retry it, described under connecting to a package:

CodeWhat happened
'invalid'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.

The six rows a reader of this page meets most, each linked to its row on every error:

MessageWhat happened
packages: no FKN transport in this realm - use relayWorker to bridge workersA packages.* call ran with neither window nor self. A worker has self, so there the call waits for the page to relay it (see workers).
packages: another package prompt is already openA pick() or a confirming install() ran while another card was open. Wait for it to close, or install with noConfirm: true.
packages.<call>: '<uri>' is not installed by this appconnect or mount found no record for this app. Call install(uri) first, then pass the uri it gave back.
packages.connect: '<uri>' failed to boot: <failure>The package’s frame reported why it could not start, in its own words after the colon.
packages.connect: '<uri>' did not register a connection handlerThe package never called onConnect within the boot wait. Call it at boot.
packages.mount: the iframe's sandbox attribute must include <tokens>, or the package cannot startThe iframe carries a sandbox attribute without allow-scripts and allow-same-origin. Add both, or drop the attribute.

Every other message has its row on every error, and how to match one is on handling errors.