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

@amritk/lynx-location

v0.2.2

Published

Device location for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.

Readme

@amritk/lynx-location

Device location for Lynx. An Android native module, an iOS native module, and a promise-shaped facade that reaches them from a main-thread @amritk/mini-lynx tree.

import { getCurrentPosition, getPermissionStatus, requestPermission } from '@amritk/lynx-location'

if ((await getPermissionStatus()) === 'undetermined') await requestPermission()

const result = await getCurrentPosition({ accuracy: 'high', maximumAge: 30_000 })
if (result.ok) console.log(result.position.latitude, result.position.longitude)

Why this exists

Lynx ships no location module. Neither does Sparkling — its built-ins are navigation, storage and media, and anything else is a Sparkling Method you write yourself. There is one published community package, @sigx/lynx-location, and its JavaScript half calls NativeModules directly, which is background-thread only — so a mini-lynx component importing it gets undefined with no error to read. Lynx's official answer is "write native code and send it into your Lynx code", so that is what this is.

Install

bun add @amritk/lynx-location

@amritk/mini-lynx-native comes with it — it is the wire every call travels.

JavaScript setup

One line, in your background chunk:

import { installNativeBridge } from '@amritk/mini-lynx-native/background'

installNativeBridge()

Without it every call queues forever and nothing says why. NativeModules is a background-thread global and @amritk/mini-lynx renders on the main thread, so this package cannot reach the platform without a chunk it does not own installing the other half. See @amritk/mini-lynx-native for the reasoning.

Then, anywhere on the main thread:

import { onCleanup, signal } from '@amritk/mini-lynx'
import { type LocationFix, getCurrentPosition, watchPosition } from '@amritk/lynx-location'

const Map = () => {
  const position = signal<LocationFix | null>(null)

  getCurrentPosition({ maximumAge: 60_000 }).then((result) => {
    if (result.ok) position(result.position)
  })

  onCleanup(
    watchPosition((update) => {
      if (update.ok) position(update.position)
    }, { distanceFilter: 10 }),
  )

  return <text>{() => (position() ? `${position()?.latitude}, ${position()?.longitude}` : 'locating…')}</text>
}

Host-app setup

lynx.lib.json declares both native sources, so Lynx's autolinking picks them up. What autolinking cannot supply is the strings and entitlements that belong to your app.

Android

The two permissions are declared in this library's manifest and reach your app through manifest merging — you do not add them yourself:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Know that this happens. They appear in your merged manifest, on your Play Store listing, and in your data safety form. That is the cost of shipping a location library, and there is no version of it that avoids the declaration.

ACCESS_BACKGROUND_LOCATION is deliberately not declared. It is a second prompt and a Play Store review conversation, and nothing here can use it.

If your build does not run Lynx's annotation processor, register the module by hand:

LynxEnv.inst().registerModule("MiniLynxLocationModule", MiniLynxLocationModule::class.java)

iOS

Add the usage description to your Info.plist. This one is not optional — iOS terminates the app the moment it asks for location without it, and the crash log does say why, but only if you look:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Shows nearby results and centres the map on you.</string>

Write a real sentence there. It is the text in the system prompt, and it is the whole of what the user has to decide on.

Register the module where you build your Lynx config:

#import <MiniLynxLocation/MiniLynxLocationModule.h>

[config registerModule:MiniLynxLocationModule.class];

There is nothing to forward from your app delegate — unlike notifications, CoreLocation answers the object that asked.

API

getPermissionStatus(): Promise<LocationPermissionStatus>  // 'undetermined' | 'denied' | 'granted' | 'restricted'
requestPermission(request?: LocationPermissionRequest): Promise<LocationPermissionStatus>

getCurrentPosition(options?: PositionOptions): Promise<LocationResult>
getLastKnownPosition(): Promise<LocationFix | null>

isLocationEnabled(): Promise<boolean>    // device-wide switch, NOT permission
isLocationAvailable(): Promise<boolean>  // is the native module linked at all

watchPosition(listener: (update: WatchUpdate) => void, options?: WatchOptions): () => void

reverseGeocode(coordinates: Coordinates, options?: ReverseGeocodeOptions): Promise<GeocodeResult>

LocationResult, WatchUpdate and GeocodeResult are discriminated unions — { ok: true, … } or { ok: false, error, message } — rather than rejections. Lynx has no error convention for bridge callbacks, so a failure has to travel as a value anyway, and "the user has not granted location" is an ordinary branch in a UI rather than an exceptional condition.

Reverse geocoding

