// The Voltro client for React Native. // // Three pieces, all of them SHARED with the web client rather than // re-implemented for mobile: // // · `mobileApis` — the generated binding (`.framework/mobileApis.generated.ts`, // written by `voltro codegen` from `voltro.mobile.ts`). It carries the api's // rpc group and descriptors, so the hooks are typed end to end from the // api's own schema. // · `startMobileApis` — connect + reconnect, over the SAME supervisor // `@voltro/web` uses: exponential backoff, generation tracking, and the // stale-seed gate that must not carry one subject's rows into the next. // · `createAsyncStoragePersistence` — persisted stores over AsyncStorage, // hydrated BEFORE the first render (a render cannot await). // // The ws URL is resolved at runtime, never baked in. `localhost` on a phone is // the PHONE — the single most common reason a mobile dev loop silently never // connects — so `resolveDevWsUrl` takes the LAN host Expo already knows. import Constants from 'expo-constants' import NetInfo from '@react-native-community/netinfo' import { setStoreStorage, type ResolvedClient } from '@voltro/client' import { createAsyncStoragePersistence, netInfoOnlineSource, offlineFirstDefaults, resolveDevWsUrl, startMobileApis, toApiHandles, } from '@voltro/react-native' import { mobileApis } from '../.framework/mobileApis.generated' import { storeStorage } from './persistence' /** The api's port, as `voltro dev` allocates it. Override per environment. */ const API_PORT = Number(process.env.EXPO_PUBLIC_API_PORT ?? 4000) /** * The api's WebSocket URL. * * In development this is derived from the host running Metro, so a real device * on the same wifi reaches your machine. In production set * `EXPO_PUBLIC_API_WS_URL` to the deployed api. */ export const apiWsUrl = (): string => process.env.EXPO_PUBLIC_API_WS_URL ?? resolveDevWsUrl(Constants.expoConfig?.hostUri, API_PORT) /** The offline-first posture (local-first on, optimistic writes, sync status * surfaced). Spread into a client config, or read for UI decisions. */ export const clientPosture = offlineFirstDefaults /** Reachability, from NetInfo. Module scope on purpose: an inline * `netInfoOnlineSource(NetInfo)` is a new object every render, and the hook * would resubscribe on each one. */ export const onlineSource = netInfoOnlineSource(NetInfo) /** Persisted stores, backed by AsyncStorage — NOT the Keychain. `persistence.ts` * says why, and where a secret goes instead. */ export const persistence = createAsyncStoragePersistence({ storage: storeStorage }) /** * Load persisted state and install it as the backing store for every * `defineStore({ persist })`. * * MUST be awaited before the first render. A store reads during render and a * render cannot await, so an app that renders first shows empty state and then * flickers into the saved one. The root layout holds the splash screen until * this resolves. */ export const preparePersistence = async (): Promise => { await persistence.hydrate() setStoreStorage(persistence.provider) } /** Every api this app talks to, bound to the resolved ws URL. */ export const apis = mobileApis(() => apiWsUrl()) /** * Connect the apis and keep them connected. * * `onChange` fires on the first connect, on every reconnect swap, and on * teardown. Dispose the returned handle when the tree unmounts; call * `reconnect()` after a sign-in so the connection re-resolves who it is. */ export const connectApis = ( onChange: (clients: ReadonlyMap, initialized: boolean) => void, ) => startMobileApis({ apis, onChange }) /** Map the live clients into the `apis` prop the client provider takes. */ export const handlesFor = (clients: ReadonlyMap) => toApiHandles(apis, clients)