Imported from hemedani/pejvak (
mobile/AGENTS.md). Install upstream withnpx skills add hemedani/pejvak --skill mobile. Copyright stays with the author.
AGENTS.md — Pejvak Mobile
Agent rules for the mobile/ workspace: the offline-first Android audio player for audiobooks, long-form content, and music. Read the root ../AGENTS.md first — it is authoritative. This file documents the mobile implementation rules. Where the product docs disagree, ../AGENTS.md wins.
Non-negotiable platform rule
Expo has changed. Read the exact versioned documentation at Expo SDK 57 before using or adding any Expo API, config plugin, native module, permission, background task, notification, storage, or media feature.
Confirm that every package supports the versions in package.json before installing it, and keep package-lock.json and package.json synchronized. Prefer the Expo-supported package for the installed SDK over an unverified native alternative.
Project context
- Framework: Expo SDK 57, Expo Router (file-based routing), React 19.2, React Native 0.86.
- Entry point:
expo-router/entry. - Package manager: npm (
package-lock.json). Do not mix in yarn/pnpm. - Source:
src/app(routes),src/components,src/hooks,src/constants;src/global.css. - Path aliases (
tsconfig.json):@/*→./src/*,@/assets/*→./assets/*. - Experiments enabled:
typedRoutesandreactCompiler(app.json). - Intended additions per
../AGENTS.md:src/services(TrackPlayerService, LocalDBService, SyncService),src/store(Zustand),src/lib(generated Lesan client wrapper). - Backend:
../back(Deno + Lesan + MongoDB). Generated client types live in../back/declarations/; consume those types, do not redefine backend schemas by hand. - Product specs:
../docs/DEEPSEEK.md(data models, session math, sync engine),../docs/GROK.md(vision, flows, testing),../docs/QWEN.md(Lesan schemas, offline sync, roadmap).
Product invariants
- Offline-first. Every write goes to SQLite first; a background
SyncServicepushes it later. Never block the UI on a network call. - Record every session. Each continuous listen produces a
PlaybackSessionwithstartedAt,endedAt,startPositionSec,endPositionSec,durationListenedSec,playbackSpeed,completed,interrupted, anddeviceInfo. - Integer seconds only. Never store audio offsets or durations as floats — audiobooks exceed 20 hours and floating-point drift is unacceptable.
contentHashis the identity of a track (SHA-256 of the first 1 MB + file size). Never dedupe or match by filename or path; history and annotations follow the hash across devices.durationListenedSecis wall-clock time played, notendPositionSec - startPositionSec. On each progress event computedelta = currentPosition - lastPosition; adddeltaonly when0 < delta < 5(normal play). On a seek (delta > 5ordelta < 0) update the position without adding.- Never lose a session on app kill. Persist a lightweight checkpoint every 10 s; on launch, recover any checkpoint without a matching ended session (
endedAt = checkpoint.timestamp). - Idempotent sync. Generate one persistent
clientIdper session/annotation and reuse it for every retry. After the server acknowledges, replace the temp id with the server id and mark the rowsynced. - Sessions are append-only. Never mutate a finalized session. A speed change or seek starts a new session.
- Annotations are editable; last-write-wins on
updatedAt. - Auth wire format: send the JWT in the
tokenheader with noBearerprefix, and treat the backend envelope{ success, body }as authoritative — checksuccessbefore readingbody.
Architecture rules
- Keep route screens thin. Put reusable behavior in hooks, services, or domain modules so it is testable without rendering.
- Own the player through a single
TrackPlayerService; own persistence throughLocalDBService; own delivery throughSyncService. Screens talk to these, not to SQLite orfetchdirectly. - All network calls go through the typed Lesan client in
src/lib, which wraps the generated standardlesanApifetch client (back/declarations/selectInp.ts). Do not add another HTTP or server-state library. All persistence goes throughLocalDBService. - Keep state separated: server data via the typed Lesan client (
src/lib), local queue/DB (SQLite), player/session state (Zustand), and UI state. Do not mirror the same data in two stores. - Strict TypeScript. No
any. Derive API types from the backend declarations; runnpm run gen:apiafter backend contract changes. - Never build SQL with user input; use parameterized
expo-sqliteAPIs and migrations. - Preserve sync state transitions explicitly:
pending -> syncing -> synced(orpending -> failed). A failed row stays queued for retry. - Make retries idempotent, bounded, observable, and resumable. Never create a duplicate session because a request timed out — the server keys on
clientId.
Audio engine (TrackPlayerService)
- Use Expo SDK 57 first-party
expo-audio(createAudioPlayer,setAudioModeAsync) for playback, background audio, and lock-screen controls. It replacedreact-native-track-player, which neither runs in Expo Go nor ships a New-Architecture codegen config for RN 0.86. - Background playback + lock-screen controls come from the
expo-audioconfig plugin (enableBackgroundPlayback: true, recording disabled) plusplayer.setActiveForLockScreen(...)at runtime. Expo Go cannot run the plugin's backgroundAudioControlsService, soTrackPlayerServicedetects Expo Go (Constants.executionEnvironment) and skips lock-screen/background calls there — foreground playback is unaffected, but background/lock-screen only work in a development/production build. - Never use
setIntervalin the JS thread to track position. DrivedurationListenedSecfrom the player'splaybackStatusUpdateevents (1 s update interval) through the puresrc/lib/sessionTrackingstate machine. - JS status events are not guaranteed while the app is backgrounded, so persist a checkpoint every 10 s and reconcile on resume/launch via
recoverOrphanedSessions()(finalizes any checkpoint whose session never ended). - On
play, end any open session and start a new one with the current position and speed. Onpause/stop/track end, finalize the session withendPositionSec,durationListenedSec, andcompleted. - A speed change splits the session; within-session seeks are ignored by the delta rule.
Local database (LocalDBService)
Local schema lives in expo-sqlite. Minimum tables: tracks, sessions, annotations, playlists, plus playback_checkpoints for kill recovery. Every syncable table carries:
id(local temp id),server_id(nullable), andsync_status(pending|syncing|synced|failed).- Renaming is not identity: match tracks by
content_hash. - Store positions/durations as integers (seconds), not floats.
Sync (SyncService)
- Batch rows where
sync_status = 'pending'. - Call the Lesan
syncLocalDataact with the batch (sessions and annotations keyed byclientId). - On success, write
server_id, setsync_status = 'synced'. - On failure, keep the row
pendingand retry later.
Trigger sync on app start, on returning to the foreground, after each session ends, and on a periodic timer. Never surface sync errors as blocking UI errors; the app must remain fully usable offline.
API and backend integration
- Send Lesan requests as
POSTbodies shaped{ model, act, details: { set, get } }; never invent REST paths. - The relevant backend acts are
register,login,getMe,registerTrack,getMyTracks, andsyncLocalData(see../back/AGENTS.md). - Use deep
getprojections to fetch exactly the fields a screen needs (e.g. a track with its recent sessions/annotations). - Request/response types must come from the generated declarations in
../back/declarations/; keep the mobile alias pointed at the backend output and update adapters when the generator changes.
Screens and annotation UX
- Home / Library — the user's tracks and playlists, with play count, last listened, and annotation count.
- Player — full controls, speed (0.5x–3.0x), sleep timer, and a timeline where every annotation is a colored marker; tapping a marker seeks to it.
- History — chronological session list grouped by day.
- Track Detail — per-track stats, session timeline, and annotations.
- Annotation editor — create/edit a note at a position (text, tags, color).
- Settings — account, sync status, storage.
Annotation behavior: long-press (or a quick-add control) creates a note at the current position in one step; markers on the progress bar seek to their position and show the note.
Testing and validation
- Jest + React Native Testing Library for units. Highest-value tests:
durationListenedSeccalculation, session finalization, checkpoint recovery, sync id mapping, and sync retry/idempotency. - Test offline → online recovery and the queue's bounded retry behavior.
- Run
npm run lintafter edits and the TypeScript check when applicable. - Do not claim a feature is complete until its offline, retry, and recovery states have been exercised.
Development commands
cd mobile
npm install
npm start # Expo dev server
npm run android # build & run on Android device/emulator
npm run lint # expo lint
Do not automatically start a development server, emulator, watcher, build, or other long-running process unless explicitly asked.
Agent rules
- Inspect the nearby route/component and its existing patterns before editing; prefer the smallest atomic change and avoid unrelated cleanup.
- Remove unused imports, variables, and debug logging introduced by your change.
- Do not commit, reset, or revert user changes unless explicitly requested.
- When unsure, re-read the relevant section of
../docs/and the root../AGENTS.md; do not invent APIs that contradict the backend.