@mustafaaksoy41/react-native-offline-queue 0.1.3 → 0.1.4
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.
- package/README.md +118 -82
- package/lib/commonjs/hooks/useOfflineMutation.js +29 -6
- package/lib/commonjs/hooks/useOfflineMutation.js.map +1 -1
- package/lib/module/hooks/useOfflineMutation.js +30 -7
- package/lib/module/hooks/useOfflineMutation.js.map +1 -1
- package/lib/typescript/hooks/useOfflineMutation.d.ts +14 -3
- package/lib/typescript/hooks/useOfflineMutation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useOfflineMutation.ts +50 -8
package/README.md
CHANGED
|
@@ -307,39 +307,34 @@ Each `handler` is self-contained: when the user goes offline, actions are queued
|
|
|
307
307
|
|
|
308
308
|
### Using with React Query (TanStack Query)
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
Use your **existing `useMutation` hook** — the handler calls `mutateAsync`. No duplicate fetch, no extra API layer. Your mutation does everything (POST, cache invalidation, etc.).
|
|
311
311
|
|
|
312
|
-
**Pattern
|
|
313
|
-
|
|
314
|
-
Define your API calls in one place. Use them in the offline queue `handler` and optionally in React Query's `useMutation`:
|
|
312
|
+
**Pattern — Handler uses `mutateAsync` from `useMutation`**
|
|
315
313
|
|
|
316
314
|
```tsx
|
|
317
|
-
|
|
318
|
-
export async function createPost(payload: { title: string; body: string }) {
|
|
319
|
-
const res = await fetch('https://api.myapp.com/posts', {
|
|
320
|
-
method: 'POST',
|
|
321
|
-
headers: { 'Content-Type': 'application/json' },
|
|
322
|
-
body: JSON.stringify(payload),
|
|
323
|
-
});
|
|
324
|
-
if (!res.ok) throw new Error(await res.text());
|
|
325
|
-
return res.json();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// CreatePostForm.tsx
|
|
329
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
315
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
330
316
|
import { useOfflineMutation } from '@mustafaaksoy41/react-native-offline-queue';
|
|
331
|
-
import { createPost } from './api/posts';
|
|
332
317
|
|
|
333
318
|
function CreatePostForm() {
|
|
334
319
|
const queryClient = useQueryClient();
|
|
335
320
|
|
|
321
|
+
// Your existing React Query mutation — mutationFn, onSuccess, retry, etc.
|
|
322
|
+
const { mutateAsync } = useMutation({
|
|
323
|
+
mutationFn: (payload: { title: string; body: string }) =>
|
|
324
|
+
fetch('https://api.myapp.com/posts', {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: { 'Content-Type': 'application/json' },
|
|
327
|
+
body: JSON.stringify(payload),
|
|
328
|
+
}).then((r) => r.json()),
|
|
329
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Handler = your mutation. Online: runs immediately. Offline: queued, runs when back online.
|
|
336
333
|
const { mutateOffline } = useOfflineMutation('CREATE_POST', {
|
|
337
334
|
handler: async (payload) => {
|
|
338
|
-
await
|
|
339
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
335
|
+
await mutateAsync(payload);
|
|
340
336
|
},
|
|
341
337
|
onOptimisticSuccess: (payload) => {
|
|
342
|
-
// Add to local list immediately (optimistic update)
|
|
343
338
|
queryClient.setQueryData(['posts'], (old: any) =>
|
|
344
339
|
old ? [...old, { ...payload, id: 'temp', pending: true }] : [payload]
|
|
345
340
|
);
|
|
@@ -355,62 +350,40 @@ function CreatePostForm() {
|
|
|
355
350
|
}
|
|
356
351
|
```
|
|
357
352
|
|
|
358
|
-
**
|
|
353
|
+
**With custom hooks / API layer**
|
|
359
354
|
|
|
360
|
-
|
|
355
|
+
If your mutations live in custom hooks:
|
|
361
356
|
|
|
362
357
|
```tsx
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
import { OfflineProvider } from '@mustafaaksoy41/react-native-offline-queue';
|
|
366
|
-
import { createPost, likePost } from './api';
|
|
367
|
-
|
|
368
|
-
function AppWithProviders() {
|
|
358
|
+
// hooks/useCreatePost.ts
|
|
359
|
+
export function useCreatePost() {
|
|
369
360
|
const queryClient = useQueryClient();
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
storageType: 'mmkv',
|
|
375
|
-
syncMode: 'auto',
|
|
376
|
-
onSyncAction: async (action) => {
|
|
377
|
-
switch (action.actionName) {
|
|
378
|
-
case 'CREATE_POST':
|
|
379
|
-
await createPost(action.payload);
|
|
380
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
381
|
-
break;
|
|
382
|
-
case 'LIKE_POST':
|
|
383
|
-
await likePost(action.payload);
|
|
384
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
}}
|
|
389
|
-
>
|
|
390
|
-
<YourApp />
|
|
391
|
-
</OfflineProvider>
|
|
392
|
-
);
|
|
361
|
+
return useMutation({
|
|
362
|
+
mutationFn: api.createPost, // your axios/fetch wrapper
|
|
363
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
364
|
+
});
|
|
393
365
|
}
|
|
394
366
|
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
367
|
+
// CreatePostForm.tsx
|
|
368
|
+
function CreatePostForm() {
|
|
369
|
+
const { mutateAsync } = useCreatePost();
|
|
370
|
+
|
|
371
|
+
const { mutateOffline } = useOfflineMutation('CREATE_POST', {
|
|
372
|
+
handler: async (payload) => await mutateAsync(payload),
|
|
373
|
+
onOptimisticSuccess: (payload) => { /* ... */ },
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return <Button onPress={() => mutateOffline({ title, body })} />;
|
|
402
377
|
}
|
|
403
378
|
```
|
|
404
379
|
|
|
405
380
|
**Summary**
|
|
406
381
|
|
|
407
|
-
|
|
|
408
|
-
|
|
409
|
-
|
|
|
410
|
-
| Cache invalidation | `queryClient.invalidateQueries()` inside `handler` or `onSyncAction` |
|
|
411
|
-
| Optimistic updates | `onOptimisticSuccess` + `queryClient.setQueryData()` |
|
|
382
|
+
| Handler does | Your mutation does |
|
|
383
|
+
|--------------|--------------------|
|
|
384
|
+
| Calls `mutateAsync(payload)` | POST/GET, retry, cache invalidation, error handling |
|
|
412
385
|
|
|
413
|
-
|
|
386
|
+
No duplicate logic. Same mutation for online and offline sync.
|
|
414
387
|
|
|
415
388
|
## Configuration
|
|
416
389
|
|
|
@@ -502,31 +475,94 @@ Omit `onOnlineRestore` entirely. Nothing happens — you handle sync manually th
|
|
|
502
475
|
|
|
503
476
|
### `useOfflineMutation(actionName, options?)`
|
|
504
477
|
|
|
505
|
-
Queue-aware mutation hook. Calls the handler directly when online, queues when offline.
|
|
478
|
+
Queue-aware mutation hook with built-in state tracking. Calls the handler directly when online, queues when offline.
|
|
506
479
|
|
|
507
|
-
|
|
508
|
-
const { mutateOffline } = useOfflineMutation('CREATE_POST', {
|
|
509
|
-
handler: async (payload) => {
|
|
510
|
-
// Your API call — runs directly when online, or during sync when offline
|
|
511
|
-
await api.createPost(payload);
|
|
512
|
-
},
|
|
513
|
-
onOptimisticSuccess: (payload) => {
|
|
514
|
-
// Runs immediately — update your local state here
|
|
515
|
-
},
|
|
516
|
-
onError: (error, payload) => {
|
|
517
|
-
// Runs if the direct API call fails while online
|
|
518
|
-
},
|
|
519
|
-
});
|
|
480
|
+
**Returns:**
|
|
520
481
|
|
|
521
|
-
|
|
482
|
+
```tsx
|
|
483
|
+
const {
|
|
484
|
+
mutateOffline, // (payload) => Promise<void>
|
|
485
|
+
status, // 'idle' | 'loading' | 'success' | 'error' | 'queued'
|
|
486
|
+
isIdle, // true before any mutation
|
|
487
|
+
isLoading, // true while the handler is running (online only)
|
|
488
|
+
isSuccess, // true after a successful direct call
|
|
489
|
+
isError, // true if the direct call threw (action still queued as fallback)
|
|
490
|
+
isQueued, // true when the action was added to the offline queue
|
|
491
|
+
error, // Error | null
|
|
492
|
+
reset, // () => void — reset status back to idle
|
|
493
|
+
} = useOfflineMutation('ACTION_NAME', options);
|
|
522
494
|
```
|
|
523
495
|
|
|
524
496
|
| Option | Type | Description |
|
|
525
497
|
|--------|------|-------------|
|
|
526
|
-
| `handler` | `(payload) => Promise<void>` | API call for this
|
|
527
|
-
| `onOptimisticSuccess` | `(payload) => void` | Fires immediately
|
|
498
|
+
| `handler` | `(payload) => Promise<void>` | API call for this action. Registered automatically, used during sync. |
|
|
499
|
+
| `onOptimisticSuccess` | `(payload) => void` | Fires immediately — update your local state here |
|
|
500
|
+
| `onSuccess` | `(payload) => void` | Fires only after a successful direct call (online) |
|
|
528
501
|
| `onError` | `(error, payload) => void` | Fires if the direct call fails while online |
|
|
529
502
|
|
|
503
|
+
**With `fetch`:**
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
function LikeButton({ postId }) {
|
|
507
|
+
const { mutateOffline, isLoading, isQueued } = useOfflineMutation('LIKE_POST', {
|
|
508
|
+
handler: async (payload) => {
|
|
509
|
+
await fetch('/api/likes', {
|
|
510
|
+
method: 'POST',
|
|
511
|
+
body: JSON.stringify(payload),
|
|
512
|
+
});
|
|
513
|
+
},
|
|
514
|
+
onOptimisticSuccess: () => setLiked(true),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<Button
|
|
519
|
+
title={isLoading ? '⏳' : isQueued ? '📡 Queued' : '❤️ Like'}
|
|
520
|
+
onPress={() => mutateOffline({ postId })}
|
|
521
|
+
disabled={isLoading}
|
|
522
|
+
/>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**With React Query:**
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
531
|
+
|
|
532
|
+
function LikeButton({ postId }) {
|
|
533
|
+
const queryClient = useQueryClient();
|
|
534
|
+
|
|
535
|
+
const { mutateAsync } = useMutation({
|
|
536
|
+
mutationFn: (payload) => api.likePost(payload),
|
|
537
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const { mutateOffline, isQueued } = useOfflineMutation('LIKE_POST', {
|
|
541
|
+
handler: async (payload) => await mutateAsync(payload),
|
|
542
|
+
onOptimisticSuccess: () => {
|
|
543
|
+
queryClient.setQueryData(['posts', postId], (old) => ({
|
|
544
|
+
...old, liked: true,
|
|
545
|
+
}));
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<Button
|
|
551
|
+
title={isQueued ? '📡 Queued' : '❤️ Like'}
|
|
552
|
+
onPress={() => mutateOffline({ postId })}
|
|
553
|
+
/>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**State flow:**
|
|
559
|
+
|
|
560
|
+
| Scenario | `status` flow |
|
|
561
|
+
|----------|---------------|
|
|
562
|
+
| Online + success | `idle` → `loading` → `success` |
|
|
563
|
+
| Online + API fails | `idle` → `loading` → `queued` (fallback) |
|
|
564
|
+
| Offline | `idle` → `queued` |
|
|
565
|
+
|
|
530
566
|
### `useOfflineQueue()`
|
|
531
567
|
|
|
532
568
|
Access the live queue state. Uses `useSyncExternalStore` under the hood — only re-renders when the queue actually changes.
|
|
@@ -13,6 +13,8 @@ function useOfflineMutation(actionName, options) {
|
|
|
13
13
|
} = (0, _OfflineProvider.useNetworkStatus)();
|
|
14
14
|
const handlerRef = (0, _react.useRef)(options?.handler);
|
|
15
15
|
handlerRef.current = options?.handler;
|
|
16
|
+
const [status, setStatus] = (0, _react.useState)('idle');
|
|
17
|
+
const [error, setError] = (0, _react.useState)(null);
|
|
16
18
|
|
|
17
19
|
// Register per-action handler (persists even after unmount)
|
|
18
20
|
(0, _react.useEffect)(() => {
|
|
@@ -20,7 +22,11 @@ function useOfflineMutation(actionName, options) {
|
|
|
20
22
|
_OfflineManager.OfflineManager.registerHandler(actionName, payload => handlerRef.current(payload));
|
|
21
23
|
}
|
|
22
24
|
}, [actionName]);
|
|
23
|
-
const
|
|
25
|
+
const reset = (0, _react.useCallback)(() => {
|
|
26
|
+
setStatus('idle');
|
|
27
|
+
setError(null);
|
|
28
|
+
}, []);
|
|
29
|
+
const mutateOffline = (0, _react.useCallback)(async payload => {
|
|
24
30
|
// Resolve which handler to use: per-action handler > global onSyncAction
|
|
25
31
|
const handler = handlerRef.current || _OfflineManager.OfflineManager.getHandler(actionName);
|
|
26
32
|
const globalHandler = _OfflineManager.OfflineManager.onSyncAction;
|
|
@@ -28,6 +34,8 @@ function useOfflineMutation(actionName, options) {
|
|
|
28
34
|
if (isOnline && hasHandler) {
|
|
29
35
|
// ── ONLINE: Execute directly, skip the queue ──
|
|
30
36
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
|
|
37
|
+
setStatus('loading');
|
|
38
|
+
setError(null);
|
|
31
39
|
try {
|
|
32
40
|
if (handler) {
|
|
33
41
|
await handler(payload);
|
|
@@ -40,22 +48,37 @@ function useOfflineMutation(actionName, options) {
|
|
|
40
48
|
retryCount: 0
|
|
41
49
|
});
|
|
42
50
|
}
|
|
51
|
+
setStatus('success');
|
|
43
52
|
options?.onOptimisticSuccess?.(payload);
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
options?.onSuccess?.(payload);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
|
|
56
|
+
// API failed even though online → fallback to queue
|
|
46
57
|
await _OfflineManager.OfflineManager.push(actionName, payload);
|
|
58
|
+
setStatus('queued');
|
|
59
|
+
setError(err);
|
|
47
60
|
options?.onOptimisticSuccess?.(payload);
|
|
48
|
-
options?.onError?.(
|
|
61
|
+
options?.onError?.(err, payload);
|
|
49
62
|
}
|
|
50
63
|
} else {
|
|
51
64
|
// ── OFFLINE: Add to queue + optimistic update ──
|
|
52
65
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
|
|
53
66
|
await _OfflineManager.OfflineManager.push(actionName, payload);
|
|
67
|
+
setStatus('queued');
|
|
68
|
+
setError(null);
|
|
54
69
|
options?.onOptimisticSuccess?.(payload);
|
|
55
70
|
}
|
|
56
|
-
};
|
|
71
|
+
}, [actionName, isOnline, options]);
|
|
57
72
|
return {
|
|
58
|
-
mutateOffline
|
|
73
|
+
mutateOffline,
|
|
74
|
+
status,
|
|
75
|
+
isIdle: status === 'idle',
|
|
76
|
+
isLoading: status === 'loading',
|
|
77
|
+
isSuccess: status === 'success',
|
|
78
|
+
isError: status === 'error',
|
|
79
|
+
isQueued: status === 'queued',
|
|
80
|
+
error,
|
|
81
|
+
reset
|
|
59
82
|
};
|
|
60
83
|
}
|
|
61
84
|
//# sourceMappingURL=useOfflineMutation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","require","_OfflineManager","_OfflineProvider","useOfflineMutation","actionName","options","isOnline","useNetworkStatus","handlerRef","useRef","handler","current","useEffect","OfflineManager","registerHandler","payload","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","
|
|
1
|
+
{"version":3,"names":["_react","require","_OfflineManager","_OfflineProvider","useOfflineMutation","actionName","options","isOnline","useNetworkStatus","handlerRef","useRef","handler","current","status","setStatus","useState","error","setError","useEffect","OfflineManager","registerHandler","payload","reset","useCallback","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","onSuccess","err","warn","push","onError","isIdle","isLoading","isSuccess","isError","isQueued"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,eAAA,GAAAD,OAAA;AACA,IAAAE,gBAAA,GAAAF,OAAA;AAgBO,SAASG,kBAAkBA,CAChCC,UAAkB,EAClBC,OAKC,EACgC;EACjC,MAAM;IAAEC;EAAS,CAAC,GAAG,IAAAC,iCAAgB,EAAC,CAAC;EACvC,MAAMC,UAAU,GAAG,IAAAC,aAAM,EAACJ,OAAO,EAAEK,OAAO,CAAC;EAC3CF,UAAU,CAACG,OAAO,GAAGN,OAAO,EAAEK,OAAO;EAErC,MAAM,CAACE,MAAM,EAAEC,SAAS,CAAC,GAAG,IAAAC,eAAQ,EAAiB,MAAM,CAAC;EAC5D,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAG,IAAAF,eAAQ,EAAe,IAAI,CAAC;;EAEtD;EACA,IAAAG,gBAAS,EAAC,MAAM;IACd,IAAIT,UAAU,CAACG,OAAO,EAAE;MACtBO,8BAAc,CAACC,eAAe,CAACf,UAAU,EAAGgB,OAAY,IACtDZ,UAAU,CAACG,OAAO,CAAES,OAAO,CAC7B,CAAC;IACH;EACF,CAAC,EAAE,CAAChB,UAAU,CAAC,CAAC;EAEhB,MAAMiB,KAAK,GAAG,IAAAC,kBAAW,EAAC,MAAM;IAC9BT,SAAS,CAAC,MAAM,CAAC;IACjBG,QAAQ,CAAC,IAAI,CAAC;EAChB,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMO,aAAa,GAAG,IAAAD,kBAAW,EAAC,MAAOF,OAAiB,IAAK;IAC7D;IACA,MAAMV,OAAO,GAAGF,UAAU,CAACG,OAAO,IAAIO,8BAAc,CAACM,UAAU,CAACpB,UAAU,CAAC;IAC3E,MAAMqB,aAAa,GAAGP,8BAAc,CAACQ,YAAY;IACjD,MAAMC,UAAU,GAAGjB,OAAO,IAAIe,aAAa;IAE3C,IAAInB,QAAQ,IAAIqB,UAAU,EAAE;MAC1B;MACA,IAAIC,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0B1B,UAAU,WAAW,CAAC;MACzES,SAAS,CAAC,SAAS,CAAC;MACpBG,QAAQ,CAAC,IAAI,CAAC;MACd,IAAI;QACF,IAAIN,OAAO,EAAE;UACX,MAAMA,OAAO,CAACU,OAAO,CAAC;QACxB,CAAC,MAAM,IAAIK,aAAa,EAAE;UACxB,MAAMA,aAAa,CAAC;YAClBM,EAAE,EAAE,EAAE;YACN3B,UAAU;YACVgB,OAAO;YACPY,SAAS,EAAEC,IAAI,CAACC,GAAG,CAAC,CAAC;YACrBC,UAAU,EAAE;UACd,CAAC,CAAC;QACJ;QACAtB,SAAS,CAAC,SAAS,CAAC;QACpBR,OAAO,EAAE+B,mBAAmB,GAAGhB,OAAO,CAAC;QACvCf,OAAO,EAAEgC,SAAS,GAAGjB,OAAO,CAAC;MAC/B,CAAC,CAAC,OAAOkB,GAAQ,EAAE;QACjBT,OAAO,CAACU,IAAI,CAAC,0BAA0BnC,UAAU,gCAAgC,EAAEkC,GAAG,CAAC;QACvF;QACA,MAAMpB,8BAAc,CAACsB,IAAI,CAACpC,UAAU,EAAEgB,OAAO,CAAC;QAC9CP,SAAS,CAAC,QAAQ,CAAC;QACnBG,QAAQ,CAACsB,GAAG,CAAC;QACbjC,OAAO,EAAE+B,mBAAmB,GAAGhB,OAAO,CAAC;QACvCf,OAAO,EAAEoC,OAAO,GAAGH,GAAG,EAAElB,OAAO,CAAC;MAClC;IACF,CAAC,MAAM;MACL;MACA,IAAIQ,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0B1B,UAAU,WAAW,CAAC;MACzE,MAAMc,8BAAc,CAACsB,IAAI,CAACpC,UAAU,EAAEgB,OAAO,CAAC;MAC9CP,SAAS,CAAC,QAAQ,CAAC;MACnBG,QAAQ,CAAC,IAAI,CAAC;MACdX,OAAO,EAAE+B,mBAAmB,GAAGhB,OAAO,CAAC;IACzC;EACF,CAAC,EAAE,CAAChB,UAAU,EAAEE,QAAQ,EAAED,OAAO,CAAC,CAAC;EAEnC,OAAO;IACLkB,aAAa;IACbX,MAAM;IACN8B,MAAM,EAAE9B,MAAM,KAAK,MAAM;IACzB+B,SAAS,EAAE/B,MAAM,KAAK,SAAS;IAC/BgC,SAAS,EAAEhC,MAAM,KAAK,SAAS;IAC/BiC,OAAO,EAAEjC,MAAM,KAAK,OAAO;IAC3BkC,QAAQ,EAAElC,MAAM,KAAK,QAAQ;IAC7BG,KAAK;IACLM;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
4
|
import { OfflineManager } from '../core/OfflineManager';
|
|
5
5
|
import { useNetworkStatus } from '../components/OfflineProvider';
|
|
6
6
|
export function useOfflineMutation(actionName, options) {
|
|
@@ -9,6 +9,8 @@ export function useOfflineMutation(actionName, options) {
|
|
|
9
9
|
} = useNetworkStatus();
|
|
10
10
|
const handlerRef = useRef(options?.handler);
|
|
11
11
|
handlerRef.current = options?.handler;
|
|
12
|
+
const [status, setStatus] = useState('idle');
|
|
13
|
+
const [error, setError] = useState(null);
|
|
12
14
|
|
|
13
15
|
// Register per-action handler (persists even after unmount)
|
|
14
16
|
useEffect(() => {
|
|
@@ -16,7 +18,11 @@ export function useOfflineMutation(actionName, options) {
|
|
|
16
18
|
OfflineManager.registerHandler(actionName, payload => handlerRef.current(payload));
|
|
17
19
|
}
|
|
18
20
|
}, [actionName]);
|
|
19
|
-
const
|
|
21
|
+
const reset = useCallback(() => {
|
|
22
|
+
setStatus('idle');
|
|
23
|
+
setError(null);
|
|
24
|
+
}, []);
|
|
25
|
+
const mutateOffline = useCallback(async payload => {
|
|
20
26
|
// Resolve which handler to use: per-action handler > global onSyncAction
|
|
21
27
|
const handler = handlerRef.current || OfflineManager.getHandler(actionName);
|
|
22
28
|
const globalHandler = OfflineManager.onSyncAction;
|
|
@@ -24,6 +30,8 @@ export function useOfflineMutation(actionName, options) {
|
|
|
24
30
|
if (isOnline && hasHandler) {
|
|
25
31
|
// ── ONLINE: Execute directly, skip the queue ──
|
|
26
32
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
|
|
33
|
+
setStatus('loading');
|
|
34
|
+
setError(null);
|
|
27
35
|
try {
|
|
28
36
|
if (handler) {
|
|
29
37
|
await handler(payload);
|
|
@@ -36,22 +44,37 @@ export function useOfflineMutation(actionName, options) {
|
|
|
36
44
|
retryCount: 0
|
|
37
45
|
});
|
|
38
46
|
}
|
|
47
|
+
setStatus('success');
|
|
39
48
|
options?.onOptimisticSuccess?.(payload);
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
options?.onSuccess?.(payload);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
|
|
52
|
+
// API failed even though online → fallback to queue
|
|
42
53
|
await OfflineManager.push(actionName, payload);
|
|
54
|
+
setStatus('queued');
|
|
55
|
+
setError(err);
|
|
43
56
|
options?.onOptimisticSuccess?.(payload);
|
|
44
|
-
options?.onError?.(
|
|
57
|
+
options?.onError?.(err, payload);
|
|
45
58
|
}
|
|
46
59
|
} else {
|
|
47
60
|
// ── OFFLINE: Add to queue + optimistic update ──
|
|
48
61
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
|
|
49
62
|
await OfflineManager.push(actionName, payload);
|
|
63
|
+
setStatus('queued');
|
|
64
|
+
setError(null);
|
|
50
65
|
options?.onOptimisticSuccess?.(payload);
|
|
51
66
|
}
|
|
52
|
-
};
|
|
67
|
+
}, [actionName, isOnline, options]);
|
|
53
68
|
return {
|
|
54
|
-
mutateOffline
|
|
69
|
+
mutateOffline,
|
|
70
|
+
status,
|
|
71
|
+
isIdle: status === 'idle',
|
|
72
|
+
isLoading: status === 'loading',
|
|
73
|
+
isSuccess: status === 'success',
|
|
74
|
+
isError: status === 'error',
|
|
75
|
+
isQueued: status === 'queued',
|
|
76
|
+
error,
|
|
77
|
+
reset
|
|
55
78
|
};
|
|
56
79
|
}
|
|
57
80
|
//# sourceMappingURL=useOfflineMutation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useEffect","useRef","OfflineManager","useNetworkStatus","useOfflineMutation","actionName","options","isOnline","handlerRef","handler","current","registerHandler","payload","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","
|
|
1
|
+
{"version":3,"names":["useEffect","useRef","useState","useCallback","OfflineManager","useNetworkStatus","useOfflineMutation","actionName","options","isOnline","handlerRef","handler","current","status","setStatus","error","setError","registerHandler","payload","reset","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","onSuccess","err","warn","push","onError","isIdle","isLoading","isSuccess","isError","isQueued"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,WAAW,QAAQ,OAAO;AAChE,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,gBAAgB,QAAQ,+BAA+B;AAgBhE,OAAO,SAASC,kBAAkBA,CAChCC,UAAkB,EAClBC,OAKC,EACgC;EACjC,MAAM;IAAEC;EAAS,CAAC,GAAGJ,gBAAgB,CAAC,CAAC;EACvC,MAAMK,UAAU,GAAGT,MAAM,CAACO,OAAO,EAAEG,OAAO,CAAC;EAC3CD,UAAU,CAACE,OAAO,GAAGJ,OAAO,EAAEG,OAAO;EAErC,MAAM,CAACE,MAAM,EAAEC,SAAS,CAAC,GAAGZ,QAAQ,CAAiB,MAAM,CAAC;EAC5D,MAAM,CAACa,KAAK,EAAEC,QAAQ,CAAC,GAAGd,QAAQ,CAAe,IAAI,CAAC;;EAEtD;EACAF,SAAS,CAAC,MAAM;IACd,IAAIU,UAAU,CAACE,OAAO,EAAE;MACtBR,cAAc,CAACa,eAAe,CAACV,UAAU,EAAGW,OAAY,IACtDR,UAAU,CAACE,OAAO,CAAEM,OAAO,CAC7B,CAAC;IACH;EACF,CAAC,EAAE,CAACX,UAAU,CAAC,CAAC;EAEhB,MAAMY,KAAK,GAAGhB,WAAW,CAAC,MAAM;IAC9BW,SAAS,CAAC,MAAM,CAAC;IACjBE,QAAQ,CAAC,IAAI,CAAC;EAChB,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMI,aAAa,GAAGjB,WAAW,CAAC,MAAOe,OAAiB,IAAK;IAC7D;IACA,MAAMP,OAAO,GAAGD,UAAU,CAACE,OAAO,IAAIR,cAAc,CAACiB,UAAU,CAACd,UAAU,CAAC;IAC3E,MAAMe,aAAa,GAAGlB,cAAc,CAACmB,YAAY;IACjD,MAAMC,UAAU,GAAGb,OAAO,IAAIW,aAAa;IAE3C,IAAIb,QAAQ,IAAIe,UAAU,EAAE;MAC1B;MACA,IAAIC,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0BpB,UAAU,WAAW,CAAC;MACzEO,SAAS,CAAC,SAAS,CAAC;MACpBE,QAAQ,CAAC,IAAI,CAAC;MACd,IAAI;QACF,IAAIL,OAAO,EAAE;UACX,MAAMA,OAAO,CAACO,OAAO,CAAC;QACxB,CAAC,MAAM,IAAII,aAAa,EAAE;UACxB,MAAMA,aAAa,CAAC;YAClBM,EAAE,EAAE,EAAE;YACNrB,UAAU;YACVW,OAAO;YACPW,SAAS,EAAEC,IAAI,CAACC,GAAG,CAAC,CAAC;YACrBC,UAAU,EAAE;UACd,CAAC,CAAC;QACJ;QACAlB,SAAS,CAAC,SAAS,CAAC;QACpBN,OAAO,EAAEyB,mBAAmB,GAAGf,OAAO,CAAC;QACvCV,OAAO,EAAE0B,SAAS,GAAGhB,OAAO,CAAC;MAC/B,CAAC,CAAC,OAAOiB,GAAQ,EAAE;QACjBT,OAAO,CAACU,IAAI,CAAC,0BAA0B7B,UAAU,gCAAgC,EAAE4B,GAAG,CAAC;QACvF;QACA,MAAM/B,cAAc,CAACiC,IAAI,CAAC9B,UAAU,EAAEW,OAAO,CAAC;QAC9CJ,SAAS,CAAC,QAAQ,CAAC;QACnBE,QAAQ,CAACmB,GAAG,CAAC;QACb3B,OAAO,EAAEyB,mBAAmB,GAAGf,OAAO,CAAC;QACvCV,OAAO,EAAE8B,OAAO,GAAGH,GAAG,EAAEjB,OAAO,CAAC;MAClC;IACF,CAAC,MAAM;MACL;MACA,IAAIO,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0BpB,UAAU,WAAW,CAAC;MACzE,MAAMH,cAAc,CAACiC,IAAI,CAAC9B,UAAU,EAAEW,OAAO,CAAC;MAC9CJ,SAAS,CAAC,QAAQ,CAAC;MACnBE,QAAQ,CAAC,IAAI,CAAC;MACdR,OAAO,EAAEyB,mBAAmB,GAAGf,OAAO,CAAC;IACzC;EACF,CAAC,EAAE,CAACX,UAAU,EAAEE,QAAQ,EAAED,OAAO,CAAC,CAAC;EAEnC,OAAO;IACLY,aAAa;IACbP,MAAM;IACN0B,MAAM,EAAE1B,MAAM,KAAK,MAAM;IACzB2B,SAAS,EAAE3B,MAAM,KAAK,SAAS;IAC/B4B,SAAS,EAAE5B,MAAM,KAAK,SAAS;IAC/B6B,OAAO,EAAE7B,MAAM,KAAK,OAAO;IAC3B8B,QAAQ,EAAE9B,MAAM,KAAK,QAAQ;IAC7BE,KAAK;IACLI;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
export type MutationStatus = 'idle' | 'loading' | 'success' | 'error' | 'queued';
|
|
2
|
+
export interface OfflineMutationResult<TPayload> {
|
|
3
|
+
mutateOffline: (payload: TPayload) => Promise<void>;
|
|
4
|
+
status: MutationStatus;
|
|
5
|
+
isIdle: boolean;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
isSuccess: boolean;
|
|
8
|
+
isError: boolean;
|
|
9
|
+
isQueued: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
reset: () => void;
|
|
12
|
+
}
|
|
1
13
|
export declare function useOfflineMutation<TPayload>(actionName: string, options?: {
|
|
2
14
|
handler?: (payload: TPayload) => Promise<void>;
|
|
3
15
|
onOptimisticSuccess?: (payload: TPayload) => void;
|
|
4
16
|
onError?: (error: Error, payload: TPayload) => void;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
17
|
+
onSuccess?: (payload: TPayload) => void;
|
|
18
|
+
}): OfflineMutationResult<TPayload>;
|
|
8
19
|
//# sourceMappingURL=useOfflineMutation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOfflineMutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/useOfflineMutation.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,QAAQ,EACzC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"useOfflineMutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/useOfflineMutation.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEjF,MAAM,WAAW,qBAAqB,CAAC,QAAQ;IAC7C,aAAa,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EACzC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;CACzC,GACA,qBAAqB,CAAC,QAAQ,CAAC,CA8EjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mustafaaksoy41/react-native-offline-queue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A flexible, high-performance offline queue and synchronizer for React Native. Works great with React Query (TanStack Query).",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
2
|
import { OfflineManager } from '../core/OfflineManager';
|
|
3
3
|
import { useNetworkStatus } from '../components/OfflineProvider';
|
|
4
4
|
|
|
5
|
+
export type MutationStatus = 'idle' | 'loading' | 'success' | 'error' | 'queued';
|
|
6
|
+
|
|
7
|
+
export interface OfflineMutationResult<TPayload> {
|
|
8
|
+
mutateOffline: (payload: TPayload) => Promise<void>;
|
|
9
|
+
status: MutationStatus;
|
|
10
|
+
isIdle: boolean;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
isSuccess: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
isQueued: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export function useOfflineMutation<TPayload>(
|
|
6
20
|
actionName: string,
|
|
7
21
|
options?: {
|
|
8
22
|
handler?: (payload: TPayload) => Promise<void>;
|
|
9
23
|
onOptimisticSuccess?: (payload: TPayload) => void;
|
|
10
24
|
onError?: (error: Error, payload: TPayload) => void;
|
|
25
|
+
onSuccess?: (payload: TPayload) => void;
|
|
11
26
|
}
|
|
12
|
-
) {
|
|
27
|
+
): OfflineMutationResult<TPayload> {
|
|
13
28
|
const { isOnline } = useNetworkStatus();
|
|
14
29
|
const handlerRef = useRef(options?.handler);
|
|
15
30
|
handlerRef.current = options?.handler;
|
|
16
31
|
|
|
32
|
+
const [status, setStatus] = useState<MutationStatus>('idle');
|
|
33
|
+
const [error, setError] = useState<Error | null>(null);
|
|
34
|
+
|
|
17
35
|
// Register per-action handler (persists even after unmount)
|
|
18
36
|
useEffect(() => {
|
|
19
37
|
if (handlerRef.current) {
|
|
@@ -23,7 +41,12 @@ export function useOfflineMutation<TPayload>(
|
|
|
23
41
|
}
|
|
24
42
|
}, [actionName]);
|
|
25
43
|
|
|
26
|
-
const
|
|
44
|
+
const reset = useCallback(() => {
|
|
45
|
+
setStatus('idle');
|
|
46
|
+
setError(null);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const mutateOffline = useCallback(async (payload: TPayload) => {
|
|
27
50
|
// Resolve which handler to use: per-action handler > global onSyncAction
|
|
28
51
|
const handler = handlerRef.current || OfflineManager.getHandler(actionName);
|
|
29
52
|
const globalHandler = OfflineManager.onSyncAction;
|
|
@@ -32,6 +55,8 @@ export function useOfflineMutation<TPayload>(
|
|
|
32
55
|
if (isOnline && hasHandler) {
|
|
33
56
|
// ── ONLINE: Execute directly, skip the queue ──
|
|
34
57
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
|
|
58
|
+
setStatus('loading');
|
|
59
|
+
setError(null);
|
|
35
60
|
try {
|
|
36
61
|
if (handler) {
|
|
37
62
|
await handler(payload);
|
|
@@ -44,20 +69,37 @@ export function useOfflineMutation<TPayload>(
|
|
|
44
69
|
retryCount: 0,
|
|
45
70
|
});
|
|
46
71
|
}
|
|
72
|
+
setStatus('success');
|
|
47
73
|
options?.onOptimisticSuccess?.(payload);
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
options?.onSuccess?.(payload);
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
|
|
77
|
+
// API failed even though online → fallback to queue
|
|
50
78
|
await OfflineManager.push(actionName, payload);
|
|
79
|
+
setStatus('queued');
|
|
80
|
+
setError(err);
|
|
51
81
|
options?.onOptimisticSuccess?.(payload);
|
|
52
|
-
options?.onError?.(
|
|
82
|
+
options?.onError?.(err, payload);
|
|
53
83
|
}
|
|
54
84
|
} else {
|
|
55
85
|
// ── OFFLINE: Add to queue + optimistic update ──
|
|
56
86
|
if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
|
|
57
87
|
await OfflineManager.push(actionName, payload);
|
|
88
|
+
setStatus('queued');
|
|
89
|
+
setError(null);
|
|
58
90
|
options?.onOptimisticSuccess?.(payload);
|
|
59
91
|
}
|
|
60
|
-
};
|
|
92
|
+
}, [actionName, isOnline, options]);
|
|
61
93
|
|
|
62
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
mutateOffline,
|
|
96
|
+
status,
|
|
97
|
+
isIdle: status === 'idle',
|
|
98
|
+
isLoading: status === 'loading',
|
|
99
|
+
isSuccess: status === 'success',
|
|
100
|
+
isError: status === 'error',
|
|
101
|
+
isQueued: status === 'queued',
|
|
102
|
+
error,
|
|
103
|
+
reset,
|
|
104
|
+
};
|
|
63
105
|
}
|