Lynx/Modules/Native Bridge/API reference
@sigx/lynx-core · Stable

API reference#

Exports of @sigx/lynx-core v0.35.0.

The package has a single public entry point. Everything below is imported from @sigx/lynx-core. All exports work on both iOS and Android.

TypeScript
import {
  getModule,
  callSync,
  callAsync,
  isModuleAvailable,
  guardModule,
  base64ToArrayBuffer,
  arrayBufferToBase64,
  Platform,
  OS,
  select,
  DeviceInfo,
  subscribeNative,
  unwrapNative,
  unwrapNativeVoid,
  SigxError,
  isSigxError,
  useScreen,
  useScreenMT,
  useOrientation,
  readGlobalScreen,
  Orientation,
  Breakpoint,
  useWidthClass,
  useHeightClass,
  useWidthAtLeast,
  useHeightAtLeast,
  useWidthClassMT,
  useHeightClassMT,
  widthClassOf,
  heightClassOf,
} from '@sigx/lynx-core';
import type {
  PermissionStatus,
  PermissionResponse,
  PlatformOS,
  PlatformSelectSpec,
  DeviceInfoResult,
  IosDeviceInfo,
  AndroidDeviceInfo,
  SubscribeNativeOptions,
  ScreenMetrics,
  ScreenOrientation,
  CoarseOrientation,
  OrientationLock,
  WidthClass,
  HeightClass,
} from '@sigx/lynx-core';

Everything here is also re-exported from the @sigx/lynx barrel, so app code can import Platform, DeviceInfo and friends straight from @sigx/lynx.

The bridge helpers read from a global NativeModules object that the Lynx runtime injects — a Record whose properties are the registered native modules. It is not itself an export; it is the surface getModule and isModuleAvailable inspect.

Functions#

getModule#

TypeScript
export function getModule(name: string): Record<string, (...args: any[]) => any>

Returns the native module proxy injected by the Lynx runtime as NativeModules[name]. Throws a SigxError with code: 'module_unavailable' and package: 'lynx-core' if the module is unavailable — the message names the package to install (mapping known module names through an internal table, e.g. Haptics to @sigx/lynx-haptics) and points at sigx prebuild.

  • name — the registered native module name (for example 'Haptics', 'Camera').
  • Returns — the module proxy, an object whose properties are its native methods. Never returns undefined; it throws when the module is missing.
  • Platform — iOS and Android.

callSync#

TypeScript
export function callSync<T = void>(module: string, method: string, ...args: any[]): T

Invokes a synchronous bridge method on the named module and returns its result directly, cast to T. Throws 'module_unavailable' (via getModule) when the module is not linked.

  • module — the native module name.
  • method — the method to invoke on that module.
  • ...args — arguments forwarded to the native method.
  • Returns — the native method's return value, typed as T (defaults to void).
  • Platform — iOS and Android.

callAsync#

TypeScript
export function callAsync<T = any>(module: string, method: string, ...args: any[]): Promise<T>

Invokes an async bridge method that takes a result callback as its last argument, wrapping it in a Promise that resolves with the callback's value.

The native method is expected to call a node-style callback that receives only a success value, not (err, res). There is no native-side error channel in this signature, so the returned Promise only rejects if the synchronous invocation itself throws — including the 'module_unavailable' SigxError when the module is not linked. A native module reports its own failures by resolving { error }; pass the result through unwrapNative to turn that into a rejection.

  • module — the native module name.
  • method — the async method to invoke.
  • ...args — arguments forwarded to the native method (the result callback is appended automatically).
  • Returns — a Promise<T> that resolves with the callback's value.
  • Platform — iOS and Android.

isModuleAvailable#

TypeScript
export function isModuleAvailable(name: string): boolean

Feature-detects whether a native module is registered in the current runtime, without throwing. Use this when you want to degrade gracefully rather than fail.

  • name — the native module name.
  • Returns — true if the module is registered, otherwise false.
  • Platform — iOS and Android.

guardModule#

TypeScript
export function guardModule(name: string): void

Throws the descriptive 'module_unavailable' SigxError if the module is not linked; meant for early failure at module-package entry points. Call it at the top of each public wrapper function so missing native linkage fails loudly and early.

  • name — the native module name to assert.
  • Returns — nothing. Throws if the module is missing.
  • Platform — iOS and Android.

