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

@urun-sh/react

v0.6.31

Published

React bindings for the urun TypeScript SDK

Readme

@urun-sh/react

React bindings for TypeScript clients that proxy apps deployed on urun from Python.

Install

npm install @urun-sh/react @urun-sh/core

Quick start

import '@urun-sh/react/styles.css'
import { UrunProvider, useApp, Session, Camera, Video } from '@urun-sh/react'
import { UrunWorkOSProvider } from '@urun-sh/react/workos'

function Restyle() {
  const app = useApp()
  const session = app.generate({ prompt: 'A sunset' })

  return (
    <Session session={session}>
      <Camera stream="cam" front /> {/* publish the webcam → the runtime's ctx.stream("cam") */}
      <Video stream="video" />      {/* render the runtime's video output */}
      <button onClick={() => session.doc('control').set({ desired: { prompt: { text: 'A forest' } } })}>
        Restyle
      </button>
    </Session>
  )
}

export default function App() {
  return (
    <UrunWorkOSProvider clientId="client_...">
      <UrunProvider
        baseUrl="https://urun.sh"
        orgId="your-org-id"
        authProvider="workos"
        appId="my-app"
      >
        <Restyle />
      </UrunProvider>
    </UrunWorkOSProvider>
  )
}

useApp() is render-safe with zero user-side memoization: app is the same proxy across re-renders, app.generate is the same function reference on every access, and repeated app.generate(args) calls during React re-renders return the same session reference for the same function and args (a new reference appears only after the session actually changed, i.e. it was disconnected/ended and the next call dials fresh). Treat app.<function>() like a hook: call it unconditionally during render and let it own the session lifecycle.

React Compiler note (default Next lint, react-hooks/purity): the compiler only freezes values that flow through hook-named calls. If nothing hook-shaped touches the session, an event handler in the same component that both writes through it (session.doc(...).set(...)) and calls an impure function (Date.now(), Math.random()) is conservatively flagged as render-scope. Keep it compiler-clean without memoization by reading the session through a hook you want anyway — e.g. const phase = useSessionPhase(session) for the wake/status UI — which freezes the session for the compiler. This contract is pinned by the fixtures in src/__tests__/react-compiler-lint.test.ts.

Drive the app by writing desired state into the control doc (doc('control').set({ desired: … })), not an imperative start/update/stop. The Python runtime reads cheap config inline from the live doc.desired.* props and uses doc.bind(construct, [live props]) (React useMemo) to reconstruct a stage on structural change — the single reconcile lane (no field-mapping). Streams are symmetric: session.stream(name) consumes the runtime's output, and .attach(track) makes the client the producer of that name. See the root README.md "How this mirrors the Python runtime DSL" and the urun-sdk-canonical-patterns skill.

Authentication

Browser clients authenticate with an organization ID plus a user/session JWT. During urun setup, the organization is provisioned with an auth provider configuration, such as a WorkOS client ID mapped to a JWKS URL.

When authProvider="workos", wrap UrunProvider in an auth bridge that supplies short-lived access tokens. For pure React WorkOS apps, import UrunWorkOSProvider from @urun-sh/react/workos. For Next.js WorkOS apps, import UrunWorkOSProvider from @urun-sh/react/next-workos.

For other auth providers, use UrunAuthProvider with a getAccessToken function, or UrunJwtProvider for a static JWT in tests. urun validates JWTs server-side against the JWKS configured for the organization. Do not pass server API keys or long-lived secrets to browser clients.

API

  • UrunProvider — configures the app proxy (and is the auth onramp).
  • useApp() — returns the deployed app proxy configured by appId.
  • app.<function>(args?) — starts or reconnects a running function call and returns a render-safe session proxy.
  • session.stream(name) — returns a render-safe named symmetric stream (first use decides direction) with .track, .attach(track), .detach(), .seek(seconds | 'live'), and lifecycle events.
  • session.doc(name) — returns a render-safe synced document with .get(), .set(patch) (write desired.* state), and lifecycle events.

Prebaked media components

The declarative send/show surface, keyed on the runtime's stream names. Bind one session for a whole subtree with <Session session={session}> (every component also takes an explicit session prop). They own capture, the persistent srcObject, iOS attributes, the autoplay unlock, and front/back switching — hand-rolled getUserMedia / .track → srcObject effects in app code are the anti-pattern.

  • Send: <Camera stream="cam" front|back visible /> (viewless unless visible), <Mic stream="mic" />.
  • Show: <Video stream="video" /> (live inbound video), <Image stream="image" /> (the platform image lane — H.264 intra frames), <Audio stream="audio" controls? />, <Voice stream="audio" /> (full-duplex voice loop on one name).
  • VOD/recordings playback (seek/scrub): VideoPlayer from @urun-sh/react/video — video.js is an optional peer, deliberately not in the root export.
  • The UrunAudio / UrunVoice / UrunCamera (root) and UrunVideo (/video subpath) aliases are deprecated and removed next minor — write the bare names.

Muxed A/V: consume an "av" parent through ONE element

A runtime that egresses ctx.stream("av", video_codec="h264", audio_codec="opus") puts two RTP tracks on the wire. Name the parent and <Video> puts both into ONE MediaStream on ONE element, so the browser's native A/V sync machinery owns the alignment:

const video = useRef<VideoHandle>(null)

<button onClick={() => { video.current?.unlock(); void start() }}>Start</button>
<Video ref={video} stream="av" muted={false} onUnlockChange={setSoundReady} />

