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

expo-resilient-location

v0.1.0

Published

Background location for Expo that keeps reporting even when the app is force-quit — iOS Significant-Location-Change + Android foreground service. Transport-agnostic: you supply the upload endpoint and headers.

Readme

expo-resilient-location

Background location for Expo (Prebuild / Dev Client) that keeps reporting even after the app is force-quit — the gap expo-location does not cover on iOS.

  • iOS — Significant-Location-Change API relaunches the app after termination (the only Apple-sanctioned mechanism). Once alive, startUpdatingLocation layers on for fine-grained foreground fixes.
  • Android — a START_STICKY foreground service backed by Google's FusedLocationProvider survives app-kill. A boot receiver automatically restarts tracking after a reboot or app update.
  • Transport-agnostic — uploads batches to a URL you configure, with headers you set. Never imports your auth or domain logic. Arbitrary extras (e.g. group_id) are merged into every uploaded point.

How tracking works

iOS

When start() is called the module registers two overlapping location modes:

| Mode | What it does | Survives force-quit? | |---|---|---| | startMonitoringSignificantLocationChanges | Wakes the app when the device moves ~500 m or switches cell towers (~15 min cadence). This is the relaunch engine. | Yes — the only Apple-approved way | | startUpdatingLocation | High-accuracy continuous fixes while the app is foregrounded or backgrounded-but-alive. Auto-pauses when stationary. | No (foreground / alive only) |

On relaunch from a force-quit, the module re-arms SLC immediately in OnCreate (the kTracking flag in UserDefaults survives process death). Fixes are persisted to a UserDefaults-backed buffer so a background wake that ends abruptly never loses data.

iOS permission escalation: the OS requires NSLocationWhenInUseUsageDescription before you can even ask for Always. requestPermissions() triggers both prompts in sequence; the second (Always) is only shown by the OS after the first has been granted.

Android

start() persists config to SharedPreferences and brings up a foreground service:

  1. Foreground service (ResilientLocationService) — registers with LocationServices.getFusedLocationProviderClient at 20 s intervals, high accuracy, no distance gate (so a stationary device still heartbeats). The OS mandates a visible persistent notification for background location; the module builds it from androidNotification.title / body you supply — surface the trip name and end date there, it is a trust feature not friction.
  2. Immediate fix — on service start, getCurrentLocation fires a forced fresh fix (maxUpdateAgeMillis = 0). Without this, a stationary device with no cached location can wait a very long time (or forever) for the periodic stream to first emit.
  3. START_STICKY — if the OS kills the service under memory pressure, Android restarts it automatically, picking up the persisted config.
  4. BootReceiver — listens for BOOT_COMPLETED and MY_PACKAGE_REPLACED, re-starts the service if kTracking is true and stopAfter has not yet passed. Covers the "phone died mid-trip" case.

Buffering and uploads

Both platforms share the same invariants:

  • On-device buffer capped at 1 000 points (oldest dropped when full).
  • Uploads are chunked at 150 ticks per request — sending the whole buffer at once caused HTTP 500 once it exceeded the server's body-size limit; chunking is the fix.
  • Each chunk is dropped from the front of the buffer only after a 2xx response, so a crash mid-drain or an offline period never re-sends or loses points.
  • On failure (network error, 4xx, 5xx) the buffer is kept and retried on the next fix or the next explicit flush() call.
  • Headers (e.g. a refreshed JWT) can be updated at any time via setHeaders() without restarting tracking; the next upload attempt picks them up.

Good use cases

  • Live group location sharing during a trip — one URL receives ticks from every member; your backend fans them out. The extras field carries the group_id so you never need a separate handshake.
  • Travel apps that want a breadcrumb trail even when users background the app for hours or days.
  • Field-service or delivery apps that need periodic location without a continuous GPS lock burning the battery.
  • Any app that needs location to keep flowing across force-quit → reopen or reboot without requiring the user to manually restart sharing.

Exceptions and known limitations

| Situation | Effect | |---|---| | Aggressive OEM battery optimisers (Xiaomi/MIUI, Oppo/ColorOS, Vivo/OriginOS, some Samsung "Adaptive Battery" profiles) | The foreground service can be killed even with START_STICKY. The user must whitelist the app in the OEM battery settings. There is no SDK workaround. | | Android battery-saver mode | The OS throttles or suspends background processes. The service may stop emitting until the user exits battery-saver. | | iOS SLC coarseness | ~500 m movement or a cell-tower change, roughly every 15 minutes. Not suitable for street-level real-time tracking while the app is force-quit — only for "is the user still roughly here?" | | iOS Always permission not granted | SLC requires Always. If the user grants only When In Use, the app will not be relaunched after a force-quit. requestPermissions() returns level: "whenInUse" so you can nudge the user to Settings. | | iOS Background App Refresh disabled (per-app or system-wide) | SLC relaunches are suppressed. The module cannot detect or fix this — show a settings prompt. | | Android ACCESS_BACKGROUND_LOCATION denied (Android 10+) | FusedLocation cannot collect while backgrounded. The permission must be requested in a separate step after ACCESS_FINE_LOCATION (Google Play requires a separate rationale screen). | | stopAfter as the only stop mechanism | stopAfter is a local belt-and-braces guard for when the server's expired event never arrives. It should not be the primary stop signal — always call stop() from your server or app when the trip ends. | | No geofences | The bg-fence source type is reserved in the types but not yet implemented. | | Expo Go | Does not work — requires Prebuild + Dev Client. |


Install

