npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@low6dev/bifrost

v0.0.7

Published

Tiny, dependency-free postMessage bridge between an arcade game iframe and its host (with React Native WebView support).

Readme

bifrost

Bifröst — a tiny, dependency-free postMessage bridge between an embedded arcade game (<iframe>) and its host page, with React Native WebView support.

A game runs inside an <iframe> (or a React Native WebView) and exchanges lifecycle events with the host via postMessage. bifrost wraps both sides of that channel with a symmetric APInotify() and listen() work in either direction. The game side posts to window.parent by default; the host side posts into the iframe's contentWindow (configured via the target option), so the host can reply with payloads, push state, or send commands using the same API the game uses.

It also ships with Muninn — a symmetric storage helper that lets either side ask the other to persist data. Useful because Safari silently evicts localStorage inside iframes after an undefined period; the iframe can push its state to the host, which has durable storage. The same sendStore works in reverse so the host can hydrate the game with state on GAME_READY.

Install

From JSR (recommended — native TypeScript):

deno add jsr:@low6/bifrost
# Node / Bun / pnpm / Yarn:
npx jsr add @low6/bifrost
bunx jsr add @low6/bifrost

Or from npm:

bun add @low6/bifrost
# or: npm install @low6/bifrost

Usage

From the game (inside the iframe / WebView)

import { Bifrost } from "@low6/bifrost";

// `host` is the targetOrigin to post to (default "*"). Set the host origin in production.
const bifrost = new Bifrost("https://host.example.com");

// Tell the host the game is ready for its payload
bifrost.notify("GAME_READY");

// Send a round result
bifrost.notify("GAME_ROUND_END", { ticketId: 101, won: true });

From the host page

import { Bifrost, type GameEvent } from "@low6/bifrost";

const iframe = document.querySelector<HTMLIFrameElement>("#game")!;
const bifrost = new Bifrost("https://game.example.com", {
  // Where to post host→game messages. Pass the iframe element directly —
  // Bifrost reads `.contentWindow` at send time, so it's safe to construct
  // before the iframe finishes loading.
  target: iframe,
  // When `host` is set, only that origin is accepted by default.
  // Set this explicitly to widen or narrow the allow-list.
  allowedOrigins: ["https://game.example.com"],
});

const cleanup = bifrost.listen((event: GameEvent) => {
  switch (event.subject) {
    case "GAME_READY":
      // notify symmetrically — same API as the game side
      bifrost.notify("GAME_PAYLOAD", { seed: 42 });
      break;
    case "GAME_ROUND_END":
      // event.message holds the round result
      break;
  }
});

// Update the target later (e.g. after a route change swaps the iframe):
bifrost.setTarget(newIframe);

// On teardown (React unmount, route change, etc.) — detach the listener:
cleanup();

notify() and listen() are symmetric — calling notify() on the host side posts into the iframe's contentWindow, and the game's bifrost.listen() receives it. The same API works in both directions; the difference is just which target each side is pointing at.

Persisting data across reloads (Muninn)

Safari periodically clears localStorage inside an iframe. Push the data to the host instead:

// In the game
bifrost.sendStore("score", "1250");       // ask the host to persist this
bifrost.store("score", "1250");           // (optional) also cache locally; returns boolean
const cached = bifrost.muninn.get("score"); // read whatever's in *this* side's storage
// In the host — once you've called bifrost.listen(...), incoming store envelopes
// are automatically written to the host's localStorage by Muninn. No extra wiring.
bifrost.listen((event) => { /* your existing game-event handler */ });

// Read what the game has saved
const score = bifrost.muninn.get("score");

// Or push state down to the iframe (e.g. on bifrost.notify("GAME_READY"))
bifrost.sendStore("score", "1250");

sendStore is symmetric — call it from either side and the peer's Bifrost listener writes the value into its own Muninn-backed storage. store writes locally only.

Forwarding errors from the game to the host