base64ToArrayBuffer#

TypeScript
export function base64ToArrayBuffer(b64: string): ArrayBuffer

Decodes a base64 string into an ArrayBuffer. Uses atob when available, otherwise a pure-JS fallback decoder that tolerates whitespace and newlines, strips trailing padding, and sizes the output exactly (no trailing zero padding).

  • b64 — the base64-encoded string.
  • Returns — the decoded bytes as an ArrayBuffer.
  • Platform — iOS and Android.

arrayBufferToBase64#

TypeScript
export function arrayBufferToBase64(buf: ArrayBuffer | ArrayBufferView): string

Encodes an ArrayBuffer or typed-array view to base64 for native transport. Honors a view's byteOffset/byteLength — it encodes only the view's window, not the whole backing buffer — and chunks the encode (at 0x8000 bytes) to avoid call-stack limits on large payloads. Falls back to a pure-JS encoder that produces output identical to the native path when btoa is unavailable.

  • buf — an ArrayBuffer, Uint8Array, or any ArrayBufferView.
  • Returns — the base64-encoded string.
  • Platform — iOS and Android.

The module contract#

Three helpers every @sigx/lynx-* package uses to talk to the native side. Use them when writing your own native module rather than hand-rolling an emitter shim or an { error } unwrap — they carry the edge cases that only show up on a device.

subscribeNative#

TypeScript
export function subscribeNative<T = unknown>(
  channel: string,
  cb: (event: T) => void,
  options?: SubscribeNativeOptions<T>,
): () => void

export interface SubscribeNativeOptions<T> {
  /** Payload guard — events failing it are dropped rather than delivered as a malformed `T`. */
  validate?: (raw: unknown) => raw is T;
  /** Namespace for the "listener threw" diagnostic. Defaults to the channel name. */
  namespace?: string;
}

Subscribes to a native event channel on the runtime's GlobalEventEmitter. It handles the emitter lookup, a payload that arrives as a JSON string rather than an object, and a listener that throws (contained and reported through the logger, so one listener's bug cannot unsubscribe the others). Off-device — web preview, SSR, tests — it is a silent no-op returning a no-op disposer, so a package can subscribe unconditionally without branching on availability.

  • channel — the emitter channel name, for example '__sigxAppState'.
  • cb — called with each valid payload.
  • options.validate — a type guard; payloads that fail it are dropped.
  • options.namespace — tags the diagnostic; pass the package's own namespace so the warning is greppable.
  • Returns — an unsubscribe function, never a { remove() } object, so it is directly usable as an effect cleanup. Calling it more than once is a no-op.
  • Platform — iOS and Android; no-op elsewhere.

unwrapNative#

TypeScript
export function unwrapNative<T>(pkg: string, action: string, raw: T): T
export function unwrapNativeVoid(pkg: string, action: string, raw: unknown): void

callAsync rejects only when the synchronous bridge call throws; a native module reports its own failure by resolving { error: string }. unwrapNative turns that envelope into a rejection: it throws a SigxError with code: 'native_error', the message [@sigx/<pkg>] <action> failed: <cause> (so pkg: 'lynx-camera' yields [@sigx/lynx-camera] takePicture failed: …) and the raw payload on cause, and otherwise returns raw unchanged. unwrapNativeVoid is the same for methods whose success payload is nothing.

TypeScript
const raw = await callAsync<{ uri?: string; error?: string }>('Camera', 'takePicture', opts);
const { uri } = unwrapNative('lynx-camera', 'takePicture', raw);
  • pkg — the package name without the @sigx/ scope, for example 'lynx-camera'; it is interpolated verbatim into the [@sigx/<pkg>] prefix.
  • action — what was attempted, for the message — usually the method name.
  • raw — whatever the bridge resolved.
  • Platform — iOS and Android.

SigxError#

TypeScript
export class SigxError extends Error {
  /** Stable, machine-readable discriminant — branch on this, not the message. */
  readonly code: string;
  /** The `@sigx/lynx-*` package that raised it. */
  readonly package: string;
  /** The underlying value — usually the raw native payload. */
  readonly cause?: unknown;
  constructor(pkg: string, code: string, message: string, options?: { cause?: unknown });
}

