@doync/mobile
v0.4.0
Published
doync React Native client: single-process ClientEngine over op-sqlite
Maintainers
Readme
@doync/mobile
Single-process React Native client for doync: one in-process engine, one WebSocket, one op-sqlite Replica.
Install
pnpm add @doync/mobile @doync/client @doync/core @doync/react
pnpm add @op-engineering/op-sqlite # bare RN
# or, for Expo dev builds:
npx expo install @op-engineering/op-sqlite@op-engineering/op-sqlite is a native module. It needs a dev build (npx expo prebuild / EAS Build / bare RN native compile). It does not run in Expo Go. react-native is also a peer (AppState foreground reconnect); the library never pins either peer's version.
There is no Expo SQLite / Expo Go path. Expo apps use a prebuild.
Wiring
One call boots the client. Pass the same shared definition module the web app uses (schema / queries / mutations from createDoync / createDoyncDrizzle), the Mirror WebSocket URL (required on RN — no origin to derive from), and one authData value (or null).
import { createMobileClient } from '@doync/mobile'
import { DoyncProvider, useQuery, useMutation, useConnectionStatus } from '@doync/react'
// Shared with the web app — one data layer for both clients.
import { ctxValidationSchema, schema, queries, mutations } from './shared/data'
const client = createMobileClient({
name: 'tasks', // Database name; defaults to 'doync'
schema,
queries,
mutations,
url: 'wss://example.com/sync',
authData: { userId, token, ctx }, // or null for anonymous
ctxValidationSchema, // types authData.ctx / updateAuth and validates here
// name, // optional; default 'doync'; use when you have multiple DO databases
// logoutBehavior: 'keep', // optional; omit leaves a stored choice alone
})
// Same hooks, same provider — nothing RN-specific in the React layer.
export function App() {
return (
<DoyncProvider client={client}>
<TaskList />
</DoyncProvider>
)
}createMobileClient is sync. Pass the same stable client object into <DoyncProvider> for the life of the app; drive login/logout through client.updateAuth rather than reconstructing.
Options
| Option | Required | Notes |
| --- | --- | --- |
| schema | yes | Consumer schema. |
| queries | yes | Nested query tree (parity with web's createWorker). |
| mutations | yes | Nested mutation tree. |
| url | yes | Mirror WebSocket URL. |
| authData | yes | AuthData \| null — { userId, token?, ctx } or null anonymous. |
| name | no | Database name; default 'doync'. |
| logoutBehavior | no | Initial 'keep' | 'forget' for this identity. Omit leaves a previously stored choice alone across restarts. |
Auth and logout
updateAuth asserts the whole identity (AuthData | null) atomically (shared semantics with @doync/client / @doync/web). Same userId (including both null) updates auth in place; a changed userId swaps to that identity's Replica while keeping the client object identity stable so <DoyncProvider> and live hooks do not remount.
client.updateAuth({ userId: nextUserId, token: nextToken, ctx: nextCtx })
client.updateAuth(null) // logoutPer-identity logout retention (LogoutBehavior — same policy as @doync/client / @doync/web):
'keep'(default) — outgoing Replica and pending queue stay on disk across auserIdchange. Logging back in boots warm.'forget'— outgoing Replica is deleted from the device. Use on a shared device.
const client = createMobileClient({
/* … */,
logoutBehavior: 'forget',
})
client.setLogoutBehavior('keep') // flip later; honored at the next swapRecovery verbs (same resync / forget contract as @doync/client):
client.resync() // wipe Replica / Memberships / cookie; drop pendings; mint fresh clientId
client.forget() // erase the active identity's local data
client.forget('other-user-id') // delete another identity's file only
client.close() // tear down socket + handle; leaves the durable file intactWhen you believe the network is back, connect your own NetInfo logic or a manual "Reconnect" button to client.reconnect(). The client already reconnects on its own when the app returns to the foreground; reconnect() is for the hints only you can see. It is safe to call anytime.
Connection and schema status for banners (same shapes as web / useSchemaStatus):
client.connectionStatus // 'connecting' | 'connected' | 'disconnected' | 'error' | 'needs-auth'
client.onConnectionChange(() => {
/* re-read client.connectionStatus */
})
client.schemaStatus // null when nominal, else { kind, message }
client.onSchemaChange(() => {
/* re-read client.schemaStatus */
})Usage with @doync/react
Hooks are environment-agnostic. After the Provider is mounted, web and mobile components share one shape:
import {
useQuery,
useMutation,
useConnectionStatus,
useSchemaStatus,
} from '@doync/react'
import { queries, mutations } from './shared/data'
function TaskList({ projectId }: { projectId: string }) {
const [tasks, status] = useQuery(queries.tasks.open({ projectId }))
const createTask = useMutation(mutations.task.create)
const connection = useConnectionStatus()
const schema = useSchemaStatus()
return (
<>
<Text>{connection}</Text>
{schema ? <Text>{schema.message}</Text> : null}
{status.status !== 'complete' && tasks.length === 0 ? (
<Text>Loading…</Text>
) : (
tasks.map((t) => <Text key={t.id}>{t.title}</Text>)
)}
<Button
onPress={() => {
void createTask({ id: newId(), projectId, title: 'New' }).client
}}
/>
</>
)
}See @doync/react for the full hook surface (useQueryOnce, useLocalQuery, falsy "no query", options).
One createMobileClient call = one Database name. An app with several synced databases runs several clients with distinct names and provides each where needed.
Public surface
| Export | Role |
| --- | --- |
| createMobileClient / CreateMobileClientOptions | Boot the RN client |
| MobileClient | Returned client (subscribe / warmup / once / local / mutate + auth / reconnect / recovery / close) |
| DoyncClient / View / OnceView / ViewStatus / QueryStatus / ConnectionStatus / SchemaEvent / SchemaEventKind / FalsyQuery / MutationOptions / MutationResult / LogoutBehavior / SubscribeOptions / PreloadOptions / PreloadHandle / WarmupOptions / WarmupHandle | Client call-surface family (re-exported from @doync/client) |
Internal API
The main entry (.) is the semver-governed public surface documented here. Anything imported from @doync/mobile/internal may change in any release, including patches, without notice — use it only if you accept that risk.
