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

@sigx/lynx-updates

v0.11.0

Published

OTA bundle updates for sigx-lynx — pluggable backends, update modes, crash rollback

Downloads

1,376

Readme

@sigx/lynx-updates

Over-the-air (OTA) bundle updates for sigx-lynx. Ship JS-only releases to installed apps without a store round-trip — with pluggable backends, every update mode from fully-automatic to fully-manual, and crash-driven rollback.

pnpm add @sigx/lynx-updates
sigx prebuild   # links the native module + bakes the runtime fingerprint

Quick start

// src/main.tsx
import { defineApp } from '@sigx/lynx';
import { Updates } from '@sigx/lynx-updates';
import App from './App';

Updates.configure({
    provider: { url: 'https://cdn.example.com/myapp/production/manifest.json' },
    mode: 'silent',                    // download now, apply on next launch
    checkOn: ['launch', 'foreground'],
});

defineApp(<App />).mount(null);

Publish an update:

sigx build                       # produces dist/main.lynx.bundle
sigx updates:publish             # writes updates-dist/production/{manifest.json, updates/<id>/...}
# upload updates-dist/production/ to any static host — done.

Update modes

| Mode | Behavior | |---|---| | 'silent' (default) | Auto check + download; the update applies on the next cold launch. | | 'immediate' | Auto check + download, then applies immediately via an in-place reload. | | 'manual' | Nothing automatic — drive checkForUpdate() / download() / apply() yourself. |

Mandatory updates (mandatory: true in the manifest, --mandatory on publish) override every mode: state.mandatory becomes true (block the UI — see <UpdateGate> in @sigx/lynx-updates-ui), and the update downloads and applies automatically. Opt out with honorMandatory: false.

Runtime-version compatibility

An OTA bundle can only run on a native binary that has the native modules it expects. sigx prebuild computes a runtime fingerprint from the linked native modules' source content, the Lynx SDK version and the scaffold revision, and bakes it into the binary. sigx updates:publish stamps the same fingerprint into the manifest, and the client refuses mismatches:

  • Add/remove/update a native module package → new fingerprint → published updates no longer match → ship a store release. The check surfaces this as { type: 'incompatible' } / the incompatibleUpdate event.
  • JS-only changes (any lockstep release that doesn't touch native code) keep the fingerprint stable — published updates stay valid.
  • Prefer manual control? Pin it: updates: { runtimeVersion: '1.0.0' } in signalx.config.ts (Expo-style — you own the compatibility guarantee).

After a store update, all downloaded OTA updates are dropped automatically (the binary's fingerprint/versionCode no longer match the recorded state).

Rollback safety

Updates commit in two phases. A downloaded update is pending until the app signals a healthy boot via markReady() — called automatically just after Updates.configure() (set autoMarkReady: false to gate on your own signal, e.g. first screen rendered). If the app crashes before markReady() on rollback.maxFailedLaunches consecutive launches (default 2), the native side deletes the update and reverts to the previous bundle. Detect it:

const { didRollBack } = await Updates.getCurrentlyRunning();

API

Updates.configure(config)         // sync, idempotent — call before defineApp()
Updates.checkForUpdate()          // → { type: 'update-available' | 'up-to-date' | 'incompatible', ... }
Updates.download(manifest?)       // download + verify + stage for next launch
Updates.apply()                   // apply staged update NOW (in-place reload; only rejects)
Updates.markReady()               // health signal — commits the pending update
Updates.getCurrentlyRunning()     // { updateId, isEmbedded, isFirstLaunchAfterUpdate, didRollBack, ... }
Updates.clearUpdates()            // back to the baked bundle on next launch
Updates.getState() / Updates.addListener(fn) / Updates.isAvailable()
useUpdates()                      // Computed<UpdatesState> for components

State machine: idle → checking → up-to-date | available | incompatible, available → downloading → ready → applying; failures land in error and every transition fires a typed UpdatesEvent.

Self-hosted & authenticated backends

The built-in StaticManifestProvider works against more than a static CDN — you don't need a custom backend just to point at a runtime-resolved host or attach a short-lived token. Pass these on the provider shorthand (or to new StaticManifestProvider({ ... })):

Updates.configure({
    provider: {
        // Host discovered after launch (sign-in, environment selection):
        // a resolver runs before every check. Return a URL, or { url, headers }.
        url: (ctx) => `${session.apiBase}/updates/${ctx.channel}/manifest.json`,

        // Static headers, merged into BOTH the manifest fetch and the download.
        headers: { Accept: 'application/json' },

        // Per-request auth — inject (and refresh) a short-lived token. The
        // returned headers are merged over `headers`. onBeforeCheck guards the
        // manifest request; onBeforeDownload guards the bundle download.
        onBeforeCheck: async (ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
        onBeforeDownload: async (manifest, ctx) => ({ Authorization: `Bearer ${await auth.token()}` }),
    },
});
  • Relative bundleUrls resolve against whatever URL the resolver returned for that check, so per-environment hosts just work.
  • The hooks run on every check/download — return a fresh token each time and refresh inside the hook when it's near expiry.

Replacing the provider at runtime. Updates.configure() is idempotent: a second call swaps the provider (and channel/mode) for subsequent checks without re-running the launch check. Use it for a wholesale backend switch; for the common "discover the host after sign-in" case prefer the url resolver above — no re-wiring.

Custom backends

The static-manifest provider is ~150 lines over fetch. Anything else — signed manifests, staged rollout services, the Expo Updates protocol — implements UpdateProvider in its own package, no core changes:

import type { UpdateProvider } from '@sigx/lynx-updates';

const myBackend: UpdateProvider = {
    name: 'my-backend',
    async checkForUpdate(ctx) {
        // ctx: { platform, runtimeVersion, currentUpdateId, embeddedVersion, channel }
        const res = await fetch(`https://updates.example.com/check`, { ... });
        // normalize your protocol's answer to an UpdateManifest
        return { type: 'update-available', manifest };
    },
    async resolveDownload(manifest) {
        return { url: manifest.bundleUrl, sha256: manifest.sha256, headers: { Authorization: '…' } };
    },
};

Updates.configure({ provider: myBackend });

The byte transfer always happens natively (streamed to disk with incremental SHA-256 verification) — providers only decide what to download.

Static manifest format

sigx updates:publish maintains this document; serve it from any static host:

{
    "schemaVersion": 1,
    "updates": [{
        "id": "a1b2c3d4e5f60718",
        "version": "1.4.2",
        "channel": "production",
        "platforms": ["android"],
        "runtimeVersion": "fp1-3aa01b2c44de9921",
        "bundleUrl": "updates/a1b2c3d4e5f60718/main.lynx.bundle",
        "sha256": "<64-hex>",
        "mandatory": false,
        "createdAt": "2026-06-12T10:00:00Z",
        "metadata": { "releaseNotes": "Bug fixes." }
    }]
}

One URL serves every channel/runtime version: old binaries keep matching their entries while new binaries pick up new ones. bundleUrl may be relative (resolved against the manifest URL).

Notes

  • Dev builds: when running from a dev server URL, OTA is inert (the dev server owns the bundle). Baked-bundle debug runs DO consult the update store, so rollback can be exercised locally.
  • Web: no-ops gracefully — every API degrades like the other native modules.
  • Prebuilt UI (update prompt, blocking gate, progress, restart banner): @sigx/lynx-updates-ui.