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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@open-game-system/app-bridge-native

v0.20250411.3

Published

React Native specific implementation of the app-bridge ecosystem

Readme

@open-game-system/app-bridge-native

React Native specific implementation of the app-bridge ecosystem.

Installation

npm install @open-game-system/app-bridge-native

API Reference

createNativeBridge

/**
 * Creates a native bridge instance for use in React Native applications
 * @template TStores Store definitions for the bridge
 * @returns A NativeBridge instance
 */
function createNativeBridge<TStores extends BridgeStores>(): NativeBridge<TStores>;

createStore

/**
 * Creates a new store with the given configuration
 */
function createStore<S extends State, E extends Event>(config: StoreConfig<S, E>): Store<S, E>;
  • config.producer: An optional function (draft: S, event: E) => void (compatible with Immer) that modifies the state based on dispatched events.
  • config.on: An optional object where keys are event types (E['type']) and values are listener functions (event: E, store: Store<S, E>) => Promise<void> | void. These listeners are executed after the producer updates the state for a given dispatched event. They can be async and have access to the store instance (e.g., to dispatch further events or read the latest state).

NativeBridge Interface

interface NativeBridge<TStores extends BridgeStores> {
  /**
   * Get a store by its key
   * Returns undefined if the store doesn't exist
   */
  getStore: <K extends keyof TStores>(
    key: K
  ) => Store<TStores[K]["state"], TStores[K]["events"]> | undefined;

  /**
   * Set or remove a store for a given key
   */
  setStore: <K extends keyof TStores>(
    key: K,
    store: Store<TStores[K]["state"], TStores[K]["events"]> | undefined
  ) => void;

  /**
   * Subscribe to store availability changes
   * Returns an unsubscribe function
   */
  subscribe: (listener: () => void) => () => void;

  /**
   * Process a message received from the WebView
   */
  handleWebMessage: (message: string | { nativeEvent: { data: string } }) => void;

  /**
   * Register a WebView to receive state updates
   * Returns an unsubscribe function
   */
  registerWebView: (webView: WebView | null | undefined) => () => void;

  /**
   * Unregister a WebView from receiving state updates
   */
  unregisterWebView: (webView: WebView | null | undefined) => void;

  /**
   * Subscribe to ready state changes for a specific WebView
   * Returns an unsubscribe function
   */
  subscribeToReadyState: (
    webView: WebView | null | undefined,
    callback: (isReady: boolean) => void
  ) => () => void;

  /**
   * Get the current ready state for a specific WebView
   */
  getReadyState: (webView: WebView | null | undefined) => boolean;
}

Store Interface

interface Store<S extends State, E extends Event> {
  /**
   * Get the current state
   */
  getSnapshot: () => S;

  /**
   * Subscribe to state changes
   * Returns an unsubscribe function
   */
  subscribe: (callback: (state: S) => void) => () => void;

  /**
   * Dispatch an event to the store. Synchronously updates the state via the producer
   * and then triggers any configured or dynamic 'on' listeners for the event type.
   * Async listeners are not awaited.
   */
  dispatch: (event: E) => void;

  /**
   * Reset store to its initial state
   */
  reset: () => void;

  /**
   * Dynamically add a listener for a specific dispatched event type.
   * Listeners can be async and receive the event object and the store instance.
   * Returns an unsubscribe function.
   */
  on: <EventType extends E['type']>(
    eventType: EventType,
    listener: (event: Extract<E, { type: EventType }>, store: Store<S, E>) => Promise<void> | void
  ) => () => void;
}

Usage Examples

Example 1: Store with on Config for Side Effects

import { createNativeBridge, createStore } from '@open-game-system/app-bridge-native';
import type { AppStores, CounterState, CounterEvents } from './types'; // Assuming types are defined

function MyComponent() {
  const webViewRef = useRef<WebView>(null);
  const bridge = useMemo(() => createNativeBridge<AppStores>(), []);

  // Create and register a store with initial state and declarative listeners
  useEffect(() => {
    const store = createStore<CounterState, CounterEvents>({
      initialState: { value: 0 },
      producer: (draft, event) => {
        switch (event.type) {
          case "INCREMENT":
            draft.value += 1;
            break;
          case "DECREMENT":
            if (draft.value > 0) draft.value -= 1;
            break;
        }
      },
      // Declarative listeners in config
      on: {
          INCREMENT: async (event, store) => {
              console.log(`[Config] Incremented! New value: ${store.getSnapshot().value}`);
              // Example: Dispatch another event after a delay
              await new Promise(r => setTimeout(r, 500));
              store.dispatch({ type: "LOG_UPDATE", value: store.getSnapshot().value }); // Assuming LOG_UPDATE exists
          },
          DECREMENT: (event, store) => {
              console.log(`[Config] Decremented! New value: ${store.getSnapshot().value}`);
          }
      }
    });

    bridge.setStore('counter', store);

    // Example: Adding a dynamic listener
    const unsubscribeLog = store.on('LOG_UPDATE', (event) => {
        console.log('[Dynamic] Logged update:', event.value);
    });

    return () => {
        bridge.setStore('counter', undefined);
        unsubscribeLog(); // Clean up dynamic listener
    };
  }, [bridge]);

  // Register WebView
  useEffect(() => {
    const unregister = bridge.registerWebView(webViewRef.current);
    return () => unregister();
  }, []);

  return (
    <WebView 
      ref={webViewRef}
      onMessage={event => bridge.handleWebMessage(event.nativeEvent.data)}
    />
  );
}

