@roeehrl/tinode-sdk 0.25.1-sqlite.5 → 0.25.1-sqlite.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@roeehrl/tinode-sdk",
3
3
  "description": "Tinode SDK fork with Storage interface for SQLite persistence in React Native",
4
- "version": "0.25.1-sqlite.5",
4
+ "version": "0.25.1-sqlite.7",
5
5
  "types": "./types/index.d.ts",
6
6
  "scripts": {
7
7
  "format": "js-beautify -r src/*.js",
@@ -13,7 +13,9 @@
13
13
  "vers": "echo \"export const PACKAGE_VERSION = \\\"`node -p -e \"require('./package.json').version\"`\\\";\" > version.js",
14
14
  "test": "jest"
15
15
  },
16
- "browserslist": ["defaults"],
16
+ "browserslist": [
17
+ "defaults"
18
+ ],
17
19
  "repository": {
18
20
  "type": "git",
19
21
  "url": "git+https://github.com/roeehrl/tinode-js.git"
package/src/connection.js CHANGED
@@ -489,6 +489,20 @@ export default class Connection {
489
489
  if (this.#socket && (this.#socket.readyState == this.#socket.OPEN)) {
490
490
  this.#socket.send(msg);
491
491
  } else {
492
+ // Socket is not connected - trigger disconnect callback if not already done
493
+ // This handles the case where the server dies but onclose hasn't fired yet
494
+ if (this.onDisconnect) {
495
+ // Use setTimeout to avoid blocking the throw
496
+ setTimeout(() => {
497
+ if (this.onDisconnect) {
498
+ this.onDisconnect(new CommError(NETWORK_ERROR_TEXT, NETWORK_ERROR), NETWORK_ERROR);
499
+ }
500
+ }, 0);
501
+ }
502
+ // Clean up the socket reference if it exists but isn't connected
503
+ if (this.#socket) {
504
+ this.#socket = null;
505
+ }
492
506
  throw new Error("Websocket is not connected");
493
507
  }
494
508
  };
package/src/db.js CHANGED
@@ -220,6 +220,14 @@ export default class DB {
220
220
  * @returns {Promise} promise resolved/rejected on operation completion.
221
221
  */
222
222
  updTopic(topic) {
223
+ // Skip topics that haven't been confirmed by the server yet.
224
+ // The _new flag is true for topics created locally but not yet subscribed.
225
+ // Only persist after subscribe succeeds and server assigns the real topic name.
226
+ if (topic?._new) {
227
+ console.log('[DB] updTopic DEFERRED - topic not yet confirmed by server:', topic.name);
228
+ return Promise.resolve();
229
+ }
230
+
223
231
  console.log('[DB] updTopic CALLED:', topic?.name, 'shouldDelegate:', this.#shouldDelegate());
224
232
  // Delegate to custom storage if set
225
233
  if (this.#shouldDelegate()) {
@@ -198,6 +198,75 @@ 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
+ console.log('[SQLiteStorage] Attempting database recovery...');
211
+
212
+ // Close existing connection if any
213
+ if (self._db) {
214
+ try {
215
+ await self._db.closeAsync();
216
+ } catch (closeErr) {
217
+ // Ignore close errors - the handle may already be invalid
218
+ console.log('[SQLiteStorage] Close during recovery failed (expected):', closeErr.message);
219
+ }
220
+ self._db = null;
221
+ }
222
+
223
+ // Re-open the database
224
+ self._ready = false;
225
+ await self.initDatabase();
226
+
227
+ console.log('[SQLiteStorage] Database recovery successful');
228
+ return true;
229
+ } catch (err) {
230
+ console.error('[SQLiteStorage] Database recovery failed:', err);
231
+ return false;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Wrapper to execute database operations with automatic recovery.
237
+ * If an operation fails with NullPointerException, attempts recovery and retry.
238
+ * @param {function} operation - Async function to execute
239
+ * @param {string} operationName - Name for logging
240
+ * @returns {Promise<any>} Result of the operation
241
+ */
242
+ async _withRecovery(operation, operationName) {
243
+ const self = this;
244
+ try {
245
+ return await operation();
246
+ } catch (err) {
247
+ // Check if this is a stale database handle error
248
+ const isStaleHandle = err.message && (
249
+ err.message.includes('NullPointerException') ||
250
+ err.message.includes('prepareAsync') ||
251
+ err.message.includes('database is not open') ||
252
+ err.message.includes('SQLiteDatabase')
253
+ );
254
+
255
+ if (isStaleHandle) {
256
+ console.warn('[SQLiteStorage]', operationName, 'failed with stale handle, attempting recovery...');
257
+ const recovered = await self._recoverDatabase();
258
+ if (recovered) {
259
+ // Retry the operation once
260
+ console.log('[SQLiteStorage] Retrying', operationName, 'after recovery...');
261
+ return await operation();
262
+ }
263
+ }
264
+
265
+ // Re-throw if not recoverable
266
+ throw err;
267
+ }
268
+ }
269
+
201
270
  // ==================== Topics ====================
202
271
 
203
272
  /**
@@ -207,11 +276,20 @@ export default class SQLiteStorage {
207
276
  */
208
277
  async updTopic(topic) {
209
278
  const self = this;
279
+
280
+ // Skip topics that haven't been confirmed by the server yet.
281
+ // The _new flag is true for topics created locally but not yet subscribed.
282
+ // Only persist after subscribe succeeds and server assigns the real topic name.
283
+ if (topic?._new) {
284
+ console.log('[SQLiteStorage] updTopic DEFERRED - topic not yet confirmed by server:', topic.name);
285
+ return Promise.resolve();
286
+ }
287
+
210
288
  if (!self.isReady()) {
211
289
  return Promise.resolve();
212
290
  }
213
291
 
214
- try {
292
+ return self._withRecovery(async () => {
215
293
  // Get existing topic to merge data
216
294
  const existing = await self._db.getFirstAsync(
217
295
  'SELECT * FROM topics WHERE name = ?',
@@ -237,10 +315,7 @@ export default class SQLiteStorage {
237
315
  data._aux, data._deleted, data.tags, data.acs
238
316
  ]);
239
317
  console.log('[SQLiteStorage] updTopic SUCCESS:', data.name);
240
- } catch (err) {
241
- console.error('[SQLiteStorage] updTopic FAILED:', err.message, 'topic:', topic.name);
242
- throw err;
243
- }
318
+ }, 'updTopic');
244
319
  }
245
320
 
246
321
  /**
@@ -255,15 +330,12 @@ export default class SQLiteStorage {
255
330
  return Promise.resolve();
256
331
  }
257
332
 
258
- try {
333
+ return self._withRecovery(async () => {
259
334
  await self._db.runAsync(
260
335
  'UPDATE topics SET _deleted = ? WHERE name = ?',
261
336
  [deleted ? 1 : 0, name]
262
337
  );
263
- } catch (err) {
264
- self._logger('SQLiteStorage', 'markTopicAsDeleted error:', err);
265
- throw err;
266
- }
338
+ }, 'markTopicAsDeleted');
267
339
  }
268
340
 
269
341
  /**
@@ -277,7 +349,7 @@ export default class SQLiteStorage {
277
349
  return Promise.resolve();
278
350
  }
279
351
 
280
- try {
352
+ return self._withRecovery(async () => {
281
353
  // Delete topic, subscriptions, and messages in a transaction
282
354
  await self._db.withTransactionAsync(async function() {
283
355
  await self._db.runAsync('DELETE FROM topics WHERE name = ?', [name]);
@@ -285,10 +357,7 @@ export default class SQLiteStorage {
285
357
  await self._db.runAsync('DELETE FROM messages WHERE topic = ?', [name]);
286
358
  await self._db.runAsync('DELETE FROM dellog WHERE topic = ?', [name]);
287
359
  });
288
- } catch (err) {
289
- self._logger('SQLiteStorage', 'remTopic error:', err);
290
- throw err;
291
- }
360
+ }, 'remTopic');
292
361
  }
293
362
 
294
363
  /**
@@ -303,7 +372,7 @@ export default class SQLiteStorage {
303
372
  return [];
304
373
  }
305
374
 
306
- try {
375
+ return self._withRecovery(async () => {
307
376
  const rows = await self._db.getAllAsync('SELECT * FROM topics');
308
377
  const topics = rows.map(function(row) {
309
378
  return self._deserializeTopicRow(row);
@@ -316,10 +385,7 @@ export default class SQLiteStorage {
316
385
  }
317
386
 
318
387
  return topics;
319
- } catch (err) {
320
- self._logger('SQLiteStorage', 'mapTopics error:', err);
321
- throw err;
322
- }
388
+ }, 'mapTopics');
323
389
  }
324
390
 
325
391
  /**
@@ -361,15 +427,12 @@ export default class SQLiteStorage {
361
427
  return Promise.resolve();
362
428
  }
363
429
 
364
- try {
430
+ return self._withRecovery(async () => {
365
431
  await self._db.runAsync(
366
432
  'INSERT OR REPLACE INTO users (uid, public) VALUES (?, ?)',
367
433
  [uid, JSON.stringify(pub)]
368
434
  );
369
- } catch (err) {
370
- self._logger('SQLiteStorage', 'updUser error:', err);
371
- throw err;
372
- }
435
+ }, 'updUser');
373
436
  }
374
437
 
375
438
  /**
@@ -383,12 +446,9 @@ export default class SQLiteStorage {
383
446
  return Promise.resolve();
384
447
  }
385
448
 
386
- try {
449
+ return self._withRecovery(async () => {
387
450
  await self._db.runAsync('DELETE FROM users WHERE uid = ?', [uid]);
388
- } catch (err) {
389
- self._logger('SQLiteStorage', 'remUser error:', err);
390
- throw err;
391
- }
451
+ }, 'remUser');
392
452
  }
393
453
 
394
454
  /**
@@ -403,7 +463,7 @@ export default class SQLiteStorage {
403
463
  return [];
404
464
  }
405
465
 
406
- try {
466
+ return self._withRecovery(async () => {
407
467
  const rows = await self._db.getAllAsync('SELECT * FROM users');
408
468
  const users = rows.map(function(row) {
409
469
  return {
@@ -419,10 +479,7 @@ export default class SQLiteStorage {
419
479
  }
420
480
 
421
481
  return users;
422
- } catch (err) {
423
- self._logger('SQLiteStorage', 'mapUsers error:', err);
424
- throw err;
425
- }
482
+ }, 'mapUsers');
426
483
  }
427
484
 
428
485
  /**
@@ -436,7 +493,7 @@ export default class SQLiteStorage {
436
493
  return undefined;
437
494
  }
438
495
 
439
- try {
496
+ return self._withRecovery(async () => {
440
497
  const row = await self._db.getFirstAsync(
441
498
  'SELECT * FROM users WHERE uid = ?',
442
499
  [uid]
@@ -450,10 +507,7 @@ export default class SQLiteStorage {
450
507
  uid: row.uid,
451
508
  public: self._parseJSON(row.public)
452
509
  };
453
- } catch (err) {
454
- self._logger('SQLiteStorage', 'getUser error:', err);
455
- throw err;
456
- }
510
+ }, 'getUser');
457
511
  }
458
512
 
459
513
  // ==================== Subscriptions ====================
@@ -471,7 +525,7 @@ export default class SQLiteStorage {
471
525
  return Promise.resolve();
472
526
  }
473
527
 
474
- try {
528
+ return self._withRecovery(async () => {
475
529
  // Get existing subscription
476
530
  const existing = await self._db.getFirstAsync(
477
531
  'SELECT * FROM subscriptions WHERE topic = ? AND uid = ?',
@@ -484,10 +538,7 @@ export default class SQLiteStorage {
484
538
  'INSERT OR REPLACE INTO subscriptions (topic, uid, updated, mode, read, recv, clear, lastSeen, userAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
485
539
  [data.topic, data.uid, data.updated, data.mode, data.read, data.recv, data.clear, data.lastSeen, data.userAgent]
486
540
  );
487
- } catch (err) {
488
- self._logger('SQLiteStorage', 'updSubscription error:', err);
489
- throw err;
490
- }
541
+ }, 'updSubscription');
491
542
  }
492
543
 
493
544
  /**
@@ -503,7 +554,7 @@ export default class SQLiteStorage {
503
554
  return [];
504
555
  }
505
556
 
506
- try {
557
+ return self._withRecovery(async () => {
507
558
  const rows = await self._db.getAllAsync(
508
559
  'SELECT * FROM subscriptions WHERE topic = ?',
509
560
  [topicName]
@@ -520,10 +571,7 @@ export default class SQLiteStorage {
520
571
  }
521
572
 
522
573
  return subs;
523
- } catch (err) {
524
- self._logger('SQLiteStorage', 'mapSubscriptions error:', err);
525
- throw err;
526
- }
574
+ }, 'mapSubscriptions');
527
575
  }
528
576
 
529
577
  // ==================== Messages ====================
@@ -539,41 +587,9 @@ export default class SQLiteStorage {
539
587
  return Promise.resolve();
540
588
  }
541
589
 
542
- try {
590
+ return self._withRecovery(async () => {
543
591
  const data = self._serializeMessage(null, msg);
544
592
 
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
593
  // Build params array explicitly, converting undefined to null for SQLite
578
594
  const params = [
579
595
  data.topic,
@@ -585,19 +601,12 @@ export default class SQLiteStorage {
585
601
  data.content !== undefined ? data.content : null
586
602
  ];
587
603
 
588
- console.log('[SQLiteStorage] addMessage params array:', params.map((p, i) => `[${i}]=${typeof p}:${p === null ? 'null' : p === undefined ? 'undefined' : 'value'}`).join(', '));
589
-
590
604
  await self._db.runAsync(
591
605
  `INSERT OR REPLACE INTO messages (topic, seq, ts, _status, from_uid, head, content) VALUES (?, ?, ?, ?, ?, ?, ?)`,
592
606
  params
593
607
  );
594
608
  console.log('[SQLiteStorage] addMessage SUCCESS:', data.topic, data.seq);
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
- }
609
+ }, 'addMessage');
601
610
  }
602
611
 
603
612
  /**
@@ -613,15 +622,12 @@ export default class SQLiteStorage {
613
622
  return Promise.resolve();
614
623
  }
615
624
 
616
- try {
625
+ return self._withRecovery(async () => {
617
626
  await self._db.runAsync(
618
627
  'UPDATE messages SET _status = ? WHERE topic = ? AND seq = ?',
619
628
  [status, topicName, seq]
620
629
  );
621
- } catch (err) {
622
- self._logger('SQLiteStorage', 'updMessageStatus error:', err);
623
- throw err;
624
- }
630
+ }, 'updMessageStatus');
625
631
  }
626
632
 
627
633
  /**
@@ -637,7 +643,7 @@ export default class SQLiteStorage {
637
643
  return Promise.resolve();
638
644
  }
639
645
 
640
- try {
646
+ return self._withRecovery(async () => {
641
647
  if (!from && !to) {
642
648
  // Delete all messages for topic
643
649
  await self._db.runAsync(
@@ -657,10 +663,7 @@ export default class SQLiteStorage {
657
663
  [topicName, from]
658
664
  );
659
665
  }
660
- } catch (err) {
661
- self._logger('SQLiteStorage', 'remMessages error:', err);
662
- throw err;
663
- }
666
+ }, 'remMessages');
664
667
  }
665
668
 
666
669
  /**
@@ -675,40 +678,29 @@ export default class SQLiteStorage {
675
678
  const self = this;
676
679
  query = query || {};
677
680
 
678
- console.log('[SQLiteStorage] readMessages CALLED:', {
679
- topicName,
680
- query: JSON.stringify(query),
681
- hasCallback: !!callback
682
- });
683
-
684
681
  if (!self.isReady()) {
685
- console.log('[SQLiteStorage] readMessages: DB NOT READY');
686
682
  return [];
687
683
  }
688
684
 
689
- try {
685
+ return self._withRecovery(async () => {
690
686
  var result = [];
691
687
 
692
688
  // Handle individual message ranges
693
689
  if (Array.isArray(query.ranges)) {
694
- console.log('[SQLiteStorage] readMessages: Using RANGES query, ranges:', query.ranges.length);
695
690
  for (var i = 0; i < query.ranges.length; i++) {
696
691
  var range = query.ranges[i];
697
692
  var msgs;
698
693
  if (range.hi) {
699
- console.log('[SQLiteStorage] readMessages: Range', i, '- low:', range.low, 'hi:', range.hi);
700
694
  msgs = await self._db.getAllAsync(
701
695
  'SELECT * FROM messages WHERE topic = ? AND seq >= ? AND seq < ? ORDER BY seq DESC',
702
696
  [topicName, range.low, range.hi]
703
697
  );
704
698
  } else {
705
- console.log('[SQLiteStorage] readMessages: Range', i, '- single seq:', range.low);
706
699
  msgs = await self._db.getAllAsync(
707
700
  'SELECT * FROM messages WHERE topic = ? AND seq = ?',
708
701
  [topicName, range.low]
709
702
  );
710
703
  }
711
- console.log('[SQLiteStorage] readMessages: Range', i, 'returned', msgs.length, 'rows');
712
704
 
713
705
  var deserialized = msgs.map(function(row) {
714
706
  return self._deserializeMessageRow(row);
@@ -720,7 +712,6 @@ export default class SQLiteStorage {
720
712
 
721
713
  result = result.concat(deserialized);
722
714
  }
723
- console.log('[SQLiteStorage] readMessages: RANGES query total result:', result.length);
724
715
  return result;
725
716
  }
726
717
 
@@ -737,32 +728,12 @@ export default class SQLiteStorage {
737
728
  params.push(limit);
738
729
  }
739
730
 
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
731
  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
732
 
760
733
  result = rows.map(function(row) {
761
734
  return self._deserializeMessageRow(row);
762
735
  });
763
736
 
764
- console.log('[SQLiteStorage] readMessages: Returning', result.length, 'messages');
765
-
766
737
  if (callback) {
767
738
  result.forEach(function(msg) {
768
739
  callback.call(context, msg);
@@ -770,11 +741,7 @@ export default class SQLiteStorage {
770
741
  }
771
742
 
772
743
  return result;
773
- } catch (err) {
774
- console.error('[SQLiteStorage] readMessages ERROR:', err);
775
- self._logger('SQLiteStorage', 'readMessages error:', err);
776
- throw err;
777
- }
744
+ }, 'readMessages');
778
745
  }
779
746
 
780
747
  // ==================== Delete Log ====================
@@ -792,7 +759,7 @@ export default class SQLiteStorage {
792
759
  return Promise.resolve();
793
760
  }
794
761
 
795
- try {
762
+ return self._withRecovery(async () => {
796
763
  // Use withTransactionAsync for proper transaction handling
797
764
  await self._db.withTransactionAsync(async function() {
798
765
  for (var i = 0; i < ranges.length; i++) {
@@ -803,10 +770,7 @@ export default class SQLiteStorage {
803
770
  );
804
771
  }
805
772
  });
806
- } catch (err) {
807
- self._logger('SQLiteStorage', 'addDelLog error:', err);
808
- throw err;
809
- }
773
+ }, 'addDelLog');
810
774
  }
811
775
 
812
776
  /**
@@ -823,7 +787,7 @@ export default class SQLiteStorage {
823
787
  return [];
824
788
  }
825
789
 
826
- try {
790
+ return self._withRecovery(async () => {
827
791
  var result = [];
828
792
 
829
793
  // Handle individual message ranges
@@ -868,10 +832,7 @@ export default class SQLiteStorage {
868
832
  }
869
833
 
870
834
  return result;
871
- } catch (err) {
872
- self._logger('SQLiteStorage', 'readDelLog error:', err);
873
- throw err;
874
- }
835
+ }, 'readDelLog');
875
836
  }
876
837
 
877
838
  /**
@@ -885,7 +846,7 @@ export default class SQLiteStorage {
885
846
  return undefined;
886
847
  }
887
848
 
888
- try {
849
+ return self._withRecovery(async () => {
889
850
  const row = await self._db.getFirstAsync(
890
851
  'SELECT * FROM dellog WHERE topic = ? ORDER BY clear DESC LIMIT 1',
891
852
  [topicName]
@@ -901,10 +862,7 @@ export default class SQLiteStorage {
901
862
  low: row.low,
902
863
  hi: row.hi
903
864
  };
904
- } catch (err) {
905
- self._logger('SQLiteStorage', 'maxDelId error:', err);
906
- throw err;
907
- }
865
+ }, 'maxDelId');
908
866
  }
909
867
 
910
868
  // ==================== Private Helper Methods ====================
package/types/index.d.ts CHANGED
@@ -206,7 +206,8 @@ declare module '@roeehrl/tinode-sdk' {
206
206
 
207
207
  export interface SetDesc {
208
208
  defacs?: DefAcs;
209
- public?: Record<string, unknown>;
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
  }