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

@arlotv/embed-core

v0.9.0

Published

Headless client SDK for Arlo embed rails, chat, and telemetry

Readme

@arlotv/embed-core

Headless client SDK for Arlo embed rails, chat, and telemetry. Zero runtime dependencies, ships as ESM (dist/index.js + .d.ts) for bundler consumers and as an ES5 IIFE (dist/arlo-embed.iife.js, ~3kB gzipped) for direct <script> embedding on TV/legacy browser engines.

Install

npm install @arlotv/embed-core
import { createArloEmbed } from '@arlotv/embed-core';

const arlo = createArloEmbed({
  apiKey: 'YOUR_API_KEY',
  customerId: 'customer-123',
  // baseUrl defaults to https://api.arlotv.ai
});

The signals rule: always {id, title}

Every signals array (likes, dislikes, favorites) is normalized before it leaves the SDK. Always pass {id, title} pairs — bare content ids lose the human-readable title the backend needs for cold-start/attribution matching:

await arlo.rails({
  likes: [
    { id: 'tt0111161', title: 'The Shawshank Redemption' },
    { id: 'tt0068646', title: 'The Godfather' },
  ],
  dislikes: [{ id: 'tt0120655', title: 'Spawn' }],
});

Bare strings (e.g. likes: ['tt0111161']) are still accepted for backward compatibility and get coerced to { id: v, title: v }, but the title will just be the id — do not rely on this in new integrations.

watched — viewing history

A fourth signals bucket carries viewing history (WatchedSig): { id, title?, progress?, completed?, last_watched_at? }. Finished watches (completed: true or progress >= ~0.95) act as implicit taste — the ranker naturally down-ranks what the user already saw (never hard-suppresses). Capped at 50 server-side.

await arlo.rails({
  watched: [
    { id: 'tt0111161', title: 'The Shawshank Redemption', completed: true },
    { id: 'tt0068646', title: 'The Godfather', progress: 0.4 },
  ],
});

Server-side taste persistence (v2)

By default the server now persists signals per customer_id (opaque, no PII; org-controllable, right-to-erasure via DELETE /v1/embed/users/:id). Two consequences:

  • rails({}) with an empty signals object is still personalized once the server has stored taste for that customer — you no longer need to assemble the full signal blob on every call.
  • Injected signals always win on conflict and are merged with stored state, so existing full-injection integrations behave exactly as before.

Prefer the delta emitters (reaction / favorite / watchProgress, below) for keeping the store current as the user acts.

rails(signals, opts?)

Returns Promise<RailsResponse>. Fetches personalized rails for the given signals.

const { rails, attribution } = await arlo.rails(
  { likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }] },
  { max: 20, country: 'US' },
);
  • opts.max?: number — cap the number of items returned.
  • opts.country?: string — country-scope availability.
  • Rejects with { code: 'rails_http_<status>', message } on a non-2xx response — callers should await/.catch() it.

The response is served stale-while-revalidate: the server answers instantly from its cache and recomputes personalization in the background. Two additive fields tell you what you got:

  • rails_meta?: { stale?: boolean; refreshing?: boolean }stale means the rails predate the customer's newest signals; refreshing means a fresh personalized set is composing server-side (refetch shortly if you care).
  • each rail carries personalized?: booleanfalse for generic cold-start/editorial fill, true when shaped by this customer's own taste.

Each rail item also carries the display title and an images object, so you render a full rail from this one call — no per-title hydration:

const { rails } = await arlo.rails(signals);
rails.forEach((rail) => {
  renderRailHeader(rail.title);
  rail.items.forEach(({ content_id, rec_id, title, images }) => {
    renderCard({ id: content_id, rec_id, title, poster: images?.poster, backdrop: images?.backdrop });
  });
});

