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

@alivelabs/mobile-react-client

v0.3.0

Published

React client for Alive Mobile: REST client, polling hooks, streaming logs, and a live, drivable simulator or emulator.

Readme

@alivelabs/mobile-react-client

React client for Alive Mobile's two-stage build/simulation API. Watch a build to completion, then render the live simulation: streaming logs, the live iOS simulator (H.264 decoded with WebCodecs onto a <canvas>), status, and interaction (click to tap, drag to swipe, type to type).

No orchestration lives here: this package talks to the API and renders. Status, logs and capacity arrive over the API's event stream rather than a poll; see Live updates.

Breaking change: Run is now Simulation

The second stage was called a run; it is now a simulation: booting a built artifact on a native iOS simulator. Everywhere else in the system a run is an execution inside a Tart VM, which is exactly what this stage is not. Every export below is a mechanical rename with no behaviour change, except SimulatorScreen, which is now SimulatorCanvas.

| Was | Now | |---|---| | Run | Simulation | | RunList | SimulationList | | RunStatus | SimulationStatus | | RunStream | SimulationStream | | CreateRunBody | CreateSimulationBody | | ListRunsQuery | ListSimulationsQuery | | useRun | useSimulation; its result field run is now simulation | | useRuns | useSimulations; its result field runs is now simulations | | UseRunResult | UseSimulationResult | | UseRunsResult | UseSimulationsResult | | RunScreen / RunScreenProps | SimulationScreen / SimulationScreenProps | | SimulatorScreen / SimulatorScreenProps | SimulatorCanvas / SimulatorCanvasProps | | client.createRun | client.createSimulation | | client.getRun | client.getSimulation | | client.listRuns | client.listSimulations | | client.getRunLogs | client.getSimulationLogs | | client.cancelRun | client.cancelSimulation | | client.setRunOrientation | client.setSimulationOrientation |

The wire follows: the API is now /api/simulations/…, the id field is simulationId (prefix sim_), and the error codes are SIMULATION_NOT_FOUND / SIMULATION_NOT_STREAMING. Status values are unchanged: queued, dispatched, booting, streaming, ended, failed, canceled. There are no deprecated aliases; the old names are gone.

Install

bun add @alivelabs/mobile-react-client   # peers: react, react-dom (>=18)

Quick start

import {
  OrchestratorClient, SimulationScreen, LogConsole, StatusBadge, useBuild, useSimulation,
} from "@alivelabs/mobile-react-client";

const client = new OrchestratorClient({
  baseUrl: "https://your-orchestrator.example.com",
  token: import.meta.env.VITE_API_TOKEN,
});

function Pipeline({ buildId, simulationId }: { buildId: string; simulationId: string | null }) {
  const { build, logs: buildLogs } = useBuild(client, buildId);
  const { simulation, logs: simulationLogs, stream } = useSimulation(client, simulationId);

  return (
    <>
      <StatusBadge status={build?.status ?? null} />
      <LogConsole logs={buildLogs} />
      {simulation?.status === "streaming" && stream && (
        <SimulationScreen
          stream={stream}
          onRotate={(orientation) =>
            client.setSimulationOrientation(simulation.simulationId, orientation)
          }
        />
      )}
      <LogConsole logs={simulationLogs} />
    </>
  );
}

SimulationScreen owns the WebSocket to the simulation's stream, the WebCodecs decode, and the input wiring. Give it stream (from useSimulation().stream or Simulation.stream) and it renders a drivable simulator. Create the build/simulation with the client (createBuild / createSimulation) and pass the ids in; the hooks keep the rest current. Rotation is the one control it cannot do alone: it travels REST rather than the stream socket, so pass onRotate (omit it and the rotate buttons are hidden).

WebCodecs needs a modern browser (Chrome/Edge, Safari 16.4+, recent Firefox) and a secure context (HTTPS or localhost). For full control, drive your own canvas with SimulatorCanvas + SimulatorDecoder.

Live updates

The hooks read the API's SSE event stream (GET /api/events) instead of polling it once a second. No hook signature changed: useBuild(client, id) still returns { build, logs, error } and still keeps itself current. What changed is the cost. Over one 133-second build, the old client would have issued 266 requests (status and logs at 1 Hz); the new one held a single connection carrying 3 job events and 24 log frames (4,987 log lines), plus 8 safety-net reads.

