@mustafaaksoy41/react-native-offline-queue 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +673 -0
  3. package/lib/commonjs/adapters/index.js +128 -0
  4. package/lib/commonjs/adapters/index.js.map +1 -0
  5. package/lib/commonjs/components/OfflineProvider.js +51 -0
  6. package/lib/commonjs/components/OfflineProvider.js.map +1 -0
  7. package/lib/commonjs/components/OfflineSyncPrompt.js +37 -0
  8. package/lib/commonjs/components/OfflineSyncPrompt.js.map +1 -0
  9. package/lib/commonjs/core/OfflineManager.js +308 -0
  10. package/lib/commonjs/core/OfflineManager.js.map +1 -0
  11. package/lib/commonjs/core/StorageAdapter.js +31 -0
  12. package/lib/commonjs/core/StorageAdapter.js.map +1 -0
  13. package/lib/commonjs/core/types.js +15 -0
  14. package/lib/commonjs/core/types.js.map +1 -0
  15. package/lib/commonjs/global.d.js +2 -0
  16. package/lib/commonjs/global.d.js.map +1 -0
  17. package/lib/commonjs/hooks/useOfflineMutation.js +61 -0
  18. package/lib/commonjs/hooks/useOfflineMutation.js.map +1 -0
  19. package/lib/commonjs/hooks/useOfflineQueue.js +21 -0
  20. package/lib/commonjs/hooks/useOfflineQueue.js.map +1 -0
  21. package/lib/commonjs/hooks/useOfflineSyncInterceptor.js +42 -0
  22. package/lib/commonjs/hooks/useOfflineSyncInterceptor.js.map +1 -0
  23. package/lib/commonjs/hooks/useSyncProgress.js +33 -0
  24. package/lib/commonjs/hooks/useSyncProgress.js.map +1 -0
  25. package/lib/commonjs/index.js +134 -0
  26. package/lib/commonjs/index.js.map +1 -0
  27. package/lib/commonjs/package.json +1 -0
  28. package/lib/module/adapters/index.js +121 -0
  29. package/lib/module/adapters/index.js.map +1 -0
  30. package/lib/module/components/OfflineProvider.js +43 -0
  31. package/lib/module/components/OfflineProvider.js.map +1 -0
  32. package/lib/module/components/OfflineSyncPrompt.js +31 -0
  33. package/lib/module/components/OfflineSyncPrompt.js.map +1 -0
  34. package/lib/module/core/OfflineManager.js +304 -0
  35. package/lib/module/core/OfflineManager.js.map +1 -0
  36. package/lib/module/core/StorageAdapter.js +25 -0
  37. package/lib/module/core/StorageAdapter.js.map +1 -0
  38. package/lib/module/core/types.js +11 -0
  39. package/lib/module/core/types.js.map +1 -0
  40. package/lib/module/global.d.js +2 -0
  41. package/lib/module/global.d.js.map +1 -0
  42. package/lib/module/hooks/useOfflineMutation.js +57 -0
  43. package/lib/module/hooks/useOfflineMutation.js.map +1 -0
  44. package/lib/module/hooks/useOfflineQueue.js +17 -0
  45. package/lib/module/hooks/useOfflineQueue.js.map +1 -0
  46. package/lib/module/hooks/useOfflineSyncInterceptor.js +38 -0
  47. package/lib/module/hooks/useOfflineSyncInterceptor.js.map +1 -0
  48. package/lib/module/hooks/useSyncProgress.js +29 -0
  49. package/lib/module/hooks/useSyncProgress.js.map +1 -0
  50. package/lib/module/index.js +20 -0
  51. package/lib/module/index.js.map +1 -0
  52. package/lib/module/package.json +1 -0
  53. package/lib/typescript/adapters/index.d.ts +12 -0
  54. package/lib/typescript/adapters/index.d.ts.map +1 -0
  55. package/lib/typescript/components/OfflineProvider.d.ts +13 -0
  56. package/lib/typescript/components/OfflineProvider.d.ts.map +1 -0
  57. package/lib/typescript/components/OfflineSyncPrompt.d.ts +11 -0
  58. package/lib/typescript/components/OfflineSyncPrompt.d.ts.map +1 -0
  59. package/lib/typescript/core/OfflineManager.d.ts +53 -0
  60. package/lib/typescript/core/OfflineManager.d.ts.map +1 -0
  61. package/lib/typescript/core/StorageAdapter.d.ts +21 -0
  62. package/lib/typescript/core/StorageAdapter.d.ts.map +1 -0
  63. package/lib/typescript/core/types.d.ts +23 -0
  64. package/lib/typescript/core/types.d.ts.map +1 -0
  65. package/lib/typescript/hooks/useOfflineMutation.d.ts +8 -0
  66. package/lib/typescript/hooks/useOfflineMutation.d.ts.map +1 -0
  67. package/lib/typescript/hooks/useOfflineQueue.d.ts +8 -0
  68. package/lib/typescript/hooks/useOfflineQueue.d.ts.map +1 -0
  69. package/lib/typescript/hooks/useOfflineSyncInterceptor.d.ts +9 -0
  70. package/lib/typescript/hooks/useOfflineSyncInterceptor.d.ts.map +1 -0
  71. package/lib/typescript/hooks/useSyncProgress.d.ts +23 -0
  72. package/lib/typescript/hooks/useSyncProgress.d.ts.map +1 -0
  73. package/lib/typescript/index.d.ts +11 -0
  74. package/lib/typescript/index.d.ts.map +1 -0
  75. package/package.json +73 -0
  76. package/src/adapters/index.ts +141 -0
  77. package/src/components/OfflineProvider.tsx +52 -0
  78. package/src/components/OfflineSyncPrompt.tsx +32 -0
  79. package/src/core/OfflineManager.ts +338 -0
  80. package/src/core/StorageAdapter.ts +42 -0
  81. package/src/core/types.ts +33 -0
  82. package/src/global.d.ts +1 -0
  83. package/src/hooks/useOfflineMutation.ts +63 -0
  84. package/src/hooks/useOfflineQueue.ts +17 -0
  85. package/src/hooks/useOfflineSyncInterceptor.ts +39 -0
  86. package/src/hooks/useSyncProgress.ts +32 -0
  87. package/src/index.ts +17 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mustafa Aksoy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,673 @@
