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

@reactor-models/happy-oyster

v1.0.0

Published

Strongly-typed SDK for the HappyOyster model on Reactor

Readme

@reactor-models/happy-oyster

Typed JavaScript + React SDK for the HappyOyster model on Reactor. Tracks model release v3.0.1, and requires @reactor-team/js-sdk 3.x.


Install

npm install @reactor-models/happy-oyster
pnpm add @reactor-models/happy-oyster

The Reactor session this SDK runs on is @reactor-team/js-sdk 3.x, which the package depends on directly and installs for you. Import it yourself only if your app also drives Reactor at that level; pin the same major if you do.

The package exports a plain-JavaScript client and a set of React bindings. Import whichever you need from @reactor-models/happy-oyster:

import { HappyOysterModel } from "@reactor-models/happy-oyster";
import {
  HappyOysterProvider,
  useHappyOyster,
} from "@reactor-models/happy-oyster";

React 18 or later is required when using the provider and hooks. The token-loading examples below use React 19's use(); on React 18, fetch the JWT in a useEffect and pass it to the provider once it resolves.

HappyOyster comes in two experiences, chosen with the mode option and fixed for the life of the session:


Authenticate

Reactor uses short-lived JWTs for session auth. You hold your API key on your server, mint a token on demand, and the client never sees the raw key. Tokens are valid for 6 hours — if one leaks, it expires on its own.

Mint a JWT with POST https://api.reactor.inc/tokens and the Reactor-API-Key header; the response JSON is { "jwt": "..." }.

JavaScript (Next.js route handler)

// app/api/reactor/token/route.ts
import { NextResponse } from "next/server";

export async function POST() {
  const res = await fetch("https://api.reactor.inc/tokens", {
    method: "POST",
    headers: { "Reactor-API-Key": process.env.REACTOR_API_KEY! },
  });
  const { jwt } = await res.json();
  return NextResponse.json({ jwt });
}

React (provider)

Call the /api/reactor/token route above from a client component and pass the result to the provider:

"use client";

import { use } from "react";
import {
  HappyOysterProvider,
  HappyOysterVideo,
} from "@reactor-models/happy-oyster";

async function getToken() {
  const r = await fetch("/api/reactor/token", { method: "POST" });
  const { jwt } = await r.json();
  return jwt;
}

const tokenPromise = getToken();

export default function App() {
  const token = use(tokenPromise);
  return (
    <HappyOysterProvider mode="adventure" jwt={token} autoConnect>
      <HappyOysterVideo className="w-full aspect-video" />
    </HappyOysterProvider>
  );
}

Connect

Opening a session connects to the experience you picked with mode. Connecting only opens the session — create or attach a world next, then start streaming.

JavaScript

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure", videoElement });
await happyOyster.connect(jwt);

React

The provider takes the mode and the JWT as props; fetch the token from the same /api/reactor/token route the Authenticate example mints:

"use client";

import { use } from "react";
import {
  HappyOysterProvider,
  useHappyOyster,
} from "@reactor-models/happy-oyster";

async function getToken() {
  const r = await fetch("/api/reactor/token", { method: "POST" });
  const { jwt } = await r.json();
  return jwt;
}

const tokenPromise = getToken();

function Controller() {
  const { phase } = useHappyOyster();
  return <span>Status: {phase}</span>;
}

export default function App() {
  const token = use(tokenPromise);
  return (
    <HappyOysterProvider mode="adventure" jwt={token}>
      <Controller />
    </HappyOysterProvider>
  );
}

Attach to an existing session

By default connect() opens a fresh session. To adopt a session your backend already created — for example one handed out by an admission queue — pass its sessionId (and, if the backend pre-registered one, its connectionId) through connectOptions. connect() then attaches to that session instead of calling POST /sessions.

Pair it with a jwt resolver: the SDK calls it before every authenticated Reactor API request, so a short-lived token is never presented after it has aged out. The resolver must return a token that is valid for this session — a session-scoped Reactor JWT may only operate the sessions it created, so a backend handing out an attachable session must hand out the token bound to it (or mint a replacement bound to the same session id). A resolver that returns a freshly minted, unbound token will 403 the first upload or clip call.

"use client";
import { HappyOysterProvider } from "@reactor-models/happy-oyster";