Three things matter if you build on this:

  • Polling is the fallback, not the design. A client that cannot hold a stream falls back to 2 s for a job page and 5 s for a list. The log cursor is shared between both paths, so a stream that drops mid-build carries on from where it stopped instead of rewinding to line one, and recovering to the stream resumes from the same number.
  • A resync runs even while the stream is healthy, about every 30 s, at the cadence the server's hello frame asks for. It is the backstop for an event that was never emitted, which is the one failure this design can have, and it is why a hook cannot sit permanently on a stale status.
  • Hooks that need no log subscription share one connection per client. An overview with three lists and worker health holds one socket rather than four, which matters because browsers cap HTTP/1.1 at six per origin. The sharing key is the OrchestratorClient instance, so memoise it rather than constructing one per render.

client.openEvents(handlers) is that stream unwrapped, for code that is not a hook:

const handle = client.openEvents({
  logOwner: "build",                       // with logId: subscribe to one job's logs
  logId: buildId,
  onJob: (e) => { if (e.kind === "build") setBuild(e.entity); },   // whole entity
  onLog: (e) => append(e.logs),            // { logs, nextSince, ownerType, ownerId }
  onWorkers: (list) => setWorkers(list.workers),
  onStatus: (s) => setStatus(s),           // "connecting" | "open" | "degraded"
  onFatal: (err) => setError(err),         // refused outright (401/403/404)
});
handle.close();                            // or its retry loop outlives the caller

StreamStatus, EventStreamHandlers and ClientEventStreamOptions are exported for it. Job and worker events reach every stream, so filter onJob by id. A dropped connection is retried with jittered backoff and is not an error; onFatal fires only for a response the server refused, which is a configuration problem rather than a blip.

Theming

The components style themselves with inline CSSProperties and ship no stylesheet, so there is nothing to import and nothing to override with a selector. They are still themeable: every colour is written var(--alive-x, <default>), where the default is the dark value they have always used.

Define nothing and nothing changes. Define these on :root (or on any ancestor of the components) and they follow, including under prefers-color-scheme since a custom property can be redefined in a media query where an inline style cannot.

| Variable | What it colours | |---|---| | --alive-bg · --alive-surface | the console body, the controls panel | | --alive-border · --alive-border-strong · --alive-border-focus | panel edges, the text input, the focus ring | | --alive-fg · --alive-fg-muted · --alive-fg-dim | body text, headings, timestamps | | --alive-accent · --alive-ok | the primary button, the "live" dot | | --alive-danger-surface · --alive-danger-line · --alive-danger-fg | the error banner and the rotate error | | --alive-log-{stdout,stderr,system} and each -tag | a log line's message, and its brighter level tag | | --alive-pill-idle-bg · --alive-pill-idle-fg | StatusBadge in its two colourless states |

Two things are deliberately not themeable. SimulatorCanvas draws a phone: its bezel gradient and the black behind the video stay dark, because a phone is a dark object on any desk and a white one reads as a rendering fault rather than as a light theme. And the nine saturated status pills keep their own colours, since white on a solid red or green is legible against either background.

Exports

| Export | Kind | Description | |---|---|---| | OrchestratorClient | Class | REST client for the API (below). | | OrchestratorError | Class | Thrown by every client method on a non-2xx: status, code, message, details. | | useBuild | Hook | (client, buildId \| null){ build, logs, error }; a build + its logs, live, until terminal. | | useSimulation | Hook | (client, simulationId \| null){ simulation, logs, stream, error }; a simulation + its logs, live, surfacing stream once streaming. | | useBuilds | Hook | (client, query?){ builds, nextCursor, error }; a page of builds, kept current, forever. | | useSimulations | Hook | (client, query?){ simulations, nextCursor, error }. | | useGhaJobs | Hook | (client, query?){ jobs, nextCursor, error }. | | useWorkers | Hook | (client){ workers, error }; worker health and slot usage. | | SimulationScreen | Component | All-in-one live simulation: connects to stream, decodes video, renders a drivable canvas. | | SimulatorCanvas | Component | Lower-level canvas renderer (click = tap, drag = swipe); used by SimulationScreen. | | SimulatorDecoder | Class | WebCodecs H.264/AVCC decoder painting to a <canvas> (advanced). | | LogConsole | Component | Scrollable, color-coded, auto-scrolling log viewer. | | StatusBadge | Component | Colored pill for any PipelineStatus: build, simulation, or GitHub Actions job. |

