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/dist/index.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AsyncStorageSync: () => AsyncStorageSync,
|
|
24
|
+
getSyncQueue: () => getSyncQueue,
|
|
25
|
+
initSyncQueue: () => initSyncQueue,
|
|
26
|
+
setStorageDriver: () => setStorageDriver
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/drivers/AsyncStorageDriver.ts
|
|
31
|
+
var _AsyncStorageDriver = class _AsyncStorageDriver {
|
|
32
|
+
static setStorageClient(storage) {
|
|
33
|
+
_AsyncStorageDriver.injectedStorage = storage;
|
|
34
|
+
}
|
|
35
|
+
constructor() {
|
|
36
|
+
if (_AsyncStorageDriver.injectedStorage) {
|
|
37
|
+
this.storage = _AsyncStorageDriver.injectedStorage;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const injectedStorage = globalThis.__ASYNC_STORAGE__;
|
|
41
|
+
if (injectedStorage) {
|
|
42
|
+
this.storage = injectedStorage;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
this.storage = require("@react-native-async-storage/async-storage").default;
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"[async-storage-sync] AsyncStorageDriver requires @react-native-async-storage/async-storage."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async get(key) {
|
|
54
|
+
return this.storage.getItem(key);
|
|
55
|
+
}
|
|
56
|
+
async set(key, value) {
|
|
57
|
+
try {
|
|
58
|
+
await this.storage.setItem(key, value);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (String(error).includes("quota") || String(error).includes("full")) {
|
|
61
|
+
throw new Error("STORAGE_FULL");
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async remove(key) {
|
|
67
|
+
await this.storage.removeItem(key);
|
|
68
|
+
}
|
|
69
|
+
async getAllKeys() {
|
|
70
|
+
const keys = await this.storage.getAllKeys();
|
|
71
|
+
return keys ? [...keys] : [];
|
|
72
|
+
}
|
|
73
|
+
async clear() {
|
|
74
|
+
await this.storage.clear();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
_AsyncStorageDriver.injectedStorage = null;
|
|
78
|
+
var AsyncStorageDriver = _AsyncStorageDriver;
|
|
79
|
+
|
|
80
|
+
// src/core/queue.ts
|
|
81
|
+
var QUEUE_KEY = "asyncstorage::__queue__";
|
|
82
|
+
var MAX_RETRIES = 5;
|
|
83
|
+
var Queue = class {
|
|
84
|
+
constructor(driver) {
|
|
85
|
+
this.driver = driver;
|
|
86
|
+
this.items = [];
|
|
87
|
+
this.loaded = false;
|
|
88
|
+
}
|
|
89
|
+
async load() {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await this.driver.get(QUEUE_KEY);
|
|
92
|
+
this.items = raw ? JSON.parse(raw) : [];
|
|
93
|
+
} catch {
|
|
94
|
+
this.items = [];
|
|
95
|
+
}
|
|
96
|
+
this.loaded = true;
|
|
97
|
+
}
|
|
98
|
+
async enqueue(item) {
|
|
99
|
+
this.assertLoaded();
|
|
100
|
+
this.items.push(item);
|
|
101
|
+
await this.persist();
|
|
102
|
+
}
|
|
103
|
+
getPending() {
|
|
104
|
+
this.assertLoaded();
|
|
105
|
+
return this.items.filter((item) => !item.synced && item.retries < MAX_RETRIES);
|
|
106
|
+
}
|
|
107
|
+
getPendingForCollection(collectionName) {
|
|
108
|
+
return this.getPending().filter((item) => item.key === collectionName);
|
|
109
|
+
}
|
|
110
|
+
getPendingForRecord(recordId) {
|
|
111
|
+
return this.getPending().find((item) => item.recordId === recordId);
|
|
112
|
+
}
|
|
113
|
+
getAll() {
|
|
114
|
+
this.assertLoaded();
|
|
115
|
+
return [...this.items];
|
|
116
|
+
}
|
|
117
|
+
async markSynced(itemId) {
|
|
118
|
+
this.assertLoaded();
|
|
119
|
+
const item = this.items.find((entry) => entry.id === itemId);
|
|
120
|
+
if (item) {
|
|
121
|
+
item.synced = true;
|
|
122
|
+
item.retries = 0;
|
|
123
|
+
}
|
|
124
|
+
await this.persist();
|
|
125
|
+
}
|
|
126
|
+
async incrementRetry(itemId) {
|
|
127
|
+
this.assertLoaded();
|
|
128
|
+
const item = this.items.find((entry) => entry.id === itemId);
|
|
129
|
+
if (item) {
|
|
130
|
+
item.retries += 1;
|
|
131
|
+
}
|
|
132
|
+
await this.persist();
|
|
133
|
+
}
|
|
134
|
+
async remove(itemId) {
|
|
135
|
+
this.assertLoaded();
|
|
136
|
+
this.items = this.items.filter((entry) => entry.id !== itemId);
|
|
137
|
+
await this.persist();
|
|
138
|
+
}
|
|
139
|
+
async clear() {
|
|
140
|
+
this.items = [];
|
|
141
|
+
await this.persist();
|
|
142
|
+
}
|
|
143
|
+
async persist() {
|
|
144
|
+
await this.driver.set(QUEUE_KEY, JSON.stringify(this.items));
|
|
145
|
+
}
|
|
146
|
+
assertLoaded() {
|
|
147
|
+
if (!this.loaded) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"[Queue] Queue has not been loaded yet. Ensure AsyncStorageSync.init() has completed before use."
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/core/sync-engine.ts
|
|
156
|
+
var DEBOUNCE_MS = 500;
|
|
157
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
158
|
+
var SyncEngine = class {
|
|
159
|
+
constructor(config, queue, driver) {
|
|
160
|
+
this.config = config;
|
|
161
|
+
this.queue = queue;
|
|
162
|
+
this.driver = driver;
|
|
163
|
+
this.isFlushing = false;
|
|
164
|
+
}
|
|
165
|
+
onSynced(cb) {
|
|
166
|
+
this.onSyncedCb = cb;
|
|
167
|
+
}
|
|
168
|
+
onAuthError(cb) {
|
|
169
|
+
this.onAuthErrorCb = cb;
|
|
170
|
+
}
|
|
171
|
+
onStorageFull(cb) {
|
|
172
|
+
this.onStorageFullCb = cb;
|
|
173
|
+
}
|
|
174
|
+
emitStorageFull() {
|
|
175
|
+
this.onStorageFullCb?.();
|
|
176
|
+
}
|
|
177
|
+
start() {
|
|
178
|
+
console.log("[SyncEngine] start() called");
|
|
179
|
+
try {
|
|
180
|
+
const NetInfo = require("@react-native-community/netinfo").default;
|
|
181
|
+
console.log("[SyncEngine] NetInfo loaded \u2705");
|
|
182
|
+
this.unsubscribeNetInfo = NetInfo.addEventListener((state) => {
|
|
183
|
+
console.log("[SyncEngine] NetInfo change \u2192 isConnected:", state.isConnected);
|
|
184
|
+
if (state.isConnected) {
|
|
185
|
+
this.scheduleFlush();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
NetInfo.fetch().then((state) => {
|
|
189
|
+
console.log("[SyncEngine] NetInfo.fetch() \u2192 isConnected:", state.isConnected);
|
|
190
|
+
if (state.isConnected) {
|
|
191
|
+
this.scheduleFlush();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.warn("[SyncEngine] \u26A0\uFE0F NetInfo not available \u2014 auto-sync disabled. Error:", e);
|
|
196
|
+
console.warn("[SyncEngine] Install @react-native-community/netinfo to enable auto-sync.");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
stop() {
|
|
200
|
+
if (this.unsubscribeNetInfo) {
|
|
201
|
+
this.unsubscribeNetInfo();
|
|
202
|
+
this.unsubscribeNetInfo = void 0;
|
|
203
|
+
}
|
|
204
|
+
if (this.debounceTimer) {
|
|
205
|
+
clearTimeout(this.debounceTimer);
|
|
206
|
+
this.debounceTimer = void 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
scheduleFlush() {
|
|
210
|
+
console.log("[SyncEngine] scheduleFlush() \u2014 debounce", DEBOUNCE_MS, "ms");
|
|
211
|
+
if (this.debounceTimer) {
|
|
212
|
+
clearTimeout(this.debounceTimer);
|
|
213
|
+
}
|
|
214
|
+
this.debounceTimer = setTimeout(() => {
|
|
215
|
+
void this.flush();
|
|
216
|
+
}, DEBOUNCE_MS);
|
|
217
|
+
}
|
|
218
|
+
async flush() {
|
|
219
|
+
if (this.isFlushing) {
|
|
220
|
+
console.log("[SyncEngine] flush() skipped \u2014 already flushing");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.isFlushing = true;
|
|
224
|
+
try {
|
|
225
|
+
const pending = this.queue.getPending();
|
|
226
|
+
console.log("[SyncEngine] flush() \u2014 pending items:", pending.length);
|
|
227
|
+
if (pending.length === 0) {
|
|
228
|
+
console.log("[SyncEngine] Nothing to sync.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const item of pending) {
|
|
232
|
+
await this.syncItem(item);
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
this.isFlushing = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async flushCollection(collectionName) {
|
|
239
|
+
const pending = this.queue.getPendingForCollection(collectionName);
|
|
240
|
+
for (const item of pending) {
|
|
241
|
+
await this.syncItem(item);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async flushRecord(recordId) {
|
|
245
|
+
const item = this.queue.getPendingForRecord(recordId);
|
|
246
|
+
if (item) {
|
|
247
|
+
await this.syncItem(item);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async syncItem(item) {
|
|
251
|
+
if (item.retries > 0) {
|
|
252
|
+
const backoffMs = Math.pow(2, item.retries) * BACKOFF_BASE_MS;
|
|
253
|
+
const elapsed = Date.now() - item.ts;
|
|
254
|
+
if (elapsed < backoffMs) {
|
|
255
|
+
console.log(`[SyncEngine] syncItem backoff \u2014 retries: ${item.retries}, wait: ${backoffMs - elapsed}ms remaining`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const url = `${this.config.serverUrl}${this.config.endpoint}`;
|
|
261
|
+
console.log(`[SyncEngine] \u{1F4E4} POST ${url} \u2014 key: ${item.key}, recordId: ${item.recordId}`);
|
|
262
|
+
const raw = JSON.parse(item.payload);
|
|
263
|
+
const body = this.config.payloadTransformer ? this.config.payloadTransformer(raw) : raw;
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
Authorization: `Bearer ${this.config.credentials.apiKey}`
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify(body)
|
|
271
|
+
});
|
|
272
|
+
console.log(`[SyncEngine] Response: ${response.status} ${response.statusText}`);
|
|
273
|
+
if (response.ok) {
|
|
274
|
+
await this.handleSuccess(item);
|
|
275
|
+
} else if (response.status >= 400 && response.status < 500) {
|
|
276
|
+
await this.handleClientError(item, response.status);
|
|
277
|
+
} else if (response.status >= 500) {
|
|
278
|
+
await this.handleServerError(item);
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.warn("[SyncEngine] \u{1F50C} Network error (offline?) \u2014 will retry on next flush:", e);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async handleSuccess(item) {
|
|
285
|
+
await this.queue.markSynced(item.id);
|
|
286
|
+
const strategy = this.config.onSyncSuccess ?? "keep";
|
|
287
|
+
if (strategy === "delete") {
|
|
288
|
+
await this.removeRecordFromCollection(item.key, item.recordId);
|
|
289
|
+
} else if (strategy === "ttl") {
|
|
290
|
+
this.scheduleRecordTTL(item.key, item.recordId);
|
|
291
|
+
} else {
|
|
292
|
+
await this.updateRecordSyncStatus(item.key, item.recordId, "synced");
|
|
293
|
+
}
|
|
294
|
+
this.onSyncedCb?.(item);
|
|
295
|
+
}
|
|
296
|
+
async handleClientError(item, status) {
|
|
297
|
+
await this.queue.remove(item.id);
|
|
298
|
+
await this.updateRecordSyncStatus(item.key, item.recordId, "failed");
|
|
299
|
+
if (status === 401 || status === 403) {
|
|
300
|
+
this.onAuthErrorCb?.(status, item);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async handleServerError(item) {
|
|
304
|
+
await this.queue.incrementRetry(item.id);
|
|
305
|
+
}
|
|
306
|
+
collectionKey(name) {
|
|
307
|
+
return `asyncstorage::${name}`;
|
|
308
|
+
}
|
|
309
|
+
async getCollection(name) {
|
|
310
|
+
const raw = await this.driver.get(this.collectionKey(name));
|
|
311
|
+
return raw ? JSON.parse(raw) : [];
|
|
312
|
+
}
|
|
313
|
+
async saveCollection(name, records) {
|
|
314
|
+
await this.driver.set(this.collectionKey(name), JSON.stringify(records));
|
|
315
|
+
}
|
|
316
|
+
async updateRecordSyncStatus(collectionName, recordId, status) {
|
|
317
|
+
const records = await this.getCollection(collectionName);
|
|
318
|
+
const index = records.findIndex((record) => record._id === recordId);
|
|
319
|
+
if (index !== -1) {
|
|
320
|
+
records[index]._synced = status;
|
|
321
|
+
await this.saveCollection(collectionName, records);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async removeRecordFromCollection(collectionName, recordId) {
|
|
325
|
+
const records = await this.getCollection(collectionName);
|
|
326
|
+
const filtered = records.filter((record) => record._id !== recordId);
|
|
327
|
+
await this.saveCollection(collectionName, filtered);
|
|
328
|
+
}
|
|
329
|
+
scheduleRecordTTL(collectionName, recordId) {
|
|
330
|
+
const ttl = this.config.ttl ?? 7 * 24 * 60 * 60 * 1e3;
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
void this.removeRecordFromCollection(collectionName, recordId);
|
|
333
|
+
}, ttl);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/core/singleton.ts
|
|
338
|
+
function generateId() {
|
|
339
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
340
|
+
const random = Math.random() * 16 | 0;
|
|
341
|
+
const value = char === "x" ? random : random & 3 | 8;
|
|
342
|
+
return value.toString(16);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function collectionKey(name) {
|
|
346
|
+
return `asyncstorage::${name}`;
|
|
347
|
+
}
|
|
348
|
+
var DEFAULTS = {
|
|
349
|
+
autoSync: true,
|
|
350
|
+
endpoint: "/sync",
|
|
351
|
+
onSyncSuccess: "keep",
|
|
352
|
+
ttl: 7 * 24 * 60 * 60 * 1e3,
|
|
353
|
+
duplicateStrategy: "append"
|
|
354
|
+
};
|
|
355
|
+
var _AsyncStorageSync = class _AsyncStorageSync {
|
|
356
|
+
constructor(config, driver) {
|
|
357
|
+
this.config = config;
|
|
358
|
+
this.driver = driver;
|
|
359
|
+
this.queue = new Queue(driver);
|
|
360
|
+
this.engine = new SyncEngine(config, this.queue, driver);
|
|
361
|
+
}
|
|
362
|
+
static async init(config) {
|
|
363
|
+
if (_AsyncStorageSync.instance) {
|
|
364
|
+
return _AsyncStorageSync.instance;
|
|
365
|
+
}
|
|
366
|
+
let driver;
|
|
367
|
+
if (config.driver === "asyncstorage") {
|
|
368
|
+
driver = new AsyncStorageDriver();
|
|
369
|
+
} else {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`[async-storage-sync] Unknown driver: "${config.driver}". Only "asyncstorage" is available in v1.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const fullConfig = {
|
|
375
|
+
autoSync: DEFAULTS.autoSync,
|
|
376
|
+
endpoint: DEFAULTS.endpoint,
|
|
377
|
+
onSyncSuccess: DEFAULTS.onSyncSuccess,
|
|
378
|
+
ttl: DEFAULTS.ttl,
|
|
379
|
+
duplicateStrategy: DEFAULTS.duplicateStrategy,
|
|
380
|
+
...config
|
|
381
|
+
};
|
|
382
|
+
const instance = new _AsyncStorageSync(fullConfig, driver);
|
|
383
|
+
await instance.queue.load();
|
|
384
|
+
await instance.requeueFailed();
|
|
385
|
+
if (fullConfig.autoSync) {
|
|
386
|
+
console.log("[AsyncStorageSync] autoSync enabled \u2014 starting sync engine");
|
|
387
|
+
instance.engine.start();
|
|
388
|
+
} else {
|
|
389
|
+
console.log("[AsyncStorageSync] autoSync disabled \u2014 call flush() manually");
|
|
390
|
+
}
|
|
391
|
+
_AsyncStorageSync.instance = instance;
|
|
392
|
+
return instance;
|
|
393
|
+
}
|
|
394
|
+
static getInstance() {
|
|
395
|
+
if (!_AsyncStorageSync.instance) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
"[async-storage-sync] getInstance() called before init(). Call AsyncStorageSync.init(...) first."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return _AsyncStorageSync.instance;
|
|
401
|
+
}
|
|
402
|
+
async save(name, data, options = {}) {
|
|
403
|
+
const strategy = options.duplicateStrategy ?? this.config.duplicateStrategy ?? DEFAULTS.duplicateStrategy;
|
|
404
|
+
const type = options.type ?? name;
|
|
405
|
+
const records = await this.getCollection(name);
|
|
406
|
+
if (strategy === "overwrite") {
|
|
407
|
+
const existingIndex = records.findIndex((record2) => record2._type === type);
|
|
408
|
+
if (existingIndex !== -1) {
|
|
409
|
+
const existing = records[existingIndex];
|
|
410
|
+
const updated = {
|
|
411
|
+
...existing,
|
|
412
|
+
...data,
|
|
413
|
+
_ts: Date.now(),
|
|
414
|
+
_synced: "pending",
|
|
415
|
+
_type: type
|
|
416
|
+
};
|
|
417
|
+
records[existingIndex] = updated;
|
|
418
|
+
await this.saveCollection(name, records);
|
|
419
|
+
await this.enqueueRecord(name, updated, options);
|
|
420
|
+
return updated;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const record = {
|
|
424
|
+
...data,
|
|
425
|
+
_id: generateId(),
|
|
426
|
+
_ts: Date.now(),
|
|
427
|
+
_synced: "pending",
|
|
428
|
+
_type: type,
|
|
429
|
+
_retries: 0
|
|
430
|
+
};
|
|
431
|
+
records.push(record);
|
|
432
|
+
try {
|
|
433
|
+
await this.saveCollection(name, records);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (String(error).includes("STORAGE_FULL")) {
|
|
436
|
+
this.engine.emitStorageFull();
|
|
437
|
+
throw new Error("[async-storage-sync] Storage is full.");
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
await this.enqueueRecord(name, record, options);
|
|
442
|
+
return record;
|
|
443
|
+
}
|
|
444
|
+
async enqueueRecord(name, record, _options) {
|
|
445
|
+
const queueItem = {
|
|
446
|
+
id: generateId(),
|
|
447
|
+
key: name,
|
|
448
|
+
recordId: record._id,
|
|
449
|
+
payload: JSON.stringify(record),
|
|
450
|
+
endpoint: this.config.endpoint,
|
|
451
|
+
ts: Date.now(),
|
|
452
|
+
retries: 0,
|
|
453
|
+
synced: false
|
|
454
|
+
};
|
|
455
|
+
await this.queue.enqueue(queueItem);
|
|
456
|
+
}
|
|
457
|
+
async getAll(name) {
|
|
458
|
+
return this.getCollection(name);
|
|
459
|
+
}
|
|
460
|
+
async getById(name, id) {
|
|
461
|
+
const records = await this.getCollection(name);
|
|
462
|
+
return records.find((record) => record._id === id) ?? null;
|
|
463
|
+
}
|
|
464
|
+
async deleteById(name, id) {
|
|
465
|
+
const records = await this.getCollection(name);
|
|
466
|
+
await this.saveCollection(
|
|
467
|
+
name,
|
|
468
|
+
records.filter((record) => record._id !== id)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
async deleteCollection(name) {
|
|
472
|
+
await this.driver.remove(collectionKey(name));
|
|
473
|
+
}
|
|
474
|
+
async sync(name) {
|
|
475
|
+
await this.engine.flushCollection(name);
|
|
476
|
+
}
|
|
477
|
+
async syncById(_name, id) {
|
|
478
|
+
await this.engine.flushRecord(id);
|
|
479
|
+
}
|
|
480
|
+
async flush() {
|
|
481
|
+
await this.engine.flush();
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Re-enqueue any records marked as 'failed' so they are retried on next flush.
|
|
485
|
+
* Called automatically on init to recover from previous 4xx/500 failures.
|
|
486
|
+
*/
|
|
487
|
+
async requeueFailed() {
|
|
488
|
+
const allKeys = await this.driver.getAllKeys();
|
|
489
|
+
const collectionKeys = allKeys.filter((k) => k.startsWith("asyncstorage::") && k !== "asyncstorage::sync_queue");
|
|
490
|
+
let total = 0;
|
|
491
|
+
for (const storageKey of collectionKeys) {
|
|
492
|
+
const collectionName = storageKey.replace("asyncstorage::", "");
|
|
493
|
+
const raw = await this.driver.get(storageKey);
|
|
494
|
+
if (!raw) continue;
|
|
495
|
+
const records = JSON.parse(raw);
|
|
496
|
+
const failed = records.filter((r) => r._synced === "failed");
|
|
497
|
+
for (const record of failed) {
|
|
498
|
+
const queueItem = {
|
|
499
|
+
id: generateId(),
|
|
500
|
+
key: collectionName,
|
|
501
|
+
recordId: record._id,
|
|
502
|
+
payload: JSON.stringify(record),
|
|
503
|
+
endpoint: this.config.endpoint,
|
|
504
|
+
ts: Date.now(),
|
|
505
|
+
retries: 0,
|
|
506
|
+
synced: false
|
|
507
|
+
};
|
|
508
|
+
await this.queue.enqueue(queueItem);
|
|
509
|
+
record._synced = "pending";
|
|
510
|
+
total++;
|
|
511
|
+
}
|
|
512
|
+
if (failed.length > 0) {
|
|
513
|
+
await this.driver.set(storageKey, JSON.stringify(records));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (total > 0) {
|
|
517
|
+
console.log(`[AsyncStorageSync] \u267B\uFE0F Re-enqueued ${total} previously failed record(s) for retry`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
onSynced(cb) {
|
|
521
|
+
this.engine.onSynced(cb);
|
|
522
|
+
}
|
|
523
|
+
onAuthError(cb) {
|
|
524
|
+
this.engine.onAuthError(cb);
|
|
525
|
+
}
|
|
526
|
+
onStorageFull(cb) {
|
|
527
|
+
this.engine.onStorageFull(cb);
|
|
528
|
+
}
|
|
529
|
+
getQueue() {
|
|
530
|
+
return this.queue.getAll();
|
|
531
|
+
}
|
|
532
|
+
async destroy() {
|
|
533
|
+
this.engine.stop();
|
|
534
|
+
await this.queue.clear();
|
|
535
|
+
await this.driver.clear();
|
|
536
|
+
_AsyncStorageSync.instance = null;
|
|
537
|
+
}
|
|
538
|
+
async getCollection(name) {
|
|
539
|
+
const raw = await this.driver.get(collectionKey(name));
|
|
540
|
+
return raw ? JSON.parse(raw) : [];
|
|
541
|
+
}
|
|
542
|
+
async saveCollection(name, records) {
|
|
543
|
+
await this.driver.set(collectionKey(name), JSON.stringify(records));
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
_AsyncStorageSync.instance = null;
|
|
547
|
+
var AsyncStorageSync = _AsyncStorageSync;
|
|
548
|
+
|
|
549
|
+
// src/index.ts
|
|
550
|
+
var initPromise = null;
|
|
551
|
+
async function initSyncQueue(config) {
|
|
552
|
+
try {
|
|
553
|
+
return AsyncStorageSync.getInstance();
|
|
554
|
+
} catch {
|
|
555
|
+
if (!initPromise) {
|
|
556
|
+
initPromise = AsyncStorageSync.init(config).finally(() => {
|
|
557
|
+
initPromise = null;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return initPromise;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function getSyncQueue() {
|
|
564
|
+
return AsyncStorageSync.getInstance();
|
|
565
|
+
}
|
|
566
|
+
function setStorageDriver(storage) {
|
|
567
|
+
AsyncStorageDriver.setStorageClient(storage);
|
|
568
|
+
}
|
|
569
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
570
|
+
0 && (module.exports = {
|
|
571
|
+
AsyncStorageSync,
|
|
572
|
+
getSyncQueue,
|
|
573
|
+
initSyncQueue,
|
|
574
|
+
setStorageDriver
|
|
575
|
+
});
|