export default function App({
  sessionId,
  connectionId,
  getToken,
}: {
  sessionId: string;
  connectionId?: number;
  getToken: () => Promise<string>;
}) {
  return (
    <HappyOysterProvider
      mode="adventure"
      jwt={getToken}
      connectOptions={{ sessionId, connectionId }}
      autoConnect
    >
      {/* … */}
    </HappyOysterProvider>
  );
}

The plain-JS client takes the same options as the second argument to connect():

await happyOyster.connect(getToken, { sessionId, connectionId });

Worlds

A world is a permanent asset on your account. A session has one current world at a time; create a new one or reopen an existing one, then startTravel to stream it.

createWorld

Create a new world for this session's experience and make it the current one. Resolves once the world is ready to enter. The parameters depend on the mode the session was opened with.

Save encrypted_world_id from the returned snapshot — it is the only way to reopen this world in a later session with attachWorld.

| Parameter | Type | Required | Description | | -------------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | prompt | string | ✅ | Natural-language description of the world to create. Up to 2000 characters. | | firstFrameImageUrl | string | | Publicly reachable image URL to use as the world's opening frame. Must be landscape with a width/height ratio between 1.5 and 2.0. Mutually exclusive with firstFrameImage. | | firstFrameImage | File \| Blob | | Local image to use as the world's opening frame, for images without a public URL. At most 2 MB, landscape with a width/height ratio between 1.5 and 2.0. Mutually exclusive with firstFrameImageUrl. | | perspective | "first_person" \| "third_person" | | Adventure only. Camera perspective. Defaults to "third_person". | | resolution | "480p" \| "720p" | | Directing only. Video resolution. Defaults to "720p". | | layout | "Stable" \| "Fast" | | Directing only. Camera movement style. | | narrative | "Calm" \| "Dramatic" \| "Normal" | | Directing only. Narrative style. |

JavaScript

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure" });
await happyOyster.connect(jwt);

const world = await happyOyster.createWorld({
  prompt: "A misty forest at dawn",
});
// Save world.encrypted_world_id to reopen this world later.

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { createWorld } = useHappyOyster();

  return (
    <button onClick={() => createWorld({ prompt: "A misty forest at dawn" })}>
      createWorld
    </button>
  );
}

attachWorld

Reopen a world you created earlier by its encrypted_world_id and make it the current one. Resolves once the world is ready to enter (immediately for one that is already built). The world must belong to the same experience the session was opened with.

JavaScript

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure" });
await happyOyster.connect(jwt);

const world = await happyOyster.attachWorld(savedWorldId);

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example({ savedWorldId }: { savedWorldId: string }) {
  const { attachWorld } = useHappyOyster();

  return <button onClick={() => attachWorld(savedWorldId)}>attachWorld</button>;
}

Travel

Traveling is the live session: the current world streams into your <video> element and accepts controls. While traveling, the world API is locked — end the travel before switching worlds.

startTravel

Enter the current world and begin streaming its live video. Call once a world is ready (createWorld or attachWorld has resolved). A <video> element must be attached first — pass videoElement to the constructor, call attachVideo(), or mount <HappyOysterVideo />.

JavaScript

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure", videoElement });
await happyOyster.connect(jwt);
await happyOyster.createWorld({ prompt: "A misty forest at dawn" });

await happyOyster.startTravel();

React

"use client";
import { useHappyOyster, HappyOysterVideo } from "@reactor-models/happy-oyster";

function Example() {
  const { startTravel } = useHappyOyster();

  return (
    <>
      <HappyOysterVideo className="w-full aspect-video" />
      <button onClick={() => startTravel()}>startTravel</button>
    </>
  );
}

Session length

HappyOyster ends a travel itself when its time runs out. Adventure travels may run for up to 2 minutes, and startTravel() always asks for that ceiling — a travel that does not ask for one gets 1 minute. Directing travels are not capped this way; their length comes from the experience.

Drive your countdown from maxExperienceTimeSec, the budget HappyOyster granted the live travel, rather than a hardcoded number. It is null while nothing is streaming and for Directing travels; ADVENTURE_MAX_EXPERIENCE_SEC is what to size the clock with before a travel opens.

JavaScript

const { session } = await happyOyster.startTravel();
console.log(`this travel gets ${session?.maxExperienceTimeSec} seconds`);

// The same value, readable at any point during the travel:
happyOyster.maxExperienceTimeSec;

React