export function isSigxError(e: unknown): e is SigxError

The error base for anything a caller might branch on. code is stable; the message is for humans and may change. isSigxError narrows unknown in a catch block. instanceof works after transpilation (the constructor restores the prototype).

Two codes cover every module's failures:

codeRaised byMeans
'module_unavailable'getModule, callSync, callAsync, guardModuleThe native module isn't in this build. Feature-detect with the module's isAvailable() instead of catching it.
'native_error'unwrapNative, unwrapNativeVoidThe platform reported a failure; the raw payload is on cause.

Individual packages may add their own codes (for example 'invalid_argument' in @sigx/lynx-secure-storage). See Handling errors for the cancel-is-not-an-error rule.

TypeScript
try {
  await Clipboard.setString(text);
} catch (e) {
  if (isSigxError(e) && e.code === 'native_error') showToast('Copy failed');
  else throw e;
}
  • Platform — iOS, Android and web.

Screen metrics and orientation#

Live screen metrics that follow rotation, split-screen resize and foldable unfold, plus the runtime orientation lock. All of these are re-exported from @sigx/lynx.

Platform.pixelWidth / pixelHeight are a load-time device snapshot and deliberately do not follow rotation — they describe the device. Anything that feeds layout should read useScreen() instead.

useScreen#

TypeScript
export function useScreen(): Computed<ScreenMetrics>

Background-thread reactive screen metrics. Read .value in render or effects and the component re-renders on rotation, split-screen resize or foldable unfold.

TSX
const screen = useScreen();
return () => <view class={screen.value.isLandscape ? 'flex-row' : 'flex-col'} />;
  • Returns — a stable Computed<ScreenMetrics>. Off-device it resolves to a typical-phone fallback.
  • Platform — iOS, Android and web (via the @sigx/lynx-web-host bridge).

useOrientation#

TypeScript
export function useOrientation(): Computed<CoarseOrientation>  // 'portrait' | 'landscape'

The coarse read for code that only branches portrait/landscape and should not re-run when the width alone moves.

useScreenMT#

TypeScript
export function useScreenMT(): ScreenMetrics

Synchronous screen metrics for 'main thread' worklet bodies. Reads the global directly with no subscription, so a gesture handler measures against the current viewport on every invocation. Resolves __globalProps.screen, then SystemInfo, then typical-phone constants.

useScreenMT is itself a 'main thread' function. Call it from worklet code only: in an app bundle its background-thread binding is a worklet reference, not a callable function. A worklet can capture and call it because a 'main thread' function crosses to the main thread as a callable worklet reference — see calling helpers from a worklet.

readGlobalScreen#

TypeScript
export function readGlobalScreen(): ScreenMetrics | null

Raw synchronous read of lynx.__globalProps.screen. Works on both threads — lynx.SystemInfo is empty on the background thread, but __globalProps is mirrored onto both, which makes this the background-safe screen-size accessor. Returns null before the publisher has populated (early cold start) or outside a Lynx host; prefer useScreen / useScreenMT when you just want a usable number.

ScreenMetrics#

TypeScript
export interface ScreenMetrics {
  /** Logical viewport width (dp on Android, pt on iOS). */
  width: number;
  /** Logical viewport height. */
  height: number;
  /** Logical-to-physical multiplier (`density` / `UIScreen.scale`). */
  scale: number;
  /** Fine-grained interface orientation. */
  orientation: ScreenOrientation;
  /** `true` for either landscape orientation — the common branch. */
  isLandscape: boolean;
}

export type ScreenOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
export type CoarseOrientation = 'portrait' | 'landscape';

landscape-left / landscape-right name the direction the device was rotated (iOS UIInterfaceOrientationLandscapeLeft/Right, Android Surface.ROTATION_90/270). Type-only exports.

Orientation#

TypeScript
const Orientation: {
  /** Request an orientation lock. Rejects if `to` isn't in the app's declared set. */
  lock(to: OrientationLock): Promise<void>;
  /** Release the runtime lock, restoring the build-time configured set. */
  unlock(): Promise<void>;
  /** Feature-detect the native module without throwing. */
  isAvailable(): boolean;
};