This is the canonical single-element muxed playout path. Consuming the same pair as two elements (<Video stream="video"> + <Audio stream="audio">) gives the browser two independently-buffered playout paths with nothing aligning them: audio's jitter buffer accumulates standing delay on every delivery gap while video renders on arrival, and the drift is bounded only by RTCP sender reports that an SFU may re-mint per track. That is still supported — pass audioStream={false} on the video element so it never claims the voice leg — but one element is the shape to reach for.

Since an unmuted element is the audio sink, it needs the same user gesture an <Audio> does: call unlock() synchronously from the app's start-button handler and render a tap-to-enable affordance from onUnlockChange(false).

The SDK also caps every inbound audio receiver's jitter buffer at 300 ms (RTCRtpReceiver.jitterBufferTarget) so standing audio delay cannot accumulate past that bound. Retune it per provider with <UrunProvider audioPlayout={{ jitterBufferTargetMs: 500 }}>.

Hooks and building blocks

  • Session doc/data: useDocStore, useSessionDoc, useStreamMessages, useSessionTrack.
  • Continuous input (keyboard/mouse/pointer riding awareness presence): useInputPresence(session).
  • Request/response over the session doc: useRequest, useCompletion, useChat.
  • Session honesty: UrunSessionStatus, UrunSessionGate, UrunSessionClock, UrunSessionPaused, UrunIdleWarning, UrunSessionWaking, UrunActivationOverlay (+ useSessionPhase, useSessionEndsAt, useSessionIdle, useSessionWake, useActivation).
  • Session history: useSessionDirectory() — list the owner's sessions (paused ones included), end/archive/unarchive one. Attach stays on useApp(): app.fn(args, { sessionId }) resumes a named session, app.fn(args) starts a new one. Model: urun-python/docs/design/session-semantics.md.
  • Session debug workbench: UrunStreamTail, UrunDocPanel, UrunControlSender, UrunEventSpine (see docs/session-workbench-building-blocks.md).
  • Component registry: registerComponent() / ComponentRenderer plus the schema-validated built-ins (ProgressCard, StatusBadge, TextStream, ImageFrame, MetricsPanel) rendered from app-defined named data streams.

Session docs as a zustand store

Session docs (prompts, desired state, status) read like a plain zustand store. The doc — a vanilla Yjs document synced by the platform — stays the single source of truth: the store is a read-projection plus write-through, never an authoritative copy. Selectors give granular re-renders; set(patch) deep-merges through the doc's field-granular CRDT write path, so concurrent writers to different subkeys both survive.

import { useDocStore } from '@urun-sh/react'

type ControlDoc = {
  session?: { status?: string }
  desired?: { prompt?: { text?: string } }
}

function PromptControls({ session }) {
  const useControl = useDocStore<ControlDoc>(session, 'control')

  // Status field read — re-renders ONLY when the selected value changes.
  const status = useControl((s) => s.doc.session?.status ?? 'idle')

  // Desired-state write — write-through to the doc (field-granular merge).
  const set = useControl((s) => s.set)

  return (
    <div>
      <span>{status}</span>
      <button onClick={() => set({ desired: { prompt: { text: 'a sunset' } } })}>
        Set prompt
      </button>
    </div>
  )
}

Outside React (or with a session handle you own), bind a store directly to a doc:

import { createDocStore } from '@urun-sh/react'

const control = createDocStore<ControlDoc>(session.doc('control'))
control.getState().doc.session?.status
control.subscribe((s) => console.log(s.doc))
control.set({ desired: { prompt: { text: 'dawn' } } })
control.unbind() // detach the projection; the session still owns the doc

Notes:

  • Selectors that return objects/arrays should use zustand's useShallow (zustand/react/shallow) — each doc change produces a fresh snapshot tree.
  • Arrays in the doc are read as plain snapshots. Do not read-modify-write an array through set for append-only logs; use the platform's stream/text primitives for growing data.
  • useSessionDoc(session, key, selector?) also accepts a selector for one-off reads.

Streaming text (Text)

Point it at a named stream; it renders the text unstyled at whatever rate the wire delivers (1k+ tok/s), because the network path touches zero React state: chunks append to a plain buffer and a requestAnimationFrame loop drains it into one bare DOM text node via appendData — one DOM write per frame, no reconciliation, no parsing in the hot loop.

import { Text } from '@urun-sh/react'

<Text session={session} stream={`llm-resp:${requestId}`} onDone={setFinalText} />

A reasoning pane is just a second Text on the other stream — nothing special:

<Text session={session} stream={`llm-think:${requestId}`} />

Props: session, stream, onDone(text), onError(error), onMeter(meter), smooth (typewriter drain below smoothThreshold, bypassed above it), smoothCharsPerFrame, smoothThreshold, className, style.

Styling is the caller's — the container is an unstyled <span> and the content is raw text. Apply markdown/code highlighting at block boundaries or on completion, never per token.

Headless: useText / useTextMeter

const { ref, meterRef, getText } = useText({ session, stream: `llm-resp:${id}` })
const { chars, tokens, tokensPerSecond } = useTextMeter(meterRef, 250)

return <pre ref={ref} />

useText holds no React state, so a component using it renders once on mount regardless of chunk count. Counters are computed in the RAF loop and written into meterRef in place; useTextMeter samples them on an interval so a tok/s readout costs O(interval) renders, never O(chunks).

Styling

Import the package CSS when using built-in components:

import '@urun-sh/react/styles.css'

Peer dependencies

@urun-sh/react supports React 18 and 19 and expects @urun-sh/core of the same release line. WorkOS React and Next.js integrations are optional peers used only by @urun-sh/react/workos and @urun-sh/react/next-workos; video.js is an optional peer used only by @urun-sh/react/video.