1
+ <div align="center">
2
+
3
+ # 📡 react-native-offline-queue
4
+
5
+ **A lightweight, high-performance offline queue and sync manager for React Native.**
6
+ Queue operations when offline, sync automatically or manually when connectivity returns.
7
+
8
+ <br />
9
+
10
+ <!-- Package Info -->
11
+ [![npm version](https://img.shields.io/npm/v/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@mustafaaksoy41/react-native-offline-queue)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@mustafaaksoy41/react-native-offline-queue)
13
+ [![license](https://img.shields.io/npm/l/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=opensourceinitiative&logoColor=white&color=3DA639)](https://github.com/mustafaaksoy41/react-native-offline-queue/blob/main/LICENSE)
14
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=webpack&logoColor=white&color=8DD6F9&label=size)](https://bundlephobia.com/package/@mustafaaksoy41/react-native-offline-queue)
15
+
16
+ <!-- Platform & Language -->
17
+ [![Platform - Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://reactnative.dev/)
18
+ [![Platform - iOS](https://img.shields.io/badge/iOS-000000?style=for-the-badge&logo=apple&logoColor=white)](https://reactnative.dev/)
19
+ [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
20
+ [![React Native](https://img.shields.io/badge/React_Native-61DAFB?style=for-the-badge&logo=react&logoColor=black)](https://reactnative.dev/)
21
+
22
+ <!-- Supported Storage Adapters -->
23
+ [![MMKV](https://img.shields.io/badge/MMKV-FF6C37?style=for-the-badge&logo=firebase&logoColor=white)](https://github.com/mrousavy/react-native-mmkv)
24
+ [![AsyncStorage](https://img.shields.io/badge/AsyncStorage-764ABC?style=for-the-badge&logo=redux&logoColor=white)](https://react-native-async-storage.github.io/async-storage/)
25
+ [![Realm](https://img.shields.io/badge/Realm-39477F?style=for-the-badge&logo=realm&logoColor=white)](https://www.mongodb.com/docs/realm/sdk/react-native/)
26
+ [![In Memory](https://img.shields.io/badge/In_Memory-00C853?style=for-the-badge&logo=databricks&logoColor=white)](#storage-adapters)
27
+
28
+ <!-- Core Dependency -->
29
+ [![NetInfo](https://img.shields.io/badge/NetInfo-0088CC?style=for-the-badge&logo=wifi&logoColor=white)](https://github.com/react-native-netinfo/react-native-netinfo)
30
+
31
+ <br />
32
+
33
+ <p align="center">
34
+ <b>🔌 Offline-First</b> · <b>⚡ Optimistic UI</b> · <b>🔄 Auto / Manual Sync</b> · <b>📦 Pluggable Storage</b> · <b>📊 Live Progress</b>
35
+ </p>
36
+
37
+ ---
38
+
39
+ </div>
40
+
41
+ ## Features
42
+
43
+ - **Offline-first mutations** — API calls are queued when offline, executed when online
44
+ - **Optimistic UI updates** — UI responds instantly, data syncs in the background
45
+ - **Flexible sync modes** — `auto` (silent sync) or `manual` (prompt the user)
46
+ - **Pluggable storage** — MMKV, AsyncStorage, or bring your own adapter
47
+ - **Live sync progress** — track each item as it syncs (pending → syncing → success/failed)
48
+ - **Zero unnecessary renders** — built on `useSyncExternalStore`
49
+ - **Customizable restore UI** — Alert, Toast, BottomSheet, or silent — you decide
50
+ - **Background task compatible** — use `OfflineManager.flushQueue()` from any context
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install @mustafaaksoy41/react-native-offline-queue
56
+
57
+ # Required peer dependency
58
+ npm install @react-native-community/netinfo
59
+
60
+ # Pick ONE storage adapter (optional — defaults to in-memory)
61
+ npm install react-native-mmkv # Recommended: fast, synchronous
62
+ # OR
63
+ npm install @react-native-async-storage/async-storage
64
+ ```
65
+
66
+ ### iOS
67
+
68
+ ```bash
69
+ cd ios && pod install
70
+ ```
71
+
72
+ ## Quick Start
73
+
74
+ ### 1. Wrap your app with `OfflineProvider`
75
+
76
+ You can handle sync in two ways. Pick the one that fits your project:
77
+
78
+ **Option A — Per-action handlers (recommended)**
79
+
80
+ Each component defines its own API call. Provider stays clean:
81
+
82
+ ```tsx
83
+ // App.tsx
84
+ import { OfflineProvider } from '@mustafaaksoy41/react-native-offline-queue';
85
+
86
+ export default function App() {
87
+ return (
88
+ <OfflineProvider config={{ storageType: 'mmkv', syncMode: 'auto' }}>
89
+ <YourApp />
90
+ </OfflineProvider>
91
+ );
92
+ }
93
+ ```
94
+
95
+ **Option B — Centralized handler**
96
+
97
+ One global function handles all actions. Useful if you want a single place to manage API calls:
98
+
99
+ ```tsx
100
+ // App.tsx
101
+ import { OfflineProvider } from '@mustafaaksoy41/react-native-offline-queue';
102
+
103
+ const offlineConfig = {
104
+ storageType: 'mmkv',
105
+ syncMode: 'auto',
106
+ onSyncAction: async (action) => {
107
+ switch (action.actionName) {
108
+ case 'LIKE_POST':
109
+ await api.likePost(action.payload);
110
+ break;
111
+ case 'CREATE_POST':
112
+ await api.createPost(action.payload);
113
+ break;
114
+ case 'SEND_MESSAGE':
115
+ await api.sendMessage(action.payload);
116
+ break;
117
+ }
118
+ },
119
+ };
120
+
121
+ export default function App() {
122
+ return (
123
+ <OfflineProvider config={offlineConfig}>
124
+ <YourApp />
125
+ </OfflineProvider>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### 2. Use mutations in your components
131
+
132
+ **With per-action handler (Option A):**
133
+
134
+ Each component defines its own API call via `handler`:
135
+
136
+ ```tsx
137
+ import { useOfflineMutation } from '@mustafaaksoy41/react-native-offline-queue';
138
+
139
+ function LikeButton({ postId }) {
140
+ const [liked, setLiked] = useState(false);
141
+
142
+ const { mutateOffline } = useOfflineMutation('LIKE_POST', {
143
+ handler: async (payload) => {
144
+ await fetch('https://api.example.com/likes', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(payload),
148
+ });
149
+ },
150
+ onOptimisticSuccess: () => setLiked(true),
151
+ });
152
+
153
+ return (
154
+ <Button
155
+ title={liked ? '❤️' : '🤍'}
156
+ onPress={() => mutateOffline({ postId })}
157
+ />
158
+ );
159
+ }
160
+ ```
161
+
162
+ **Without handler (Option B):**
163
+
164
+ If you're using a centralized `onSyncAction`, just skip the `handler` — the global function will handle it:
165
+
166
+ ```tsx
167
+ function LikeButton({ postId }) {
168
+ const [liked, setLiked] = useState(false);
169
+
170
+ const { mutateOffline } = useOfflineMutation('LIKE_POST', {
171
+ onOptimisticSuccess: () => setLiked(true),
172
+ });
173
+
174
+ return (
175
+ <Button
176
+ title={liked ? '❤️' : '🤍'}
177
+ onPress={() => mutateOffline({ postId })}
178
+ />
179
+ );
180
+ }
181
+ ```
182
+
183
+ **How it works:**
184
+ - **Online**: The handler (or `onSyncAction`) runs immediately. No queue involved.
185
+ - **Offline**: The action is saved to the queue, and `onOptimisticSuccess` fires so the UI updates instantly.
186
+ - **When connectivity returns**: Queued actions are synced — per-action handler first, then `onSyncAction` as fallback.
187
+
188
+ ### Full Example
189
+
190
+ Here's what a real app looks like with multiple offline-capable actions. Each component owns its own API logic — no central switch-case needed.
191
+
192
+ ```tsx
193
+ // App.tsx
194
+ import { OfflineProvider } from '@mustafaaksoy41/react-native-offline-queue';
195
+
196
+ export default function App() {
197
+ return (
198
+ <OfflineProvider config={{ storageType: 'mmkv', syncMode: 'auto' }}>
199
+ <HomeScreen />
200
+ </OfflineProvider>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ```tsx
206
+ // CreatePostForm.tsx
207
+ import { useOfflineMutation } from '@mustafaaksoy41/react-native-offline-queue';
208
+
209
+ function CreatePostForm() {
210
+ const { mutateOffline } = useOfflineMutation('CREATE_POST', {
211
+ handler: async (payload) => {
212
+ await fetch('/api/posts', {
213
+ method: 'POST',
214
+ body: JSON.stringify(payload),
215
+ });
216
+ },
217
+ onOptimisticSuccess: (payload) => {
218
+ // Add to local list immediately
219
+ setPosts((prev) => [...prev, { ...payload, id: 'temp', pending: true }]);
220
+ },
221
+ });
222
+
223
+ return <Button title="Post" onPress={() => mutateOffline({ title, body })} />;
224
+ }
225
+ ```
226
+
227
+ ```tsx
228
+ // CommentSection.tsx
229
+ function CommentSection({ postId }) {
230
+ const { mutateOffline } = useOfflineMutation('ADD_COMMENT', {
231
+ handler: async (payload) => {
232
+ await fetch(`/api/posts/${payload.postId}/comments`, {
233
+ method: 'POST',
234
+ body: JSON.stringify({ text: payload.text }),
235
+ });
236
+ },
237
+ onOptimisticSuccess: (payload) => {
238
+ setComments((prev) => [...prev, { text: payload.text, pending: true }]);
239
+ },
240
+ });
241
+
242
+ return <Button title="Comment" onPress={() => mutateOffline({ postId, text })} />;
243
+ }
244
+ ```
245
+
246
+ ```tsx
247
+ // MessageBubble.tsx
248
+ function MessageBubble({ chatId }) {
249
+ const { mutateOffline } = useOfflineMutation('SEND_MESSAGE', {
250
+ handler: async (payload) => {
251
+ await fetch(`/api/chats/${payload.chatId}/messages`, {
252
+ method: 'POST',
253
+ body: JSON.stringify({ text: payload.text }),
254
+ });
255
+ },
256
+ onOptimisticSuccess: (payload) => {
257
+ addMessage({ text: payload.text, status: 'sending' });
258
+ },
259
+ });
260
+
261
+ return <Button title="Send" onPress={() => mutateOffline({ chatId, text: message })} />;
262
+ }
263
+ ```
264
+
265
+ Each `handler` is self-contained: when the user goes offline, actions are queued with their `actionName`. When connectivity returns, the queue flushes and each action runs through its registered handler automatically.
266
+
267
+ ## Configuration
268
+
269
+ ```tsx
270
+ interface OfflineManagerConfig {
271
+ // Storage backend for persisting the queue
272
+ storageType?: 'mmkv' | 'async-storage' | 'memory';
273
+ storage?: StorageAdapter; // Or pass a custom adapter
274
+
275
+ // Sync behavior when connectivity restores
276
+ syncMode?: 'auto' | 'manual';
277
+
278
+ // Key used for storage persistence
279
+ storageKey?: string;
280
+
281
+ // Handler that processes each queued action during sync
282
+ onSyncAction?: (action: OfflineAction) => Promise<void>;
283
+
284
+ // Called when device goes online with pending items (manual mode only)
285
+ onOnlineRestore?: (params: {
286
+ pendingCount: number;
287
+ syncNow: () => Promise<void>;
288
+ discardQueue: () => Promise<void>;
289
+ }) => void;
290
+ }
291
+ ```
292
+
293
+ ### Sync Modes
294
+
295
+ | Mode | Behavior |
296
+ |------|----------|
297
+ | `auto` | Queue is flushed silently as soon as connectivity returns |
298
+ | `manual` | `onOnlineRestore` callback fires — you decide what to show |
299
+
300
+ ## Handling Online Restore (Manual Mode)
301
+
302
+ When `syncMode` is `'manual'`, you control what happens when the device goes back online. Set the `onOnlineRestore` callback in your config.
303
+
304
+ ### Option A: Alert
305
+
306
+ ```tsx
307
+ onOnlineRestore: ({ pendingCount, syncNow, discardQueue }) => {
308
+ Alert.alert(
309
+ 'Back Online',
310
+ `${pendingCount} pending operations. Sync now?`,
311
+ [
312
+ { text: 'Later', style: 'cancel' },
313
+ { text: 'Discard', style: 'destructive', onPress: discardQueue },
314
+ { text: 'Sync', onPress: syncNow },
315
+ ]
316
+ );
317
+ },
318
+ ```
319
+
320
+ ### Option B: Toast
321
+
322
+ ```tsx
323
+ import Toast from 'react-native-toast-message';
324
+
325
+ onOnlineRestore: ({ pendingCount, syncNow }) => {
326
+ Toast.show({
327
+ type: 'info',
328
+ text1: 'Back online',
329
+ text2: `Tap to sync ${pendingCount} pending operations`,
330
+ onPress: () => {
331
+ syncNow();
332
+ Toast.hide();
333
+ },
334
+ });
335
+ },
336
+ ```
337
+
338
+ ### Option C: Bottom Sheet
339
+
340
+ ```tsx
341
+ import { bottomSheetRef } from './BottomSheetController';
342
+
343
+ onOnlineRestore: ({ pendingCount, syncNow }) => {
344
+ // Open your bottom sheet and pass sync controls
345
+ bottomSheetRef.current?.present({ pendingCount, syncNow });
346
+ },
347
+ ```
348
+
349
+ ### Option D: Silent
350
+
351
+ Omit `onOnlineRestore` entirely. Nothing happens — you handle sync manually through the `useOfflineQueue` hook.
352
+
353
+ ## Hooks
354
+
355
+ ### `useOfflineMutation(actionName, options?)`
356
+
357
+ Queue-aware mutation hook. Calls the handler directly when online, queues when offline.
358
+
359
+ ```tsx
360
+ const { mutateOffline } = useOfflineMutation('CREATE_POST', {
361
+ handler: async (payload) => {
362
+ // Your API call — runs directly when online, or during sync when offline
363
+ await api.createPost(payload);
364
+ },
365
+ onOptimisticSuccess: (payload) => {
366
+ // Runs immediately — update your local state here
367
+ },
368
+ onError: (error, payload) => {
369
+ // Runs if the direct API call fails while online
370
+ },
371
+ });
372
+
373
+ mutateOffline({ title: 'Hello', body: 'World' });
374
+ ```
375
+
376
+ | Option | Type | Description |
377
+ |--------|------|-------------|
378
+ | `handler` | `(payload) => Promise<void>` | API call for this specific action. Registered automatically. |
379
+ | `onOptimisticSuccess` | `(payload) => void` | Fires immediately for instant UI updates |
380
+ | `onError` | `(error, payload) => void` | Fires if the direct call fails while online |
381
+
382
+ ### `useOfflineQueue()`
383
+
384
+ Access the live queue state. Uses `useSyncExternalStore` under the hood — only re-renders when the queue actually changes.
385
+
386
+ ```tsx
387
+ const { queue, pendingCount, isSyncing, syncNow, clearQueue } = useOfflineQueue();
388
+ ```
389
+
390
+ | Property | Type | Description |
391
+ |----------|------|-------------|
392
+ | `queue` | `OfflineAction[]` | Current queue contents |
393
+ | `pendingCount` | `number` | Number of pending items |
394
+ | `isSyncing` | `boolean` | Whether a sync is in progress |
395
+ | `syncNow` | `() => Promise<void>` | Trigger a manual sync |
396
+ | `clearQueue` | `() => Promise<void>` | Remove all queued items |
397
+
398
+ ### `useNetworkStatus()`
399
+
400
+ Simple connectivity status.
401
+
402
+ ```tsx
403
+ const { isOnline } = useNetworkStatus();
404
+ ```
405
+
406
+ ### `useSyncProgress()`
407
+
408
+ Live progress tracking during a sync session. Useful for building progress UIs inside sheets or modals.
409
+
410
+ ```tsx
411
+ const {
412
+ isActive, // Is a sync session running?
413
+ totalCount, // Total items in this batch
414
+ completedCount, // Successfully synced
415
+ failedCount, // Items that failed
416
+ percentage, // 0–100
417
+ currentAction, // The action being synced right now
418
+ items, // Per-item status: pending | syncing | success | failed
419
+ } = useSyncProgress();
420
+ ```
421
+
422
+ **Example: Progress list inside a Bottom Sheet**
423
+
424
+ ```tsx
425
+ {items.map((item) => (
426
+ <View key={item.action.id} style={styles.row}>
427
+ <Text>{item.status === 'success' ? '✅' : item.status === 'failed' ? '❌' : '⏳'}</Text>
428
+ <Text>{item.action.actionName}</Text>
429
+ </View>
430
+ ))}
431
+ ```
432
+
433
+ ## Direct API Access
434
+
435
+ `OfflineManager` is a singleton accessible from anywhere — not just React components. Useful for background tasks, service layers, or testing.
436
+
437
+ ```tsx
438
+ import { OfflineManager } from '@mustafaaksoy41/react-native-offline-queue';
439
+
440
+ // Queue an action manually
441
+ await OfflineManager.push('SEND_MESSAGE', { text: 'hello' });
442
+
443
+ // Flush the queue
444
+ await OfflineManager.flushQueue();
445
+
446
+ // Read the queue
447
+ const items = OfflineManager.getQueue();
448
+
449
+ // Clear everything
450
+ await OfflineManager.clear();
451
+ ```
452
+
453
+ ## Background Sync
454
+
455
+ This package doesn't manage background tasks — that's platform-specific and depends on your setup. But `OfflineManager` works outside of React, so you can call it from any background task runner:
456
+
457
+ ```tsx
458
+ import BackgroundFetch from 'react-native-background-fetch';
459
+ import { OfflineManager } from '@mustafaaksoy41/react-native-offline-queue';
460
+
461
+ BackgroundFetch.configure({
462
+ minimumFetchInterval: 15,
463
+ }, async (taskId) => {
464
+ // Re-configure if the app was killed and relaunched
465
+ await OfflineManager.configure({
466
+ storageType: 'mmkv',
467
+ onSyncAction: myApiHandler,
468
+ });
469
+
470
+ await OfflineManager.flushQueue();
471
+ BackgroundFetch.finish(taskId);
472
+ });
473
+ ```
474
+
475
+ This works with any background task library: `react-native-background-fetch`, `expo-task-manager`, iOS `BGTaskScheduler` via a native module, etc.
476
+
477
+ ## Storage Adapters
478
+
479
+ ### Built-in Adapters
480
+
481
+ | Adapter | Install | Limits | Best For |
482
+ |---------|---------|--------|----------|
483
+ | **MMKV** | `npm i react-native-mmkv` | ~**unlimited** (file-backed, grows dynamically). Per-value performance drops above ~256KB. | Small-to-medium queues (<1000 items). Fastest read/write. |
484
+ | **AsyncStorage** | `npm i @react-native-async-storage/async-storage` | Android: **6MB total** (default). iOS: no hard limit (SQLite). Per-value: **2MB on Android**. | Apps that already use AsyncStorage. |
485
+ | **Memory** | built-in | RAM only — **lost on app kill**. | Development, testing, or ephemeral queues. |
486
+
487
+ > **How much queue data is realistic?**
488
+ > A typical queued action is ~200–500 bytes (JSON). So 1000 queued actions ≈ 500KB — well within limits for both MMKV and AsyncStorage.
489
+ >
490
+ > If your queue could grow beyond 5000+ items or individual payloads exceed 1MB (e.g. base64 images), consider Realm or a custom SQLite adapter.
491
+
492
+ ### Using a Built-in Adapter
493
+
494
+ ```tsx
495
+ // MMKV (recommended)
496
+ <OfflineProvider config={{ storageType: 'mmkv', ... }} />
497
+
498
+ // AsyncStorage
499
+ <OfflineProvider config={{ storageType: 'async-storage', ... }} />
500
+
501
+ // In-memory (no persistence)
502
+ <OfflineProvider config={{ storageType: 'memory', ... }} />
503
+ ```
504
+
505
+ ### Custom Storage Adapter
506
+
507
+ Implement the `StorageAdapter` interface to use any storage backend:
508
+
509
+ ```tsx
510
+ import { OfflineProvider, type StorageAdapter } from '@mustafaaksoy41/react-native-offline-queue';
511
+
512
+ const myStorage: StorageAdapter = {
513
+ getItem: async (key) => { /* return string | null */ },
514
+ setItem: async (key, value) => { /* persist string */ },
515
+ removeItem: async (key) => { /* delete key */ },
516
+ };
517
+
518
+ <OfflineProvider config={{ storage: myStorage, syncMode: 'auto', onSyncAction: handler }}>
519
+ <App />
520
+ </OfflineProvider>
521
+ ```
522
+
523
+ ### Realm Adapter (Built-in, Record-Based)
524
+
525
+ [Realm](https://www.mongodb.com/docs/realm/sdk/react-native/) stores each queue item as a separate database record. No JSON serialization overhead, no size limits, native query support.
526
+
527
+ ```bash
528
+ npm install realm
529
+ cd ios && pod install
530
+ ```
531
+
532
+ **Zero-config (default table created automatically):**
533
+
534
+ ```tsx
535
+ <OfflineProvider config={{ storageType: 'realm', syncMode: 'auto', onSyncAction: handler }}>
536
+ <App />
537
+ </OfflineProvider>
538
+ ```
539
+
540
+ This automatically creates an `OfflineQueueItem` table in a dedicated `offline-queue.realm` file:
541
+
542
+ | Column | Type | Description |
543
+ |--------|------|-------------|
544
+ | `id` | string (PK) | Unique action ID |
545
+ | `actionName` | string | e.g. `'LIKE_POST'` |
546
+ | `payload` | string | JSON-stringified payload |
547
+ | `createdAt` | int | Timestamp |
548
+ | `retryCount` | int | Failed attempt count |
549
+
550
+ **With your own Realm instance (shared with your app's data):**
551
+
552
+ ```tsx
553
+ import Realm from 'realm';
554
+ import { OfflineProvider } from '@mustafaaksoy41/react-native-offline-queue';
555
+
556
+ // Your app's Realm schemas + queue schema
557
+ const realm = await Realm.open({
558
+ schema: [UserSchema, PostSchema, OfflineQueueItemSchema],
559
+ });
560
+
561
+ <OfflineProvider config={{
562
+ storageType: 'realm',
563
+ realmOptions: { realmInstance: realm },
564
+ syncMode: 'auto',
565
+ onSyncAction: handler,
566
+ }}>
567
+ <App />
568
+ </OfflineProvider>
569
+ ```
570
+
571
+ **Performance comparison:**
572
+
573
+ | Operation | MMKV (key-value) | Realm (record-based) |
574
+ |-----------|-------------------|----------------------|
575
+ | Push 1 item (100 items in queue) | Rewrite 100 items as JSON | Insert 1 record |
576
+ | Push 1 item (10,000 items in queue) | Rewrite 10,000 items as JSON | Insert 1 record |
577
+ | Remove 1 item | Rewrite entire queue | Delete 1 record |
578
+ | Load on startup | Parse entire JSON string | Read table records |
579
+
580
+ **When to use Realm over MMKV:**
581
+ - Queue can grow to 10,000+ items
582
+ - Individual payloads are large (>1MB)
583
+ - You need query/filter capabilities on the queue
584
+ - Your app already uses Realm for other data
585
+
586
+ ## Types
587
+
588
+ ```tsx
589
+ interface OfflineAction<TPayload = any> {
590
+ id: string;
591
+ actionName: string;
592
+ payload: TPayload;
593
+ createdAt: number;
594
+ retryCount: number;
595
+ }
596
+
597
+ type SyncItemStatus = 'pending' | 'syncing' | 'success' | 'failed';
598
+
599
+ interface SyncProgressItem {
600
+ action: OfflineAction;
601
+ status: SyncItemStatus;
602
+ error?: string;
603
+ }
604
+ ```
605
+
606
+ ## Sync Strategies
607
+
608
+ You have two ways to define how queued actions get synced. Use them together or pick one — the per-action handler always takes priority.
609
+
610
+ ### Per-action handler (recommended)
611
+
612
+ Each mutation defines its own API call. The handler is registered automatically when the component mounts, and used during sync. This keeps the API logic next to the component that triggers it.
613
+
614
+ ```tsx
615
+ const { mutateOffline } = useOfflineMutation('LIKE_POST', {
616
+ handler: async (payload) => {
617
+ await api.likePost(payload);
618
+ },
619
+ });
620
+ ```
621
+
622
+ ### Global `onSyncAction` (fallback)
623
+
624
+ A catch-all function in the provider config. Useful as a fallback, or if you prefer managing all your API calls from one place.
625
+
626
+ ```tsx
627
+ <OfflineProvider config={{
628
+ storageType: 'mmkv',
629
+ syncMode: 'auto',
630
+ onSyncAction: async (action) => {
631
+ switch (action.actionName) {
632
+ case 'CREATE_POST':
633
+ await api.createPost(action.payload);
634
+ break;
635
+ case 'DELETE_COMMENT':
636
+ await api.deleteComment(action.payload);
637
+ break;
638
+ }
639
+ },
640
+ }}>
641
+ ```
642
+
643
+ ### Resolution order
644
+
645
+ When the queue flushes, each action is resolved like this:
646
+
647
+ 1. **Per-action handler** registered via `useOfflineMutation` → used if available
648
+ 2. **Global `onSyncAction`** from provider config → used as fallback
649
+ 3. **No handler found** → action fails with an error
650
+
651
+ ## How It Works
652
+
653
+ The whole thing is built around a single `OfflineManager` singleton. It holds the queue in memory, persists it through whichever storage adapter you pick, and handles the sync logic.
654
+
655
+ `OfflineProvider` wraps your app and wires everything up: it listens for connectivity changes via NetInfo, configures the manager with your settings, and exposes the queue state to hooks.
656
+
657
+ From your components, you interact through hooks:
658
+
659
+ - **`useOfflineMutation`** — defines the API handler and pushes actions to the queue (or calls the handler directly if online)
660
+ - **`useOfflineQueue`** — reads the current queue state without unnecessary re-renders
661
+ - **`useSyncProgress`** — gives you per-item progress during a sync session
662
+
663
+ When the device comes back online, the manager either flushes the queue automatically or calls your `onOnlineRestore` callback, depending on the sync mode you chose. Each queued action is resolved through its registered handler first, then falls back to the global `onSyncAction`.
664
+
665
+ Storage is abstracted behind a simple `getItem` / `setItem` / `removeItem` interface, so swapping between MMKV, AsyncStorage, Realm, or your own backend is just a config change.
666
+
667
+ ## License
668
+
669
+ MIT
670
+
671
+ ---
672
+
673
+ <p align="center">Made with ❤️ by <a href="https://www.npmjs.com/~mustafaaksoy41">Mustafa Aksoy</a></p>