export type OrientationLock =
  | 'portrait'
  | 'portrait-upside-down'
  | 'landscape'          // either landscape; the device may still flip between them
  | 'landscape-left'
  | 'landscape-right'
  | 'all'                // every orientation, including upside-down portrait
  | 'default';           // release the runtime lock — same as unlock()

The runtime orientation lock, served by core's own SigxCore native module.

The build-time config is the ceiling. signalx.config.ts's orientation (default 'portrait') is written into android:screenOrientation and iOS's UISupportedInterfaceOrientations, and the OS will not rotate outside that set no matter what you ask for at runtime. Requesting an orientation the app did not declare rejects with a message naming the config change — it is a build misconfiguration, not a runtime condition to handle. Set orientation: 'default' (portrait + both landscapes) or 'all' to make runtime locking meaningful.

On iOS 15 a lock takes effect at the next physical rotation rather than snapping immediately — forcing rotation needs requestGeometryUpdate, which is iOS 16+. Raise ios.deploymentTarget to '16.0' for the immediate flip. On web, lock() is degraded (it needs the Screen Orientation API plus fullscreen) and rejects where unsupported.

TypeScript
import { Orientation } from '@sigx/lynx';

await Orientation.lock('landscape');   // video player
await Orientation.unlock();            // back to the configured set

Prefer useOrientationLock(to) from @sigx/lynx when the lock should last exactly as long as one screen is mounted — it locks on mount and, on unmount, restores the next holder down or unlocks when it was the last, so popping a landscape screen off another landscape-locked one keeps the lock:

TSX
import { useOrientationLock } from '@sigx/lynx';

const Player = component(() => {
  useOrientationLock('landscape');
  return () => <Video src={src} />;
});

Sheet detents (@sigx/lynx-sheet) and navigation transition geometry (@sigx/lynx-navigation) follow rotation automatically.

  • Platform — iOS and Android; degraded on web.

Window size classes#

Coarse, reactive buckets of the live window size, derived from useScreen — no second native subscription. They only notify when a threshold is crossed, so branching a layout on them doesn't re-render through a resize drag. All of these are re-exported from @sigx/lynx. See Adapting layout to the window size for guidance.

Breakpoint#

TypeScript
export const Breakpoint: {
  readonly WIDTH_MEDIUM: 600;
  readonly WIDTH_EXPANDED: 840;
  readonly WIDTH_LARGE: 1200;
  readonly WIDTH_XLARGE: 1600;
  readonly HEIGHT_MEDIUM: 480;
  readonly HEIGHT_EXPANDED: 900;
};

The lower bound of each bucket, in logical px (dp on Android, pt on iOS). Material's window-size-class thresholds.

WidthClass / HeightClass#

TypeScript
export type WidthClass = 'compact' | 'medium' | 'expanded' | 'large' | 'xlarge';
export type HeightClass = 'compact' | 'medium' | 'expanded';

Mobile-first: compact is the base case. Type-only exports.

useWidthAtLeast / useHeightAtLeast#

TypeScript
export function useWidthAtLeast(dp: number): Computed<boolean>
export function useHeightAtLeast(dp: number): Computed<boolean>

Background-thread reactive "is the window at least dp wide / tall?" — the primary read for layout branches. dp is compared against the real size, not snapped to a bucket, so any threshold is exact. The result is memoized per distinct dp for the life of the app, so pass a constant (typically a Breakpoint value), not a value derived per render.

TSX
const isWide = useWidthAtLeast(Breakpoint.WIDTH_EXPANDED);
return () => (isWide.value ? <SidebarLayout /> : <StackedLayout />);

useWidthClass / useHeightClass#

TypeScript
export function useWidthClass(): Computed<WidthClass>
export function useHeightClass(): Computed<HeightClass>

Background-thread reactive bucket. Re-fires only when the bucket changes, never on a pixel move within one.

useWidthClassMT / useHeightClassMT#

TypeScript
export function useWidthClassMT(): WidthClass
export function useHeightClassMT(): HeightClass