"use client";
import { ADVENTURE_MAX_EXPERIENCE_SEC } from "@reactor-models/happy-oyster";
import { useHappyOyster } from "@reactor-models/happy-oyster/react";

function TravelClock() {
  const { streaming, maxExperienceTimeSec } = useHappyOyster();
  const budget = maxExperienceTimeSec ?? ADVENTURE_MAX_EXPERIENCE_SEC;

  return <span>{streaming ? `${budget}s of travel` : "not traveling"}</span>;
}

disconnect

End the session: stop the live stream and close the connection. Worlds are permanent — the current world stays on your account and can be reopened later with attachWorld.

JavaScript

await happyOyster.disconnect();

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { disconnect } = useHappyOyster();

  return <button onClick={() => disconnect()}>disconnect</button>;
}

Adventure controls

Available while traveling an Adventure world. Controls are held: a direction keeps applying until you release it or call stop, and axes compose (move Front while you look Mouse_Left).

move

Hold a movement direction until it is released.

| Parameter | Type | Required | Description | | ----------- | -------------------------------------------------------------------------------------------------------- | -------- | ------------------------- | | direction | "Front" \| "Back" \| "Left" \| "Right" \| "Front_Left" \| "Front_Right" \| "Back_Left" \| "Back_Right" | ✅ | The direction to move in. |

JavaScript

await happyOyster.move("Front");

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { move } = useHappyOyster();

  return <button onClick={() => move("Front")}>move</button>;
}

look

Hold a view rotation until it is released.

| Parameter | Type | Required | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------- | | direction | "Mouse_Up" \| "Mouse_Down" \| "Mouse_Left" \| "Mouse_Right" \| "Mouse_Up_Left" \| "Mouse_Up_Right" \| "Mouse_Down_Left" \| "Mouse_Down_Right" | ✅ | The direction to turn the view. |

JavaScript

await happyOyster.look("Mouse_Left");

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { look } = useHappyOyster();

  return <button onClick={() => look("Mouse_Left")}>look</button>;
}

interact

Hold an interaction until it is released. The built-in verbs are Jump, Attack, Crouch, and Sprint; a world may advertise its own verbs, and any verb string is accepted.

| Parameter | Type | Required | Description | | --------- | ------------------------------------------------------ | -------- | --------------------------- | | action | "Jump" \| "Attack" \| "Crouch" \| "Sprint" \| string | ✅ | The interaction to perform. |

JavaScript

await happyOyster.interact("Jump");

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { interact } = useHappyOyster();

  return <button onClick={() => interact("Jump")}>interact</button>;
}

stop

Release every held control back to neutral. To release a single axis instead, call release({ translation: true }), release({ rotation: true }), or release({ interaction: true }).

JavaScript

await happyOyster.stop();

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { stop } = useHappyOyster();

  return <button onClick={() => stop()}>stop</button>;
}

Directing controls

Available while traveling a Directing world.

instruct

Steer the unfolding story with a text instruction. Resolves with whether the instruction was accepted; accepted instructions appear on the timeline in travelState.

| Parameter | Type | Required | Description | | --------- | -------- | -------- | -------------------------------------- | | content | string | ✅ | The instruction to apply to the story. |

JavaScript

await happyOyster.instruct("A storm rolls in");

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { instruct } = useHappyOyster();

  return <button onClick={() => instruct("A storm rolls in")}>instruct</button>;
}

pause

Pause the story. Resume it with resume, or step back in time with rewind.

JavaScript

await happyOyster.pause();

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { pause } = useHappyOyster();

  return <button onClick={() => pause()}>pause</button>;
}

resume

Resume a paused story.

JavaScript

await happyOyster.resume();

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { resume } = useHappyOyster();

  return <button onClick={() => resume()}>resume</button>;
}

rewind

Rewind a paused story to an earlier moment. Resolves with the second playback actually resumed at; playback continues automatically.

| Parameter | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------------------- | | rewindToSec | number | ✅ | The point to rewind to, in seconds. Rounded down to a multiple of 4. |

JavaScript

const { resumedAtSec } = await happyOyster.rewind(8);

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { rewind } = useHappyOyster();

  return <button onClick={() => rewind(8)}>rewind</button>;
}

State

The model owns all world state and broadcasts an authoritative snapshot on every change. Mirror it — treat these snapshots as the single source of truth for your UI, rather than tracking each command's outcome yourself.

