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

API reference#

Exports of @sigx/lynx-core v0.28.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,
} from '@sigx/lynx-core';
import type {
  PermissionStatus,
  PermissionResponse,
  PlatformOS,
  PlatformSelectSpec,
  DeviceInfoResult,
  IosDeviceInfo,
  AndroidDeviceInfo,
  SubscribeNativeOptions,
  ScreenMetrics,
  ScreenOrientation,
  CoarseOrientation,
  OrientationLock,
} 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 descriptive, actionable error 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.

  • 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.

  • 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.
  • Returnstrue if the module is registered, otherwise false.
  • Platform — iOS and Android.

guardModule#

TypeScript
export function guardModule(name: string): void

Throws the descriptive "module not available" error 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).

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.

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.

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). */
  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;
};

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.
  • canAskAgainfalse 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.