Pass captureErrors: true and Bifrost attaches global error and unhandledrejection listeners on its window. Each event is forwarded to the peer as a GAME_ERROR envelope with a structured GameErrorPayload body — useful from the game side to surface uncaught crashes to the host page (for Sentry routing, oncall pages, in-page diagnostic toasts, etc.).

// In the game (iframe)
import { Bifrost } from "@low6/bifrost";
const bifrost = new Bifrost("https://host.example.com", { captureErrors: true });
// In the host
import { Bifrost, GAME_ERROR_SUBJECT, type GameErrorPayload } from "@low6/bifrost";
const bifrost = new Bifrost("https://game.example.com", { target: iframe });
bifrost.listen((event) => {
  if (event.subject === GAME_ERROR_SUBJECT) {
    const err = event.message as GameErrorPayload;
    Sentry.captureMessage(`[game] ${err.name ?? err.type}: ${err.message}`, {
      extra: { stack: err.stack, filename: err.filename, line: err.lineno },
    });
  }
});

Toggle at runtime with bifrost.startErrorCapture() / bifrost.stopErrorCapture() — both are idempotent. Enabling on both sides creates a full-duplex error feed, which is rarely what you want — typically only the game side captures.

Security

Bifrost talks over postMessage, which is fundamentally broadcast — any window with a reference to yours can send your listener a message. The library applies two filters before reaching your callback:

  1. Origin allow-list. Inbound messages whose event.origin isn't in the allow-list are silently dropped. The allow-list defaults to [host] when you supply an explicit host, and to accept-any when host === "*" (the default — convenient for local dev, dangerous in production). Always pass an explicit host or an explicit allowedOrigins when your page hosts third-party iframes, browser extensions are common, or you handle sensitive data.

  2. Envelope shape check. Messages that aren't valid JSON or lack a subject field are ignored without logging — so the console isn't flooded with React DevTools / extension / third-party chatter.

For Muninn, inbound value payloads are also size-capped (default 1 MB, configurable via muninn.maxValueBytes) to defend against a misbehaving peer trying to fill your storage quota.

If you do nothing else, set an explicit host and an allowedOrigins list. React Native WebView messages can have an empty event.origin — include "" in your allow-list (or use a predicate) when targeting RN.

API

new Bifrost<E = string>(host?: string, options?: BifrostOptions)

host is the targetOrigin used when posting to the peer (default "*"). Prefer an explicit origin in production. E optionally narrows the event subject type.

type BifrostOptions = {
  muninn?: MuninnOptions;
  allowedOrigins?: string[] | ((origin: string) => boolean);
  target?: Window | HTMLIFrameElement | null;
  captureErrors?: boolean;
};
  • options.muninn is forwarded to the internal Muninn instance (all fields optional).
  • options.allowedOrigins:
    • Arrayevent.origin must exactly match an entry.
    • Function — predicate called with event.origin; return true to accept.
    • Omitted — defaults to [host] when an explicit host was supplied; accept-any when host === "*".
  • options.target — destination window for outbound notify()/sendStore(). Pass an HTMLIFrameElement to defer the contentWindow read until send time (so you can construct Bifrost before the iframe loads). If omitted, sends route via window.parent (iframe case) or the last-seen listen() peer (host fallback).
  • options.captureErrors — when true, attaches global error and unhandledrejection listeners and forwards each event to the peer as a GAME_ERROR envelope. Default false. See bifrost.startErrorCapture() for runtime toggling.

bifrost.listen(cb: (event: GameEvent) => void): () => void

Subscribes to inbound messages and returns a cleanup function. The cleanup removes only this specific subscriber; invoke it on teardown (React unmount, route change, etc.) to avoid stale callbacks.

One DOM listener, many subscribers. A Bifrost instance attaches at most one message event listener to the DOM — every listen() call registers its callback against the same internal listener, and Bifrost fans inbound events out to all subscribers. The DOM listener is attached lazily on the first listen() call and detached again automatically when the last subscriber's cleanup runs (the parse / origin / Muninn pipeline happens exactly once per message, not once per subscriber).

