async-storage-sync 1.0.4 → 1.0.6

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
@@ -48,8 +48,9 @@ await store.save('forms', {
48
48
  timestamp: new Date().toISOString()
49
49
  });
50
50
 
51
- // Sync all pending records to server
52
- await store.flush();
51
+ // Sync and get summary result
52
+ const result = await store.flushWithResult();
53
+ console.log(`Synced: ${result.synced}, Failed: ${result.failed}, Remaining: ${result.remainingPending}`);
53
54
 
54
55
  // List pending records
55
56
  const pending = await store.getAll('forms');
@@ -62,8 +63,8 @@ console.log(`${pending.length} forms waiting to sync`);
62
63
  - `autoSync: true` also listens for reconnect events and retries pending items automatically.
63
64
  - `autoSync: false`: no automatic syncing; call sync methods manually when you choose.
64
65
  - Manual methods:
65
- - `store.flush()` → sync all pending items
66
- - `store.sync(collection)` → sync one collection
66
+ - `store.flushWithResult()` → sync all pending and return summary counts
67
+ - `store.syncWithResult(collection)` → sync one collection and return summary counts
67
68
  - `store.syncById(collection, id)` → sync one record
68
69
  - Sync destination is controlled by your config: `serverUrl + endpoint`.
69
70
 
@@ -86,8 +87,8 @@ console.log(`${pending.length} forms waiting to sync`);
86
87
  | `store.getById(collection, id)` | Get one record by internal `_id` |
87
88
  | `store.deleteById(collection, id)` | Delete one record by internal `_id` |
88
89
  | `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 |
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()`) |
91
92
  | `store.syncById(collection, id)` | Sync one specific record by internal `_id` |
92
93
  | `store.requeueFailed()` | Move `failed` records back to pending queue for retry |
93
94
  | `store.onSynced(callback)` | Event callback for successful sync of each item |
@@ -105,7 +106,7 @@ import { getSyncQueue } from 'async-storage-sync';
105
106
 
106
107
  NetInfo.addEventListener(state => {
107
108
  if (state.isConnected) {
108
- void getSyncQueue().flush();
109
+ void getSyncQueue().flushWithResult();
109
110
  }
110
111
  });
