async-storage-sync 1.0.0
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 +198 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +575 -0
- package/dist/index.mjs +552 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# async-storage-sync
|
|
2
|
+
|
|
3
|
+
**Offline-first data layer for React Native** — Save locally, sync when connected.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install async-storage-sync @react-native-async-storage/async-storage @react-native-community/netinfo
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@react-native-async-storage/async-storage` and `@react-native-community/netinfo` are required peer dependencies used by this package.
|
|
12
|
+
|
|
13
|
+
## 30-Second Start
|
|
14
|
+
|
|
15
|
+
Initialize once in your app (no separate files required):
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// App.tsx
|
|
19
|
+
import React, { useEffect } from 'react';
|
|
20
|
+
import { initSyncQueue } from 'async-storage-sync';
|
|
21
|
+
|
|
22
|
+
export default function App() {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
void initSyncQueue({
|
|
25
|
+
driver: 'asyncstorage',
|
|
26
|
+
serverUrl: 'https://api.example.com',
|
|
27
|
+
credentials: { apiKey: 'YOUR_API_KEY' },
|
|
28
|
+
endpoint: '/submit',
|
|
29
|
+
autoSync: false,
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return <YourApp />;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Now use it anywhere in your app:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { getSyncQueue } from 'async-storage-sync';
|
|
41
|
+
|
|
42
|
+
const store = getSyncQueue();
|
|
43
|
+
|
|
44
|
+
// Save record (saved locally immediately)
|
|
45
|
+
await store.save('forms', {
|
|
46
|
+
formId: '123',
|
|
47
|
+
name: 'John',
|
|
48
|
+
timestamp: new Date().toISOString()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Sync all pending records to server
|
|
52
|
+
await store.flush();
|
|
53
|
+
|
|
54
|
+
// List pending records
|
|
55
|
+
const pending = await store.getAll('forms');
|
|
56
|
+
console.log(`${pending.length} forms waiting to sync`);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Sync Behavior (Auto vs Manual)
|
|
60
|
+
|
|
61
|
+
- `autoSync: true` (default): when the app starts, the package checks connectivity and attempts sync if online.
|
|
62
|
+
- `autoSync: true` also listens for reconnect events and retries pending items automatically.
|
|
63
|
+
- `autoSync: false`: no automatic syncing; call sync methods manually when you choose.
|
|
64
|
+
- Manual methods:
|
|
65
|
+
- `store.flush()` → sync all pending items
|
|
66
|
+
- `store.sync(collection)` → sync one collection
|
|
67
|
+
- `store.syncById(collection, id)` → sync one record
|
|
68
|
+
- Sync destination is controlled by your config: `serverUrl + endpoint`.
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### Setup
|
|
73
|
+
|
|
74
|
+
| Function | Purpose |
|
|
75
|
+
|--------|---------|
|
|
76
|
+
| `initSyncQueue(config)` | Initialize singleton once at app startup (safe to call repeatedly) |
|
|
77
|
+
| `getSyncQueue()` | Get initialized singleton instance |
|
|
78
|
+
| `setStorageDriver(storage)` | Inject storage client explicitly (useful for symlink/local package setups) |
|
|
79
|
+
|
|
80
|
+
### Store Methods (`const store = getSyncQueue()`)
|
|
81
|
+
|
|
82
|
+
| Method | Purpose |
|
|
83
|
+
|--------|---------|
|
|
84
|
+
| `store.save(collection, data, options?)` | Save one record locally and enqueue for sync |
|
|
85
|
+
| `store.getAll(collection)` | Get all records from one collection |
|
|
86
|
+
| `store.getById(collection, id)` | Get one record by internal `_id` |
|
|
87
|
+
| `store.deleteById(collection, id)` | Delete one record by internal `_id` |
|
|
88
|
+
| `store.deleteCollection(collection)` | Delete all records in one collection |
|
|
89
|
+
| `store.flush()` | Sync all pending queue items |
|
|
90
|
+
| `store.sync(collection)` | Sync pending items for one collection only |
|
|
91
|
+
| `store.syncById(collection, id)` | Sync one specific record by internal `_id` |
|
|
92
|
+
| `store.requeueFailed()` | Move `failed` records back to pending queue for retry |
|
|
93
|
+
| `store.onSynced(callback)` | Event callback for successful sync of each item |
|
|
94
|
+
| `store.onAuthError(callback)` | Event callback when sync returns `401` or `403` |
|
|
95
|
+
| `store.onStorageFull(callback)` | Event callback when local storage is full on save |
|
|
96
|
+
| `store.getQueue()` | Inspect in-memory queue items (debug/metrics) |
|
|
97
|
+
| `store.destroy()` | Stop engine, clear queue/storage, and reset singleton |
|
|
98
|
+
|
|
99
|
+
## Quick Examples
|
|
100
|
+
|
|
101
|
+
**Auto-sync when internet reconnects:**
|
|
102
|
+
```ts
|
|
103
|
+
import NetInfo from '@react-native-community/netinfo';
|
|
104
|
+
import { getSyncQueue } from 'async-storage-sync';
|
|
105
|
+
|
|
106
|
+
NetInfo.addEventListener(state => {
|
|
107
|
+
if (state.isConnected) {
|
|
108
|
+
void getSyncQueue().flush();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Handle authentication errors:**
|
|
114
|
+
```ts
|
|
115
|
+
const store = getSyncQueue();
|
|
116
|
+
|
|
117
|
+
store.onAuthError((statusCode, item) => {
|
|
118
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
119
|
+
console.log('Session expired, re-login needed');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Transform data before sending to server:**
|
|
125
|
+
```ts
|
|
126
|
+
initSyncQueue({
|
|
127
|
+
driver: 'asyncstorage',
|
|
128
|
+
serverUrl: 'https://api.example.com',
|
|
129
|
+
credentials: { apiKey: 'KEY' },
|
|
130
|
+
endpoint: '/submit',
|
|
131
|
+
payloadTransformer: (record) => {
|
|
132
|
+
const { _id, _ts, _synced, _retries, ...payload } = record;
|
|
133
|
+
return payload;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Handle duplicates:**
|
|
139
|
+
```ts
|
|
140
|
+
// Keep all (default)
|
|
141
|
+
await store.save('logs', { event: 'tap' });
|
|
142
|
+
|
|
143
|
+
// Replace existing type
|
|
144
|
+
await store.save('profile', { userId: 1, name: 'Bob' }, {
|
|
145
|
+
type: 'currentUser',
|
|
146
|
+
duplicateStrategy: 'overwrite',
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Logout cleanup:**
|
|
151
|
+
```ts
|
|
152
|
+
const store = getSyncQueue();
|
|
153
|
+
await store.deleteCollection('submissions');
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
initSyncQueue({
|
|
160
|
+
driver: 'asyncstorage', // (required) only driver type
|
|
161
|
+
serverUrl: string, // (required) API base URL
|
|
162
|
+
credentials: { apiKey: string }, // (required) auth
|
|
163
|
+
endpoint?: '/submit', // route to POST data
|
|
164
|
+
autoSync?: false, // auto-sync on reconnect
|
|
165
|
+
onSyncSuccess?: 'keep', // after sync: keep|delete|ttl
|
|
166
|
+
ttl?: 7 * 24 * 60 * 60 * 1000, // if ttl mode, keep duration
|
|
167
|
+
duplicateStrategy?: 'append', // append or overwrite
|
|
168
|
+
payloadTransformer?: (r) => r, // optional: shape before send
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## How It Works
|
|
173
|
+
|
|
174
|
+
1. **Save** — Records written to AsyncStorage immediately
|
|
175
|
+
2. **Queue** — Each save queued for syncing
|
|
176
|
+
3. **Sync** — `flush()` POSTs all pending to your server
|
|
177
|
+
4. **Status** — Records marked synced, then kept or deleted
|
|
178
|
+
5. **Retry** — Failed syncs retry automatically (max 5x)
|
|
179
|
+
6. **Persist** — Everything survives app restart
|
|
180
|
+
|
|
181
|
+
## Storage
|
|
182
|
+
|
|
183
|
+
- `asyncstorage::<collectionName>` — Your records + metadata (`_id`, `_ts`, `_synced`, `_retries`)
|
|
184
|
+
- `asyncstorage::__queue__` — Sync queue
|
|
185
|
+
|
|
186
|
+
## Limits
|
|
187
|
+
|
|
188
|
+
✅ Works: 100s-1000s of records, multiple collections, TypeScript support, app crashes
|
|
189
|
+
❌ Limits: Max 5 retries, not a full database, config locks after init
|
|
190
|
+
|
|
191
|
+
## Production Checklist
|
|
192
|
+
|
|
193
|
+
- [ ] Use `payloadTransformer` to remove `_` fields before server
|
|
194
|
+
- [ ] Handle `onAuthError` for 401/403
|
|
195
|
+
- [ ] Set `autoSync: false` if you control sync timing
|
|
196
|
+
- [ ] Test with `NetInfo` reconnect listener
|
|
197
|
+
- [ ] Call `deleteCollection()` on logout
|
|
198
|
+
- [ ] Monitor queue with `getQueue()` for ops metrics
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
type DriverName = 'asyncstorage';
|
|
2
|
+
type OnSyncSuccess = 'keep' | 'delete' | 'ttl';
|
|
3
|
+
type DuplicateStrategy = 'append' | 'overwrite';
|
|
4
|
+
interface InitConfig {
|
|
5
|
+
driver: DriverName;
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
credentials: {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Optional transform applied to the stored record before it is sent to the server.
|
|
12
|
+
* Use this to strip internal meta fields, rename keys, or reshape the payload
|
|
13
|
+
* to match your backend's expected schema.
|
|
14
|
+
* If omitted, the full stored record is sent as-is.
|
|
15
|
+
*/
|
|
16
|
+
payloadTransformer?: (record: Record<string, unknown>) => Record<string, unknown>;
|
|
17
|
+
autoSync?: boolean;
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
onSyncSuccess?: OnSyncSuccess;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
duplicateStrategy?: DuplicateStrategy;
|
|
22
|
+
}
|
|
23
|
+
type SyncStatus = 'pending' | 'synced' | 'failed';
|
|
24
|
+
interface RecordMeta {
|
|
25
|
+
_id: string;
|
|
26
|
+
_ts: number;
|
|
27
|
+
_synced: SyncStatus;
|
|
28
|
+
_type: string;
|
|
29
|
+
_retries: number;
|
|
30
|
+
}
|
|
31
|
+
type StoredRecord<T = Record<string, unknown>> = RecordMeta & T;
|
|
32
|
+
interface QueueItem {
|
|
33
|
+
id: string;
|
|
34
|
+
key: string;
|
|
35
|
+
recordId: string;
|
|
36
|
+
payload: string;
|
|
37
|
+
endpoint: string;
|
|
38
|
+
ts: number;
|
|
39
|
+
retries: number;
|
|
40
|
+
synced: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface SaveOptions {
|
|
43
|
+
type?: string;
|
|
44
|
+
onSyncSuccess?: OnSyncSuccess;
|
|
45
|
+
duplicateStrategy?: DuplicateStrategy;
|
|
46
|
+
}
|
|
47
|
+
type SyncedCallback = (item: QueueItem) => void;
|
|
48
|
+
type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
|
|
49
|
+
type StorageFullCallback = () => void;
|
|
50
|
+
|
|
51
|
+
declare class AsyncStorageSync {
|
|
52
|
+
private readonly config;
|
|
53
|
+
private readonly driver;
|
|
54
|
+
private static instance;
|
|
55
|
+
private readonly queue;
|
|
56
|
+
private readonly engine;
|
|
57
|
+
private constructor();
|
|
58
|
+
static init(config: InitConfig): Promise<AsyncStorageSync>;
|
|
59
|
+
static getInstance(): AsyncStorageSync;
|
|
60
|
+
save<T extends Record<string, unknown>>(name: string, data: T, options?: SaveOptions): Promise<StoredRecord<T>>;
|
|
61
|
+
private enqueueRecord;
|
|
62
|
+
getAll<T extends Record<string, unknown>>(name: string): Promise<StoredRecord<T>[]>;
|
|
63
|
+
getById<T extends Record<string, unknown>>(name: string, id: string): Promise<StoredRecord<T> | null>;
|
|
64
|
+
deleteById(name: string, id: string): Promise<void>;
|
|
65
|
+
deleteCollection(name: string): Promise<void>;
|
|
66
|
+
sync(name: string): Promise<void>;
|
|
67
|
+
syncById(_name: string, id: string): Promise<void>;
|
|
68
|
+
flush(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Re-enqueue any records marked as 'failed' so they are retried on next flush.
|
|
71
|
+
* Called automatically on init to recover from previous 4xx/500 failures.
|
|
72
|
+
*/
|
|
73
|
+
requeueFailed(): Promise<void>;
|
|
74
|
+
onSynced(cb: SyncedCallback): void;
|
|
75
|
+
onAuthError(cb: AuthErrorCallback): void;
|
|
76
|
+
onStorageFull(cb: StorageFullCallback): void;
|
|
77
|
+
getQueue(): QueueItem[];
|
|
78
|
+
destroy(): Promise<void>;
|
|
79
|
+
private getCollection;
|
|
80
|
+
private saveCollection;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type AsyncStorageClient = {
|
|
84
|
+
getItem(key: string): Promise<string | null>;
|
|
85
|
+
setItem(key: string, value: string): Promise<void>;
|
|
86
|
+
removeItem(key: string): Promise<void>;
|
|
87
|
+
getAllKeys(): Promise<readonly string[] | null>;
|
|
88
|
+
clear(): Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initialize once at app startup (safe to call repeatedly).
|
|
93
|
+
*/
|
|
94
|
+
declare function initSyncQueue(config: InitConfig): Promise<AsyncStorageSync>;
|
|
95
|
+
/**
|
|
96
|
+
* Get initialized singleton instance.
|
|
97
|
+
*/
|
|
98
|
+
declare function getSyncQueue(): AsyncStorageSync;
|
|
99
|
+
/**
|
|
100
|
+
* Inject storage implementation explicitly (recommended for symlink/local package usage).
|
|
101
|
+
*/
|
|
102
|
+
declare function setStorageDriver(storage: AsyncStorageClient): void;
|
|
103
|
+
|
|
104
|
+
export { AsyncStorageSync, type AuthErrorCallback, type DriverName, type DuplicateStrategy, type InitConfig, type OnSyncSuccess, type QueueItem, type RecordMeta, type SaveOptions, type StorageFullCallback, type StoredRecord, type SyncStatus, type SyncedCallback, getSyncQueue, initSyncQueue, setStorageDriver };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
type DriverName = 'asyncstorage';
|
|
2
|
+
type OnSyncSuccess = 'keep' | 'delete' | 'ttl';
|
|
3
|
+
type DuplicateStrategy = 'append' | 'overwrite';
|
|
4
|
+
interface InitConfig {
|
|
5
|
+
driver: DriverName;
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
credentials: {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Optional transform applied to the stored record before it is sent to the server.
|
|
12
|
+
* Use this to strip internal meta fields, rename keys, or reshape the payload
|
|
13
|
+
* to match your backend's expected schema.
|
|
14
|
+
* If omitted, the full stored record is sent as-is.
|
|
15
|
+
*/
|
|
16
|
+
payloadTransformer?: (record: Record<string, unknown>) => Record<string, unknown>;
|
|
17
|
+
autoSync?: boolean;
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
onSyncSuccess?: OnSyncSuccess;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
duplicateStrategy?: DuplicateStrategy;
|
|
22
|
+
}
|
|
23
|
+
type SyncStatus = 'pending' | 'synced' | 'failed';
|
|
24
|
+
interface RecordMeta {
|
|
25
|
+
_id: string;
|
|
26
|
+
_ts: number;
|
|
27
|
+
_synced: SyncStatus;
|
|
28
|
+
_type: string;
|
|
29
|
+
_retries: number;
|
|
30
|
+
}
|
|
31
|
+
type StoredRecord<T = Record<string, unknown>> = RecordMeta & T;
|
|
32
|
+
interface QueueItem {
|
|
33
|
+
id: string;
|
|
34
|
+
key: string;
|
|
35
|
+
recordId: string;
|
|
36
|
+
payload: string;
|
|
37
|
+
endpoint: string;
|
|
38
|
+
ts: number;
|
|
39
|
+
retries: number;
|
|
40
|
+
synced: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface SaveOptions {
|
|
43
|
+
type?: string;
|
|
44
|
+
onSyncSuccess?: OnSyncSuccess;
|
|
45
|
+
duplicateStrategy?: DuplicateStrategy;
|
|
46
|
+
}
|
|
47
|
+
type SyncedCallback = (item: QueueItem) => void;
|
|
48
|
+
type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
|
|
49
|
+
type StorageFullCallback = () => void;
|
|
50
|
+
|
|
51
|
+
declare class AsyncStorageSync {
|
|
52
|
+
private readonly config;
|
|
53
|
+
private readonly driver;
|
|
54
|
+
private static instance;
|
|
55
|
+
private readonly queue;
|
|
56
|
+
private readonly engine;
|
|
57
|
+
private constructor();
|
|
58
|
+
static init(config: InitConfig): Promise<AsyncStorageSync>;
|
|
59
|
+
static getInstance(): AsyncStorageSync;
|
|
60
|
+
save<T extends Record<string, unknown>>(name: string, data: T, options?: SaveOptions): Promise<StoredRecord<T>>;
|
|
61
|
+
private enqueueRecord;
|
|
62
|
+
getAll<T extends Record<string, unknown>>(name: string): Promise<StoredRecord<T>[]>;
|
|
63
|
+
getById<T extends Record<string, unknown>>(name: string, id: string): Promise<StoredRecord<T> | null>;
|
|
64
|
+
deleteById(name: string, id: string): Promise<void>;
|
|
65
|
+
deleteCollection(name: string): Promise<void>;
|
|
66
|
+
sync(name: string): Promise<void>;
|
|
67
|
+
syncById(_name: string, id: string): Promise<void>;
|
|
68
|
+
flush(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Re-enqueue any records marked as 'failed' so they are retried on next flush.
|
|
71
|
+
* Called automatically on init to recover from previous 4xx/500 failures.
|
|
72
|
+
*/
|
|
73
|
+
requeueFailed(): Promise<void>;
|
|
74
|
+
onSynced(cb: SyncedCallback): void;
|
|
75
|
+
onAuthError(cb: AuthErrorCallback): void;
|
|
76
|
+
onStorageFull(cb: StorageFullCallback): void;
|
|
77
|
+
getQueue(): QueueItem[];
|
|
78
|
+
destroy(): Promise<void>;
|
|
79
|
+
private getCollection;
|
|
80
|
+
private saveCollection;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type AsyncStorageClient = {
|
|
84
|
+
getItem(key: string): Promise<string | null>;
|
|
85
|
+
setItem(key: string, value: string): Promise<void>;
|
|
86
|
+
removeItem(key: string): Promise<void>;
|
|
87
|
+
getAllKeys(): Promise<readonly string[] | null>;
|
|
88
|
+
clear(): Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initialize once at app startup (safe to call repeatedly).
|
|
93
|
+
*/
|
|
94
|
+
declare function initSyncQueue(config: InitConfig): Promise<AsyncStorageSync>;
|
|
95
|
+
/**
|
|
96
|
+
* Get initialized singleton instance.
|
|
97
|
+
*/
|
|
98
|
+
declare function getSyncQueue(): AsyncStorageSync;
|
|
99
|
+
/**
|
|
100
|
+
* Inject storage implementation explicitly (recommended for symlink/local package usage).
|
|
101
|
+
*/
|
|
102
|
+
declare function setStorageDriver(storage: AsyncStorageClient): void;
|
|
103
|
+
|
|
104
|
+
export { AsyncStorageSync, type AuthErrorCallback, type DriverName, type DuplicateStrategy, type InitConfig, type OnSyncSuccess, type QueueItem, type RecordMeta, type SaveOptions, type StorageFullCallback, type StoredRecord, type SyncStatus, type SyncedCallback, getSyncQueue, initSyncQueue, setStorageDriver };
|