images carries poster, poster_large, backdrop, and title_card — the last is a 16:9 image with the title baked into the artwork (render it as-is; don't overlay your own title). Any images field may be null (catalog art gaps are real) — supply your own placeholder. content_id remains the stable id for telemetry and for anything not in the response.

railsWithCache(client, customerId, signals, opts?) — instant first paint

Optional client-side layer over the same endpoint: returns the last-known rails from storage synchronously (cached), refetches (fresh), stores the result, and calls opts.onUpdate(fresh). Storage is injectable (RN: AsyncStorage adapter); defaults to localStorage.

import { railsWithCache } from '@arlotv/embed-core';
const { cached, fresh } = railsWithCache(arlo, customerId, {}, { onUpdate: render });
if (cached) render(cached);   // paints in the same frame as the host app

anonymousCustomerId(storage?) — logged-out visitors

Mints one UUID per browser and persists it under arlo:customer_id so signals accrue to a stable identity across sessions (the blessed convention for anonymous visitors). Pass a storage adapter on RN; falls back to a per-session id when storage is unavailable.

similar(opts)

Returns Promise<SimilarResponse>. A content-to-content "More Like This" rail for a single title — ideal for a PDP. The response carries only ids; the host hydrates each card from its own catalog.

const { items, attribution } = await arlo.similar({
  contentId: 'the-shawshank-redemption', // your catalog id / slug (required)
  max: 12,        // capped at 20 server-side; default 12
  country: 'US',
});
// items: [{ content_id, rec_id, position }, ...]
items.forEach(({ content_id, rec_id }) => renderCard(myCatalog[content_id], rec_id));
  • opts.contentId: string — the title to find neighbors for (required).
  • opts.max?: number — cap the number of neighbors (server caps at 20).
  • opts.country?: string — country-scope availability.
  • opts.customerId?: string — attribution-only session id (not a login); defaults to the configured customerId. Present ⇒ PDP clicks attribute via the rec_id ledger; absent ⇒ the rail still renders, clicks just aren't attributed.
  • Fail-silent: an unknown/unindexed title resolves to { items: [], reason } (never throws) so a PDP never breaks. Rejects with { code: 'similar_http_<status>', message } only on a non-2xx response, and { code: 'similar_bad_content_id' } if contentId is missing.

chat(message, opts?)

import { DEFAULT_MOOD_CHIPS } from '@arlotv/embed-core';
// Cold start: render DEFAULT_MOOD_CHIPS; on tap call chat(chip).

const handle = arlo.chat('recommend something like Shawshank', {
  signals: { likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }] },
  history: [{ role: 'user', content: 'hi' }],
  onToken: (t) => appendToTranscript(t),
  onPicks: (cards) => renderPicks(cards),
  onChips: (chips) => renderChips(chips), // per-turn quick replies; clear on next user action
  onAttribution: (a) => renderAttribution(a), // e.g. "Powered by Arlo" — render when a.required
  onDone: (usage) => console.log('done', usage),
  onError: (e) => console.error(e.code, e.message),
});

// later, e.g. on unmount or a new message starting
handle.cancel();

chat() returns a plain { cancel(): void } handle synchronously — it is not a Promise. All results arrive via the onToken / onPicks / onChips / onAttribution / onDone / onError callbacks, which are safe to call with no opts at all (arlo.chat('hi') does not throw).

DEFAULT_MOOD_CHIPS is a static, host-overridable string[] the host renders before the first message (there's no prior turn to derive per-turn chips from). Tapping one is an ordinary chat(chipText) call — nothing special on the wire. Hosts are free to render their own list instead; this is only a sensible default, not configuration.

onChips(chips) delivers per-turn quick-reply suggestions for the next thing the user might say (distinct from DEFAULT_MOOD_CHIPS, which only appears pre-first-message). Fires from the stream's chips frame (and from the poll response's suggested_replies); omitted or empty when the turn has none.

onAttribution(a) delivers the attribution payload the host must render on the chat surface — { required, text, logo_url, link, placement_hint }. When a.required is true, rendering it (e.g. "Powered by Arlo") is a compliance requirement, same as the attribution object returned by rails(). It fires from the stream's attribution frame (and from the poll response's attribution).

Transport ladder

chat() auto-selects a transport and silently falls back if a rung isn't available in the current runtime:

  1. xhr (default) — progressive XHR against POST /v1/embed/chat, parsing newline-delimited data: frames out of responseText as they arrive. Chosen by default whenever XMLHttpRequest exists.
  2. sse — opt-in only (opts.transport = 'sse'), uses native EventSource against a GET+query-encoded variant of the same endpoint. Not the default because the current /v1/embed/chat endpoint is POST-only; this rung is for a future GET variant. If EventSource isn't available, it downgrades to xhr (then poll).
  3. poll — one-shot JSON fetch (POST, stream:false), used when neither xhr nor sse is viable, or as the final fallback of the ladder. Always available.