String payloads are JSON.parsed; objects are passed through. Muninn store envelopes are intercepted and applied to local storage before any subscriber runs. Messages from origins not in the allow-list, and messages without a subject field, are silently ignored. Subscribing the same function reference twice is a no-op (Set semantics).

The listener attaches to window by default. React Native hosts that still hit the legacy Android RNWebView quirk (incoming messages fire on document) can inject window.ReactNativeOS = "android" before constructing Bifrost to opt in to the document path — no user-agent sniffing.

bifrost.notify<T>(subject: E, message?: T | null): void

Posts { subject, message } to the peer. Routing: window.ReactNativeWebView.postMessage(JSON.stringify(...)) if running inside a React Native WebView; otherwise the destination from the resolution chain (targetwindow.parent if in an iframe → last-seen listen() peer). A silent no-op if nothing in the chain is usable.

bifrost.setTarget(target: Window | HTMLIFrameElement | null): void

Set or replace the destination window for notify() / sendStore(). Pass an HTMLIFrameElement for deferred .contentWindow resolution, or null to fall back to the default chain.

bifrost.startErrorCapture(): () => void

Attaches global error and unhandledrejection listeners to this window. Each event is forwarded to the peer as a GAME_ERROR envelope. Returns a cleanup function (also stored internally; equivalent to calling stopErrorCapture()). Idempotent — calling twice doesn't double-attach.

bifrost.stopErrorCapture(): void

Detach the error/rejection listeners. Idempotent.

GAME_ERROR_SUBJECT / type GameErrorPayload

The reserved subject and structured payload shape used by error capture:

type GameErrorPayload = {
  type: "error" | "rejection";
  message: string;
  name?: string;      // Error constructor name
  stack?: string;
  filename?: string;
  lineno?: number;
  colno?: number;
  timestamp: number;  // Date.now() at dispatch
};

bifrost.store(key: string, value: string): boolean

Persist value against key in this side's local Muninn storage. Synchronous. Returns true on success, false if the underlying Storage rejected the write (Safari Private Browsing, quota exceeded, etc.).

bifrost.sendStore(key: string, value: string): void

Ask the peer to persist key/value via their Muninn. The peer's bifrost.listen writes it on receipt. Use this when the local side's storage isn't reliable (Safari iframes) and the peer's is.

bifrost.muninn: Muninn

Direct access to the underlying Muninn instance — useful for get, remove, clear, keys.

type GameEvent<T = unknown, E = string>

type GameEvent<T = unknown, E = string> = {
  subject: E;
  message?: T;
};

Type the subject enum end-to-end: new Bifrost<MySubjects>() with GameEvent<Payload, MySubjects>.

new Muninn(host?: string, opts?: MuninnOptions)

Sync, namespaced wrapper around a Storage backend (default window.localStorage). Constructed automatically by Bifrost; you can also import and use it standalone via @low6/bifrost/muninn.

type MuninnOptions = {
  namespace?: string;                            // key prefix (default "muninn")
  storage?: Storage;                             // backing store (default window.localStorage)
  maxValueBytes?: number;                        // cap on inbound value length (default 1_000_000)
  target?: Window | HTMLIFrameElement | null;    // destination for sendSet; same chain as Bifrost
};

Methods:

  • set(key, value): boolean — returns whether the underlying Storage accepted the write.
  • get(key): string | null
  • remove(key): void
  • clear(): void — removes only entries under this namespace.
  • keys(): string[] — unprefixed keys under this namespace.
  • sendSet(key, value): void — fire-and-forget peer relay.
  • receive(envelope): boolean — intercept inbound; returns true if it was a Muninn envelope (handled or rejected).
  • setTarget(target): void — update the destination window post-construction.
  • resolveTarget(): Window | null — peek at the destination the next send would use.

All storage methods catch and console.warn on Safari Private Browsing / quota errors rather than throwing.

License

MIT