async-storage-sync 1.0.3 → 1.0.5

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,7 +63,7 @@ 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.flushWithResult()` → sync all pending and return summary counts
66
67
  - `store.sync(collection)` → sync one collection
67
68
  - `store.syncById(collection, id)` → sync one record
68
69
  - Sync destination is controlled by your config: `serverUrl + endpoint`.
@@ -86,7 +87,7 @@ 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.flushWithResult()` | Sync all pending and return detailed summary (`attempted`, `synced`, `failed`, `retried`, `remainingPending`, `items`) |
90
91
  | `store.sync(collection)` | Sync pending items for one collection only |
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 |
@@ -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
@@ -4,9 +4,7 @@ type DuplicateStrategy = 'append' | 'overwrite';
4
4
  interface InitConfig {
5
5
  driver: DriverName;
6
6
  serverUrl: string;
7
- credentials: {
8
- apiKey: string;
9
- };
7
+ credentials: Record<string, string>;
10
8
  /**
11
9
  * Optional transform applied to the stored record before it is sent to the server.
12
10
  * Use this to strip internal meta fields, rename keys, or reshape the payload
@@ -44,6 +42,25 @@ interface SaveOptions {
44
42
  onSyncSuccess?: OnSyncSuccess;
45
43
  duplicateStrategy?: DuplicateStrategy;
46
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
+ }
47
64
  type SyncedCallback = (item: QueueItem) => void;
48
65
  type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
49
66
  type StorageFullCallback = () => void;
@@ -65,7 +82,7 @@ declare class AsyncStorageSync {
65
82
  deleteCollection(name: string): Promise<void>;
66
83
  sync(name: string): Promise<void>;
67
84
  syncById(_name: string, id: string): Promise<void>;
68
- flush(): Promise<void>;
85
+ flushWithResult(): Promise<FlushResult>;
69
86
  /**
70
87
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
71
88
  * Called automatically on init to recover from previous 4xx/500 failures.
@@ -101,4 +118,4 @@ declare function getSyncQueue(): AsyncStorageSync;
101
118
  */
102
119
  declare function setStorageDriver(storage: AsyncStorageClient): void;
103
120
 
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 };
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
@@ -4,9 +4,7 @@ type DuplicateStrategy = 'append' | 'overwrite';
4
4
  interface InitConfig {
5
5
  driver: DriverName;
6
6
  serverUrl: string;
7
- credentials: {
8
- apiKey: string;
9
- };
7
+ credentials: Record<string, string>;
10
8
  /**
11
9
  * Optional transform applied to the stored record before it is sent to the server.
12
10
  * Use this to strip internal meta fields, rename keys, or reshape the payload
@@ -44,6 +42,25 @@ interface SaveOptions {
44
42
  onSyncSuccess?: OnSyncSuccess;
45
43
  duplicateStrategy?: DuplicateStrategy;
46
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
+ }
47
64
  type SyncedCallback = (item: QueueItem) => void;
48
65
  type AuthErrorCallback = (statusCode: number, item: QueueItem) => void;
49
66
  type StorageFullCallback = () => void;
@@ -65,7 +82,7 @@ declare class AsyncStorageSync {
65
82
  deleteCollection(name: string): Promise<void>;
66
83
  sync(name: string): Promise<void>;
67
84
  syncById(_name: string, id: string): Promise<void>;
68
- flush(): Promise<void>;
85
+ flushWithResult(): Promise<FlushResult>;
69
86
  /**
70
87
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
71
88
  * Called automatically on init to recover from previous 4xx/500 failures.
@@ -101,4 +118,4 @@ declare function getSyncQueue(): AsyncStorageSync;
101
118
  */
102
119
  declare function setStorageDriver(storage: AsyncStorageClient): void;
103
120
 
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 };
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
@@ -155,6 +155,15 @@ var Queue = class {
155
155
  // src/core/sync-engine.ts
156
156
  var DEBOUNCE_MS = 500;
157
157
  var BACKOFF_BASE_MS = 1e3;
158
+ function buildAuthHeaders(credentials) {
159
+ const headers = { ...credentials };
160
+ const apiKey = headers.apiKey;
161
+ if (apiKey && !headers.Authorization && !headers.authorization) {
162
+ headers.Authorization = `Bearer ${apiKey}`;
163
+ }
164
+ delete headers.apiKey;
165
+ return headers;
166
+ }
158
167
  var SyncEngine = class {
159
168
  constructor(config, queue, driver) {
160
169
  this.config = config;
@@ -212,25 +221,62 @@ var SyncEngine = class {
212
221
  clearTimeout(this.debounceTimer);
213
222
  }
214
223
  this.debounceTimer = setTimeout(() => {
215
- void this.flush();
224
+ void this.flushWithResult();
216
225
  }, DEBOUNCE_MS);
217
226
  }
218
- async flush() {
227
+ async flushWithResult() {
219
228
  if (this.isFlushing) {
220
229
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
221
- 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
+ };
222
241
  }
223
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
+ };
224
254
  try {
225
255
  const pending = this.queue.getPending();
226
256
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
227
257
  if (pending.length === 0) {
228
258
  console.log("[SyncEngine] Nothing to sync.");
229
- return;
259
+ result.remainingPending = 0;
260
+ return result;
230
261
  }
231
262
  for (const item of pending) {
232
- 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
+ }
233
277
  }
278
+ result.remainingPending = this.queue.getPending().length;
279
+ return result;
234
280
  } finally {
235
281
  this.isFlushing = false;
236
282
  }
@@ -253,7 +299,12 @@ var SyncEngine = class {
253
299
  const elapsed = Date.now() - item.ts;
254
300
  if (elapsed < backoffMs) {
255
301
  console.log(`[SyncEngine] syncItem backoff \u2014 retries: ${item.retries}, wait: ${backoffMs - elapsed}ms remaining`);
256
- return;
302
+ return {
303
+ itemId: item.id,
304
+ collection: item.key,
305
+ recordId: item.recordId,
306
+ status: "deferred-backoff"
307
+ };
257
308
  }
258
309
  }
259
310
  try {
@@ -265,20 +316,54 @@ var SyncEngine = class {
265
316
  method: "POST",
266
317
  headers: {
267
318
  "Content-Type": "application/json",
268
- Authorization: `Bearer ${this.config.credentials.apiKey}`
319
+ ...buildAuthHeaders(this.config.credentials)
269
320
  },
270
321
  body: JSON.stringify(body)
271
322
  });
272
323
  console.log(`[SyncEngine] Response: ${response.status} ${response.statusText}`);
273
324
  if (response.ok) {
274
325
  await this.handleSuccess(item);
326
+ return {
327
+ itemId: item.id,
328
+ collection: item.key,
329
+ recordId: item.recordId,
330
+ status: "synced",
331
+ httpStatus: response.status
332
+ };
275
333
  } else if (response.status >= 400 && response.status < 500) {
276
334
  await this.handleClientError(item, response.status);
335
+ return {
336
+ itemId: item.id,
337
+ collection: item.key,
338
+ recordId: item.recordId,
339
+ status: "failed",
340
+ httpStatus: response.status
341
+ };
277
342
  } else if (response.status >= 500) {
278
343
  await this.handleServerError(item);
344
+ return {
345
+ itemId: item.id,
346
+ collection: item.key,
347
+ recordId: item.recordId,
348
+ status: "retried",
349
+ httpStatus: response.status
350
+ };
279
351
  }
352
+ return {
353
+ itemId: item.id,
354
+ collection: item.key,
355
+ recordId: item.recordId,
356
+ status: "failed",
357
+ httpStatus: response.status
358
+ };
280
359
  } catch (e) {
281
360
  console.warn("[SyncEngine] \u{1F50C} Network error (offline?) \u2014 will retry on next flush:", e);
361
+ return {
362
+ itemId: item.id,
363
+ collection: item.key,
364
+ recordId: item.recordId,
365
+ status: "network-error"
366
+ };
282
367
  }