# npm
npm install expo-resilient-location

# pnpm monorepo (workspace)
pnpm add expo-resilient-location

Add the config plugin to your app.config.ts / app.json:

// app.config.ts
export default {
  plugins: [
    [
      'expo-resilient-location',
      {
        // Optional — override the default iOS permission strings:
        iosWhenInUsePermission:
          'Show your location to your trip group while the app is open.',
        iosAlwaysPermission:
          'Share your live location with your trip group for the trip dates, even when the app is closed.',
      },
    ],
  ],
};

Then regenerate the native projects and build:

npx expo prebuild
npx expo run:ios    # or run:android

Requires Expo SDK ≥ 53 and Dev Client — not Expo Go.

Android peer dependency

The Android side uses Google Play Services play-services-location. This is already a transitive dependency of most React Native apps; if yours does not have it, add it to android/build.gradle:

implementation 'com.google.android.gms:play-services-location:21.x.x'

API

import * as ResilientLocation from 'expo-resilient-location';

configure(opts: ConfigureOptions): void

Set the upload endpoint and headers. Call once at app start, before start(). Persisted natively so a background-relaunched process can still upload without JS being fully initialised.

ResilientLocation.configure({
  url: 'https://api.example.com/trips/ticks',
  headers: { Authorization: `Bearer ${token}` },
});

setHeaders(headers: Record<string, string>): void

Update headers without restarting tracking. Call after refreshing an access token — the next upload attempt will use the new headers, and any buffered-but-unsent points will not be lost.

requestPermissions(): Promise<PermissionStatus>

Prompt for location permission. On iOS, requests WhenInUse first then escalates to Always (the OS enforces this two-step). Returns the current status immediately if a decision has already been made.

const status = await ResilientLocation.requestPermissions();
// { granted: true, level: 'always' | 'whenInUse' | 'denied', canAskAgain: boolean }
if (status.level !== 'always') {
  // Nudge the user to Settings — SLC requires Always on iOS.
}

start(options?: StartOptions): Promise<void>

Begin tracking. Idempotent — calling again replaces the options without creating a duplicate service.

await ResilientLocation.start({
  extras: { group_id: trip.groupId },          // merged into every uploaded tick
  stopAfter: trip.endsAt.toISOString(),        // local self-stop guard (ISO-8601)
  distanceFilterM: 25,                          // minimum metres before a new fix (default 25)
  accuracyTier: 'high',                         // 'low' | 'balanced' | 'high' (default high)
  androidNotification: {
    title: `Sharing location — ${trip.name}`,
    body: `Stops automatically on ${trip.endDateDisplay}`,
  },
});

androidNotification is required on Android — the OS mandates a visible persistent notification for any app collecting background location. Surface the trip name and end date: it is a transparency and trust feature, not friction.

stop(): Promise<void>

Stop tracking and tear down the background service / SLC monitoring.

flush(): Promise<void>

Force an immediate upload of any buffered points. Useful after calling setHeaders with a refreshed token to drain points that were blocked by a 401.

getStatus(): Promise<TrackingStatus>

const s = await ResilientLocation.getStatus();
// { tracking: boolean, permission: PermissionLevel, bufferedCount: number }

addLocationListener(listener): EventSubscription

Receive fixes in real time while a JS listener is attached (e.g. to update a foreground map). Background fixes are uploaded natively and do not require a listener.

const sub = ResilientLocation.addLocationListener((fix) => {
  // { lat, lng, accuracy_m, source, recorded_at }
  updateMarker(fix);
});
// ...
sub.remove();

addStatusListener(listener): EventSubscription

Notified whenever tracking or permission state changes.


Upload contract

POST <url>
Content-Type: application/json
<your headers>

{
  "ticks": [
    {
      "lat": 15.3254,
      "lng": 73.7614,
      "accuracy_m": 12.4,
      "source": "fg | bg-motion | bg-slc | bg-fence",
      "recorded_at": "2026-05-21T08:14:33.012Z",
      "group_id": "01HZ…"    ← your extras, merged in
    }
  ]
}

source values:

| Value | Meaning | |---|---| | fg | Collected while the app was in the foreground (high accuracy, frequent) | | bg-motion | Android FusedLocation periodic update while backgrounded | | bg-slc | iOS Significant-Location-Change wake (coarse, ~500 m / ~15 min) | | bg-fence | Reserved (geofence transition — not yet implemented) |

Your server should respond 2xx to acknowledge the batch. On 2xx the sent ticks are dropped from the on-device buffer. On any other response or network error the ticks are kept and retried — so a dead network or an expired token never permanently loses points.


Types

interface LocationFix {
  lat: number;
  lng: number;
  accuracy_m: number | null;
  source: 'fg' | 'bg-motion' | 'bg-slc' | 'bg-fence';
  recorded_at: string; // ISO-8601
}

interface PermissionStatus {
  granted: boolean;
  level: 'denied' | 'whenInUse' | 'always';
  canAskAgain: boolean; // false once permanently denied → send user to Settings
}

interface TrackingStatus {
  tracking: boolean;
  permission: 'denied' | 'whenInUse' | 'always';
  bufferedCount: number;
}

interface StartOptions {
  extras?: Record<string, string>;
  stopAfter?: string | null;   // ISO-8601
  distanceFilterM?: number;    // default 25
  accuracyTier?: 'low' | 'balanced' | 'high';
  androidNotification?: { title: string; body: string };
}

interface ConfigureOptions {
  url: string;
  headers?: Record<string, string>;
}

License

MIT