dop-wallet-v6 1.2.20 → 1.3.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/dist/services/dop/core/react-native-init.d.ts +12 -5
- package/dist/services/dop/core/react-native-init.js +400 -34
- package/dist/services/dop/core/react-native-init.js.map +1 -1
- package/dist/services/dop/crypto/react-native-crypto-provider.d.ts +1 -1
- package/dist/services/dop/crypto/react-native-crypto-provider.js +20 -2
- package/dist/services/dop/crypto/react-native-crypto-provider.js.map +1 -1
- package/dist/services/dop/dop-txids/graphql/index.js +3 -6
- package/dist/services/dop/dop-txids/graphql/index.js.map +1 -1
- package/dist/services/dop/quick-sync/V2/graphql/index.js +3 -6
- package/dist/services/dop/quick-sync/V2/graphql/index.js.map +1 -1
- package/dist/services/dop/quick-sync/V3/graphql/index.js +3 -6
- package/dist/services/dop/quick-sync/V3/graphql/index.js.map +1 -1
- package/dist/tests/balances.test.d.ts +1 -0
- package/dist/tests/balances.test.js +419 -0
- package/dist/tests/balances.test.js.map +1 -0
- package/dist/tests/setup.test.js +9 -3
- package/dist/tests/setup.test.js.map +1 -1
- package/package.json +3 -3
- package/patches/dop-engine-v3+1.4.9.patch +252 -2
|
@@ -7,15 +7,22 @@ export declare class ReactNativeLevelDB {
|
|
|
7
7
|
private db;
|
|
8
8
|
private storageKey;
|
|
9
9
|
private AsyncStorage;
|
|
10
|
+
private persistTimeout;
|
|
11
|
+
private isDirty;
|
|
10
12
|
constructor(name: string);
|
|
11
|
-
open(callback
|
|
13
|
+
open(callbackOrOptions?: any, callback?: (error?: Error) => void): Promise<void>;
|
|
12
14
|
close(callback: (error?: Error) => void): void;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
forcePersist(): Promise<void>;
|
|
16
|
+
clearPersistedData(): Promise<void>;
|
|
17
|
+
put(key: any, value: any, optionsOrCallback?: any, callback?: (error?: Error) => void): void;
|
|
18
|
+
get(key: any, optionsOrCallback?: any, callback?: (error?: Error, value?: any) => void): void;
|
|
19
|
+
del(key: any, optionsOrCallback?: any, callback?: (error?: Error) => void): void;
|
|
20
|
+
clear(options: any, callback: (error?: Error) => void): void;
|
|
21
|
+
batch(operationsOrCallback?: any, optionsOrCallback?: any, callback?: (error?: Error) => void): any;
|
|
17
22
|
iterator(options?: any): any;
|
|
23
|
+
private _schedulePersistence;
|
|
18
24
|
private _persistData;
|
|
25
|
+
private _cleanupOldChunks;
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Initialize DOP Engine specifically for React Native environments.
|
|
@@ -19,6 +19,8 @@ class ReactNativeLevelDB {
|
|
|
19
19
|
db;
|
|
20
20
|
storageKey;
|
|
21
21
|
AsyncStorage;
|
|
22
|
+
persistTimeout = null;
|
|
23
|
+
isDirty = false;
|
|
22
24
|
constructor(name) {
|
|
23
25
|
this.storageKey = `leveldb_${name}`;
|
|
24
26
|
this.db = memdown();
|
|
@@ -33,18 +35,87 @@ class ReactNativeLevelDB {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
// Implement AbstractLevelDOWN interface
|
|
36
|
-
async open(callback) {
|
|
38
|
+
async open(callbackOrOptions, callback) {
|
|
39
|
+
// Handle both open(callback) and open(options, callback) signatures
|
|
40
|
+
let cb;
|
|
41
|
+
if (typeof callbackOrOptions === 'function') {
|
|
42
|
+
// open(callback) signature
|
|
43
|
+
cb = callbackOrOptions;
|
|
44
|
+
}
|
|
45
|
+
else if (typeof callback === 'function') {
|
|
46
|
+
// open(options, callback) signature
|
|
47
|
+
cb = callback;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// No callback provided - this shouldn't happen in AbstractLevelDOWN
|
|
51
|
+
throw new Error('No callback provided to open method');
|
|
52
|
+
}
|
|
37
53
|
try {
|
|
38
54
|
// Load persisted data from AsyncStorage (only if available)
|
|
39
55
|
if (this.AsyncStorage) {
|
|
40
56
|
try {
|
|
41
|
-
|
|
57
|
+
let persistedData = null;
|
|
58
|
+
// Check if data is stored in chunks
|
|
59
|
+
const chunksCount = await this.AsyncStorage.getItem(`${this.storageKey}_chunks`);
|
|
60
|
+
if (chunksCount) {
|
|
61
|
+
console.log(`📦 Loading database from ${String(chunksCount)} chunks...`);
|
|
62
|
+
// Reassemble from chunks
|
|
63
|
+
const chunks = [];
|
|
64
|
+
const count = parseInt(chunksCount, 10);
|
|
65
|
+
if (Number.isNaN(count) || count < 0 || count > 1000) {
|
|
66
|
+
throw new Error(`Invalid chunk count: ${String(chunksCount)}`);
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < count; i += 1) {
|
|
69
|
+
const chunk = await this.AsyncStorage.getItem(`${this.storageKey}_chunk_${i}`);
|
|
70
|
+
if (chunk) {
|
|
71
|
+
chunks.push(chunk);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.warn(`⚠️ Missing chunk ${i} of ${count}, data may be incomplete`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (chunks.length === 0) {
|
|
78
|
+
throw new Error('No chunks found, but chunk count was set');
|
|
79
|
+
}
|
|
80
|
+
persistedData = chunks.join('');
|
|
81
|
+
console.log(`📦 Reassembled ${chunks.length}/${count} chunks (${(persistedData.length / 1024 / 1024).toFixed(2)}MB)`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Try to get data directly
|
|
85
|
+
persistedData = await this.AsyncStorage.getItem(this.storageKey);
|
|
86
|
+
if (persistedData) {
|
|
87
|
+
console.log(`📦 Loading database directly (${(persistedData.length / 1024).toFixed(2)}KB)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
42
90
|
if (persistedData) {
|
|
43
|
-
|
|
91
|
+
// Validate JSON before parsing
|
|
92
|
+
if (!persistedData.trim().startsWith('{') || !persistedData.trim().endsWith('}')) {
|
|
93
|
+
throw new Error('Persisted data does not look like valid JSON (missing braces)');
|
|
94
|
+
}
|
|
95
|
+
let data;
|
|
96
|
+
try {
|
|
97
|
+
data = JSON.parse(persistedData);
|
|
98
|
+
}
|
|
99
|
+
catch (jsonError) {
|
|
100
|
+
throw new Error(`JSON parse failed: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
|
|
101
|
+
}
|
|
44
102
|
// Restore data to memdown instance
|
|
103
|
+
const keys = Object.keys(data);
|
|
104
|
+
console.log(`📦 Restoring ${keys.length} entries to database...`);
|
|
45
105
|
for (const [key, value] of Object.entries(data)) {
|
|
46
106
|
await new Promise((resolve, reject) => {
|
|
47
|
-
|
|
107
|
+
// Restore Buffer/Uint8Array types from base64 with type marker
|
|
108
|
+
let restoredValue = value;
|
|
109
|
+
if (value && typeof value === 'object' && '__type' in value) {
|
|
110
|
+
const typedValue = value;
|
|
111
|
+
if (typedValue.__type === 'Buffer' && typeof typedValue.data === 'string') {
|
|
112
|
+
restoredValue = Buffer.from(typedValue.data, 'base64');
|
|
113
|
+
}
|
|
114
|
+
else if (typedValue.__type === 'Uint8Array' && typeof typedValue.data === 'string') {
|
|
115
|
+
restoredValue = new Uint8Array(Buffer.from(typedValue.data, 'base64'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.db.put(key, restoredValue, (err) => {
|
|
48
119
|
if (err)
|
|
49
120
|
reject(err);
|
|
50
121
|
else
|
|
@@ -52,58 +123,250 @@ class ReactNativeLevelDB {
|
|
|
52
123
|
});
|
|
53
124
|
});
|
|
54
125
|
}
|
|
126
|
+
console.log('✅ Successfully restored database from AsyncStorage');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log('ℹ️ No persisted data found, starting with empty database');
|
|
55
130
|
}
|
|
56
131
|
}
|
|
57
132
|
catch (asyncStorageError) {
|
|
58
|
-
|
|
133
|
+
console.error('❌ Failed to load from AsyncStorage, clearing corrupted data and starting fresh:', asyncStorageError);
|
|
134
|
+
// Clear corrupted data
|
|
135
|
+
try {
|
|
136
|
+
await this.AsyncStorage.removeItem(this.storageKey);
|
|
137
|
+
await this.AsyncStorage.removeItem(`${this.storageKey}_chunks`);
|
|
138
|
+
// Try to remove chunks (up to 100 chunks max)
|
|
139
|
+
for (let i = 0; i < 100; i += 1) {
|
|
140
|
+
const chunkKey = `${this.storageKey}_chunk_${i}`;
|
|
141
|
+
const exists = await this.AsyncStorage.getItem(chunkKey);
|
|
142
|
+
if (exists) {
|
|
143
|
+
await this.AsyncStorage.removeItem(chunkKey);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
break; // No more chunks
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log('🧹 Cleared corrupted AsyncStorage data');
|
|
150
|
+
}
|
|
151
|
+
catch (clearError) {
|
|
152
|
+
console.warn('⚠️ Failed to clear corrupted data:', clearError);
|
|
153
|
+
}
|
|
59
154
|
}
|
|
60
155
|
}
|
|
61
156
|
// Open the memdown database
|
|
62
|
-
this.db.open(
|
|
157
|
+
this.db.open(cb);
|
|
63
158
|
}
|
|
64
159
|
catch (error) {
|
|
65
|
-
|
|
160
|
+
cb(error);
|
|
66
161
|
}
|
|
67
162
|
}
|
|
68
163
|
close(callback) {
|
|
69
|
-
|
|
164
|
+
// Clear pending persistence timeout
|
|
165
|
+
if (this.persistTimeout) {
|
|
166
|
+
clearTimeout(this.persistTimeout);
|
|
167
|
+
this.persistTimeout = null;
|
|
168
|
+
}
|
|
169
|
+
// Force immediate persistence before closing if dirty
|
|
170
|
+
if (this.isDirty && this.AsyncStorage) {
|
|
171
|
+
console.log('💾 Persisting database before close...');
|
|
172
|
+
this.isDirty = false;
|
|
173
|
+
this._persistData()
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
175
|
+
.then(() => {
|
|
176
|
+
console.log('✅ Database persisted, closing...');
|
|
177
|
+
this.db.close(callback);
|
|
178
|
+
})
|
|
179
|
+
.catch((error) => {
|
|
180
|
+
console.warn('⚠️ Failed to persist on close:', error);
|
|
181
|
+
this.db.close(callback);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.db.close(callback);
|
|
186
|
+
}
|
|
70
187
|
}
|
|
71
|
-
|
|
188
|
+
// Public method to force immediate persistence (useful after scan completes)
|
|
189
|
+
async forcePersist() {
|
|
190
|
+
if (this.persistTimeout) {
|
|
191
|
+
clearTimeout(this.persistTimeout);
|
|
192
|
+
this.persistTimeout = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.isDirty && this.AsyncStorage) {
|
|
195
|
+
this.isDirty = false;
|
|
196
|
+
await this._persistData();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Public method to clear all persisted data (use when recovering from storage full errors)
|
|
200
|
+
async clearPersistedData() {
|
|
201
|
+
console.log('🧹 Clearing all persisted database data from AsyncStorage...');
|
|
202
|
+
await this._cleanupOldChunks();
|
|
203
|
+
console.log('✅ Cleared all persisted data');
|
|
204
|
+
}
|
|
205
|
+
put(key, value, optionsOrCallback, callback) {
|
|
206
|
+
// Handle both put(key, value, callback) and put(key, value, options, callback) signatures
|
|
207
|
+
let cb;
|
|
208
|
+
if (typeof optionsOrCallback === 'function') {
|
|
209
|
+
// put(key, value, callback) signature
|
|
210
|
+
cb = optionsOrCallback;
|
|
211
|
+
}
|
|
212
|
+
else if (typeof callback === 'function') {
|
|
213
|
+
// put(key, value, options, callback) signature
|
|
214
|
+
cb = callback;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
throw new Error('No callback provided to put method');
|
|
218
|
+
}
|
|
72
219
|
this.db.put(key, value, (err) => {
|
|
73
220
|
if (!err && this.AsyncStorage) {
|
|
74
|
-
//
|
|
75
|
-
|
|
221
|
+
// Schedule throttled persistence
|
|
222
|
+
this._schedulePersistence();
|
|
76
223
|
}
|
|
77
|
-
|
|
224
|
+
cb(err);
|
|
78
225
|
});
|
|
79
226
|
}
|
|
80
|
-
get(key, callback) {
|
|
81
|
-
|
|
227
|
+
get(key, optionsOrCallback, callback) {
|
|
228
|
+
// Handle both get(key, callback) and get(key, options, callback) signatures
|
|
229
|
+
let cb;
|
|
230
|
+
if (typeof optionsOrCallback === 'function') {
|
|
231
|
+
// get(key, callback) signature
|
|
232
|
+
cb = optionsOrCallback;
|
|
233
|
+
}
|
|
234
|
+
else if (typeof callback === 'function') {
|
|
235
|
+
// get(key, options, callback) signature
|
|
236
|
+
cb = callback;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
throw new Error('No callback provided to get method');
|
|
240
|
+
}
|
|
241
|
+
this.db.get(key, cb);
|
|
82
242
|
}
|
|
83
|
-
del(key, callback) {
|
|
243
|
+
del(key, optionsOrCallback, callback) {
|
|
244
|
+
// Handle both del(key, callback) and del(key, options, callback) signatures
|
|
245
|
+
let cb;
|
|
246
|
+
if (typeof optionsOrCallback === 'function') {
|
|
247
|
+
// del(key, callback) signature
|
|
248
|
+
cb = optionsOrCallback;
|
|
249
|
+
}
|
|
250
|
+
else if (typeof callback === 'function') {
|
|
251
|
+
// del(key, options, callback) signature
|
|
252
|
+
cb = callback;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
throw new Error('No callback provided to del method');
|
|
256
|
+
}
|
|
84
257
|
this.db.del(key, (err) => {
|
|
85
258
|
if (!err && this.AsyncStorage) {
|
|
86
|
-
//
|
|
87
|
-
|
|
259
|
+
// Schedule throttled persistence
|
|
260
|
+
this._schedulePersistence();
|
|
88
261
|
}
|
|
89
|
-
|
|
262
|
+
cb(err);
|
|
90
263
|
});
|
|
91
264
|
}
|
|
92
|
-
|
|
265
|
+
clear(options, callback) {
|
|
266
|
+
// Handle clear operation for deleting ranges of keys
|
|
267
|
+
// This is required by dop-engine-v3's clearNamespace function
|
|
268
|
+
const { gte, lte, gt, lt } = options || {};
|
|
269
|
+
if (!gte && !gt) {
|
|
270
|
+
callback(new Error('clear() requires gte or gt option'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Use iterator to find all keys in the range and delete them
|
|
274
|
+
const keysToDelete = [];
|
|
275
|
+
const iterator = this.db.iterator({
|
|
276
|
+
gte,
|
|
277
|
+
lte,
|
|
278
|
+
gt,
|
|
279
|
+
lt,
|
|
280
|
+
keys: true,
|
|
281
|
+
values: false,
|
|
282
|
+
});
|
|
283
|
+
const processNext = () => {
|
|
284
|
+
iterator.next((err, key) => {
|
|
285
|
+
if (err) {
|
|
286
|
+
iterator.end(() => {
|
|
287
|
+
callback(err);
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (key === undefined) {
|
|
292
|
+
// No more keys - now delete all collected keys
|
|
293
|
+
iterator.end(() => {
|
|
294
|
+
if (keysToDelete.length === 0) {
|
|
295
|
+
callback();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Delete keys in batch
|
|
299
|
+
const operations = keysToDelete.map(k => ({ type: 'del', key: k }));
|
|
300
|
+
this.db.batch(operations, (batchErr) => {
|
|
301
|
+
if (!batchErr && this.AsyncStorage) {
|
|
302
|
+
this._schedulePersistence();
|
|
303
|
+
}
|
|
304
|
+
callback(batchErr);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
keysToDelete.push(key);
|
|
310
|
+
processNext();
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
processNext();
|
|
314
|
+
}
|
|
315
|
+
batch(operationsOrCallback, optionsOrCallback, callback) {
|
|
316
|
+
// Handle multiple batch signatures:
|
|
317
|
+
// batch() - returns chained batch
|
|
318
|
+
// batch(operations, callback)
|
|
319
|
+
// batch(operations, options, callback)
|
|
320
|
+
if (arguments.length === 0) {
|
|
321
|
+
// batch() - return chained batch (not commonly used in LevelUp)
|
|
322
|
+
return this.db.batch();
|
|
323
|
+
}
|
|
324
|
+
// Handle batch operations with callback
|
|
325
|
+
const operations = operationsOrCallback;
|
|
326
|
+
let cb;
|
|
327
|
+
if (typeof optionsOrCallback === 'function') {
|
|
328
|
+
// batch(operations, callback) signature
|
|
329
|
+
cb = optionsOrCallback;
|
|
330
|
+
}
|
|
331
|
+
else if (typeof callback === 'function') {
|
|
332
|
+
// batch(operations, options, callback) signature
|
|
333
|
+
cb = callback;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
throw new Error('No callback provided to batch method');
|
|
337
|
+
}
|
|
93
338
|
this.db.batch(operations, (err) => {
|
|
94
339
|
if (!err && this.AsyncStorage) {
|
|
95
|
-
//
|
|
96
|
-
|
|
340
|
+
// Schedule throttled persistence
|
|
341
|
+
this._schedulePersistence();
|
|
97
342
|
}
|
|
98
|
-
|
|
343
|
+
cb(err);
|
|
99
344
|
});
|
|
100
345
|
}
|
|
101
346
|
iterator(options) {
|
|
347
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
102
348
|
return this.db.iterator(options);
|
|
103
349
|
}
|
|
350
|
+
_schedulePersistence() {
|
|
351
|
+
this.isDirty = true;
|
|
352
|
+
// Clear existing timeout
|
|
353
|
+
if (this.persistTimeout) {
|
|
354
|
+
clearTimeout(this.persistTimeout);
|
|
355
|
+
}
|
|
356
|
+
// Schedule persistence with longer throttling during heavy writes (every 30 seconds)
|
|
357
|
+
// This dramatically improves scan performance by reducing serialization overhead
|
|
358
|
+
this.persistTimeout = setTimeout(() => {
|
|
359
|
+
if (this.isDirty) {
|
|
360
|
+
this.isDirty = false;
|
|
361
|
+
void this._persistData().catch(console.warn);
|
|
362
|
+
}
|
|
363
|
+
}, 30000); // Increased from 5s to 30s
|
|
364
|
+
}
|
|
104
365
|
async _persistData() {
|
|
105
366
|
if (!this.AsyncStorage)
|
|
106
367
|
return;
|
|
368
|
+
const startTime = Date.now();
|
|
369
|
+
let entryCount = 0;
|
|
107
370
|
try {
|
|
108
371
|
const data = {};
|
|
109
372
|
const iterator = this.db.iterator();
|
|
@@ -118,16 +381,118 @@ class ReactNativeLevelDB {
|
|
|
118
381
|
iterator.end(() => resolve());
|
|
119
382
|
return;
|
|
120
383
|
}
|
|
121
|
-
|
|
384
|
+
entryCount += 1;
|
|
385
|
+
// Preserve Buffer/Uint8Array types by converting to base64 with type marker
|
|
386
|
+
if (Buffer.isBuffer(value)) {
|
|
387
|
+
data[key.toString()] = {
|
|
388
|
+
__type: 'Buffer',
|
|
389
|
+
data: value.toString('base64'),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
else if (value instanceof Uint8Array) {
|
|
393
|
+
data[key.toString()] = {
|
|
394
|
+
__type: 'Uint8Array',
|
|
395
|
+
data: Buffer.from(value).toString('base64'),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
data[key.toString()] = value;
|
|
400
|
+
}
|
|
122
401
|
processNext();
|
|
123
402
|
});
|
|
124
403
|
};
|
|
125
404
|
processNext();
|
|
126
405
|
});
|
|
127
|
-
|
|
406
|
+
// Serialize data
|
|
407
|
+
let jsonData;
|
|
408
|
+
try {
|
|
409
|
+
jsonData = JSON.stringify(data);
|
|
410
|
+
}
|
|
411
|
+
catch (stringifyError) {
|
|
412
|
+
throw new Error(`Failed to stringify data: ${stringifyError instanceof Error ? stringifyError.message : String(stringifyError)}`);
|
|
413
|
+
}
|
|
414
|
+
const serializeTime = Date.now() - startTime;
|
|
415
|
+
const dataSizeMB = jsonData.length / 1024 / 1024;
|
|
416
|
+
// Clean up old chunks before writing new data
|
|
417
|
+
await this._cleanupOldChunks();
|
|
418
|
+
// React Native AsyncStorage has a 6MB per-item limit and total quota limits
|
|
419
|
+
// Use 500KB chunks to stay well within limits and avoid SQLITE_FULL errors
|
|
420
|
+
const chunkSize = 512 * 1024; // 500KB chunks (conservative for Android)
|
|
421
|
+
if (jsonData.length > chunkSize) {
|
|
422
|
+
// Split large data into chunks
|
|
423
|
+
const chunks = [];
|
|
424
|
+
for (let i = 0; i < jsonData.length; i += chunkSize) {
|
|
425
|
+
chunks.push(jsonData.slice(i, i + chunkSize));
|
|
426
|
+
}
|
|
427
|
+
console.log(`💾 Writing ${chunks.length} chunks (${dataSizeMB.toFixed(2)}MB)...`);
|
|
428
|
+
// Check if data is too large (warn if over 50MB)
|
|
429
|
+
if (dataSizeMB > 50) {
|
|
430
|
+
console.warn(`⚠️ Database is very large (${dataSizeMB.toFixed(2)}MB). Consider clearing old data.`);
|
|
431
|
+
}
|
|
432
|
+
// Write chunks sequentially to avoid overwhelming AsyncStorage
|
|
433
|
+
// Parallel writes can cause SQLITE_FULL errors
|
|
434
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
435
|
+
try {
|
|
436
|
+
await this.AsyncStorage.setItem(`${this.storageKey}_chunk_${i}`, chunks[i]);
|
|
437
|
+
}
|
|
438
|
+
catch (chunkError) {
|
|
439
|
+
// If we hit storage quota, clean up and throw
|
|
440
|
+
console.error(`❌ Failed to write chunk ${i}/${chunks.length}:`, chunkError);
|
|
441
|
+
if (chunkError?.message?.includes('full') || chunkError?.code === 13) {
|
|
442
|
+
console.error('💥 Storage quota exceeded! Cleaning up partial writes...');
|
|
443
|
+
await this._cleanupOldChunks();
|
|
444
|
+
throw new Error('AsyncStorage quota exceeded. Please clear app data or restart the app.');
|
|
445
|
+
}
|
|
446
|
+
throw chunkError;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Only update chunk count after all chunks are written successfully
|
|
450
|
+
await this.AsyncStorage.setItem(`${this.storageKey}_chunks`, chunks.length.toString());
|
|
451
|
+
const totalTime = Date.now() - startTime;
|
|
452
|
+
console.log(`✅ Persisted ${entryCount} entries (${dataSizeMB.toFixed(2)}MB in ${chunks.length} chunks) in ${totalTime}ms (serialize: ${serializeTime}ms)`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// Small data, store directly
|
|
456
|
+
await this.AsyncStorage.setItem(this.storageKey, jsonData);
|
|
457
|
+
const totalTime = Date.now() - startTime;
|
|
458
|
+
console.log(`✅ Persisted ${entryCount} entries (${(jsonData.length / 1024).toFixed(2)}KB) in ${totalTime}ms (serialize: ${serializeTime}ms)`);
|
|
459
|
+
}
|
|
128
460
|
}
|
|
129
461
|
catch (error) {
|
|
130
|
-
console.
|
|
462
|
+
console.error('❌ Failed to persist data to AsyncStorage:', error);
|
|
463
|
+
// If quota exceeded, clear everything to recover
|
|
464
|
+
if (error?.message?.includes('quota') || error?.message?.includes('full')) {
|
|
465
|
+
console.error('💥 Storage full! Clearing all AsyncStorage data for recovery...');
|
|
466
|
+
await this._cleanupOldChunks();
|
|
467
|
+
}
|
|
468
|
+
// Mark as dirty again so we retry on next schedule
|
|
469
|
+
this.isDirty = true;
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async _cleanupOldChunks() {
|
|
474
|
+
if (!this.AsyncStorage)
|
|
475
|
+
return;
|
|
476
|
+
try {
|
|
477
|
+
// Remove chunk metadata
|
|
478
|
+
await this.AsyncStorage.removeItem(`${this.storageKey}_chunks`);
|
|
479
|
+
// Remove old direct storage (if exists)
|
|
480
|
+
await this.AsyncStorage.removeItem(this.storageKey);
|
|
481
|
+
// Remove all chunk entries (try up to 200 chunks)
|
|
482
|
+
for (let i = 0; i < 200; i += 1) {
|
|
483
|
+
const chunkKey = `${this.storageKey}_chunk_${i}`;
|
|
484
|
+
try {
|
|
485
|
+
await this.AsyncStorage.removeItem(chunkKey);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Ignore errors, chunk may not exist
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (cleanupError) {
|
|
494
|
+
console.warn('⚠️ Error during chunk cleanup:', cleanupError);
|
|
495
|
+
// Don't throw, continue with persistence
|
|
131
496
|
}
|
|
132
497
|
}
|
|
133
498
|
}
|
|
@@ -146,16 +511,17 @@ exports.ReactNativeLevelDB = ReactNativeLevelDB;
|
|
|
146
511
|
* @param databaseName - Name for the database (used as prefix in AsyncStorage)
|
|
147
512
|
*/
|
|
148
513
|
const startDopEngineReactNative = async (walletSource, shouldDebug, artifactStore, useNativeArtifacts = true, skipMerkletreeScans = false, verboseScanLogging = false, databaseName = 'dop-wallet-db') => {
|
|
149
|
-
// Create React Native compatible database instance
|
|
150
|
-
|
|
514
|
+
// Create React Native compatible database instance with AsyncStorage persistence
|
|
515
|
+
const db = new ReactNativeLevelDB(databaseName);
|
|
151
516
|
// Ensure database is opened before proceeding
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
517
|
+
await new Promise((resolve, reject) => {
|
|
518
|
+
db.open((error) => {
|
|
519
|
+
if (error)
|
|
520
|
+
reject(error);
|
|
521
|
+
else
|
|
522
|
+
resolve();
|
|
523
|
+
});
|
|
524
|
+
});
|
|
159
525
|
// Initialize the DOP Engine with the React Native database
|
|
160
526
|
return (0, init_1.startDopEngine)(walletSource, db, // Cast to any since TypeScript doesn't know about our custom implementation
|
|
161
527
|
shouldDebug, artifactStore, useNativeArtifacts, skipMerkletreeScans, verboseScanLogging);
|