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

@rumbamove/sdk

v0.2.1

Published

The Rumba SDK — upload, transcode, publish and play, from a server or from an app

Readme

@rumbamove/sdk

The client for Rumba — video ingest, transcoding, signed distribution and a shorts feed.

bun add @rumbamove/sdk    # or npm / pnpm / yarn

Which client you construct is which credential you hold

Rumba issues two kinds of API key, and the SDK builds a different client for each. That split is the whole design: the client you can ship in an app bundle has no write surface on it at all — not hidden, not guarded, absent — so putting a transcode call in a React Native screen fails to compile rather than failing code review.

| Constructor | Key | Reaches | |---|---|---| | createServerClient | rmb_sk_… | Everything: videos, uploads, clips, audio, namespaces, keys, jobs | | createPublishableClient | rmb_pk_… | The feed, view events and share links | | createPublicClient | none | The open feed and share links |

Prefixes are checked at construction, before a single request is made. A secret key handed to createPublishableClient throws immediately — by the time a call could fail, that key would already be in a bundle someone downloaded.

Server side

import { createServerClient } from '@rumbamove/sdk'

const rumba = createServerClient({
  baseUrl: 'https://video.example.com',
  secretKey: process.env.RUMBA_SECRET_KEY!,
})

const { video, upload } = await rumba.videos.create({
  namespace: 'kinshasa-cinema',
  title: 'Mbongo Tea',
  visibility: 'public',
  reference: 'film_8812',        // your own id — the feed hands it back
})

await rumba.videos.upload(video.id, Bun.file('./mbongo.mp4').stream())

Progress, and the honest kind

await rumba.videos.upload(video.id, file, {
  onProgress: ({ ratio }) => bar.set(ratio),
})

This counts bytes handed to the transport. On a fast local network that is close enough; over a phone connection the socket buffer runs several megabytes ahead of what has arrived, so the bar finishes early and then waits.

For anything where that matters — or anything big enough that a dropped connection hurts — use the resumable path, which reports what the server has actually acknowledged:

await rumba.videos.uploadResumable(video.id, file, {
  chunkSize: 8 * 1024 * 1024,
  onProgress: ({ bytes, total }) => bar.set(bytes / total!),
})

Call it again with the same arguments after a failure. It asks the server where it is before sending anything, so resuming is not a second code path you have to write — and an offset mismatch mid-flight is recovered from rather than thrown.

The body must be sliceable (a Blob, a File, a typed array). A stream is not resumable in any useful sense: recovering means re-reading bytes already consumed, which is the one thing a stream cannot do.

uploadStatus and uploadChunk are there if you want to drive the tus loop yourself.

Waiting for the transcode

const ready = await rumba.videos.waitUntilReady(video.id, {
  onProgress: ({ stage, progress }) => console.log(stage, `${progress}%`),
})

Polling, for scripts and CI. A failed transcode throws VIDEO::FAILED with the pipeline's own message; one that simply ran long throws VIDEO::TIMEOUT — worth telling apart, because the second is usually a queue backlog rather than a bad file. For anything server-side and long-lived, take the webhook instead.

Pagination

for await (const video of rumba.videos.paginate())
  …

In an app

import { createPublishableClient } from '@rumbamove/sdk'

const rumba = createPublishableClient({
  baseUrl: 'https://video.example.com',
  publishableKey: 'rmb_pk_…',
})

const { results, prefetch, expiresAt } = await rumba.feed.query({
  namespaces: openStores,       // `[]` means none — not "everybody"
  limit: 10,
  candidates: 40,               // over-fetch, re-rank locally, drop the rest
  rank: 'trending',
})

await rumba.feed.recordViews(watched)

Every URL in a feed response expires together at expiresAt. Refetch the page rather than trying to re-mint anything.

prefetch is advice, not policy — apply it to your final list, after re-ranking and dropping, or you spend the viewer's data pulling items you are about to discard.

Webhooks

Verify before you trust. Both ways of getting this wrong are silent — comparing signatures with === leaks where the first byte differs, and skipping the timestamp check leaves every delivery replayable forever — and neither fails a test, because the happy path passes either way.

import { verifyWebhook } from '@rumbamove/sdk'

const raw = await request.text()          // the raw body, byte for byte

const event = await verifyWebhook({
  secret: process.env.RUMBA_WEBHOOK_SECRET!,   // from namespaces.create, once
  body: raw,
  signature: request.headers.get('x-rumba-signature') ?? '',
})

