Skip to content

Install

@fkn/lib is one npm package, and a web app adds it with one install. A bundle that carries the socket surface also needs three Node shims. This page covers what the package ships, which import path to take, the three shims your bundler has to supply, the optional React peer, the one runtime requirement, and the headers for a cross-origin isolated page.

Every command on this site uses npm, and any package manager that reads package.json works the same way:

Terminal window
npm install @fkn/lib

That one line is the whole install. The package is ESM first, and every import path also carries a require condition for a CommonJS build.

Every import path ships three files: an ESM .js, a CJS .cjs and one bundled .d.ts. There are 28 of them, plus the manifest at @fkn/lib/package.json for tooling that reads the version. entry points lists every one and what it gives you.

osra, ip-address and @types/node arrive as dependencies. @types/node is a runtime dependency rather than a dev one, because the public net, dgram, http and fs declarations reference Node’s builtin types.

The broker is the connection your app holds into FKN, and every entry that reaches it mounts a hidden fkn.app iframe, the broker frame, or adopts one the page already holds, as soon as it evaluates in a window. The library appends a new frame to document.body, so the body has to exist by then. The exact conditions are on how it works.

The root entry gathers most of the library under one import, and each subpath carries one piece of it. The two spellings are one implementation: cloud.fetch from the root and fetch from @fkn/lib/cloud/fetch are the same function. Six subpaths stay separate, because the root does not re-export them: @fkn/lib/api, @fkn/lib/react, @fkn/lib/contract, @fkn/lib/wire, @fkn/lib/messages and @fkn/lib/attach-policy, all listed on entry points.

What differs is what the published files pull in. @fkn/lib/cloud/fetch reaches only osra. The root also pulls buffer, events and stream, three Node builtins a browser bundle has to shim, because the socket surface is built on Node’s stream and event classes, and the file and datagram surfaces hand back a Buffer. An app that fetches and nothing more can take the narrow path and skip the shims:

app.ts
import {
const fetch: (input: ProxyFetchInput, init?: ProxyFetchInit) => Promise<Response>
fetch
} from '@fkn/lib/cloud/fetch'
const
const response: Response
response
= await
function fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
fetch
('https://example.org/api/catalog.json') // a Response through the proxy, and no stream shim behind the import
const
const catalog: any
catalog
= await
const response: Response
response
.
Body.json(): Promise<any>
json
() // the app's own data

The request went through the proxy, the server that cloud.fetch sends a request through, and no socket code came along. What the proxy answers is on fetch().

The narrow path carries less code, and it still mounts the broker frame exactly as the root does. Take the root when the app uses several surfaces, since the shims below cover all of it.

The published files import buffer, events and stream as bare specifiers, only where a shape uses them:

EntryWhat it reaches
@fkn/lib, @fkn/lib/cloud, @fkn/lib/cloud/http, @fkn/lib/httpbuffer, events, stream, ip-address, osra
@fkn/lib/net, @fkn/lib/cloud/netevents, stream, ip-address, osra
@fkn/lib/dgram, @fkn/lib/cloud/dgrambuffer, events, ip-address, osra
@fkn/lib/fs, @fkn/lib/fs/promises, @fkn/lib/cloud/fs, @fkn/lib/cloud/fs/promisesbuffer, osra
@fkn/lib/opfs, @fkn/lib/opfs/promisesbuffer
@fkn/lib/cloud/fetch, @fkn/lib/dns, @fkn/lib/cloud/dns, @fkn/lib/account, @fkn/lib/packages, @fkn/lib/extension, @fkn/lib/apiosra
@fkn/lib/reactreact
@fkn/lib/desktop, @fkn/lib/contract, @fkn/lib/wire, @fkn/lib/messages, @fkn/lib/attach-policynone

osra, ip-address and react are packages your bundler resolves on its own, and the three that remain are the shims. So net needs events and stream, dgram needs buffer and events, http needs all three, and fs needs buffer. A browser has none of them, so the bundle has to provide each name before the entry that imports it evaluates. The stream shim also reads global at module scope, so the globals matter as much as the modules.

With Vite, vite-plugin-node-polyfills scoped to exactly those three does both, and it defines global, process and Buffer by default. Add it as a dev dependency and list the three names:

vite.config.ts
import {
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
} from 'vite'
import {
const nodePolyfills: (options?: PolyfillOptions) => Plugin[]

Returns a Vite plugin to polyfill Node's Core Modules for browser environments. Supports node: protocol imports.

@example

// vite.config.ts
import { defineConfig } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
export default defineConfig({
plugins: [
nodePolyfills({
// Specific modules that should not be polyfilled.
exclude: [],
// Whether to polyfill specific globals.
globals: {
Buffer: true, // can also be 'build', 'dev', or false
global: true,
process: true,
},
// Whether to polyfill `node:` protocol imports.
protocolImports: true,
}),
],
})

nodePolyfills
} from 'vite-plugin-node-polyfills'
export default
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined

Array of vite plugins to use.

plugins
: [
function nodePolyfills(options?: PolyfillOptions): Plugin[]

Returns a Vite plugin to polyfill Node's Core Modules for browser environments. Supports node: protocol imports.

@example

// vite.config.ts
import { defineConfig } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
export default defineConfig({
plugins: [
nodePolyfills({
// Specific modules that should not be polyfilled.
exclude: [],
// Whether to polyfill specific globals.
globals: {
Buffer: true, // can also be 'build', 'dev', or false
global: true,
process: true,
},
// Whether to polyfill `node:` protocol imports.
protocolImports: true,
}),
],
})

nodePolyfills
({
include?: ModuleNameWithoutNodePrefix[] | undefined

Includes specific modules. If empty, includes all modules

@example

nodePolyfills({
include: ['fs', 'path'],
})

include
: ['buffer', 'events', 'stream'],
protocolImports?: boolean | undefined

Specify whether the Node protocol version of an import (e.g. node:buffer) should be polyfilled too.

@defaulttrue

protocolImports
: false }), // a build in which @fkn/lib/net resolves its bare stream import
],
})

That is the configuration this site builds with: three names and nothing else. protocolImports: false is enough for the library, whose published files import the three names bare. Set it to true when your own code imports node:buffer, node:events or node:stream.

@fkn/vite-plugin supplies the same shims and aliases the Node names to the matching entries, so code written for Node keeps its imports. With another bundler, alias the same three names to browser implementations and define the same three globals. The production build is on bundle an app that uses sockets.

react is an optional peer at >=18, and only @fkn/lib/react imports it. Installing @fkn/lib does not pull React in. Install React only when you render ConnectButton, the one component the package exports, which is described under account and quota.

A realm is one JavaScript execution context, such as a window or a worker. @fkn/lib needs one of two: a window realm, where it mounts the broker frame and talks to the broker through it, or a worker the page relayed.

A worker is a realm of its own, and it cannot open a broker connection by itself, so the page relays its calls. Inside a worker there is no document to mount the frame in, so the worker reaches the broker only once the page awaits relayWorker. What the relay does, its options, and how it ends are on workers.

After that the worker imports @fkn/lib/net exactly as the page does. See run sockets in a worker for the pair, and workers for what works inside one. A test runner has no broker to reach, and what runs without one is on testing.

Optional: Serve a cross-origin isolated page

Section titled “Optional: Serve a cross-origin isolated page”

A page is cross-origin isolated when it is served with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, which is what unlocks SharedArrayBuffer. Such a page can embed only a frame whose response opts in, so skip this section unless the page needs isolation.

On an isolated page @fkn/lib mounts /api?coi=1 instead of /api, a variant of the broker document that carries the headers an isolated page can embed, and it sets allow="cross-origin-isolated" on the frame so the isolation is handed down. The library never falls back to the plain /api frame, by design: an isolated page blocks that frame, so a fallback would hang. Serve the two headers from your dev server and your host alike, where server covers vite dev, preview covers vite preview, and a production host sends the same two on the document:

vite.config.ts
import {
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
} from 'vite'
const
const isolation: {
'Cross-Origin-Opener-Policy': string;
'Cross-Origin-Embedder-Policy': string;
}
isolation
= {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
}
export default
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
({
UserConfig.server?: ServerOptions$1 | undefined

Server specific options, e.g. host, port, https...

server
: {
CommonServerOptions.headers?: OutgoingHttpHeaders | undefined

Specify server response headers.

headers
:
const isolation: {
'Cross-Origin-Opener-Policy': string;
'Cross-Origin-Embedder-Policy': string;
}
isolation
}, // the page loads isolated, and the broker frame mounts as /api?coi=1 rather than being blocked
UserConfig.preview?: PreviewOptions | undefined

Preview specific options, e.g. host, port, https...

preview
: {
CommonServerOptions.headers?: OutgoingHttpHeaders | undefined

Specify server response headers.

headers
:
const isolation: {
'Cross-Origin-Opener-Policy': string;
'Cross-Origin-Embedder-Policy': string;
}
isolation
},
})