Example 2: Complete React Context Integration (Conceptual)

(The structure remains largely the same as the previous example in the README, but highlights where the new on config and store.on method would fit)

// types.ts - (Assume types defined as before)
import type { BridgeStores, State, Event } from '@open-game-system/app-bridge-types';

interface CounterState extends State { /* ... */ }
interface LogState extends State { messages: string[] };
type CounterEvents = { type: 'INCREMENT' } | { type: 'DECREMENT' };
type LogEvents = { type: 'LOG'; message: string };

export interface AppStores extends BridgeStores {
  counter: { state: CounterState; events: CounterEvents };
  logger: { state: LogState; events: LogEvents };
}

// BridgeContext.tsx
import React, { /* ... */ } from 'react';
import { /* ... */ } from 'react-native';
import { createNativeBridge, createStore } from '@open-game-system/app-bridge-native';
import type { AppStores, CounterState, CounterEvents, LogState, LogEvents } from './types';

const BridgeContext = createContext<ReturnType<typeof createNativeBridge<AppStores>> | null>(null);

export function BridgeProvider({ children }: { children: React.ReactNode }) {
  const webViewRef = useRef<WebView>(null);
  const bridge = useMemo(() => createNativeBridge<AppStores>(), []);

  useEffect(() => {
    // Create logger store
    const loggerStore = createStore<LogState, LogEvents>({
        initialState: { messages: [] },
        producer: (draft, event) => {
            if (event.type === 'LOG') {
                draft.messages.push(event.message);
            }
        }
    });

    // Create counter store with 'on' config to dispatch to logger
    const counterStore = createStore<CounterState, CounterEvents>({
      initialState: { value: 0 },
      producer: (draft, event) => { /* ... increment/decrement logic ... */ },
      on: {
          INCREMENT: (event, store) => {
              // Dispatch to another store from listener
              loggerStore.dispatch({ type: 'LOG', message: `Incremented to ${store.getSnapshot().value}` });
          },
          DECREMENT: (event, store) => {
              loggerStore.dispatch({ type: 'LOG', message: `Decremented to ${store.getSnapshot().value}` });
          }
      }
    });

    bridge.setStore('counter', counterStore);
    bridge.setStore('logger', loggerStore);

    return () => {
        bridge.setStore('counter', undefined);
        bridge.setStore('logger', undefined);
    }
  }, [bridge]);

  useEffect(() => {
    const unregister = bridge.registerWebView(webViewRef.current);
    const unsubscribeReady = bridge.subscribeToReadyState(webViewRef.current, (isReady) => {
      if (isReady) {
        console.log('Bridge is ready for communication!');
      }
    });

    return () => {
      unregister();
      unsubscribeReady();
    };
  }, []);

  return (
    <BridgeContext.Provider value={bridge}>
      <WebView
        ref={webViewRef}
        onMessage={event => bridge.handleWebMessage(event)}
        // ... other WebView props
      />
      {children}
    </BridgeContext.Provider>
  );
}

export function useBridge() {
  const bridge = useContext(BridgeContext);
  if (!bridge) throw new Error('useBridge must be used within BridgeProvider');
  return bridge;
}

// Counter.tsx / Logger.tsx (Components would use useBridge and store methods as before)
// ... Example component implementations ...

Important: WebView Message Handling

The bridge requires proper message handling to function:

  1. Connect WebView Messages: You MUST connect the WebView's onMessage event to the bridge:
<WebView 
  onMessage={event => bridge.handleWebMessage(event)}
/>

Without this connection:

  • The bridge won't receive events or the ready signal from the web bridge
  • The subscribeToReadyState callback won't fire with true
  • State updates won't be sent to or received from the WebView

Note: The web bridge automatically sends the ready signal and handles internal initialization messages when registerWebView and handleWebMessage are correctly set up. You do not need to handle STATE_INIT or manually signal readiness.