Synchronous bucket for 'main thread' worklet bodies, read against the current viewport on every invocation. There is no useWidthAtLeastMT — a worklet already has the number, so write useScreenMT().width >= Breakpoint.WIDTH_EXPANDED.

Both are 'main thread' functions, like useScreenMT: call them from worklet code only. For the same reason they compute the bucket inline rather than calling widthClassOf / heightClassOf — those are plain functions, so a worklet cannot call them.

widthClassOf / heightClassOf#

TypeScript
export function widthClassOf(width: number): WidthClass
export function heightClassOf(height: number): HeightClass

Pure bucket functions, for code that already holds a width or height.

App environment#

env#

The typed env block of the app's signalx.config.ts, with the active --variant's overrides merged in. It is baked into the bundle at build time (OTA bundles included) and deep-frozen. App code imports it from the umbrella; module authors can import it from @sigx/lynx-core.

TypeScript
import { env } from '@sigx/lynx'; // or '@sigx/lynx-core'

const res = await fetch(`${env.apiBaseUrl}/me`);
TypeScript
declare const env: Readonly<SigxAppEnv>;
  • {} when the app declares no env, or where the build didn't bake one in (tests, bare rspeedy).
  • Typed by the global SigxAppEnv interface, which the app extends once with EnvOf<typeof config> in src/sigx-env.d.ts.
  • Everything in it ships inside the app, so never put secrets in it.

See App environment for declaring, typing and overriding it per variant.

Platform#

Synchronous platform information and a select() helper, read once from the Lynx SystemInfo global at module load. Use it for runtime branching between iOS and Android (which share one native bundle) and for reading display metrics.

Platform#

TypeScript
const Platform: {
  /** `'ios' | 'android' | 'web'`. */
  OS: PlatformOS;
  /** OS version string, e.g. '17.4' / '14'. Empty when unknown. */
  Version: string;
  /** Best-effort iPad detection (iOS + logical min edge ≥ 600dp). Device identity, not a layout signal. */
  isPad: boolean;
  /** Device pixel ratio (physical px per dp). `1` when unknown. */
  pixelRatio: number;
  /** Physical screen width in px. `0` when unknown. */
  pixelWidth: number;
  /** Physical screen height in px. `0` when unknown. */
  pixelHeight: number;
  /** Lynx engine version string. Empty when unknown. */
  engineVersion: string;
  /** JS runtime, e.g. 'v8' | 'jsc' | 'quickjs'. Empty when unknown. */
  runtimeType: string;
  select: typeof select;
};

isPad is device identity, frozen at load: it stays true for an iPad in a narrow Split View pane and is false on Android tablets. Branch layout on window size classes instead.

Fields fall back to neutral defaults under tests, SSR, and non-Lynx hosts. On the web bundle, OS is 'web'; on native it is 'ios' or 'android', discriminated from SystemInfo.

  • Platform — iOS, Android and web.

Platform.select#

TypeScript
export function select<T>(spec: PlatformSelectSpec<T> & { default: T }): T;
export function select<T>(spec: PlatformSelectSpec<T>): T | undefined;

Picks a value for the current platform. Precedence: the exact OS key (ios / android / web) → native (matches ios/android only) → default. Presence-based, not truthiness — an explicit undefined for the matching key is honored. Providing default makes the return type non-optional T; omitting it makes it T | undefined.

TypeScript
const padding = Platform.select({ ios: 12, android: 8, default: 10 }); // number
const transport = Platform.select({ web: webTransport, native: nativeTransport });
  • Platform — iOS, Android and web.

OS#

TypeScript
export const OS: PlatformOS; // 'ios' | 'android' | 'web'

The bare current-platform constant, also exposed as Platform.OS. Note that Platform.OS === 'web' is a runtime check and does not tree-shake — for build-time, tree-shakeable web↔native splitting, branch on the __WEB__ / __NATIVE__ defines (see the platform-specific code guide).

  • Platform — iOS, Android and web.

Device info#

Async, native-backed device metadata served by core's own SigxCore native module. Complements the synchronous Platform surface.

DeviceInfo#

TypeScript
const DeviceInfo: {
  getInfo(): Promise<DeviceInfoResult>;
  isAvailable(): boolean;
};
  • Platform — iOS and Android.