if (event.type === 'video.completed') …

Parse after verifying, never before: the signature covers the exact bytes that were sent, and JSON.parse followed by JSON.stringify reorders keys.

Throws WEBHOOK::SIGNATURE_MALFORMED, WEBHOOK::TIMESTAMP_STALE or WEBHOOK::SIGNATURE_INVALID, so a handler can answer 400 and say which.

Errors

Failures arrive as RumbaError, carrying the server's own status code alongside the HTTP one. Switch on status; the message is for humans.

import { RumbaError } from '@rumbamove/sdk'

try {
  await rumba.videos.playbackToken(id)
} catch (error) {
  if (error instanceof RumbaError && error.status === 'VIDEO::NOT_READY') { … }
}

Idempotent calls retry a 429 or 5xx with full jitter, honouring Retry-After. Uploads never retry — a stream cannot be read twice, and a second attempt would send nothing and truncate the file.

In a browser

bun add @rumbamove/sdk vidstack
import '@rumbamove/sdk/web'
import '@rumbamove/sdk/web.css'
<rumba-player src="https://video.example.com/w/aB3dK9xQ2mZ"></rumba-player>
<rumba-feed></rumba-feed>

Importing the entry registers both custom elements — that side effect is the point of it, so do not expect a tree-shaker to keep a bare import for you unless your bundler honours sideEffects (they all do).

<rumba-player>'s whole public surface is one attribute pointing at a share link. Everything behind it — the manifest, the storyboard and chapter tracks, the poster, the engine — is ours to change, so swapping engines stays an internal refactor rather than a breaking change for everyone who embedded it.

vidstack is an optional peer dependency. Only this entry needs it, so a server-side install is never asked for a media engine it will not load. A Rumba host also serves a prebuilt version of exactly these components at /static/player.js, for pages with no bundler at all.

Making it yours

Three levels, and which one you need depends on how far your design is from a plain vertical feed.

Tokens, for colour, spacing and radius. Set them anywhere that cascades:

rumba-feed {
  --rumba-scrim: none;
  --rumba-radius-pill: 8px;
  --rumba-slide-height: 70vh;
}

There is no shadow root, so your own selectors reach every element too, and web.css is a file you link or do not.

renderItem, for anything that is markup rather than a value — a rail of action buttons, a brand row, a call to action, a play overlay. The element owns the media; the overlay above it is entirely yours:

const feed = document.querySelector('rumba-feed')!

feed.renderItem = ({ item, active, escape }) => `
  <nav class="rail">
    <button data-call>Call</button>
    <button data-share>Share</button>
  </nav>
  <footer class="bar">
    <img src="${escape(item.poster ?? '')}" alt="">
    <p>${escape(item.title ?? '')}</p>
    <button data-order="${escape(item.reference ?? '')}">Order now</button>
  </footer>
`

escape is there because titles are written by whoever uploaded the video, which is not necessarily you. The overlay ignores pointer events so a tap reaches the video; anything interactive inside takes them back automatically.

renderState does the same for the loading, empty and error screens.

The controller, when the layout is not a vertical feed at all — a horizontal carousel, a grid, a native list, a React tree:

import { createFeedController } from '@rumbamove/sdk'

const feed = createFeedController({
  load: cursor => rumba.feed.query({ namespaces, cursor: cursor ?? undefined }),
  report: ids => rumba.feed.recordViews(ids),
})

feed.subscribe(state => {
  // state.plan[i] is 'play' | 'prefetch' | 'warm' | 'hold' | 'detach'
  draw(state)
})

await feed.start()

It owns paging, the byte-budgeted prefetch window, the dwell gate, re-minting before the URLs expire, and batching view events. It also tops up a first page that already sits inside the load-more trigger — otherwise a short first page leaves nothing to scroll to, so nothing moves, so nothing ever asks for page two — and appends by id, because pagination is a moving window and a later page can hand back something already on screen. Your code reads state.plan and does what it says; it never decides what to prefetch or what counts as watched. <rumba-feed> is a view over exactly this — you are not stepping off a supported path, you are taking the same one without the markup.

createFeedController is in the core entry, not ./web: it is DOM-free, so React Native and a server-rendered page can hold the same decisions.

The budget is counted in bytes rather than items, because two 8 MB shorts is not the same ask as two 400 KB ones, and only the byte count is what the viewer pays for.

