async-storage-sync 1.0.6 → 1.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # async-storage-sync
2
2
 
3
- **Offline-first data layer for React Native** — Save locally, sync when connected.
3
+ **Offline-first data layer for React Native** — save locally, sync to your server when connected.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,209 +8,190 @@
8
8
  npm install async-storage-sync @react-native-async-storage/async-storage @react-native-community/netinfo
9
9
  ```
10
10
 
11
- `@react-native-async-storage/async-storage` and `@react-native-community/netinfo` are required peer dependencies used by this package.
11
+ ## Quick Start
12
12
 
13
- ## 30-Second Start
14
-
15
- Initialize once in your app (no separate files required):
13
+ **1. Initialize once in `App.tsx`:**
16
14
 
17
15
  ```ts
18
- // App.tsx
19
- import React, { useEffect } from 'react';
20
16
  import { initSyncQueue } from 'async-storage-sync';
21
17
 
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
- }
18
+ initSyncQueue({
19
+ driver: 'asyncstorage',
20
+ serverUrl: 'https://api.example.com',
21
+ credentials: {
22
+ Authorization: 'Token abc123',
23
+ 'x-api-key': 'my-custom-key',
24
+ },
25
+ endpoint: '/submit',
26
+ autoSync: true,
27
+ });
35
28
  ```
36
29
 
37
- Now use it anywhere in your app:
30
+ **2. Save data anywhere in your app:**
38
31
 
39
32
  ```ts
40
33
  import { getSyncQueue } from 'async-storage-sync';
41
34
 
42
35
  const store = getSyncQueue();
43
36
 