DeviceInfo.getInfo#

Resolves a single snapshot of the device's hardware, OS, screen and app metadata as a platform-discriminated DeviceInfoResult. It calls the native SigxCore module's getDeviceInfo method. Values are a point-in-time snapshot and are not reactive — call it again to refresh.

TypeScript
getInfo(): Promise<DeviceInfoResult>
  • Returns — a Promise resolving to a DeviceInfoResult. Switch on its platform field to read the per-platform extras type-safely.
  • Platform — iOS and Android.

DeviceInfo.isAvailable#

TypeScript
isAvailable(): boolean

Returns whether the native SigxCore module is registered in the current build (delegates to isModuleAvailable). Use it to guard getInfo() where the module may not be linked.

  • Platform — iOS and Android.

Types#

PermissionStatus#

TypeScript
export type PermissionStatus = 'granted' | 'denied' | 'undetermined' | 'blocked'

Permission status string union returned by permission-using native modules. Type-only export.

  • Platform — iOS and Android.

PermissionResponse#

TypeScript
export interface PermissionResponse {
  /** Current permission status. */
  status: PermissionStatus;
  /**
   * Whether the app can show the OS permission dialog again.
   * `false` when the user selected "Don't ask again" (Android) or
   * after the first denial (iOS — user must go to Settings).
   */
  canAskAgain: boolean;
}

The shape returned by requestPermission() / getPermissionStatus() across permission-using modules. Combines a status with whether the OS dialog can be shown again. Type-only export.

  • status — the current PermissionStatus.
  • canAskAgain — false once the user picked "Don't ask again" on Android, or after the first denial on iOS.
  • Platform — iOS and Android.

PlatformOS#

TypeScript
export type PlatformOS = 'ios' | 'android' | 'web'

The current platform string — the type of Platform.OS and OS. Type-only export.

PlatformSelectSpec#

TypeScript
export interface PlatformSelectSpec<T> {
  ios?: T;
  android?: T;
  web?: T;
  /** Matches any native platform (ios/android), after the exact-OS key. */
  native?: T;
  /** Fallback when no platform key matches. */
  default?: T;
}

The spec object accepted by Platform.select. Type-only export.

DeviceInfoResult#

The platform-discriminated shape resolved by DeviceInfo.getInfo — a guaranteed common core plus per-platform extras, discriminated by platform.

TypeScript
export type DeviceInfoResult = IosDeviceInfo | AndroidDeviceInfo;

interface DeviceInfoCommon {
  platform: 'ios' | 'android'; // discriminant
  manufacturer: string;
  model: string;               // Android: friendly name ('Pixel 9'); iOS: generic ('iPhone')
  brand: string;
  systemName: string;
  systemVersion: string;
  appVersion: string;
  deviceId: string;            // iOS: stable per-vendor UUID; Android: Build.ID (a build id)
  screenWidth: number;         // density-independent points (dp/pt) on BOTH platforms
  screenHeight: number;        // density-independent points (dp/pt) on BOTH platforms
  screenScale: number;         // dp→px multiplier (physical px ≈ screenWidth * screenScale)
}

export interface IosDeviceInfo extends DeviceInfoCommon {
  platform: 'ios';
  modelName: string;           // hardware identifier, e.g. 'iPhone16,2'
  appBuildNumber: string;      // CFBundleVersion
  bundleId: string;
}

export interface AndroidDeviceInfo extends DeviceInfoCommon {
  platform: 'android';
  sdkVersion: number;          // Build.VERSION.SDK_INT
  appPackage: string;
}

Field semantics worth noting:

  • screenWidth / screenHeight are density-independent points (dp/pt) on both platforms; screenScale is the dp→px multiplier, so physical pixels ≈ screenWidth * screenScale.
  • model is a friendly name on Android but generic ('iPhone') on iOS — the iOS hardware identifier lives in the iOS-only modelName.
  • deviceId is a per-vendor stable UUID on iOS but Build.ID (a build id, not a stable device id) on Android.

Type-only export. Platform — iOS and Android.

See also#

  • Usage — practical patterns for wrapping native modules.
  • Overview — what the package is and where it fits.