The list hooks never stop: a list has no terminal state, since new jobs keep arriving. A matching job event invalidates the page and schedules a coalesced refetch, so a burst of jobs changing at once is one request rather than one each, and an idle system makes none. A failed read sets error and keeps the last good page on screen.

<SimulationScreen> props (SimulationScreenProps)

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | stream | SimulationStream | yes | n/a | The simulation's direct-stream descriptor (Simulation.stream / useSimulation().stream). | | onRotate | (o: Orientation) => void \| Promise<void> | no | n/a | Rotate the device; omit and the rotate controls are not rendered. | | orientation | Orientation | no | n/a | The server's truth for where the device is turned; without it a rotated device paints sideways on reload. | | fps | number | no | 10 | Target frame rate. | | bitrate | number | no | 4_000_000 | Target bitrate in bits/sec. | | maxHeight | number \| string | no | "80vh" | Cap on the rendered screen height. Ignored by chrome="compact", which sizes itself. | | layout | "stacked" \| "split" | no | "stacked" | Controls under the screen, or beside it. Ignored by chrome="compact". | | chrome | boolean \| "compact" | no | true | true: full controls panel. false: bare stream. "compact": fill a fixed-height container with the picture plus a one-row toolbar (status, Home, App Switcher, rotate arrows, type-to-send) and a "More" popover holding the preset groups and orientation grid. The picture is sized from the container and the stream's live aspect ratio — nothing scrolls, nothing is clipped. Give the container a definite height. | | className | string | no | n/a | CSS class on the root element. |

<LogConsole> props (LogConsoleProps)

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | logs | Log[] | yes | n/a | Log entries ({ level, message, timestamp }). | | title | string | no | "Logs" | Heading text. | | maxHeight | string \| number | no | 400 | Max-height of the scroll area. |

<StatusBadge> props (StatusBadgeProps)

| Prop | Type | Required | Description | |---|---|---|---| | status | PipelineStatus \| null | yes | Build, simulation, or GHA job status to display. |

OrchestratorClient

const client = new OrchestratorClient({ baseUrl, token });

// Build stage
client.createBuild({ source: { type: "git", repoUrl, branch }, platform, appRoot, env });
client.uploadBuildSource(buildId, gzipBytes);          // for { type: "tarball" } sources
client.getBuild(buildId);                              // → Build
client.listBuilds({ limit, cursor, status });          // → BuildList { items, nextCursor }
client.getBuildLogs(buildId, since);                   // → LogPage { logs, nextSince }
client.cancelBuild(buildId);

// Simulation stage
client.createSimulation({ buildId /* or artifactId */, simulatorName, osVersion, devServerUrl });
client.getSimulation(simulationId);                    // → Simulation (with `stream` once streaming)
client.listSimulations({ limit, cursor, status });     // → SimulationList
client.getSimulationLogs(simulationId, since);         // → LogPage
client.cancelSimulation(simulationId);

// Driving a simulation over REST (a streaming viewer should use its own socket instead)
client.setSimulationOrientation(simulationId, "landscape-left");
client.sendInput(simulationId, { type: "tap", x, y, width, height });
client.screenshot(simulationId, { scale, quality });   // → Blob
client.describeUi(simulationId, { x, y });             // → { tree }

// Overview
client.listGhaJobs({ limit, cursor, status });         // → GhaJobList
client.getWorkers();                                   // → { workers }
client.getDeviceCatalog();                             // → DeviceCatalog: what the simulator host can boot

// Live events (see above): returns a handle you must close
client.openEvents({ logOwner, logId, since, onJob, onLog, onWorkers, onStatus, onFatal });

Pass getBuildLogs/getSimulationLogs the previous response's nextSince to fetch only newer lines, and carry it forward even when a page came back empty. Dropping it and the next read rewinds to the start of the log. The id on a log event is the same cursor, so the two ways of reading logs interoperate exactly.

Every method throws OrchestratorError on failure, carrying the API's own code so a caller can branch on it instead of matching an English sentence.

The wire types (Build, Simulation, SimulationStream, Log, DeviceInput, Orientation, …) are re-exported from @alivelabs/mobile-schemas.