UNPKG

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

Version:

React Native specific implementation of the app-bridge ecosystem

318 lines (262 loc) 10 kB
# @open-game-system/app-bridge-native React Native specific implementation of the app-bridge ecosystem. ## Installation ```bash npm install @open-game-system/app-bridge-native ``` ## API Reference ### createNativeBridge ```typescript /** * 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 ```typescript /** * 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 ```typescript 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 ```typescript 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 ```typescript 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)* ```typescript // 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: ```typescript <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.