283
368
  }
284
369
  async handleSuccess(item) {
@@ -477,8 +562,8 @@ var _AsyncStorageSync = class _AsyncStorageSync {
477
562
  async syncById(_name, id) {
478
563
  await this.engine.flushRecord(id);
479
564
  }
480
- async flush() {
481
- await this.engine.flush();
565
+ async flushWithResult() {
566
+ return this.engine.flushWithResult();
482
567
  }
483
568
  /**
484
569
  * Re-enqueue any records marked as 'failed' so they are retried on next flush.
package/dist/index.mjs CHANGED
@@ -133,6 +133,15 @@ var Queue = class {
133
133
  // src/core/sync-engine.ts
134
134
  var DEBOUNCE_MS = 500;
135
135
  var BACKOFF_BASE_MS = 1e3;
136
+ function buildAuthHeaders(credentials) {
137
+ const headers = { ...credentials };
138
+ const apiKey = headers.apiKey;
139
+ if (apiKey && !headers.Authorization && !headers.authorization) {
140
+ headers.Authorization = `Bearer ${apiKey}`;
141
+ }
142
+ delete headers.apiKey;
143
+ return headers;
144
+ }
136
145
  var SyncEngine = class {
137
146
  constructor(config, queue, driver) {
138
147
  this.config = config;
@@ -190,25 +199,62 @@ var SyncEngine = class {
190
199
  clearTimeout(this.debounceTimer);
191
200
  }
192
201
  this.debounceTimer = setTimeout(() => {
193
- void this.flush();
202
+ void this.flushWithResult();
194
203
  }, DEBOUNCE_MS);
195
204
  }
196
- async flush() {
205
+ async flushWithResult() {
197
206
  if (this.isFlushing) {
198
207
  console.log("[SyncEngine] flush() skipped \u2014 already flushing");
199
- 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
+ };
200
219
  }
201
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
+ };
202
232
  try {
203
233
  const pending = this.queue.getPending();
204
234
  console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
205
235
  if (pending.length === 0) {
206
236
  console.log("[SyncEngine] Nothing to sync.");
207
- return;
237
+ result.remainingPending = 0;
238
+ return result;
208
239
  }
209
240
  for (const item of pending) {
210
- 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
+ }
211
255
  }
256
+ result.remainingPending = this.queue.getPending().length;
257
+ return result;
212
258
  } finally {
213
259
  this.isFlushing = false;
214
260
  }
@@ -231,7 +277,12 @@ var SyncEngine = class {
231
277
  const elapsed = Date.now() - item.ts;
232
278
  if (elapsed < backoffMs) {
233
279
  console.log(`[SyncEngine] syncItem backoff \u2014 retries: ${item.retries}, wait: ${backoffMs - elapsed}ms remaining`);
234
- return;
280
+ return {
281
+ itemId: item.id,
282
+ collection: item.key,
283
+ recordId: item.recordId,
284
+ status: "deferred-backoff"
285
+ };
235
286
  }
236
287
  }
237
288
  try {
@@ -243,20 +294,54 @@ var SyncEngine = class {
243
294
  method: "POST",
244
295
  headers: {
245
296
  "Content-Type": "application/json",
246
- Authorization: `Bearer ${this.config.credentials.apiKey}`
297
+ ...buildAuthHeaders(this.config.credentials)
247
298
  },
248
299
  body: JSON.stringify(body)
249
300
  });
250
301
  console.log(`[SyncEngine] Response: ${response.status} ${response.statusText}`);
251
302
  if (response.ok) {
252
303
  await this.handleSuccess(item);
304
+ return {
305
+ itemId: item.id,
306
+ collection: item.key,
307
+ recordId: item.recordId,
308
+ status: "synced",
309
+ httpStatus: response.status
310
+ };
253
311
  } else if (response.status >= 400 && response.status < 500) {
254
312
  await this.handleClientError(item, response.status);
313
+ return {
314
+ itemId: item.id,
315
+ collection: item.key,
316
+ recordId: item.recordId,
317
+ status: "failed",
318
+ httpStatus: response.status
319
+ };
255
320
  } else if (response.status >= 500) {
256
321
  await this.handleServerError(item);
322
+ return {
323
+ itemId: item.id,
324
+ collection: item.key,
325
+ recordId: item.recordId,
326
+ status: "retried",
327
+ httpStatus: response.status
328
+ };
257
329
  }
330
+ return {
331
+ itemId: item.id,
332
+ collection: item.key,
333
+ recordId: item.recordId,
334
+ status: "failed",
335
+ httpStatus: response.status
336
+ };
258
337
  } catch (e) {
259
338
  console.warn("[SyncEngine] \u{1F50C} Network error (offline?) \u2014 will retry on next flush:", e);
339
+ return {
340
+ itemId: item.id,
341
+ collection: item.key,
342
+ recordId: item.recordId,
343
+ status: "network-error"
344
+ };
260
345
  }
261
346
  }
262
347
  async handleSuccess(item) {
@@ -455,8 +540,8 @@ var _AsyncStorageSync = class _AsyncStorageSync {
455
540
  async syncById(_name, id) {
456
541
  await this.engine.flushRecord(id);
457
542
  }
458
- async flush() {
459
- await this.engine.flush();
543
+ async flushWithResult() {
544
+ return this.engine.flushWithResult();
460
545
  }
461
546
  /**
462
547
  * 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.3",
3
+ "version": "1.0.5",
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",