worldState

The current world's snapshot: its lifecycle phase and details. In React it is reactive on useHappyOyster(); in plain JS, read model.worldState or subscribe with model.onWorldState(...).

| Field | Type | Description | | -------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | phase | "no_world" \| "creating" \| "building" \| "ready" \| "traveling" \| "failed" | Where the current world is in its lifecycle. | | encrypted_world_id | string \| null | The id to save and reopen this world later with attachWorld. | | prompt | string \| null | The prompt the world was created from. | | first_frame | string \| null | URL of the world's opening frame. | | mode | number \| null | The experience this world belongs to (1 Adventure, 2 Directing). |

JavaScript

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure" });
happyOyster.onWorldState((state) => {
  console.log("world_state", state.phase, state.encrypted_world_id);
});
await happyOyster.connect(jwt);

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { worldState } = useHappyOyster();
  return <span>Phase: {worldState?.phase}</span>;
}

travelState

The live travel's snapshot, updated as the world plays. Directing worlds populate the instruction timeline and chapters; Adventure worlds populate the available action verbs. null while not traveling.

| Field | Type | Description | | --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | status | "init" \| "pending" \| "running" \| "failed" \| "completed" | The travel's lifecycle status. | | user_instructions | TravelInstruction[] | Directing: the instructions applied so far, each with its scheduled window on the timeline. | | chapters | TravelChapter[] | Directing: auto-detected chapters of the story. | | character_actions | string[] | Adventure: interaction verbs this world advertises for interact. | | environment_actions | string[] | Adventure: environment verbs this world advertises for interact. |

JavaScript

happyOyster.onTravelState((state) => {
  console.log("travel_state", state.status, state.chapters);
});

React

"use client";
import { useHappyOyster } from "@reactor-models/happy-oyster";

function Example() {
  const { travelState } = useHappyOyster();
  return <span>Travel: {travelState?.status ?? "idle"}</span>;
}

Video

The live world renders into a <video> element you provide.

JavaScript

Pass the element to the constructor, or attach it later with attachVideo(), before calling startTravel:

import { HappyOysterModel } from "@reactor-models/happy-oyster";

const happyOyster = new HappyOysterModel({ mode: "adventure" });
happyOyster.attachVideo(videoElement);
await happyOyster.connect(jwt);

React

Mount <HappyOysterVideo /> anywhere under the provider; it wires itself up automatically. <HappyOysterStateVideo /> renders the model's status frame for an optional at-a-glance overlay.

"use client";
import { HappyOysterVideo } from "@reactor-models/happy-oyster";

export function Example() {
  return <HappyOysterVideo className="w-full aspect-video" />;
}

Upgrading to 1.0.0

1.0.0 moves the SDK onto @reactor-team/js-sdk 3.x. Most apps use the HappyOysterModel facade — connect, createWorld, attachWorld, startTravel, the controls, worldState, travelState — and that surface is unchanged, so those apps upgrade by bumping the version.

Three things changed for code that drives the low-level layer (HappyOysterBase, useHappyOysterBase) directly.

A command's answer belongs to the call. The model answers get_credentials, end_travel and get_state with a message correlated to the command that asked, addressed to that one connection — and those three methods now resolve with it.

The answer still reaches that connection's message event as well, so a subscription for it is not dead. It is unusable, which is why onTravelCredentials / onTravelEnded and the useHappyOysterBaseTravelCredentials / useHappyOysterBaseTravelEnded hooks are removed: a subscription cannot say which getCredentials() call an answer belongs to, and it never fires at all for a second client in the same session. The awaited call settles both questions. Read the answer instead:

// before
base.onTravelCredentials((credentials) => open(credentials));
await base.getCredentials();

// after
const credentials = await base.getCredentials();
if (credentials) open(credentials);

requestState() is the exception worth knowing: its snapshot answers the call and is published to onWorldState subscribers, because a snapshot is authoritative however it travelled. Asking for state still refreshes every mirror.

Broadcasts are unchanged. onWorldState, onTravelState and onActionError carry what the model sends to the whole session — every snapshot, and every refusal — exactly as before. useHappyOysterBaseTravelState is new, and rounds out the set.

sessionExpiration is gone from useHappyOysterBase(). It never carried a value in the 2.x SDK either; it was dead surface, so nothing replaces it.