Reporting views

rank: 'trending' is computed from view events, so a feed that never sends them ranks on a signal nobody is feeding. POST /feed/events needs a credential, so give the element a publishable key and it reports for you:

<rumba-feed key="rmb_pk_…"></rumba-feed>

Safe in an attribute — that key is public the moment it deploys, and the server fixes its scopes to feed:read and feed:events. Without one the feed is the install's open catalogue and nothing is reported.

In React Native

bun add @rumbamove/sdk expo-video
import { RumbaFeed } from '@rumbamove/sdk/react-native'

<RumbaFeed
  load={cursor => rumba.feed.query({ namespaces, cursor: cursor ?? undefined })}
  report={ids => rumba.feed.recordViews(ids)}
  renderOverlay={({ item, active }) => <Caption item={item} active={active} />}
/>

A paging FlatList over expo-video, and almost nothing else — paging, the prefetch window, the dwell gate, re-minting and view events are the same createFeedController the browser component uses. Two renderings of one set of decisions, not two implementations of them.

renderOverlay rather than renderItem, because renderItem in React Native already means "the whole row" and this is explicitly not that. The component owns the player; everything drawn over it is yours.

For a layout the component does not do, take the hook:

import { useRumbaFeed } from '@rumbamove/sdk/react-native'

const feed = useRumbaFeed({ load: cursor => rumba.feed.query({ cursor: cursor ?? undefined }) })
// feed.plan[i] is 'play' | 'prefetch' | 'warm' | 'hold' | 'detach'

Single videos from a share link:

<RumbaPlayer baseUrl="https://video.example.com" shareId="aB3dK9xQ2mZ" />

Every URL behind a share link is presigned and short-lived, and a VOD playlist is static — the player never re-fetches it. So RumbaPlayer mints a fresh token a minute before the current one lapses and swaps the source in place, which is what stops a feature-length film simply stopping. Tune with refreshMarginMs.

One honest difference from the web. expo-video has no preload call — a player begins buffering the moment it is created, and releasing it is the only way to stop. So native has three states where the browser has five: playing, created, released. warm and prefetch are the same thing here, which makes the effective window one item wider than in a browser. Lower prefetch.maxItems if the viewer's data is the binding constraint.

react, react-native and expo-video are optional peer dependencies, so a server-side install is never asked for a native player. Install expo-video yourself — and install it with npx expo install expo-video rather than by pinning a version, because the right one is decided by your Expo SDK, not by this package.

This package is typechecked against the oldest expo-video it claims to support, not the newest — expo-video is pinned in devDependencies at the floor of the peer range for exactly that reason. Developing against the newest is how a peer range becomes fiction: the range said >=2 for a release while the build compiled against 57, and nothing noticed that version 2 could not compile at all.

expo-video 3 or newer. Version 2 is not merely untested, it is wrong: the feed sets fullscreenOptions, which does not exist before 3 — so a shorts feed on expo-video 2 would silently allow fullscreen. Expo SDK 54 resolves to ~3.0.15, which is the floor this is verified against; SDK 55 and later use Expo's unified versioning (55.x, 56.x, 57.x) and all satisfy >=3.

FlatList's render window is derived from prefetch.maxItems rather than fixed: it unmounts rows outside itself, and unmounting a slide releases its player, so a window narrower than the budget would silently overrule it.

Types

import type { FeedItem, VideoResource } from '@rumbamove/sdk/types'

The same declarations the server is compiled against. Importable without pulling in the client, for the places where you model Rumba's resources in your own code and do not want the transport in that file.

Coverage

Every route the server mounts is reachable from the SDK — 34 of 34, checked by a test that reads both sides and compares them, so a new route without a client method fails the suite rather than surfacing as a support thread.

The unauthenticated surfaces (health(), playback.token(), watch.get()) are on all three clients, because they take no credential.

Notes

  • The core entry has zero dependencies. fetch and nothing else — no Node built-ins, no DOM, no engine. The same file runs on Bun, Node, browsers, React Native and Cloudflare Workers. bun run verify:entries walks the emitted import graph and fails if that stops being true, which is what keeps ./web from leaking into a Worker bundle over code the Worker would never run.
  • Inject fetch to test without a network, or to route through a proxy.
  • AbortSignal is accepted on every call, and is combined with the timeout rather than replacing it.

Licence

MIT