111
112
  ```
@@ -190,7 +191,7 @@ Notes:
190
191
 
191
192
  1. **Save** — Records written to AsyncStorage immediately
192
193
  2. **Queue** — Each save queued for syncing
193
- 3. **Sync** — `flush()` POSTs all pending to your server
194
+ 3. **Sync** — `flushWithResult()` POSTs all pending to your server and returns summary output
194
195
  4. **Status** — Records marked synced, then kept or deleted
195
196
  5. **Retry** — Failed syncs retry automatically (max 5x)
196
197
  6. **Persist** — Everything survives app restart
package/dist/index.d.mts CHANGED
@@ -42,6 +42,25 @@ interface SaveOptions {
42
42
  onSyncSuccess?: OnSyncSuccess;
43
43
  duplicateStrategy?: DuplicateStrategy;
44
44
  }
45
+ type FlushItemStatus = 'synced' | 'failed' | 'retried' | 'deferred-backoff' | 'network-error';
46
+ interface FlushItemResult {
47
+ itemId: string;
48
+ collection: string;
49
+ recordId: string;
50
+ status: FlushItemStatus;
51
+ httpStatus?: number;
52
+ }
53
+ interface FlushResult {
54
+ attempted: number;
55
+ synced: number;
56
+ failed: number;
57
+ retried: number;
58
+ deferred: number;
59
+ networkErrors: number;
60
+ remainingPending: number;
61
+ skippedAlreadyFlushing: boolean;
62
+ items: FlushItemResult[];
63
+ }
45
64
  type SyncedCallback = (item: QueueItem) => void;
46
65
  type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
47
66
  type StorageFullCallback = () => void;
@@ -61,9 +80,9 @@ declare class AsyncStorageSync {
61
80
  getById<T extends Record<string, unknown>>(name: string, id: string): Promise<StoredRecord<T> | null>;
62
81
  deleteById(name: string, id: string): Promise<void>;
63
82
  deleteCollection(name: string): Promise<void>;
64
- sync(name: string): Promise<void>;
83
+ syncWithResult(name: string): Promise<FlushResult>;
65
84
  syncById(_name: string, id: string): Promise<void>;
66
- flush(): Promise<void>;
85
+ flushWithResult(): Promise<FlushResult>;
67
86
  /**
68
87
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
69
88
  * Called automatically on init to recover from previous 4xx/500 failures.
@@ -99,4 +118,4 @@ declare function getSyncQueue(): AsyncStorageSync;
99
118
  */
100
119
  declare function setStorageDriver(storage: AsyncStorageClient): void;
101
120
 
102
- 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 };
121
+ export { AsyncStorageSync, type AuthErrorCallback, type DriverName, type DuplicateStrategy, type FlushItemResult, type FlushItemStatus, type FlushResult, 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 CHANGED
@@ -42,6 +42,25 @@ interface SaveOptions {
42
42
  onSyncSuccess?: OnSyncSuccess;
43
43
  duplicateStrategy?: DuplicateStrategy;
44
44
  }
45
+ type FlushItemStatus = 'synced' | 'failed' | 'retried' | 'deferred-backoff' | 'network-error';
46
+ interface FlushItemResult {
47
+ itemId: string;
48
+ collection: string;
49
+ recordId: string;
50
+ status: FlushItemStatus;
51
+ httpStatus?: number;
52
+ }
53
+ interface FlushResult {
54
+ attempted: number;
55
+ synced: number;
56
+ failed: number;
57
+ retried: number;
58
+ deferred: number;
59
+ networkErrors: number;
60
+ remainingPending: number;
61
+ skippedAlreadyFlushing: boolean;
62
+ items: FlushItemResult[];
63
+ }
45
64
  type SyncedCallback = (item: QueueItem) => void;
46
65
  type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
47
66
  type StorageFullCallback = () => void;
@@ -61,9 +80,9 @@ declare class AsyncStorageSync {
61
80
  getById<T extends Record<string, unknown>>(name: string, id: string): Promise<StoredRecord<T> | null>;
62
81
  deleteById(name: string, id: string): Promise<void>;
63
82
  deleteCollection(name: string): Promise<void>;
64
- sync(name: string): Promise<void>;
83
+ syncWithResult(name: string): Promise<FlushResult>;
65
84
  syncById(_name: string, id: string): Promise<void>;
66
- flush(): Promise<void>;
85
+ flushWithResult(): Promise<FlushResult>;
67
86
  /**
68
87
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
69
88
  * Called automatically on init to recover from previous 4xx/500 failures.
@@ -99,4 +118,4 @@ declare function getSyncQueue(): AsyncStorageSync;
99
118
  */
100
119
  declare function setStorageDriver(storage: AsyncStorageClient): void;
101
120
 
102
- 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 };
121
+ export { AsyncStorageSync, type AuthErrorCallback, type DriverName, type DuplicateStrategy, type FlushItemResult, type FlushItemStatus, type FlushResult, type InitConfig, type OnSyncSuccess, type QueueItem, type RecordMeta, type SaveOptions, type StorageFullCallback, type StoredRecord, type SyncStatus, type SyncedCallback, getSyncQueue, initSyncQueue, setStorageDriver };
package/dist/index.js CHANGED
@@ -221,34 +221,101 @@ var SyncEngine = class {
221
221
  clearTimeout(this.debounceTimer);
222
222
  }
223
223
  this.debounceTimer = setTimeout(() => {
224
- void this.flush();
224
+ void this.flushWithResult();
225
225
  }, DEBOUNCE_MS);
226
226
  }
227
- async flush() {
227
+ async flushWithResult() {
228
228
  if (this.isFlushing) {
229
229
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
230
- return;
230
+ return {
231
+ attempted: 0,
232
+ synced: 0,
233
+ failed: 0,
234
+ retried: 0,
235
+ deferred: 0,
236
+ networkErrors: 0,
237
+ remainingPending: this.queue.getPending().length,
238
+ skippedAlreadyFlushing: true,
239
+ items: []
240
+ };
231
241
  }
232
242
  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
+ };
233
254
  try {
234
255
  const pending = this.queue.getPending();
235
256
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
236
257
  if (pending.length === 0) {
237
258
  console.log("[SyncEngine] Nothing to sync.");
238
- return;
259
+ result.remainingPending = 0;
260
+ return result;
239
261
  }
240
262
  for (const item of pending) {
241
- await this.syncItem(item);
263
+ result.attempted += 1;
264
+ const itemResult = await this.syncItem(item);
265
+ result.items.push(itemResult);
266
+ if (itemResult.status === "synced") {
267
+ result.synced += 1;
268
+ } else if (itemResult.status === "failed") {
269
+ result.failed += 1;
270
+ } else if (itemResult.status === "retried") {
271
+ result.retried += 1;
272
+ } else if (itemResult.status === "deferred-backoff") {
273
+ result.deferred += 1;
274
+ } else if (itemResult.status === "network-error") {
275
+ result.networkErrors += 1;
276
+ }
242
277
  }
278
+ result.remainingPending = this.queue.getPending().length;
279
+ return result;
243
280
  } finally {
244
281
  this.isFlushing = false;
245
282
  }
246
283
  }
247
- async flushCollection(collectionName) {
284
+ 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
+ };
248
296
  const pending = this.queue.getPendingForCollection(collectionName);
297
+ if (pending.length === 0) {
298
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
299
+ return result;
300
+ }
249
301
  for (const item of pending) {
250
- await this.syncItem(item);
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;
315
+ }
251
316
  }
317
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
318
+ return result;
252
319
  }
253
320
  async flushRecord(recordId) {
254
321
  const item = this.queue.getPendingForRecord(recordId);
@@ -262,7 +329,12 @@ var SyncEngine = class {
262
329
  const elapsed = Date.now() - item.ts;
263
330
  if (elapsed < backoffMs) {
264
331
  console.log(`[SyncEngine] syncItem backoff \u2014 retries: ${item.retries}, wait: ${backoffMs - elapsed}ms remaining`);
265
- return;
332
+ return {
333
+ itemId: item.id,
334
+ collection: item.key,
335
+ recordId: item.recordId,
336
+ status: "deferred-backoff"
337
+ };
266
338
  }
267
339
  }
268
340
  try {
@@ -281,13 +353,47 @@ var SyncEngine = class {
281
353
  console.log(`[SyncEngine] Response: ${response.status} ${response.statusText}`);
282
354
  if (response.ok) {
283
355
  await this.handleSuccess(item);
356
+ return {
357
+ itemId: item.id,
358
+ collection: item.key,
359
+ recordId: item.recordId,
360
+ status: "synced",
361
+ httpStatus: response.status
362
+ };
284
363
  } else if (response.status >= 400 && response.status < 500) {
285
364
  await this.handleClientError(item, response.status);
365
+ return {
366
+ itemId: item.id,
367
+ collection: item.key,
368
+ recordId: item.recordId,
369
+ status: "failed",
370
+ httpStatus: response.status
371
+ };
286
372
  } else if (response.status >= 500) {
287
373
  await this.handleServerError(item);
374
+ return {
375
+ itemId: item.id,
376
+ collection: item.key,
377
+ recordId: item.recordId,
378
+ status: "retried",
379
+ httpStatus: response.status
380
+ };
288
381
  }
382
+ return {
383
+ itemId: item.id,
384
+ collection: item.key,
385
+ recordId: item.recordId,
386
+ status: "failed",
387
+ httpStatus: response.status
388
+ };
289
389
  } catch (e) {
290
390
  console.warn("[SyncEngine] \u{1F50C} Network error (offline?) \u2014 will retry on next flush:", e);
391
+ return {
392
+ itemId: item.id,
393
+ collection: item.key,
394
+ recordId: item.recordId,
395
+ status: "network-error"
396
+ };
291
397
  }
292
398
  }
293
399
  async handleSuccess(item) {
@@ -480,14 +586,14 @@ var _AsyncStorageSync = class _AsyncStorageSync {
480
586
  async deleteCollection(name) {
481
587
  await this.driver.remove(collectionKey(name));
482
588
  }
483
- async sync(name) {
484
- await this.engine.flushCollection(name);
589
+ async syncWithResult(name) {
590
+ return this.engine.flushCollectionWithResult(name);
485
591
  }
486
592
  async syncById(_name, id) {
487
593
  await this.engine.flushRecord(id);
488
594
  }
489
- async flush() {
490
- await this.engine.flush();
595
+ async flushWithResult() {
596
+ return this.engine.flushWithResult();
491
597
  }
492
598
  /**
493
599
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
package/dist/index.mjs CHANGED
@@ -199,34 +199,101 @@ var SyncEngine = class {
199
199
  clearTimeout(this.debounceTimer);
200
200
  }
201
201
  this.debounceTimer = setTimeout(() => {
202
- void this.flush();
202
+ void this.flushWithResult();
203
203
  }, DEBOUNCE_MS);
204
204
  }
205
- async flush() {
205
+ async flushWithResult() {
206
206
  if (this.isFlushing) {
207
207
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
208
- return;
208
+ return {
209
+ attempted: 0,
210
+ synced: 0,
211
+ failed: 0,
212
+ retried: 0,
213
+ deferred: 0,
214
+ networkErrors: 0,
215
+ remainingPending: this.queue.getPending().length,
216
+ skippedAlreadyFlushing: true,
217
+ items: []
218
+ };
209
219
  }
210
220
  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
+ };
211
232
  try {
212
233
  const pending = this.queue.getPending();
213
234
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
214
235
  if (pending.length === 0) {
215
236
  console.log("[SyncEngine] Nothing to sync.");
216
- return;
237
+ result.remainingPending = 0;
238
+ return result;
217
239
  }
218
240
  for (const item of pending) {
219
- await this.syncItem(item);
241
+ result.attempted += 1;
242
+ const itemResult = await this.syncItem(item);
243
+ result.items.push(itemResult);
244
+ if (itemResult.status === "synced") {
245
+ result.synced += 1;
246
+ } else if (itemResult.status === "failed") {
247
+ result.failed += 1;
248
+ } else if (itemResult.status === "retried") {
249
+ result.retried += 1;
250
+ } else if (itemResult.status === "deferred-backoff") {
251
+ result.deferred += 1;
252
+ } else if (itemResult.status === "network-error") {
253
+ result.networkErrors += 1;
254
+ }
220
255
  }
256
+ result.remainingPending = this.queue.getPending().length;
257
+ return result;
221
258
  } finally {
222
259
  this.isFlushing = false;
223
260
  }
224
261
  }
225
- async flushCollection(collectionName) {
262
+ 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
+ };
226
274
  const pending = this.queue.getPendingForCollection(collectionName);
275
+ if (pending.length === 0) {
276
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
277
+ return result;
278
+ }
227
279
  for (const item of pending) {
228
- await this.syncItem(item);
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;
293
+ }
229
294
  }
295
+ result.remainingPending = this.queue.getPendingForCollection(collectionName).length;
296
+ return result;
230
297
  }
231
298
  async flushRecord(recordId) {
232
299
  const item = this.queue.getPendingForRecord(recordId);
@@ -240,7 +307,12 @@ var SyncEngine = class {
240
307
  const elapsed = Date.now() - item.ts;
241
308
  if (elapsed < backoffMs) {
242
309
  console.log(`[SyncEngine] syncItem backoff \u2014 retries: ${item.retries}, wait: ${backoffMs - elapsed}ms remaining`);
243
- return;
310
+ return {
311
+ itemId: item.id,
312
+ collection: item.key,
313
+ recordId: item.recordId,
314
+ status: "deferred-backoff"
315
+ };
244
316
  }
245
317
  }
246
318
  try {
@@ -259,13 +331,47 @@ var SyncEngine = class {
259
331
  console.log(`[SyncEngine] Response: ${response.status} ${response.statusText}`);
260
332
  if (response.ok) {
261
333
  await this.handleSuccess(item);
334
+ return {
335
+ itemId: item.id,
336
+ collection: item.key,
337
+ recordId: item.recordId,
338
+ status: "synced",
339
+ httpStatus: response.status
340
+ };
262
341
  } else if (response.status >= 400 && response.status < 500) {
263
342
  await this.handleClientError(item, response.status);
343
+ return {
344
+ itemId: item.id,
345
+ collection: item.key,
346
+ recordId: item.recordId,
347
+ status: "failed",
348
+ httpStatus: response.status
349
+ };
264
350
  } else if (response.status >= 500) {
265
351
  await this.handleServerError(item);
352
+ return {
353
+ itemId: item.id,
354
+ collection: item.key,
355
+ recordId: item.recordId,
356
+ status: "retried",
357
+ httpStatus: response.status
358
+ };
266
359
  }
360
+ return {
361
+ itemId: item.id,
362
+ collection: item.key,
363
+ recordId: item.recordId,
364
+ status: "failed",
365
+ httpStatus: response.status
366
+ };
267
367
  } catch (e) {
268
368
  console.warn("[SyncEngine] \u{1F50C} Network error (offline?) \u2014 will retry on next flush:", e);
369
+ return {
370
+ itemId: item.id,
371
+ collection: item.key,
372
+ recordId: item.recordId,
373
+ status: "network-error"
374
+ };
269
375
  }
270
376
  }
271
377
  async handleSuccess(item) {
@@ -458,14 +564,14 @@ var _AsyncStorageSync = class _AsyncStorageSync {
458
564
  async deleteCollection(name) {
459
565
  await this.driver.remove(collectionKey(name));
460
566
  }
461
- async sync(name) {
462
- await this.engine.flushCollection(name);
567
+ async syncWithResult(name) {
568
+ return this.engine.flushCollectionWithResult(name);
463
569
  }
464
570
  async syncById(_name, id) {
465
571
  await this.engine.flushRecord(id);
466
572
  }
467
- async flush() {
468
- await this.engine.flush();
573
+ async flushWithResult() {
574
+ return this.engine.flushWithResult();
469
575
  }
470
576
  /**
471
577
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "async-storage-sync",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",