@roeehrl/tinode-sdk 0.25.1-sqlite.1 → 0.25.1-sqlite.11
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 +349 -32
- package/package.json +7 -3
- package/src/connection.js +24 -2
- package/src/db.js +9 -18
- package/src/index.native.js +9 -1
- package/src/large-file.native.js +367 -0
- package/src/storage-sqlite.js +105 -158
- package/src/topic.js +0 -4
- package/types/index.d.ts +90 -2
- package/umd/tinode.dev.js +123 -3
- package/umd/tinode.dev.js.map +1 -1
- package/umd/tinode.prod.js +1 -1
- package/umd/tinode.prod.js.map +1 -1
- package/version.js +1 -0
package/src/storage-sqlite.js
CHANGED
|
@@ -198,6 +198,69 @@ export default class SQLiteStorage {
|
|
|
198
198
|
return this._ready && this._db !== null;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Attempt to recover the database connection if it becomes stale.
|
|
203
|
+
* This handles cases where the native database handle becomes invalid
|
|
204
|
+
* after app lifecycle events or reconnections.
|
|
205
|
+
* @returns {Promise<boolean>} True if recovery succeeded.
|
|
206
|
+
*/
|
|
207
|
+
async _recoverDatabase() {
|
|
208
|
+
const self = this;
|
|
209
|
+
try {
|
|
210
|
+
// Close existing connection if any
|
|
211
|
+
if (self._db) {
|
|
212
|
+
try {
|
|
213
|
+
await self._db.closeAsync();
|
|
214
|
+
} catch (closeErr) {
|
|
215
|
+
// Ignore close errors - the handle may already be invalid
|
|
216
|
+
}
|
|
217
|
+
self._db = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Re-open the database
|
|
221
|
+
self._ready = false;
|
|
222
|
+
await self.initDatabase();
|
|
223
|
+
|
|
224
|
+
return true;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error('[SQLiteStorage] Database recovery failed:', err);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Wrapper to execute database operations with automatic recovery.
|
|
233
|
+
* If an operation fails with NullPointerException, attempts recovery and retry.
|
|
234
|
+
* @param {function} operation - Async function to execute
|
|
235
|
+
* @param {string} operationName - Name for logging
|
|
236
|
+
* @returns {Promise<any>} Result of the operation
|
|
237
|
+
*/
|
|
238
|
+
async _withRecovery(operation, operationName) {
|
|
239
|
+
const self = this;
|
|
240
|
+
try {
|
|
241
|
+
return await operation();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// Check if this is a stale database handle error
|
|
244
|
+
const isStaleHandle = err.message && (
|
|
245
|
+
err.message.includes('NullPointerException') ||
|
|
246
|
+
err.message.includes('prepareAsync') ||
|
|
247
|
+
err.message.includes('database is not open') ||
|
|
248
|
+
err.message.includes('SQLiteDatabase')
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (isStaleHandle) {
|
|
252
|
+
const recovered = await self._recoverDatabase();
|
|
253
|
+
if (recovered) {
|
|
254
|
+
// Retry the operation once
|
|
255
|
+
return await operation();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Re-throw if not recoverable
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
201
264
|
// ==================== Topics ====================
|
|
202
265
|
|
|
203
266
|
/**
|
|
@@ -207,11 +270,19 @@ export default class SQLiteStorage {
|
|
|
207
270
|
*/
|
|
208
271
|
async updTopic(topic) {
|
|
209
272
|
const self = this;
|
|
273
|
+
|
|
274
|
+
// Skip topics that haven't been confirmed by the server yet.
|
|
275
|
+
// The _new flag is true for topics created locally but not yet subscribed.
|
|
276
|
+
// Only persist after subscribe succeeds and server assigns the real topic name.
|
|
277
|
+
if (topic?._new) {
|
|
278
|
+
return Promise.resolve();
|
|
279
|
+
}
|
|
280
|
+
|
|
210
281
|
if (!self.isReady()) {
|
|
211
282
|
return Promise.resolve();
|
|
212
283
|
}
|
|
213
284
|
|
|
214
|
-
|
|
285
|
+
return self._withRecovery(async () => {
|
|
215
286
|
// Get existing topic to merge data
|
|
216
287
|
const existing = await self._db.getFirstAsync(
|
|
217
288
|
'SELECT * FROM topics WHERE name = ?',
|
|
@@ -220,8 +291,6 @@ export default class SQLiteStorage {
|
|
|
220
291
|
|
|
221
292
|
const data = self._serializeTopic(existing, topic);
|
|
222
293
|
|
|
223
|
-
console.log('[SQLiteStorage] updTopic:', data.name, 'seq:', data.seq);
|
|
224
|
-
|
|
225
294
|
// Use INSERT OR REPLACE for atomic upsert
|
|
226
295
|
await self._db.runAsync(`
|
|
227
296
|
INSERT OR REPLACE INTO topics (
|
|
@@ -236,11 +305,7 @@ export default class SQLiteStorage {
|
|
|
236
305
|
data.defacs, data.creds, data.public, data.trusted, data.private,
|
|
237
306
|
data._aux, data._deleted, data.tags, data.acs
|
|
238
307
|
]);
|
|
239
|
-
|
|
240
|
-
} catch (err) {
|
|
241
|
-
console.error('[SQLiteStorage] updTopic FAILED:', err.message, 'topic:', topic.name);
|
|
242
|
-
throw err;
|
|
243
|
-
}
|
|
308
|
+
}, 'updTopic');
|
|
244
309
|
}
|
|
245
310
|
|
|
246
311
|
/**
|
|
@@ -255,15 +320,12 @@ export default class SQLiteStorage {
|
|
|
255
320
|
return Promise.resolve();
|
|
256
321
|
}
|
|
257
322
|
|
|
258
|
-
|
|
323
|
+
return self._withRecovery(async () => {
|
|
259
324
|
await self._db.runAsync(
|
|
260
325
|
'UPDATE topics SET _deleted = ? WHERE name = ?',
|
|
261
326
|
[deleted ? 1 : 0, name]
|
|
262
327
|
);
|
|
263
|
-
}
|
|
264
|
-
self._logger('SQLiteStorage', 'markTopicAsDeleted error:', err);
|
|
265
|
-
throw err;
|
|
266
|
-
}
|
|
328
|
+
}, 'markTopicAsDeleted');
|
|
267
329
|
}
|
|
268
330
|
|
|
269
331
|
/**
|
|
@@ -277,7 +339,7 @@ export default class SQLiteStorage {
|
|
|
277
339
|
return Promise.resolve();
|
|
278
340
|
}
|
|
279
341
|
|
|
280
|
-
|
|
342
|
+
return self._withRecovery(async () => {
|
|
281
343
|
// Delete topic, subscriptions, and messages in a transaction
|
|
282
344
|
await self._db.withTransactionAsync(async function() {
|
|
283
345
|
await self._db.runAsync('DELETE FROM topics WHERE name = ?', [name]);
|
|
@@ -285,10 +347,7 @@ export default class SQLiteStorage {
|
|
|
285
347
|
await self._db.runAsync('DELETE FROM messages WHERE topic = ?', [name]);
|
|
286
348
|
await self._db.runAsync('DELETE FROM dellog WHERE topic = ?', [name]);
|
|
287
349
|
});
|
|
288
|
-
}
|
|
289
|
-
self._logger('SQLiteStorage', 'remTopic error:', err);
|
|
290
|
-
throw err;
|
|
291
|
-
}
|
|
350
|
+
}, 'remTopic');
|
|
292
351
|
}
|
|
293
352
|
|
|
294
353
|
/**
|
|
@@ -303,7 +362,7 @@ export default class SQLiteStorage {
|
|
|
303
362
|
return [];
|
|
304
363
|
}
|
|
305
364
|
|
|
306
|
-
|
|
365
|
+
return self._withRecovery(async () => {
|
|
307
366
|
const rows = await self._db.getAllAsync('SELECT * FROM topics');
|
|
308
367
|
const topics = rows.map(function(row) {
|
|
309
368
|
return self._deserializeTopicRow(row);
|
|
@@ -316,10 +375,7 @@ export default class SQLiteStorage {
|
|
|
316
375
|
}
|
|
317
376
|
|
|
318
377
|
return topics;
|
|
319
|
-
}
|
|
320
|
-
self._logger('SQLiteStorage', 'mapTopics error:', err);
|
|
321
|
-
throw err;
|
|
322
|
-
}
|
|
378
|
+
}, 'mapTopics');
|
|
323
379
|
}
|
|
324
380
|
|
|
325
381
|
/**
|
|
@@ -361,15 +417,12 @@ export default class SQLiteStorage {
|
|
|
361
417
|
return Promise.resolve();
|
|
362
418
|
}
|
|
363
419
|
|
|
364
|
-
|
|
420
|
+
return self._withRecovery(async () => {
|
|
365
421
|
await self._db.runAsync(
|
|
366
422
|
'INSERT OR REPLACE INTO users (uid, public) VALUES (?, ?)',
|
|
367
423
|
[uid, JSON.stringify(pub)]
|
|
368
424
|
);
|
|
369
|
-
}
|
|
370
|
-
self._logger('SQLiteStorage', 'updUser error:', err);
|
|
371
|
-
throw err;
|
|
372
|
-
}
|
|
425
|
+
}, 'updUser');
|
|
373
426
|
}
|
|
374
427
|
|
|
375
428
|
/**
|
|
@@ -383,12 +436,9 @@ export default class SQLiteStorage {
|
|
|
383
436
|
return Promise.resolve();
|
|
384
437
|
}
|
|
385
438
|
|
|
386
|
-
|
|
439
|
+
return self._withRecovery(async () => {
|
|
387
440
|
await self._db.runAsync('DELETE FROM users WHERE uid = ?', [uid]);
|
|
388
|
-
}
|
|
389
|
-
self._logger('SQLiteStorage', 'remUser error:', err);
|
|
390
|
-
throw err;
|
|
391
|
-
}
|
|
441
|
+
}, 'remUser');
|
|
392
442
|
}
|
|
393
443
|
|
|
394
444
|
/**
|
|
@@ -403,7 +453,7 @@ export default class SQLiteStorage {
|
|
|
403
453
|
return [];
|
|
404
454
|
}
|
|
405
455
|
|
|
406
|
-
|
|
456
|
+
return self._withRecovery(async () => {
|
|
407
457
|
const rows = await self._db.getAllAsync('SELECT * FROM users');
|
|
408
458
|
const users = rows.map(function(row) {
|
|
409
459
|
return {
|
|
@@ -419,10 +469,7 @@ export default class SQLiteStorage {
|
|
|
419
469
|
}
|
|
420
470
|
|
|
421
471
|
return users;
|
|
422
|
-
}
|
|
423
|
-
self._logger('SQLiteStorage', 'mapUsers error:', err);
|
|
424
|
-
throw err;
|
|
425
|
-
}
|
|
472
|
+
}, 'mapUsers');
|
|
426
473
|
}
|
|
427
474
|
|
|
428
475
|
/**
|
|
@@ -436,7 +483,7 @@ export default class SQLiteStorage {
|
|
|
436
483
|
return undefined;
|
|
437
484
|
}
|
|
438
485
|
|
|
439
|
-
|
|
486
|
+
return self._withRecovery(async () => {
|
|
440
487
|
const row = await self._db.getFirstAsync(
|
|
441
488
|
'SELECT * FROM users WHERE uid = ?',
|
|
442
489
|
[uid]
|
|
@@ -450,10 +497,7 @@ export default class SQLiteStorage {
|
|
|
450
497
|
uid: row.uid,
|
|
451
498
|
public: self._parseJSON(row.public)
|
|
452
499
|
};
|
|
453
|
-
}
|
|
454
|
-
self._logger('SQLiteStorage', 'getUser error:', err);
|
|
455
|
-
throw err;
|
|
456
|
-
}
|
|
500
|
+
}, 'getUser');
|
|
457
501
|
}
|
|
458
502
|
|
|
459
503
|
// ==================== Subscriptions ====================
|
|
@@ -471,7 +515,7 @@ export default class SQLiteStorage {
|
|
|
471
515
|
return Promise.resolve();
|
|
472
516
|
}
|
|
473
517
|
|
|
474
|
-
|
|
518
|
+
return self._withRecovery(async () => {
|
|
475
519
|
// Get existing subscription
|
|
476
520
|
const existing = await self._db.getFirstAsync(
|
|
477
521
|
'SELECT * FROM subscriptions WHERE topic = ? AND uid = ?',
|
|
@@ -484,10 +528,7 @@ export default class SQLiteStorage {
|
|
|
484
528
|
'INSERT OR REPLACE INTO subscriptions (topic, uid, updated, mode, read, recv, clear, lastSeen, userAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
485
529
|
[data.topic, data.uid, data.updated, data.mode, data.read, data.recv, data.clear, data.lastSeen, data.userAgent]
|
|
486
530
|
);
|
|
487
|
-
}
|
|
488
|
-
self._logger('SQLiteStorage', 'updSubscription error:', err);
|
|
489
|
-
throw err;
|
|
490
|
-
}
|
|
531
|
+
}, 'updSubscription');
|
|
491
532
|
}
|
|
492
533
|
|
|
493
534
|
/**
|
|
@@ -503,7 +544,7 @@ export default class SQLiteStorage {
|
|
|
503
544
|
return [];
|
|
504
545
|
}
|
|
505
546
|
|
|
506
|
-
|
|
547
|
+
return self._withRecovery(async () => {
|
|
507
548
|
const rows = await self._db.getAllAsync(
|
|
508
549
|
'SELECT * FROM subscriptions WHERE topic = ?',
|
|
509
550
|
[topicName]
|
|
@@ -520,10 +561,7 @@ export default class SQLiteStorage {
|
|
|
520
561
|
}
|
|
521
562
|
|
|
522
563
|
return subs;
|
|
523
|
-
}
|
|
524
|
-
self._logger('SQLiteStorage', 'mapSubscriptions error:', err);
|
|
525
|
-
throw err;
|
|
526
|
-
}
|
|
564
|
+
}, 'mapSubscriptions');
|
|
527
565
|
}
|
|
528
566
|
|
|
529
567
|
// ==================== Messages ====================
|
|
@@ -539,41 +577,9 @@ export default class SQLiteStorage {
|
|
|
539
577
|
return Promise.resolve();
|
|
540
578
|
}
|
|
541
579
|
|
|
542
|
-
|
|
580
|
+
return self._withRecovery(async () => {
|
|
543
581
|
const data = self._serializeMessage(null, msg);
|
|
544
582
|
|
|
545
|
-
// Debug: log all values with their types
|
|
546
|
-
console.log('[SQLiteStorage] addMessage PARAMS:', JSON.stringify({
|
|
547
|
-
topic: {
|
|
548
|
-
value: data.topic,
|
|
549
|
-
type: typeof data.topic
|
|
550
|
-
},
|
|
551
|
-
seq: {
|
|
552
|
-
value: data.seq,
|
|
553
|
-
type: typeof data.seq
|
|
554
|
-
},
|
|
555
|
-
ts: {
|
|
556
|
-
value: data.ts,
|
|
557
|
-
type: typeof data.ts
|
|
558
|
-
},
|
|
559
|
-
_status: {
|
|
560
|
-
value: data._status,
|
|
561
|
-
type: typeof data._status
|
|
562
|
-
},
|
|
563
|
-
from: {
|
|
564
|
-
value: data.from,
|
|
565
|
-
type: typeof data.from
|
|
566
|
-
},
|
|
567
|
-
head: {
|
|
568
|
-
value: data.head ? data.head.substring(0, 50) : null,
|
|
569
|
-
type: typeof data.head
|
|
570
|
-
},
|
|
571
|
-
content: {
|
|
572
|
-
value: data.content ? data.content.substring(0, 50) : null,
|
|
573
|
-
type: typeof data.content
|
|
574
|
-
}
|
|
575
|
-
}));
|
|
576
|
-
|
|
577
583
|
// Build params array explicitly, converting undefined to null for SQLite
|
|
578
584
|
const params = [
|
|
579
585
|
data.topic,
|
|
@@ -585,19 +591,11 @@ export default class SQLiteStorage {
|
|
|
585
591
|
data.content !== undefined ? data.content : null
|
|
586
592
|
];
|
|
587
593
|
|
|
588
|
-
console.log('[SQLiteStorage] addMessage params array:', params.map((p, i) => `[${i}]=${typeof p}:${p === null ? 'null' : p === undefined ? 'undefined' : 'value'}`).join(', '));
|
|
589
|
-
|
|
590
594
|
await self._db.runAsync(
|
|
591
595
|
`INSERT OR REPLACE INTO messages (topic, seq, ts, _status, from_uid, head, content) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
592
596
|
params
|
|
593
597
|
);
|
|
594
|
-
|
|
595
|
-
} catch (err) {
|
|
596
|
-
console.error('[SQLiteStorage] addMessage FAILED:', err);
|
|
597
|
-
console.error('[SQLiteStorage] addMessage FAILED err.message:', err.message);
|
|
598
|
-
console.error('[SQLiteStorage] addMessage FAILED err.stack:', err.stack);
|
|
599
|
-
throw err;
|
|
600
|
-
}
|
|
598
|
+
}, 'addMessage');
|
|
601
599
|
}
|
|
602
600
|
|
|
603
601
|
/**
|
|
@@ -613,15 +611,12 @@ export default class SQLiteStorage {
|
|
|
613
611
|
return Promise.resolve();
|
|
614
612
|
}
|
|
615
613
|
|
|
616
|
-
|
|
614
|
+
return self._withRecovery(async () => {
|
|
617
615
|
await self._db.runAsync(
|
|
618
616
|
'UPDATE messages SET _status = ? WHERE topic = ? AND seq = ?',
|
|
619
617
|
[status, topicName, seq]
|
|
620
618
|
);
|
|
621
|
-
}
|
|
622
|
-
self._logger('SQLiteStorage', 'updMessageStatus error:', err);
|
|
623
|
-
throw err;
|
|
624
|
-
}
|
|
619
|
+
}, 'updMessageStatus');
|
|
625
620
|
}
|
|
626
621
|
|
|
627
622
|
/**
|
|
@@ -637,7 +632,7 @@ export default class SQLiteStorage {
|
|
|
637
632
|
return Promise.resolve();
|
|
638
633
|
}
|
|
639
634
|
|
|
640
|
-
|
|
635
|
+
return self._withRecovery(async () => {
|
|
641
636
|
if (!from && !to) {
|
|
642
637
|
// Delete all messages for topic
|
|
643
638
|
await self._db.runAsync(
|
|
@@ -657,10 +652,7 @@ export default class SQLiteStorage {
|
|
|
657
652
|
[topicName, from]
|
|
658
653
|
);
|
|
659
654
|
}
|
|
660
|
-
}
|
|
661
|
-
self._logger('SQLiteStorage', 'remMessages error:', err);
|
|
662
|
-
throw err;
|
|
663
|
-
}
|
|
655
|
+
}, 'remMessages');
|
|
664
656
|
}
|
|
665
657
|
|
|
666
658
|
/**
|
|
@@ -675,40 +667,29 @@ export default class SQLiteStorage {
|
|
|
675
667
|
const self = this;
|
|
676
668
|
query = query || {};
|
|
677
669
|
|
|
678
|
-
console.log('[SQLiteStorage] readMessages CALLED:', {
|
|
679
|
-
topicName,
|
|
680
|
-
query: JSON.stringify(query),
|
|
681
|
-
hasCallback: !!callback
|
|
682
|
-
});
|
|
683
|
-
|
|
684
670
|
if (!self.isReady()) {
|
|
685
|
-
console.log('[SQLiteStorage] readMessages: DB NOT READY');
|
|
686
671
|
return [];
|
|
687
672
|
}
|
|
688
673
|
|
|
689
|
-
|
|
674
|
+
return self._withRecovery(async () => {
|
|
690
675
|
var result = [];
|
|
691
676
|
|
|
692
677
|
// Handle individual message ranges
|
|
693
678
|
if (Array.isArray(query.ranges)) {
|
|
694
|
-
console.log('[SQLiteStorage] readMessages: Using RANGES query, ranges:', query.ranges.length);
|
|
695
679
|
for (var i = 0; i < query.ranges.length; i++) {
|
|
696
680
|
var range = query.ranges[i];
|
|
697
681
|
var msgs;
|
|
698
682
|
if (range.hi) {
|
|
699
|
-
console.log('[SQLiteStorage] readMessages: Range', i, '- low:', range.low, 'hi:', range.hi);
|
|
700
683
|
msgs = await self._db.getAllAsync(
|
|
701
684
|
'SELECT * FROM messages WHERE topic = ? AND seq >= ? AND seq < ? ORDER BY seq DESC',
|
|
702
685
|
[topicName, range.low, range.hi]
|
|
703
686
|
);
|
|
704
687
|
} else {
|
|
705
|
-
console.log('[SQLiteStorage] readMessages: Range', i, '- single seq:', range.low);
|
|
706
688
|
msgs = await self._db.getAllAsync(
|
|
707
689
|
'SELECT * FROM messages WHERE topic = ? AND seq = ?',
|
|
708
690
|
[topicName, range.low]
|
|
709
691
|
);
|
|
710
692
|
}
|
|
711
|
-
console.log('[SQLiteStorage] readMessages: Range', i, 'returned', msgs.length, 'rows');
|
|
712
693
|
|
|
713
694
|
var deserialized = msgs.map(function(row) {
|
|
714
695
|
return self._deserializeMessageRow(row);
|
|
@@ -720,7 +701,6 @@ export default class SQLiteStorage {
|
|
|
720
701
|
|
|
721
702
|
result = result.concat(deserialized);
|
|
722
703
|
}
|
|
723
|
-
console.log('[SQLiteStorage] readMessages: RANGES query total result:', result.length);
|
|
724
704
|
return result;
|
|
725
705
|
}
|
|
726
706
|
|
|
@@ -737,32 +717,12 @@ export default class SQLiteStorage {
|
|
|
737
717
|
params.push(limit);
|
|
738
718
|
}
|
|
739
719
|
|
|
740
|
-
console.log('[SQLiteStorage] readMessages: SQL:', sql);
|
|
741
|
-
console.log('[SQLiteStorage] readMessages: params:', JSON.stringify(params));
|
|
742
|
-
|
|
743
|
-
// DEBUG: First check what's actually in the database for this topic
|
|
744
|
-
var allMsgsForTopic = await self._db.getAllAsync(
|
|
745
|
-
'SELECT topic, seq, ts FROM messages WHERE topic = ? ORDER BY seq DESC',
|
|
746
|
-
[topicName]
|
|
747
|
-
);
|
|
748
|
-
console.log('[SQLiteStorage] readMessages: DEBUG - ALL messages in DB for topic:', allMsgsForTopic.length,
|
|
749
|
-
allMsgsForTopic.map(m => m.seq).join(','));
|
|
750
|
-
|
|
751
720
|
var rows = await self._db.getAllAsync(sql, params);
|
|
752
|
-
console.log('[SQLiteStorage] readMessages: Raw rows returned:', rows.length);
|
|
753
|
-
if (rows.length > 0) {
|
|
754
|
-
console.log('[SQLiteStorage] readMessages: First row seq:', rows[0].seq, 'topic:', rows[0].topic);
|
|
755
|
-
if (rows.length > 1) {
|
|
756
|
-
console.log('[SQLiteStorage] readMessages: Last row seq:', rows[rows.length - 1].seq);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
721
|
|
|
760
722
|
result = rows.map(function(row) {
|
|
761
723
|
return self._deserializeMessageRow(row);
|
|
762
724
|
});
|
|
763
725
|
|
|
764
|
-
console.log('[SQLiteStorage] readMessages: Returning', result.length, 'messages');
|
|
765
|
-
|
|
766
726
|
if (callback) {
|
|
767
727
|
result.forEach(function(msg) {
|
|
768
728
|
callback.call(context, msg);
|
|
@@ -770,11 +730,7 @@ export default class SQLiteStorage {
|
|
|
770
730
|
}
|
|
771
731
|
|
|
772
732
|
return result;
|
|
773
|
-
}
|
|
774
|
-
console.error('[SQLiteStorage] readMessages ERROR:', err);
|
|
775
|
-
self._logger('SQLiteStorage', 'readMessages error:', err);
|
|
776
|
-
throw err;
|
|
777
|
-
}
|
|
733
|
+
}, 'readMessages');
|
|
778
734
|
}
|
|
779
735
|
|
|
780
736
|
// ==================== Delete Log ====================
|
|
@@ -792,7 +748,7 @@ export default class SQLiteStorage {
|
|
|
792
748
|
return Promise.resolve();
|
|
793
749
|
}
|
|
794
750
|
|
|
795
|
-
|
|
751
|
+
return self._withRecovery(async () => {
|
|
796
752
|
// Use withTransactionAsync for proper transaction handling
|
|
797
753
|
await self._db.withTransactionAsync(async function() {
|
|
798
754
|
for (var i = 0; i < ranges.length; i++) {
|
|
@@ -803,10 +759,7 @@ export default class SQLiteStorage {
|
|
|
803
759
|
);
|
|
804
760
|
}
|
|
805
761
|
});
|
|
806
|
-
}
|
|
807
|
-
self._logger('SQLiteStorage', 'addDelLog error:', err);
|
|
808
|
-
throw err;
|
|
809
|
-
}
|
|
762
|
+
}, 'addDelLog');
|
|
810
763
|
}
|
|
811
764
|
|
|
812
765
|
/**
|
|
@@ -823,7 +776,7 @@ export default class SQLiteStorage {
|
|
|
823
776
|
return [];
|
|
824
777
|
}
|
|
825
778
|
|
|
826
|
-
|
|
779
|
+
return self._withRecovery(async () => {
|
|
827
780
|
var result = [];
|
|
828
781
|
|
|
829
782
|
// Handle individual message ranges
|
|
@@ -868,10 +821,7 @@ export default class SQLiteStorage {
|
|
|
868
821
|
}
|
|
869
822
|
|
|
870
823
|
return result;
|
|
871
|
-
}
|
|
872
|
-
self._logger('SQLiteStorage', 'readDelLog error:', err);
|
|
873
|
-
throw err;
|
|
874
|
-
}
|
|
824
|
+
}, 'readDelLog');
|
|
875
825
|
}
|
|
876
826
|
|
|
877
827
|
/**
|
|
@@ -885,7 +835,7 @@ export default class SQLiteStorage {
|
|
|
885
835
|
return undefined;
|
|
886
836
|
}
|
|
887
837
|
|
|
888
|
-
|
|
838
|
+
return self._withRecovery(async () => {
|
|
889
839
|
const row = await self._db.getFirstAsync(
|
|
890
840
|
'SELECT * FROM dellog WHERE topic = ? ORDER BY clear DESC LIMIT 1',
|
|
891
841
|
[topicName]
|
|
@@ -901,10 +851,7 @@ export default class SQLiteStorage {
|
|
|
901
851
|
low: row.low,
|
|
902
852
|
hi: row.hi
|
|
903
853
|
};
|
|
904
|
-
}
|
|
905
|
-
self._logger('SQLiteStorage', 'maxDelId error:', err);
|
|
906
|
-
throw err;
|
|
907
|
-
}
|
|
854
|
+
}, 'maxDelId');
|
|
908
855
|
}
|
|
909
856
|
|
|
910
857
|
// ==================== Private Helper Methods ====================
|
package/src/topic.js
CHANGED
|
@@ -2105,16 +2105,13 @@ export default class Topic {
|
|
|
2105
2105
|
|
|
2106
2106
|
// Load most recent messages from persistent cache.
|
|
2107
2107
|
_loadMessages(db, query) {
|
|
2108
|
-
console.log('[Topic] _loadMessages CALLED:', this.name, 'query:', JSON.stringify(query), 'db:', !!db);
|
|
2109
2108
|
query = query || {};
|
|
2110
2109
|
query.limit = query.limit || Const.DEFAULT_MESSAGES_PAGE;
|
|
2111
2110
|
|
|
2112
2111
|
// Count of message loaded from DB.
|
|
2113
2112
|
let count = 0;
|
|
2114
|
-
console.log('[Topic] _loadMessages: calling db.readMessages');
|
|
2115
2113
|
return db.readMessages(this.name, query)
|
|
2116
2114
|
.then(msgs => {
|
|
2117
|
-
console.log('[Topic] _loadMessages: db.readMessages returned', msgs ? msgs.length : 0, 'messages');
|
|
2118
2115
|
msgs.forEach(data => {
|
|
2119
2116
|
if (data.seq > this._maxSeq) {
|
|
2120
2117
|
this._maxSeq = data.seq;
|
|
@@ -2126,7 +2123,6 @@ export default class Topic {
|
|
|
2126
2123
|
this._maybeUpdateMessageVersionsCache(data);
|
|
2127
2124
|
});
|
|
2128
2125
|
count = msgs.length;
|
|
2129
|
-
console.log('[Topic] _loadMessages: processed', count, 'messages, _maxSeq:', this._maxSeq, '_minSeq:', this._minSeq);
|
|
2130
2126
|
})
|
|
2131
2127
|
.then(_ => db.readDelLog(this.name, query))
|
|
2132
2128
|
.then(dellog => {
|
package/types/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @see https://github.com/tinode/tinode-js
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
declare module 'tinode-sdk' {
|
|
14
|
+
declare module '@roeehrl/tinode-sdk' {
|
|
15
15
|
// ==========================================================================
|
|
16
16
|
// Configuration Types
|
|
17
17
|
// ==========================================================================
|
|
@@ -206,7 +206,8 @@ declare module 'tinode-sdk' {
|
|
|
206
206
|
|
|
207
207
|
export interface SetDesc {
|
|
208
208
|
defacs?: DefAcs;
|
|
209
|
-
|
|
209
|
+
/** Public data. For fnd topic, can be a search query string. */
|
|
210
|
+
public?: Record<string, unknown> | string;
|
|
210
211
|
trusted?: Record<string, unknown>;
|
|
211
212
|
private?: Record<string, unknown>;
|
|
212
213
|
}
|
|
@@ -264,6 +265,8 @@ declare module 'tinode-sdk' {
|
|
|
264
265
|
duration: number;
|
|
265
266
|
filename?: string;
|
|
266
267
|
size?: number;
|
|
268
|
+
/** Promise that resolves to the upload URL (for out-of-band uploads) */
|
|
269
|
+
urlPromise?: Promise<string>;
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
export interface AttachmentDesc {
|
|
@@ -659,6 +662,91 @@ declare module 'tinode-sdk' {
|
|
|
659
662
|
cancel(): void;
|
|
660
663
|
}
|
|
661
664
|
|
|
665
|
+
// ==========================================================================
|
|
666
|
+
// LargeFileHelperNative (React Native)
|
|
667
|
+
// ==========================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* React Native file upload helper.
|
|
671
|
+
* Uses file URIs instead of Blob objects for compatibility.
|
|
672
|
+
*/
|
|
673
|
+
export class LargeFileHelperNative {
|
|
674
|
+
constructor(tinode: Tinode, version?: string);
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Upload a file from a URI (React Native specific).
|
|
678
|
+
* @param uri File URI (e.g., 'file:///path/to/audio.m4a')
|
|
679
|
+
* @param filename Filename for the upload
|
|
680
|
+
* @param mimetype MIME type of the file
|
|
681
|
+
* @param size File size in bytes (optional, for progress calculation)
|
|
682
|
+
* @param avatarFor Topic name if the upload represents an avatar
|
|
683
|
+
* @param onProgress Progress callback (0..1)
|
|
684
|
+
* @param onSuccess Success callback
|
|
685
|
+
* @param onFailure Failure callback
|
|
686
|
+
* @returns Promise resolved with the upload URL
|
|
687
|
+
*/
|
|
688
|
+
uploadUri(
|
|
689
|
+
uri: string,
|
|
690
|
+
filename: string,
|
|
691
|
+
mimetype: string,
|
|
692
|
+
size?: number,
|
|
693
|
+
avatarFor?: string,
|
|
694
|
+
onProgress?: (progress: number) => void,
|
|
695
|
+
onSuccess?: (ctrl: ControlMessage) => void,
|
|
696
|
+
onFailure?: (ctrl: ControlMessage | null) => void,
|
|
697
|
+
): Promise<string>;
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Upload a file from a URI to a specific base URL.
|
|
701
|
+
*/
|
|
702
|
+
uploadUriWithBaseUrl(
|
|
703
|
+
baseUrl: string,
|
|
704
|
+
uri: string,
|
|
705
|
+
filename: string,
|
|
706
|
+
mimetype: string,
|
|
707
|
+
size?: number,
|
|
708
|
+
avatarFor?: string,
|
|
709
|
+
onProgress?: (progress: number) => void,
|
|
710
|
+
onSuccess?: (ctrl: ControlMessage) => void,
|
|
711
|
+
onFailure?: (ctrl: ControlMessage | null) => void,
|
|
712
|
+
): Promise<string>;
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Upload (compatibility method - prefer uploadUri in React Native).
|
|
716
|
+
* Accepts objects with uri property for RN file objects.
|
|
717
|
+
*/
|
|
718
|
+
upload(
|
|
719
|
+
data: { uri: string; name?: string; type?: string; size?: number } | File | Blob,
|
|
720
|
+
avatarFor?: string,
|
|
721
|
+
onProgress?: (progress: number) => void,
|
|
722
|
+
onSuccess?: (ctrl: ControlMessage) => void,
|
|
723
|
+
onFailure?: (ctrl: ControlMessage | null) => void,
|
|
724
|
+
): Promise<string>;
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get download configuration for use with expo-file-system.
|
|
728
|
+
* @param relativeUrl Relative URL to the file
|
|
729
|
+
* @returns Object with url and headers for download
|
|
730
|
+
*/
|
|
731
|
+
getDownloadConfig(relativeUrl: string): {
|
|
732
|
+
url: string;
|
|
733
|
+
headers: Record<string, string>;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Download (placeholder - use expo-file-system instead).
|
|
738
|
+
*/
|
|
739
|
+
download(
|
|
740
|
+
relativeUrl: string,
|
|
741
|
+
filename?: string,
|
|
742
|
+
mimetype?: string,
|
|
743
|
+
onProgress?: (loaded: number) => void,
|
|
744
|
+
onError?: (error: Error | string) => void,
|
|
745
|
+
): Promise<string>;
|
|
746
|
+
|
|
747
|
+
cancel(): void;
|
|
748
|
+
}
|
|
749
|
+
|
|
662
750
|
// ==========================================================================
|
|
663
751
|
// Main Tinode Class
|
|
664
752
|
// ==========================================================================
|