Errors are routed to onError with a transport-specific code (sse_error | poll_error) — chat() never throws into the host page.

track(type, data?)

Fire-and-forget, synchronous — queues an event in an in-memory batch.

arlo.track('tile_tapped', { content_id: 'tt0111161' });
arlo.track('pick_tapped', { rec_id: 'rec_abc123' });

type is one of: 'attribution_rendered' | 'rail_impression' | 'chat_opened' | 'pick_tapped' | 'tile_tapped' (engagement), the funnel types 'video_start' | 'ad_fill' | 'video_complete' (rec_id required), or the signal deltas 'reaction_set' | 'favorite_set' | 'watch_progress'. data is optional: { rec_id?: string; content_id?: string; payload?: Record<string, unknown> }.

reaction(contentId, reaction, title?) / favorite(contentId, favorited, title?) / watchProgress(contentId, {progress, completed, title?})

Typed emitters for the server-side taste store — prefer these over raw track() for taste updates; they shape payloads correctly. reaction(id, null) clears a previous like/dislike; favorite(id, false) removes a favorite. Batched with the rest of telemetry; delivered on flush().

arlo.reaction('tt0111161', 'like', 'The Shawshank Redemption');
arlo.watchProgress('tt0111161', { completed: true });
arlo.favorite('tt0068646', true, 'The Godfather');

attributionRendered()

Shorthand for track('attribution_rendered') — call once when the attribution/branding block is actually painted on screen.

arlo.attributionRendered();

flush()

Synchronously sends the current queued-event batch via fetch(url, { keepalive: true }) so it survives page unload, then clears the buffer. Best-effort — a failed or dropped batch never throws. Call it proactively (e.g. on visibilitychange/pagehide) if you don't want to wait for the SDK's own batching.

window.addEventListener('pagehide', () => arlo.flush());

captureRecId(url?)

Pure, synchronous helper — pulls the referral id out of ?src=arlo&aid=... query params (defaults to window.location.href when no url is given). Returns string | null.

const recId = arlo.captureRecId(); // or captureRecId(location.href)
if (recId) arlo.track('pick_tapped', { rec_id: recId });

Promises and old TV engines

rails() is async and returns a real Promise. chat()'s poll fallback and flush()'s telemetry send both use fetch(...).then(...) internally even though their public surface is callback-based. Any TV or legacy browser engine consuming this SDK needs global Promise and fetch implementations (polyfill them before loading the SDK if the target engine lacks either) — createArloEmbed() reads the global fetch at construction unless you pass your own via { fetch }. Of the public API, only track, attributionRendered, and captureRecId are Promise-free (they enqueue or parse synchronously). flush() is fire-and-forget — it needs fetch + Promise to send, but is best-effort and never throws, so on an engine without them it silently drops the batch rather than erroring.

Browser <script> / IIFE usage

For engines without a bundler (Vizio/webOS/Tizen/FireTV WebViews, older Smart TV browsers), load the ES5 IIFE build directly:

<script src="https://unpkg.com/@arlotv/embed-core/dist/arlo-embed.iife.js"></script>
<script>
  var arlo = ArloEmbed.createArloEmbed({
    apiKey: 'YOUR_API_KEY',
    customerId: 'customer-123',
  });

  arlo.rails({
    likes: [{ id: 'tt0111161', title: 'The Shawshank Redemption' }],
  }).then(function (res) {
    renderRails(res.rails);
  });

  var chatHandle = arlo.chat('what should I watch tonight?', {
    onToken: function (t) { appendToTranscript(t); },
    onPicks: function (cards) { renderPicks(cards); },
    onError: function (e) { console.error(e.code, e.message); },
  });
  // chatHandle.cancel() to abort
</script>

The global is ArloEmbed, exposing the same named exports as the ESM entrypoint (createArloEmbed, captureRecId, normalizeSignals, types are erased at runtime). The bundle is built with target: 'es5' and has no runtime dependencies, so it runs unmodified on old TV JS engines — provided a Promise polyfill is present (see above).