44
- // Save record (saved locally immediately)
45
- await store.save('forms', {
46
- formId: '123',
47
- name: 'John',
48
- timestamp: new Date().toISOString()
37
+ await store.save('submissions', {
38
+ formId: '123',
39
+ name: 'John',
40
+ timestamp: new Date().toISOString(),
49
41
  });
50
-
51
- // Sync and get summary result
52
- const result = await store.flushWithResult();
53
- console.log(`Synced: ${result.synced}, Failed: ${result.failed}, Remaining: ${result.remainingPending}`);
54
-
55
- // List pending records
56
- const pending = await store.getAll('forms');
57
- console.log(`${pending.length} forms waiting to sync`);
58
42
  ```
59
43
 
60
- ## Sync Behavior (Auto vs Manual)
61
-
62
- - `autoSync: true` (default): when the app starts, the package checks connectivity and attempts sync if online.
63
- - `autoSync: true` also listens for reconnect events and retries pending items automatically.
64
- - `autoSync: false`: no automatic syncing; call sync methods manually when you choose.
65
- - Manual methods:
66
- - `store.flushWithResult()` → sync all pending and return summary counts
67
- - `store.syncWithResult(collection)` → sync one collection and return summary counts
68
- - `store.syncById(collection, id)` → sync one record
69
- - Sync destination is controlled by your config: `serverUrl + endpoint`.
44
+ **3. Sync and check the result:**
70
45
 
71
- ## API Reference
72
-
73
- ### Setup
74
-
75
- | Function | Purpose |
76
- |--------|---------|
77
- | `initSyncQueue(config)` | Initialize singleton once at app startup (safe to call repeatedly) |
78
- | `getSyncQueue()` | Get initialized singleton instance |
79
- | `setStorageDriver(storage)` | Inject storage client explicitly (useful for symlink/local package setups) |
80
-
81
- ### Store Methods (`const store = getSyncQueue()`)
82
-
83
- | Method | Purpose |
84
- |--------|---------|
85
- | `store.save(collection, data, options?)` | Save one record locally and enqueue for sync |
86
- | `store.getAll(collection)` | Get all records from one collection |
87
- | `store.getById(collection, id)` | Get one record by internal `_id` |
88
- | `store.deleteById(collection, id)` | Delete one record by internal `_id` |
89
- | `store.deleteCollection(collection)` | Delete all records in one collection |
90
- | `store.flushWithResult()` | Sync all pending and return detailed summary (`attempted`, `synced`, `failed`, `retried`, `remainingPending`, `items`) |
91
- | `store.syncWithResult(collection)` | Sync collection and return detailed summary (same format as `flushWithResult()`) |
92
- | `store.syncById(collection, id)` | Sync one specific record by internal `_id` |
93
- | `store.requeueFailed()` | Move `failed` records back to pending queue for retry |
94
- | `store.onSynced(callback)` | Event callback for successful sync of each item |
95
- | `store.onAuthError(callback)` | Event callback when sync returns `401` or `403` |
96
- | `store.onStorageFull(callback)` | Event callback when local storage is full on save |
97
- | `store.getQueue()` | Inspect in-memory queue items (debug/metrics) |
98
- | `store.destroy()` | Stop engine, clear queue/storage, and reset singleton |
99
-
100
- ## Quick Examples
101
-
102
- **Auto-sync when internet reconnects:**
103
46
  ```ts
104
- import NetInfo from '@react-native-community/netinfo';
105
- import { getSyncQueue } from 'async-storage-sync';
106
-
107
- NetInfo.addEventListener(state => {
108
- if (state.isConnected) {
109
- void getSyncQueue().flushWithResult();
110
- }
111
- });
47
+ const result = await store.flushWithResult('submissions');
48
+ console.log(`Synced: ${result.synced}, Failed: ${result.failed}, Remaining: ${result.remainingPending}`);
112
49
  ```
113
50
 
114
- **Handle authentication errors:**
115
- ```ts
116
- const store = getSyncQueue();
51
+ ---
117
52
 
118
- store.onAuthError((statusCode, item) => {
119
- if (statusCode === 401 || statusCode === 403) {
120
- console.log('Session expired, re-login needed');
121
- }
122
- });
123
- ```
53
+ ## Configuration
124
54
 
125
- **Transform data before sending to server:**
126
55
  ```ts
127
56
  initSyncQueue({
57
+ // required
128
58
  driver: 'asyncstorage',
129
59
  serverUrl: 'https://api.example.com',
130
- credentials: { apiKey: 'KEY' },
60
+ credentials: {
61
+ Authorization: 'Token abc123', // sent as-is in request headers
62
+ 'x-api-key': 'my-custom-key', // any key/value pair works
63
+ },
131
64
  endpoint: '/submit',
132
- payloadTransformer: (record) => {
65
+
66
+ // optional
67
+ autoSync: true, // auto-flush on app open + reconnect (default: true)
68
+ autoSyncCollections: ['submissions'], // limit auto-sync to these collections; empty = all
69
+ onSyncSuccess: 'delete', // what to do after a successful sync: 'keep' | 'delete' | 'ttl'
70
+ ttl: 7 * 24 * 60 * 60 * 1000, // used only when onSyncSuccess is 'ttl'
71
+ duplicateStrategy: 'append', // 'append' (default) | 'overwrite'
72
+ payloadTransformer: (record) => { // strip internal fields before sending to server
133
73
  const { _id, _ts, _synced, _retries, ...payload } = record;
134
74
  return payload;
135
75
  },
136
76
  });
137
77
  ```
138
78
 
139
- **Use custom auth headers/keys:**
79
+ > `credentials` values are merged directly into request headers. Any key/value pair is supported.
80
+
81
+ ---
82
+
83
+ ## API
84
+
85
+ ### Setup
86
+
140
87
  ```ts
141
- initSyncQueue({
142
- driver: 'asyncstorage',
143
- serverUrl: 'https://api.example.com',
144
- credentials: {
145
- Authorization: 'Token abc123',
146
- 'x-api-key': 'my-custom-key',
147
- },
148
- endpoint: '/submit',
149
- });
88
+ initSyncQueue(config) // call once at app startup — safe to call again, won't re-init
89
+ getSyncQueue() // get the instance anywhere in your app
150
90
  ```
151
91
 
152
- **Handle duplicates:**
92
+ ### Write
93
+
153
94
  ```ts
154
- // Keep all (default)
155
- await store.save('logs', { event: 'tap' });
95
+ store.save(collection, data, options?) // save locally and enqueue for sync
96
+ ```
156
97
 
157
- // Replace existing type
158
- await store.save('profile', { userId: 1, name: 'Bob' }, {
159
- type: 'currentUser',
160
- duplicateStrategy: 'overwrite',
161
- });
98
+ `options` (all optional, override global config for this call only):
99
+
100
+ | Option | Values | Description |
101
+ |--------|--------|-------------|
102
+ | `type` | `string` | Labels the record — used by `overwrite` strategy to find and replace |
103
+ | `onSyncSuccess` | `'keep'` \| `'delete'` \| `'ttl'` | What happens to the local copy after sync |
104
+ | `duplicateStrategy` | `'append'` \| `'overwrite'` | Whether to add a new record or replace an existing one of the same `type` |
105
+
106
+ ### Read
107
+
108
+ ```ts
109
+ store.getAll(collection) // all records in a collection
110
+ store.getById(collection, id) // one record by its _id
162
111
  ```
163
112
 
164
- **Logout cleanup:**
113
+ ### Delete
114
+
165
115
  ```ts
166
- const store = getSyncQueue();
167
- await store.deleteCollection('submissions');
116
+ store.deleteById(collection, id) // remove one record
117
+ store.deleteCollection(collection) // wipe entire collection — call on logout
168
118
  ```
169
119
 
170
- ## Configuration
120
+ ### Sync
171
121
 
172
122
  ```ts
173
- initSyncQueue({
174
- driver: 'asyncstorage', // (required) only driver type
175
- serverUrl: string, // (required) API base URL
176
- credentials: Record<string, string>, // (required) merged into request headers
177
- endpoint?: '/submit', // route to POST data
178
- autoSync?: false, // auto-sync on reconnect
179
- onSyncSuccess?: 'keep', // after sync: keep|delete|ttl
180
- ttl?: 7 * 24 * 60 * 60 * 1000, // if ttl mode, keep duration
181
- duplicateStrategy?: 'append', // append or overwrite
182
- payloadTransformer?: (r) => r, // optional: shape before send
183
- });
123
+ store.flushWithResult(collection) // sync one collection, get a result summary
124
+ store.syncManyWithResult(collections[]) // sync multiple collections, merged result summary
125
+ store.syncById(collection, id) // sync one specific record
126
+ store.requeueFailed() // move 'failed' records back to pending for retry
184
127
  ```
185
128
 
186
- Notes:
187
- - `credentials.apiKey` remains supported and is sent as `Authorization: Bearer <apiKey>` if no `Authorization` header is provided.
188
- - Any other key/value pairs in `credentials` are sent as-is in request headers.
129
+ **Result summary shape:**
130
+
131
+ ```ts
132
+ {
133
+ attempted, synced, failed, retried,
134
+ deferred, networkErrors, remainingPending,
135
+ skippedAlreadyFlushing, items
136
+ }
137
+ ```
138
+
139
+ ### Events
140
+
141
+ ```ts
142
+ store.onSynced(callback) // fires after each record syncs successfully
143
+ store.onAuthError(callback) // fires on 401/403 — use to trigger re-login
144
+ store.onStorageFull(callback) // fires when device storage is full
145
+ ```
146
+
147
+ ### Debug
148
+
149
+ ```ts
150
+ store.getQueue() // inspect the in-memory queue
151
+ store.destroy() // stop engine, clear everything, reset singleton
152
+ ```
153
+
154
+ ---
189
155
 
190
156
  ## How It Works
191
157
 
192
- 1. **Save** Records written to AsyncStorage immediately
193
- 2. **Queue** Each save queued for syncing
194
- 3. **Sync** `flushWithResult()` POSTs all pending to your server and returns summary output
195
- 4. **Status**Records marked synced, then kept or deleted
196
- 5. **Retry**Failed syncs retry automatically (max 5x)
197
- 6. **Persist**Everything survives app restart
158
+ 1. `save()` writes the record to AsyncStorage immediately — no network needed.
159
+ 2. The record is added to a queue with `_synced: 'pending'`.
160
+ 3. On `flushWithResult()` (or automatically on reconnect if `autoSync: true`), queued records are POSTed to your server.
161
+ 4. On `200 OK` the record is marked synced, then kept/deleted based on `onSyncSuccess`.
162
+ 5. On `5xx` retried up to 5 times with exponential backoff.
163
+ 6. On `4xx` marked `failed`, never retried. Fires `onAuthError` on 401/403.
164
+ 7. Everything persists across app restarts — the queue survives crashes.
165
+
166
+ ---
167
+
168
+ ## Stored Record Shape
198
169
 
199
- ## Storage
170
+ Every saved record gets these fields added automatically:
171
+
172
+ ```ts
173
+ {
174
+ _id: string // uuid v4
175
+ _ts: number // Date.now() at save time
176
+ _synced: 'pending' | 'synced' | 'failed'
177
+ _type: string // from save() options.type, or ''
178
+ _retries: number // sync attempt count
179
+
180
+ ...yourData // everything you passed to save()
181
+ }
182
+ ```
200
183
 
201
- - `asyncstorage::<collectionName>` — Your records + metadata (`_id`, `_ts`, `_synced`, `_retries`)
202
- - `asyncstorage::__queue__` — Sync queue
184
+ ---
203
185
 
204
186
  ## Limits
205
187
 
206
- ✅ Works: 100s-1000s of records, multiple collections, TypeScript support, app crashes
207
- Limits: Max 5 retries, not a full database, config locks after init
188
+ - Max 5 sync retries per record
189
+ - Not a full database designed for queuing records, not complex queries
190
+ - Config is locked after `initSyncQueue()` is called
208
191
 
209
192
  ## Production Checklist
210
193
 
211
- - [ ] Use `payloadTransformer` to remove `_` fields before server
212
- - [ ] Handle `onAuthError` for 401/403
213
- - [ ] Set `autoSync: false` if you control sync timing
214
- - [ ] Test with `NetInfo` reconnect listener
215
- - [ ] Call `deleteCollection()` on logout
216
- - [ ] Monitor queue with `getQueue()` for ops metrics
194
+ - [ ] Use `payloadTransformer` to strip `_` fields before they reach your server
195
+ - [ ] Handle `onAuthError` to catch expired tokens
196
+ - [ ] Call `deleteCollection()` on logout to clear user data
197
+ - [ ] Set `autoSyncCollections` to limit which collections sync automatically
package/dist/index.d.mts CHANGED
@@ -13,6 +13,7 @@ interface InitConfig {
13
13
  */
14
14
  payloadTransformer?: (record: Record<string, unknown>) => Record<string, unknown>;
15
15
  autoSync?: boolean;
16
+ autoSyncCollections?: string[];
16
17
  endpoint?: string;
17
18
  onSyncSuccess?: OnSyncSuccess;
18
19
  ttl?: number;
@@ -81,6 +82,7 @@ declare class AsyncStorageSync {
81
82
  deleteById(name: string, id: string): Promise<void>;
82
83
  deleteCollection(name: string): Promise<void>;
83
84
  syncWithResult(name: string): Promise<FlushResult>;
85
+ syncManyWithResult(names: string[]): Promise<FlushResult>;
84
86
  syncById(_name: string, id: string): Promise<void>;
85
87
  flushWithResult(): Promise<FlushResult>;
86
88
  /**
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ interface InitConfig {
13
13
  */
14
14
  payloadTransformer?: (record: Record<string, unknown>) => Record<string, unknown>;
15
15
  autoSync?: boolean;
16
+ autoSyncCollections?: string[];
16
17
  endpoint?: string;
17
18
  onSyncSuccess?: OnSyncSuccess;
18
19
  ttl?: number;
@@ -81,6 +82,7 @@ declare class AsyncStorageSync {
81
82
  deleteById(name: string, id: string): Promise<void>;
82
83
  deleteCollection(name: string): Promise<void>;
83
84
  syncWithResult(name: string): Promise<FlushResult>;
85
+ syncManyWithResult(names: string[]): Promise<FlushResult>;
84
86
  syncById(_name: string, id: string): Promise<void>;
85
87
  flushWithResult(): Promise<FlushResult>;
86
88
  /**
package/dist/index.js CHANGED
@@ -221,9 +221,49 @@ var SyncEngine = class {
221
221
  clearTimeout(this.debounceTimer);
222
222
  }
223
223
  this.debounceTimer = setTimeout(() => {
224
- void this.flushWithResult();
224
+ void this.flushAutoSyncTargetWithResult();
225
225
  }, DEBOUNCE_MS);
226
226
  }
227
+ async flushAutoSyncTargetWithResult() {
228
+ const configuredCollections = this.config.autoSyncCollections?.map((name) => name.trim()).filter((name) => name.length > 0);
229
+ if (!configuredCollections || configuredCollections.length === 0) {
230
+ return this.flushWithResult();
231
+ }
232
+ return this.flushCollectionsWithResult(configuredCollections);
233
+ }
234
+ createEmptyResult() {
235
+ return {
236
+ attempted: 0,
237
+ synced: 0,
238
+ failed: 0,
239
+ retried: 0,
240
+ deferred: 0,
241
+ networkErrors: 0,
242
+ remainingPending: 0,
243
+ skippedAlreadyFlushing: false,
244
+ items: []
245
+ };
246
+ }
247
+ mergeResult(target, source) {
248
+ target.attempted += source.attempted;
249
+ target.synced += source.synced;
250
+ target.failed += source.failed;
251
+ target.retried += source.retried;
252
+ target.deferred += source.deferred;
253
+ target.networkErrors += source.networkErrors;
254
+ target.items.push(...source.items);
255
+ target.remainingPending = source.remainingPending;
256
+ target.skippedAlreadyFlushing = target.skippedAlreadyFlushing || source.skippedAlreadyFlushing;
257
+ }
258
+ async flushCollectionsWithResult(collectionNames) {
259
+ const result = this.createEmptyResult();
260
+ for (const collectionName of collectionNames) {
261
+ const collectionResult = await this.flushCollectionWithResult(collectionName);
262
+ this.mergeResult(result, collectionResult);
263
+ }
264
+ result.remainingPending = this.queue.getPending().length;
265
+ return result;
266
+ }
227
267
  async flushWithResult() {
228
268
  if (this.isFlushing) {
229
269
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
@@ -240,17 +280,7 @@ var SyncEngine = class {
240
280
  };
241
281
  }
242
282
  this.isFlushing = true;
243
- const result = {
244
- attempted: 0,
245
- synced: 0,
246
- failed: 0,
247
- retried: 0,
248
- deferred: 0,
249
- networkErrors: 0,
250
- remainingPending: 0,
251
- skippedAlreadyFlushing: false,
252
- items: []
253
- };
283
+ const result = this.createEmptyResult();
254
284
  try {
255
285
  const pending = this.queue.getPending();
256
286
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
@@ -282,40 +312,48 @@ var SyncEngine = class {
282
312
  }
283
313
  }
284
314
  async flushCollectionWithResult(collectionName) {
285
- const result = {
286
- attempted: 0,
287
- synced: 0,
288
- failed: 0,
289
- retried: 0,
290
- deferred: 0,
291
- networkErrors: 0,
292
- remainingPending: 0,
293
- skippedAlreadyFlushing: false,
294
- items: []
295
- };
296
- const pending = this.queue.getPendingForCollection(collectionName);
297
- if (pending.length === 0) {
298
- result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
299
- return result;
315
+ if (this.isFlushing) {
316
+ return {
317
+ attempted: 0,
318
+ synced: 0,
319
+ failed: 0,
320
+ retried: 0,
321
+ deferred: 0,
322
+ networkErrors: 0,
323
+ remainingPending: this.queue.getPendingForCollection(collectionName).length,
324
+ skippedAlreadyFlushing: true,
325
+ items: []
326
+ };
300
327
  }
301
- for (const item of pending) {
302
- result.attempted += 1;
303
- const itemResult = await this.syncItem(item);
304
- result.items.push(itemResult);
305
- if (itemResult.status === "synced") {
306
- result.synced += 1;
307
- } else if (itemResult.status === "failed") {
308
- result.failed += 1;
309
- } else if (itemResult.status === "retried") {
310
- result.retried += 1;
311
- } else if (itemResult.status === "deferred-backoff") {
312
- result.deferred += 1;
313
- } else if (itemResult.status === "network-error") {
314
- result.networkErrors += 1;
328
+ this.isFlushing = true;
329
+ const result = this.createEmptyResult();
330
+ try {
331
+ const pending = this.queue.getPendingForCollection(collectionName);
332
+ if (pending.length === 0) {
333
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
334
+ return result;
315
335
  }
336
+ for (const item of pending) {
337
+ result.attempted += 1;
338
+ const itemResult = await this.syncItem(item);
339
+ result.items.push(itemResult);
340
+ if (itemResult.status === "synced") {
341
+ result.synced += 1;
342
+ } else if (itemResult.status === "failed") {
343
+ result.failed += 1;
344
+ } else if (itemResult.status === "retried") {
345
+ result.retried += 1;
346
+ } else if (itemResult.status === "deferred-backoff") {
347
+ result.deferred += 1;
348
+ } else if (itemResult.status === "network-error") {
349
+ result.networkErrors += 1;
350
+ }
351
+ }
352
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
353
+ return result;
354
+ } finally {
355
+ this.isFlushing = false;
316
356
  }
317
- result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
318
- return result;
319
357
  }
320
358
  async flushRecord(recordId) {
321
359
  const item = this.queue.getPendingForRecord(recordId);
@@ -501,7 +539,7 @@ var _AsyncStorageSync = class _AsyncStorageSync {
501
539
  console.log("[AsyncStorageSync] autoSync enabled \u2014 starting sync engine");
502
540
  instance.engine.start();
503
541
  } else {
504
- console.log("[AsyncStorageSync] autoSync disabled \u2014 call flush() manually");
542
+ console.log("[AsyncStorageSync] autoSync disabled \u2014 call flushWithResult() or syncWithResult(collection) manually");
505
543
  }
506
544
  _AsyncStorageSync.instance = instance;
507
545
  return instance;
@@ -589,6 +627,9 @@ var _AsyncStorageSync = class _AsyncStorageSync {
589
627
  async syncWithResult(name) {
590
628
  return this.engine.flushCollectionWithResult(name);
591
629
  }
630
+ async syncManyWithResult(names) {
631
+ return this.engine.flushCollectionsWithResult(names);
632
+ }
592
633
  async syncById(_name, id) {
593
634
  await this.engine.flushRecord(id);
594
635
  }
package/dist/index.mjs CHANGED
@@ -199,9 +199,49 @@ var SyncEngine = class {
199
199
  clearTimeout(this.debounceTimer);
200
200
  }
201
201
  this.debounceTimer = setTimeout(() => {
202
- void this.flushWithResult();
202
+ void this.flushAutoSyncTargetWithResult();
203
203
  }, DEBOUNCE_MS);
204
204
  }
205
+ async flushAutoSyncTargetWithResult() {
206
+ const configuredCollections = this.config.autoSyncCollections?.map((name) => name.trim()).filter((name) => name.length > 0);
207
+ if (!configuredCollections || configuredCollections.length === 0) {
208
+ return this.flushWithResult();
209
+ }
210
+ return this.flushCollectionsWithResult(configuredCollections);
211
+ }
212
+ createEmptyResult() {
213
+ return {
214
+ attempted: 0,
215
+ synced: 0,
216
+ failed: 0,
217
+ retried: 0,
218
+ deferred: 0,
219
+ networkErrors: 0,
220
+ remainingPending: 0,
221
+ skippedAlreadyFlushing: false,
222
+ items: []
223
+ };
224
+ }
225
+ mergeResult(target, source) {
226
+ target.attempted += source.attempted;
227
+ target.synced += source.synced;
228
+ target.failed += source.failed;
229
+ target.retried += source.retried;
230
+ target.deferred += source.deferred;
231
+ target.networkErrors += source.networkErrors;
232
+ target.items.push(...source.items);
233
+ target.remainingPending = source.remainingPending;
234
+ target.skippedAlreadyFlushing = target.skippedAlreadyFlushing || source.skippedAlreadyFlushing;
235
+ }
236
+ async flushCollectionsWithResult(collectionNames) {
237
+ const result = this.createEmptyResult();
238
+ for (const collectionName of collectionNames) {
239
+ const collectionResult = await this.flushCollectionWithResult(collectionName);
240
+ this.mergeResult(result, collectionResult);
241
+ }
242
+ result.remainingPending = this.queue.getPending().length;
243
+ return result;
244
+ }
205
245
  async flushWithResult() {
206
246
  if (this.isFlushing) {
207
247
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
@@ -218,17 +258,7 @@ var SyncEngine = class {
218
258
  };
219
259
  }
220
260
  this.isFlushing = true;
221
- const result = {
222
- attempted: 0,
223
- synced: 0,
224
- failed: 0,
225
- retried: 0,
226
- deferred: 0,
227
- networkErrors: 0,
228
- remainingPending: 0,
229
- skippedAlreadyFlushing: false,
230
- items: []
231
- };
261
+ const result = this.createEmptyResult();
232
262
  try {
233
263
  const pending = this.queue.getPending();
234
264
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
@@ -260,40 +290,48 @@ var SyncEngine = class {
260
290
  }
261
291
  }
262
292
  async flushCollectionWithResult(collectionName) {
263
- const result = {
264
- attempted: 0,
265
- synced: 0,
266
- failed: 0,
267
- retried: 0,
268
- deferred: 0,
269
- networkErrors: 0,
270
- remainingPending: 0,
271
- skippedAlreadyFlushing: false,
272
- items: []
273
- };
274
- const pending = this.queue.getPendingForCollection(collectionName);
275
- if (pending.length === 0) {
276
- result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
277
- return result;
293
+ if (this.isFlushing) {
294
+ return {
295
+ attempted: 0,
296
+ synced: 0,
297
+ failed: 0,
298
+ retried: 0,
299
+ deferred: 0,
300
+ networkErrors: 0,
301
+ remainingPending: this.queue.getPendingForCollection(collectionName).length,
302
+ skippedAlreadyFlushing: true,
303
+ items: []
304
+ };
278
305
  }
279
- for (const item of pending) {
280
- result.attempted += 1;
281
- const itemResult = await this.syncItem(item);
282
- result.items.push(itemResult);
283
- if (itemResult.status === "synced") {
284
- result.synced += 1;
285
- } else if (itemResult.status === "failed") {
286
- result.failed += 1;
287
- } else if (itemResult.status === "retried") {
288
- result.retried += 1;
289
- } else if (itemResult.status === "deferred-backoff") {
290
- result.deferred += 1;
291
- } else if (itemResult.status === "network-error") {
292
- result.networkErrors += 1;
306
+ this.isFlushing = true;
307
+ const result = this.createEmptyResult();
308
+ try {
309
+ const pending = this.queue.getPendingForCollection(collectionName);
310
+ if (pending.length === 0) {
311
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
312
+ return result;
293
313
  }
314
+ for (const item of pending) {
315
+ result.attempted += 1;
316
+ const itemResult = await this.syncItem(item);
317
+ result.items.push(itemResult);
318
+ if (itemResult.status === "synced") {
319
+ result.synced += 1;
320
+ } else if (itemResult.status === "failed") {
321
+ result.failed += 1;
322
+ } else if (itemResult.status === "retried") {
323
+ result.retried += 1;
324
+ } else if (itemResult.status === "deferred-backoff") {
325
+ result.deferred += 1;
326
+ } else if (itemResult.status === "network-error") {
327
+ result.networkErrors += 1;
328
+ }
329
+ }
330
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
331
+ return result;
332
+ } finally {
333
+ this.isFlushing = false;
294
334
  }
295
- result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
296
- return result;
297
335
  }
298
336
  async flushRecord(recordId) {
299
337
  const item = this.queue.getPendingForRecord(recordId);
@@ -479,7 +517,7 @@ var _AsyncStorageSync = class _AsyncStorageSync {
479
517
  console.log("[AsyncStorageSync] autoSync enabled \u2014 starting sync engine");
480
518
  instance.engine.start();
481
519
  } else {
482
- console.log("[AsyncStorageSync] autoSync disabled \u2014 call flush() manually");
520
+ console.log("[AsyncStorageSync] autoSync disabled \u2014 call flushWithResult() or syncWithResult(collection) manually");
483
521
  }
484
522
  _AsyncStorageSync.instance = instance;
485
523
  return instance;
@@ -567,6 +605,9 @@ var _AsyncStorageSync = class _AsyncStorageSync {
567
605
  async syncWithResult(name) {
568
606
  return this.engine.flushCollectionWithResult(name);
569
607
  }
608
+ async syncManyWithResult(names) {
609
+ return this.engine.flushCollectionsWithResult(names);
610
+ }
570
611
  async syncById(_name, id) {
571
612
  await this.engine.flushRecord(id);
572
613
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "async-storage-sync",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Offline-first data layer for React Native with local-first storage and automatic sync",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",