reverseGeocode turns coordinates into a postal address. It is the one function here that never reads the device's own location, so it needs no permission and will never prompt — an app can label a saved venue or a map centre without asking the user for anything.

const result = await reverseGeocode({ latitude: 45.5017, longitude: -73.5673 })
if (result.ok) console.log(result.addresses[0]?.formattedAddress)

A LocationFix satisfies Coordinates, so the two calls compose without a mapping step:

const position = await getCurrentPosition()
if (position.ok) await reverseGeocode(position.position)

Every field of a GeocodeAddress is nullable and most are usually null — a geocoder answers from whatever its map data holds. formattedAddress is built by the OS (getAddressLine on Android, CNPostalAddressFormatter on iOS), so it puts each country's postcode where that country puts it; a string assembled from the other fields by hand will not. isoCountryCode is the only field stable across locales, which makes it the only one worth storing or branching on.

The things that actually bite

  • Permission and the device switch are different questions. A perfectly granted app on a device with Location Services off gets nothing, and sending that user to your app's permission screen shows them a setting that already looks correct. Check isLocationEnabled() first; it is why it exists.
  • You get one prompt, ever. iOS shows it once per install; Android stops after two dismissals. A second request displays nothing and reports the standing answer. Spend it when the user has just asked for something that needs their location — not on first launch.
  • denied is final, restricted is worse. The route back from denied is the system settings app. There is no route back from restricted: it is off by parental controls or an MDM profile, and offering a settings link is offering a dead end.
  • getCurrentPosition can take seconds. A cold radio indoors at high accuracy is the worst case and it is not rare, which is why timeout defaults to something finite. Set maximumAge when an approximate answer will do — it turns a three-second wait into an immediate one.
  • A watch holds a provider open. That is a battery cost the user can see in their settings app. Hand the returned function to onCleanup.
  • heading is course over ground, not compass heading. It is derived from consecutive fixes, so a stationary device reports null no matter which way it is pointing.
  • Foreground only. Both platforms stop delivering to a backgrounded app. That is the platform working as designed, not a gap to work around.
  • WatchOptions.interval is Android-only. CoreLocation has no equivalent and decides its own cadence. Pace with distanceFilter, which both honour.
  • Apple rate-limits reverse geocoding, and does not publish the limit. Their guidance is at most one request per user action. An app that geocodes every row of a list as it scrolls starts getting network errors for reasons that have nothing to do with the network. Geocode on demand, cache the answer, and key the cache on rounded coordinates rather than on the address.
  • Geocoder.isPresent() is false on plenty of Android devices. Anything built without Google's services has no geocoding backend, for the life of the device. That is unavailable, and retrying will never help — fall back to showing the coordinates.

Testing

@amritk/lynx-location/testing ships the native module's contract as an in-memory fake, which is what this package's own suite runs against:

import { installNativeBridge } from '@amritk/mini-lynx-native/background'
import { createFakeContexts, createFakeEmitter } from '@amritk/mini-lynx-native/testing'
import { MODULE } from '@amritk/lynx-location'
import { createFakeLocation } from '@amritk/lynx-location/testing'

const contexts = createFakeContexts()
const emitter = createFakeEmitter()
const location = createFakeLocation(emitter)

setPeerContext(contexts.mainThread)
installNativeBridge({ peer: contexts.background, emitter, modules: { [MODULE]: location.module } })

location.setPermissionStatus('granted')
location.setNextFix({ latitude: 51.5072, longitude: -0.1276, accuracy: 12, /* … */ })

It reproduces the platforms rather than smoothing them over: a request after a refusal returns the refusal, a device with location switched off fails as locationDisabled even when permission is granted, and a device with no fix to give times out. A fake friendlier than a device would hide the bugs worth catching.

Status, and what is actually verified

Pre-alpha, and nothing here has run on a device. Three checks of decreasing reach stand behind it, and it is worth knowing which one a green run came from:

| Check | Command | Strength | | --- | --- | --- | | Facade behaviour | bun run test | real code, fake platform | | Cross-language signatures | src/native-contract.test.ts | parses Kotlin + Objective-C, compares to TypeScript | | Kotlin compiles + packages | bun run check:android | real Lynx AAR, real Android SDK | | Objective-C compiles | pod lib lint (manual, macOS — not in CI) | real Lynx pod, real iOS SDK |

None of that is a device. Permission flows, provider selection, what a fix actually contains outdoors, whether a watch survives a backgrounding, and whether Lynx's annotation processor registers the module without the generated Spec its own template extends are all unverified. See AGENTS.md.

Licence

MIT