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 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
@@ -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 };
@@ -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 };