@roeehrl/tinode-sdk 0.25.1-sqlite.1
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/LICENSE +201 -0
- package/README.md +47 -0
- package/package.json +76 -0
- package/src/access-mode.js +567 -0
- package/src/cbuffer.js +244 -0
- package/src/cbuffer.test.js +107 -0
- package/src/comm-error.js +14 -0
- package/src/config.js +71 -0
- package/src/connection.js +537 -0
- package/src/db.js +1021 -0
- package/src/drafty.js +2758 -0
- package/src/drafty.test.js +1600 -0
- package/src/fnd-topic.js +123 -0
- package/src/index.js +29 -0
- package/src/index.native.js +35 -0
- package/src/large-file.js +325 -0
- package/src/me-topic.js +480 -0
- package/src/meta-builder.js +283 -0
- package/src/storage-sqlite.js +1081 -0
- package/src/tinode.js +2382 -0
- package/src/topic.js +2160 -0
- package/src/utils.js +309 -0
- package/src/utils.test.js +456 -0
- package/types/index.d.ts +1227 -0
- package/umd/tinode.dev.js +6856 -0
- package/umd/tinode.dev.js.map +1 -0
- package/umd/tinode.prod.js +2 -0
- package/umd/tinode.prod.js.map +1 -0
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SQLite storage implementation for React Native.
|
|
3
|
+
* Provides persistent storage using expo-sqlite, matching the DB class API.
|
|
4
|
+
*
|
|
5
|
+
* This file is only imported in React Native environments via index.native.js.
|
|
6
|
+
* Metro bundler handles platform-specific resolution automatically.
|
|
7
|
+
*
|
|
8
|
+
* @copyright 2025 Activon
|
|
9
|
+
* @license Apache 2.0
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
// Direct import - this file is only loaded in React Native via index.native.js
|
|
14
|
+
import * as SQLite from 'expo-sqlite';
|
|
15
|
+
|
|
16
|
+
// Serializable topic fields (matching db.js)
|
|
17
|
+
const TOPIC_FIELDS = ['created', 'updated', 'deleted', 'touched', 'read', 'recv', 'seq',
|
|
18
|
+
'clear', 'defacs', 'creds', 'public', 'trusted', 'private', '_aux', '_deleted'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Serializable subscription fields
|
|
22
|
+
const SUBSCRIPTION_FIELDS = ['updated', 'mode', 'read', 'recv', 'clear', 'lastSeen', 'userAgent'];
|
|
23
|
+
|
|
24
|
+
// Serializable message fields
|
|
25
|
+
const MESSAGE_FIELDS = ['topic', 'seq', 'ts', '_status', 'from', 'head', 'content'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SQLite storage implementation for React Native.
|
|
29
|
+
* Implements the Storage interface matching the DB class API.
|
|
30
|
+
*/
|
|
31
|
+
export default class SQLiteStorage {
|
|
32
|
+
/**
|
|
33
|
+
* Create SQLiteStorage instance.
|
|
34
|
+
* @param {string} dbName - Database file name (default: 'tinode.db')
|
|
35
|
+
* @param {function} onError - Error callback
|
|
36
|
+
* @param {function} logger - Logger callback
|
|
37
|
+
*/
|
|
38
|
+
constructor(dbName, onError, logger) {
|
|
39
|
+
this._onError = function() {};
|
|
40
|
+
this._logger = function() {};
|
|
41
|
+
this._db = null;
|
|
42
|
+
this._dbName = 'tinode.db';
|
|
43
|
+
this._ready = false;
|
|
44
|
+
|
|
45
|
+
if (typeof dbName === 'string') {
|
|
46
|
+
this._dbName = dbName;
|
|
47
|
+
} else if (typeof dbName === 'function') {
|
|
48
|
+
// Handle case where dbName is actually onError (backwards compat)
|
|
49
|
+
this._onError = dbName;
|
|
50
|
+
this._logger = onError || this._logger;
|
|
51
|
+
}
|
|
52
|
+
if (typeof onError === 'function') {
|
|
53
|
+
this._onError = onError;
|
|
54
|
+
}
|
|
55
|
+
if (typeof logger === 'function') {
|
|
56
|
+
this._logger = logger;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the SQLite database and create tables.
|
|
62
|
+
* @returns {Promise} Promise resolved when database is ready.
|
|
63
|
+
*/
|
|
64
|
+
async initDatabase() {
|
|
65
|
+
const self = this;
|
|
66
|
+
try {
|
|
67
|
+
self._db = await SQLite.openDatabaseAsync(self._dbName);
|
|
68
|
+
|
|
69
|
+
// Enable WAL mode for better performance
|
|
70
|
+
await self._db.execAsync('PRAGMA journal_mode = WAL');
|
|
71
|
+
|
|
72
|
+
// Create all tables
|
|
73
|
+
await self._createTables();
|
|
74
|
+
|
|
75
|
+
self._ready = true;
|
|
76
|
+
self._logger('SQLiteStorage', 'Database initialized:', self._dbName);
|
|
77
|
+
return self._db;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
self._logger('SQLiteStorage', 'initDatabase error:', err);
|
|
80
|
+
self._onError(err);
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create all required tables.
|
|
87
|
+
*/
|
|
88
|
+
async _createTables() {
|
|
89
|
+
const self = this;
|
|
90
|
+
// Topics table - primary key is 'name'
|
|
91
|
+
await self._db.execAsync(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS topics (
|
|
93
|
+
name TEXT PRIMARY KEY,
|
|
94
|
+
created TEXT,
|
|
95
|
+
updated TEXT,
|
|
96
|
+
deleted TEXT,
|
|
97
|
+
touched TEXT,
|
|
98
|
+
read INTEGER DEFAULT 0,
|
|
99
|
+
recv INTEGER DEFAULT 0,
|
|
100
|
+
seq INTEGER DEFAULT 0,
|
|
101
|
+
clear INTEGER DEFAULT 0,
|
|
102
|
+
defacs TEXT,
|
|
103
|
+
creds TEXT,
|
|
104
|
+
public TEXT,
|
|
105
|
+
trusted TEXT,
|
|
106
|
+
private TEXT,
|
|
107
|
+
_aux TEXT,
|
|
108
|
+
_deleted INTEGER DEFAULT 0,
|
|
109
|
+
tags TEXT,
|
|
110
|
+
acs TEXT
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
// Users table - primary key is 'uid'
|
|
115
|
+
await self._db.execAsync(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
117
|
+
uid TEXT PRIMARY KEY,
|
|
118
|
+
public TEXT
|
|
119
|
+
)
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
// Subscriptions table - composite primary key (topic, uid)
|
|
123
|
+
await self._db.execAsync(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
125
|
+
topic TEXT NOT NULL,
|
|
126
|
+
uid TEXT NOT NULL,
|
|
127
|
+
updated TEXT,
|
|
128
|
+
mode TEXT,
|
|
129
|
+
read INTEGER DEFAULT 0,
|
|
130
|
+
recv INTEGER DEFAULT 0,
|
|
131
|
+
clear INTEGER DEFAULT 0,
|
|
132
|
+
lastSeen TEXT,
|
|
133
|
+
userAgent TEXT,
|
|
134
|
+
PRIMARY KEY (topic, uid)
|
|
135
|
+
)
|
|
136
|
+
`);
|
|
137
|
+
|
|
138
|
+
// Messages table - composite primary key (topic, seq)
|
|
139
|
+
await self._db.execAsync(`
|
|
140
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
141
|
+
topic TEXT NOT NULL,
|
|
142
|
+
seq INTEGER NOT NULL,
|
|
143
|
+
ts TEXT,
|
|
144
|
+
_status INTEGER DEFAULT 0,
|
|
145
|
+
from_uid TEXT,
|
|
146
|
+
head TEXT,
|
|
147
|
+
content TEXT,
|
|
148
|
+
PRIMARY KEY (topic, seq)
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
// Delete log table - composite primary key (topic, low, hi)
|
|
153
|
+
await self._db.execAsync(`
|
|
154
|
+
CREATE TABLE IF NOT EXISTS dellog (
|
|
155
|
+
topic TEXT NOT NULL,
|
|
156
|
+
clear INTEGER NOT NULL,
|
|
157
|
+
low INTEGER NOT NULL,
|
|
158
|
+
hi INTEGER NOT NULL,
|
|
159
|
+
PRIMARY KEY (topic, low, hi)
|
|
160
|
+
)
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
// Create index for efficient clear ID lookups
|
|
164
|
+
await self._db.execAsync(`
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_dellog_topic_clear ON dellog(topic, clear)
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Delete the database.
|
|
171
|
+
* @returns {Promise<boolean>} Promise resolved when database is deleted.
|
|
172
|
+
*/
|
|
173
|
+
async deleteDatabase() {
|
|
174
|
+
const self = this;
|
|
175
|
+
try {
|
|
176
|
+
if (self._db) {
|
|
177
|
+
await self._db.closeAsync();
|
|
178
|
+
self._db = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await SQLite.deleteDatabaseAsync(self._dbName);
|
|
182
|
+
|
|
183
|
+
self._ready = false;
|
|
184
|
+
self._logger('SQLiteStorage', 'Database deleted:', self._dbName);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
self._logger('SQLiteStorage', 'deleteDatabase error:', err);
|
|
188
|
+
self._onError(err);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if database is ready.
|
|
195
|
+
* @returns {boolean} True if database is initialized and ready.
|
|
196
|
+
*/
|
|
197
|
+
isReady() {
|
|
198
|
+
return this._ready && this._db !== null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ==================== Topics ====================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Save or update topic in the database.
|
|
205
|
+
* @param {Object} topic - Topic object to save.
|
|
206
|
+
* @returns {Promise} Promise resolved on completion.
|
|
207
|
+
*/
|
|
208
|
+
async updTopic(topic) {
|
|
209
|
+
const self = this;
|
|
210
|
+
if (!self.isReady()) {
|
|
211
|
+
return Promise.resolve();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Get existing topic to merge data
|
|
216
|
+
const existing = await self._db.getFirstAsync(
|
|
217
|
+
'SELECT * FROM topics WHERE name = ?',
|
|
218
|
+
[topic.name]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const data = self._serializeTopic(existing, topic);
|
|
222
|
+
|
|
223
|
+
console.log('[SQLiteStorage] updTopic:', data.name, 'seq:', data.seq);
|
|
224
|
+
|
|
225
|
+
// Use INSERT OR REPLACE for atomic upsert
|
|
226
|
+
await self._db.runAsync(`
|
|
227
|
+
INSERT OR REPLACE INTO topics (
|
|
228
|
+
name, created, updated, deleted, touched,
|
|
229
|
+
read, recv, seq, clear,
|
|
230
|
+
defacs, creds, public, trusted, private,
|
|
231
|
+
_aux, _deleted, tags, acs
|
|
232
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
233
|
+
`, [
|
|
234
|
+
data.name, data.created, data.updated, data.deleted, data.touched,
|
|
235
|
+
data.read, data.recv, data.seq, data.clear,
|
|
236
|
+
data.defacs, data.creds, data.public, data.trusted, data.private,
|
|
237
|
+
data._aux, data._deleted, data.tags, data.acs
|
|
238
|
+
]);
|
|
239
|
+
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
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Mark or unmark topic as deleted.
|
|
248
|
+
* @param {string} name - Topic name.
|
|
249
|
+
* @param {boolean} deleted - Deleted status.
|
|
250
|
+
* @returns {Promise} Promise resolved on completion.
|
|
251
|
+
*/
|
|
252
|
+
async markTopicAsDeleted(name, deleted) {
|
|
253
|
+
const self = this;
|
|
254
|
+
if (!self.isReady()) {
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await self._db.runAsync(
|
|
260
|
+
'UPDATE topics SET _deleted = ? WHERE name = ?',
|
|
261
|
+
[deleted ? 1 : 0, name]
|
|
262
|
+
);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
self._logger('SQLiteStorage', 'markTopicAsDeleted error:', err);
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Remove topic and all related data from database.
|
|
271
|
+
* @param {string} name - Topic name to remove.
|
|
272
|
+
* @returns {Promise} Promise resolved on completion.
|
|
273
|
+
*/
|
|
274
|
+
async remTopic(name) {
|
|
275
|
+
const self = this;
|
|
276
|
+
if (!self.isReady()) {
|
|
277
|
+
return Promise.resolve();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Delete topic, subscriptions, and messages in a transaction
|
|
282
|
+
await self._db.withTransactionAsync(async function() {
|
|
283
|
+
await self._db.runAsync('DELETE FROM topics WHERE name = ?', [name]);
|
|
284
|
+
await self._db.runAsync('DELETE FROM subscriptions WHERE topic = ?', [name]);
|
|
285
|
+
await self._db.runAsync('DELETE FROM messages WHERE topic = ?', [name]);
|
|
286
|
+
await self._db.runAsync('DELETE FROM dellog WHERE topic = ?', [name]);
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
self._logger('SQLiteStorage', 'remTopic error:', err);
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Execute callback for each stored topic.
|
|
296
|
+
* @param {function} callback - Callback for each topic.
|
|
297
|
+
* @param {Object} context - Callback context.
|
|
298
|
+
* @returns {Promise<Array>} Promise resolved with all topics.
|
|
299
|
+
*/
|
|
300
|
+
async mapTopics(callback, context) {
|
|
301
|
+
const self = this;
|
|
302
|
+
if (!self.isReady()) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const rows = await self._db.getAllAsync('SELECT * FROM topics');
|
|
308
|
+
const topics = rows.map(function(row) {
|
|
309
|
+
return self._deserializeTopicRow(row);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (callback) {
|
|
313
|
+
topics.forEach(function(topic) {
|
|
314
|
+
callback.call(context, topic);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return topics;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
self._logger('SQLiteStorage', 'mapTopics error:', err);
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Copy data from serialized object to topic.
|
|
327
|
+
* @param {Object} topic - Target topic object.
|
|
328
|
+
* @param {Object} src - Source data.
|
|
329
|
+
*/
|
|
330
|
+
deserializeTopic(topic, src) {
|
|
331
|
+
TOPIC_FIELDS.forEach(function(f) {
|
|
332
|
+
if (src.hasOwnProperty(f)) {
|
|
333
|
+
topic[f] = src[f];
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (Array.isArray(src.tags)) {
|
|
337
|
+
topic._tags = src.tags;
|
|
338
|
+
}
|
|
339
|
+
if (src.acs) {
|
|
340
|
+
topic.setAccessMode(src.acs);
|
|
341
|
+
}
|
|
342
|
+
topic.seq |= 0;
|
|
343
|
+
topic.read |= 0;
|
|
344
|
+
topic.unread = Math.max(0, topic.seq - topic.read);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ==================== Users ====================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Add or update user in database.
|
|
351
|
+
* @param {string} uid - User ID.
|
|
352
|
+
* @param {Object} pub - User's public data.
|
|
353
|
+
* @returns {Promise} Promise resolved on completion.
|
|
354
|
+
*/
|
|
355
|
+
async updUser(uid, pub) {
|
|
356
|
+
const self = this;
|
|
357
|
+
if (arguments.length < 2 || pub === undefined) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!self.isReady()) {
|
|
361
|
+
return Promise.resolve();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await self._db.runAsync(
|
|
366
|
+
'INSERT OR REPLACE INTO users (uid, public) VALUES (?, ?)',
|
|
367
|
+
[uid, JSON.stringify(pub)]
|
|
368
|
+
);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
self._logger('SQLiteStorage', 'updUser error:', err);
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Remove user from database.
|
|
377
|
+
* @param {string} uid - User ID to remove.
|
|
378
|
+
* @returns {Promise} Promise resolved on completion.
|
|
379
|
+
*/
|
|
380
|
+
async remUser(uid) {
|
|
381
|
+
const self = this;
|
|
382
|
+
if (!self.isReady()) {
|
|
383
|
+
return Promise.resolve();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await self._db.runAsync('DELETE FROM users WHERE uid = ?', [uid]);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
self._logger('SQLiteStorage', 'remUser error:', err);
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Execute callback for each stored user.
|
|
396
|
+
* @param {function} callback - Callback for each user.
|
|
397
|
+
* @param {Object} context - Callback context.
|
|
398
|
+
* @returns {Promise<Array>} Promise resolved with all users.
|
|
399
|
+
*/
|
|
400
|
+
async mapUsers(callback, context) {
|
|
401
|
+
const self = this;
|
|
402
|
+
if (!self.isReady()) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const rows = await self._db.getAllAsync('SELECT * FROM users');
|
|
408
|
+
const users = rows.map(function(row) {
|
|
409
|
+
return {
|
|
410
|
+
uid: row.uid,
|
|
411
|
+
public: self._parseJSON(row.public)
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (callback) {
|
|
416
|
+
users.forEach(function(user) {
|
|
417
|
+
callback.call(context, user);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return users;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
self._logger('SQLiteStorage', 'mapUsers error:', err);
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get a single user from database.
|
|
430
|
+
* @param {string} uid - User ID.
|
|
431
|
+
* @returns {Promise<Object|undefined>} Promise resolved with user or undefined.
|
|
432
|
+
*/
|
|
433
|
+
async getUser(uid) {
|
|
434
|
+
const self = this;
|
|
435
|
+
if (!self.isReady()) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const row = await self._db.getFirstAsync(
|
|
441
|
+
'SELECT * FROM users WHERE uid = ?',
|
|
442
|
+
[uid]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (!row) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
uid: row.uid,
|
|
451
|
+
public: self._parseJSON(row.public)
|
|
452
|
+
};
|
|
453
|
+
} catch (err) {
|
|
454
|
+
self._logger('SQLiteStorage', 'getUser error:', err);
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ==================== Subscriptions ====================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Add or update subscription in database.
|
|
463
|
+
* @param {string} topicName - Topic name.
|
|
464
|
+
* @param {string} uid - User ID.
|
|
465
|
+
* @param {Object} sub - Subscription data.
|
|
466
|
+
* @returns {Promise} Promise resolved on completion.
|
|
467
|
+
*/
|
|
468
|
+
async updSubscription(topicName, uid, sub) {
|
|
469
|
+
const self = this;
|
|
470
|
+
if (!self.isReady()) {
|
|
471
|
+
return Promise.resolve();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// Get existing subscription
|
|
476
|
+
const existing = await self._db.getFirstAsync(
|
|
477
|
+
'SELECT * FROM subscriptions WHERE topic = ? AND uid = ?',
|
|
478
|
+
[topicName, uid]
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const data = self._serializeSubscription(existing, topicName, uid, sub);
|
|
482
|
+
|
|
483
|
+
await self._db.runAsync(
|
|
484
|
+
'INSERT OR REPLACE INTO subscriptions (topic, uid, updated, mode, read, recv, clear, lastSeen, userAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
485
|
+
[data.topic, data.uid, data.updated, data.mode, data.read, data.recv, data.clear, data.lastSeen, data.userAgent]
|
|
486
|
+
);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
self._logger('SQLiteStorage', 'updSubscription error:', err);
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Execute callback for each subscription in a topic.
|
|
495
|
+
* @param {string} topicName - Topic name.
|
|
496
|
+
* @param {function} callback - Callback for each subscription.
|
|
497
|
+
* @param {Object} context - Callback context.
|
|
498
|
+
* @returns {Promise<Array>} Promise resolved with subscriptions.
|
|
499
|
+
*/
|
|
500
|
+
async mapSubscriptions(topicName, callback, context) {
|
|
501
|
+
const self = this;
|
|
502
|
+
if (!self.isReady()) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const rows = await self._db.getAllAsync(
|
|
508
|
+
'SELECT * FROM subscriptions WHERE topic = ?',
|
|
509
|
+
[topicName]
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const subs = rows.map(function(row) {
|
|
513
|
+
return self._deserializeSubscriptionRow(row);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
if (callback) {
|
|
517
|
+
subs.forEach(function(sub) {
|
|
518
|
+
callback.call(context, sub);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return subs;
|
|
523
|
+
} catch (err) {
|
|
524
|
+
self._logger('SQLiteStorage', 'mapSubscriptions error:', err);
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ==================== Messages ====================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Save message to database.
|
|
533
|
+
* @param {Object} msg - Message to save.
|
|
534
|
+
* @returns {Promise} Promise resolved on completion.
|
|
535
|
+
*/
|
|
536
|
+
async addMessage(msg) {
|
|
537
|
+
const self = this;
|
|
538
|
+
if (!self.isReady()) {
|
|
539
|
+
return Promise.resolve();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const data = self._serializeMessage(null, msg);
|
|
544
|
+
|
|
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
|
+
// Build params array explicitly, converting undefined to null for SQLite
|
|
578
|
+
const params = [
|
|
579
|
+
data.topic,
|
|
580
|
+
data.seq,
|
|
581
|
+
data.ts !== undefined ? data.ts : null,
|
|
582
|
+
data._status !== undefined ? data._status : null,
|
|
583
|
+
data.from !== undefined ? data.from : null,
|
|
584
|
+
data.head !== undefined ? data.head : null,
|
|
585
|
+
data.content !== undefined ? data.content : null
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
console.log('[SQLiteStorage] addMessage params array:', params.map((p, i) => `[${i}]=${typeof p}:${p === null ? 'null' : p === undefined ? 'undefined' : 'value'}`).join(', '));
|
|
589
|
+
|
|
590
|
+
await self._db.runAsync(
|
|
591
|
+
`INSERT OR REPLACE INTO messages (topic, seq, ts, _status, from_uid, head, content) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
592
|
+
params
|
|
593
|
+
);
|
|
594
|
+
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
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Update message delivery status.
|
|
605
|
+
* @param {string} topicName - Topic name.
|
|
606
|
+
* @param {number} seq - Message sequence number.
|
|
607
|
+
* @param {number} status - New status.
|
|
608
|
+
* @returns {Promise} Promise resolved on completion.
|
|
609
|
+
*/
|
|
610
|
+
async updMessageStatus(topicName, seq, status) {
|
|
611
|
+
const self = this;
|
|
612
|
+
if (!self.isReady()) {
|
|
613
|
+
return Promise.resolve();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
await self._db.runAsync(
|
|
618
|
+
'UPDATE messages SET _status = ? WHERE topic = ? AND seq = ?',
|
|
619
|
+
[status, topicName, seq]
|
|
620
|
+
);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
self._logger('SQLiteStorage', 'updMessageStatus error:', err);
|
|
623
|
+
throw err;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Remove messages from database.
|
|
629
|
+
* @param {string} topicName - Topic name.
|
|
630
|
+
* @param {number} from - Start of range (inclusive).
|
|
631
|
+
* @param {number} to - End of range (exclusive).
|
|
632
|
+
* @returns {Promise} Promise resolved on completion.
|
|
633
|
+
*/
|
|
634
|
+
async remMessages(topicName, from, to) {
|
|
635
|
+
const self = this;
|
|
636
|
+
if (!self.isReady()) {
|
|
637
|
+
return Promise.resolve();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
if (!from && !to) {
|
|
642
|
+
// Delete all messages for topic
|
|
643
|
+
await self._db.runAsync(
|
|
644
|
+
'DELETE FROM messages WHERE topic = ?',
|
|
645
|
+
[topicName]
|
|
646
|
+
);
|
|
647
|
+
} else if (to > 0) {
|
|
648
|
+
// Delete range [from, to)
|
|
649
|
+
await self._db.runAsync(
|
|
650
|
+
'DELETE FROM messages WHERE topic = ? AND seq >= ? AND seq < ?',
|
|
651
|
+
[topicName, from, to]
|
|
652
|
+
);
|
|
653
|
+
} else {
|
|
654
|
+
// Delete single message
|
|
655
|
+
await self._db.runAsync(
|
|
656
|
+
'DELETE FROM messages WHERE topic = ? AND seq = ?',
|
|
657
|
+
[topicName, from]
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
self._logger('SQLiteStorage', 'remMessages error:', err);
|
|
662
|
+
throw err;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Read messages from database.
|
|
668
|
+
* @param {string} topicName - Topic name.
|
|
669
|
+
* @param {Object} query - Query parameters.
|
|
670
|
+
* @param {function} callback - Callback for each message.
|
|
671
|
+
* @param {Object} context - Callback context.
|
|
672
|
+
* @returns {Promise<Array>} Promise resolved with messages.
|
|
673
|
+
*/
|
|
674
|
+
async readMessages(topicName, query, callback, context) {
|
|
675
|
+
const self = this;
|
|
676
|
+
query = query || {};
|
|
677
|
+
|
|
678
|
+
console.log('[SQLiteStorage] readMessages CALLED:', {
|
|
679
|
+
topicName,
|
|
680
|
+
query: JSON.stringify(query),
|
|
681
|
+
hasCallback: !!callback
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
if (!self.isReady()) {
|
|
685
|
+
console.log('[SQLiteStorage] readMessages: DB NOT READY');
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
var result = [];
|
|
691
|
+
|
|
692
|
+
// Handle individual message ranges
|
|
693
|
+
if (Array.isArray(query.ranges)) {
|
|
694
|
+
console.log('[SQLiteStorage] readMessages: Using RANGES query, ranges:', query.ranges.length);
|
|
695
|
+
for (var i = 0; i < query.ranges.length; i++) {
|
|
696
|
+
var range = query.ranges[i];
|
|
697
|
+
var msgs;
|
|
698
|
+
if (range.hi) {
|
|
699
|
+
console.log('[SQLiteStorage] readMessages: Range', i, '- low:', range.low, 'hi:', range.hi);
|
|
700
|
+
msgs = await self._db.getAllAsync(
|
|
701
|
+
'SELECT * FROM messages WHERE topic = ? AND seq >= ? AND seq < ? ORDER BY seq DESC',
|
|
702
|
+
[topicName, range.low, range.hi]
|
|
703
|
+
);
|
|
704
|
+
} else {
|
|
705
|
+
console.log('[SQLiteStorage] readMessages: Range', i, '- single seq:', range.low);
|
|
706
|
+
msgs = await self._db.getAllAsync(
|
|
707
|
+
'SELECT * FROM messages WHERE topic = ? AND seq = ?',
|
|
708
|
+
[topicName, range.low]
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
console.log('[SQLiteStorage] readMessages: Range', i, 'returned', msgs.length, 'rows');
|
|
712
|
+
|
|
713
|
+
var deserialized = msgs.map(function(row) {
|
|
714
|
+
return self._deserializeMessageRow(row);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
if (callback) {
|
|
718
|
+
callback.call(context, deserialized);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
result = result.concat(deserialized);
|
|
722
|
+
}
|
|
723
|
+
console.log('[SQLiteStorage] readMessages: RANGES query total result:', result.length);
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Handle single range query
|
|
728
|
+
var since = query.since > 0 ? query.since : 0;
|
|
729
|
+
var before = query.before > 0 ? query.before : Number.MAX_SAFE_INTEGER;
|
|
730
|
+
var limit = query.limit | 0;
|
|
731
|
+
|
|
732
|
+
var sql = 'SELECT * FROM messages WHERE topic = ? AND seq >= ? AND seq < ? ORDER BY seq DESC';
|
|
733
|
+
var params = [topicName, since, before];
|
|
734
|
+
|
|
735
|
+
if (limit > 0) {
|
|
736
|
+
sql += ' LIMIT ?';
|
|
737
|
+
params.push(limit);
|
|
738
|
+
}
|
|
739
|
+
|
|
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
|
+
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
|
+
|
|
760
|
+
result = rows.map(function(row) {
|
|
761
|
+
return self._deserializeMessageRow(row);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
console.log('[SQLiteStorage] readMessages: Returning', result.length, 'messages');
|
|
765
|
+
|
|
766
|
+
if (callback) {
|
|
767
|
+
result.forEach(function(msg) {
|
|
768
|
+
callback.call(context, msg);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return result;
|
|
773
|
+
} catch (err) {
|
|
774
|
+
console.error('[SQLiteStorage] readMessages ERROR:', err);
|
|
775
|
+
self._logger('SQLiteStorage', 'readMessages error:', err);
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ==================== Delete Log ====================
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Add records of deleted messages.
|
|
784
|
+
* @param {string} topicName - Topic name.
|
|
785
|
+
* @param {number} delId - Deletion transaction ID.
|
|
786
|
+
* @param {Array} ranges - Deleted message ranges.
|
|
787
|
+
* @returns {Promise} Promise resolved on completion.
|
|
788
|
+
*/
|
|
789
|
+
async addDelLog(topicName, delId, ranges) {
|
|
790
|
+
const self = this;
|
|
791
|
+
if (!self.isReady()) {
|
|
792
|
+
return Promise.resolve();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
// Use withTransactionAsync for proper transaction handling
|
|
797
|
+
await self._db.withTransactionAsync(async function() {
|
|
798
|
+
for (var i = 0; i < ranges.length; i++) {
|
|
799
|
+
var r = ranges[i];
|
|
800
|
+
await self._db.runAsync(
|
|
801
|
+
'INSERT OR REPLACE INTO dellog (topic, clear, low, hi) VALUES (?, ?, ?, ?)',
|
|
802
|
+
[topicName, delId, r.low, r.hi || (r.low + 1)]
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
} catch (err) {
|
|
807
|
+
self._logger('SQLiteStorage', 'addDelLog error:', err);
|
|
808
|
+
throw err;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Read deleted message records.
|
|
814
|
+
* @param {string} topicName - Topic name.
|
|
815
|
+
* @param {Object} query - Query parameters.
|
|
816
|
+
* @returns {Promise<Array>} Promise resolved with deletion records.
|
|
817
|
+
*/
|
|
818
|
+
async readDelLog(topicName, query) {
|
|
819
|
+
const self = this;
|
|
820
|
+
query = query || {};
|
|
821
|
+
|
|
822
|
+
if (!self.isReady()) {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
var result = [];
|
|
828
|
+
|
|
829
|
+
// Handle individual message ranges
|
|
830
|
+
if (Array.isArray(query.ranges)) {
|
|
831
|
+
for (var i = 0; i < query.ranges.length; i++) {
|
|
832
|
+
var range = query.ranges[i];
|
|
833
|
+
var hi = range.hi || (range.low + 1);
|
|
834
|
+
var entries = await self._db.getAllAsync(
|
|
835
|
+
'SELECT * FROM dellog WHERE topic = ? AND low >= ? AND low < ? ORDER BY clear DESC',
|
|
836
|
+
[topicName, range.low, hi]
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
for (var j = 0; j < entries.length; j++) {
|
|
840
|
+
result.push({
|
|
841
|
+
low: entries[j].low,
|
|
842
|
+
hi: entries[j].hi
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Handle single range query
|
|
850
|
+
var since = query.since > 0 ? query.since : 0;
|
|
851
|
+
var before = query.before > 0 ? query.before : Number.MAX_SAFE_INTEGER;
|
|
852
|
+
var limit = query.limit | 0;
|
|
853
|
+
|
|
854
|
+
var sql = 'SELECT * FROM dellog WHERE topic = ? AND clear >= ? AND clear < ? ORDER BY clear DESC';
|
|
855
|
+
var params = [topicName, since, before];
|
|
856
|
+
|
|
857
|
+
if (limit > 0) {
|
|
858
|
+
sql += ' LIMIT ?';
|
|
859
|
+
params.push(limit);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
var rows = await self._db.getAllAsync(sql, params);
|
|
863
|
+
for (var k = 0; k < rows.length; k++) {
|
|
864
|
+
result.push({
|
|
865
|
+
low: rows[k].low,
|
|
866
|
+
hi: rows[k].hi
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return result;
|
|
871
|
+
} catch (err) {
|
|
872
|
+
self._logger('SQLiteStorage', 'readDelLog error:', err);
|
|
873
|
+
throw err;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Get maximum deletion ID for a topic.
|
|
879
|
+
* @param {string} topicName - Topic name.
|
|
880
|
+
* @returns {Promise<Object|undefined>} Promise resolved with max deletion entry.
|
|
881
|
+
*/
|
|
882
|
+
async maxDelId(topicName) {
|
|
883
|
+
const self = this;
|
|
884
|
+
if (!self.isReady()) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const row = await self._db.getFirstAsync(
|
|
890
|
+
'SELECT * FROM dellog WHERE topic = ? ORDER BY clear DESC LIMIT 1',
|
|
891
|
+
[topicName]
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
if (!row) {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
topic: row.topic,
|
|
900
|
+
clear: row.clear,
|
|
901
|
+
low: row.low,
|
|
902
|
+
hi: row.hi
|
|
903
|
+
};
|
|
904
|
+
} catch (err) {
|
|
905
|
+
self._logger('SQLiteStorage', 'maxDelId error:', err);
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ==================== Private Helper Methods ====================
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Safely parse JSON, returning null on error.
|
|
914
|
+
*/
|
|
915
|
+
_parseJSON(str) {
|
|
916
|
+
if (!str) return null;
|
|
917
|
+
try {
|
|
918
|
+
return JSON.parse(str);
|
|
919
|
+
} catch (e) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Serialize topic for storage.
|
|
926
|
+
*/
|
|
927
|
+
_serializeTopic(dst, src) {
|
|
928
|
+
const self = this;
|
|
929
|
+
const res = dst ? Object.assign({}, dst) : {
|
|
930
|
+
name: src.name
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
TOPIC_FIELDS.forEach(function(f) {
|
|
934
|
+
if (src.hasOwnProperty(f)) {
|
|
935
|
+
// JSON stringify complex objects
|
|
936
|
+
if (typeof src[f] === 'object' && src[f] !== null) {
|
|
937
|
+
res[f] = JSON.stringify(src[f]);
|
|
938
|
+
} else {
|
|
939
|
+
res[f] = src[f];
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// Handle _deleted as integer
|
|
945
|
+
if (typeof res._deleted === 'boolean') {
|
|
946
|
+
res._deleted = res._deleted ? 1 : 0;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (Array.isArray(src._tags)) {
|
|
950
|
+
res.tags = JSON.stringify(src._tags);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (src.acs) {
|
|
954
|
+
res.acs = JSON.stringify(
|
|
955
|
+
typeof src.getAccessMode === 'function' ?
|
|
956
|
+
src.getAccessMode().jsonHelper() :
|
|
957
|
+
src.acs
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return res;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Deserialize topic row from database.
|
|
966
|
+
*/
|
|
967
|
+
/**
|
|
968
|
+
* Convert ISO string to Date if valid, otherwise return null.
|
|
969
|
+
*/
|
|
970
|
+
_parseDate(str) {
|
|
971
|
+
if (!str) return null;
|
|
972
|
+
const date = new Date(str);
|
|
973
|
+
return isNaN(date) ? null : date;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
_deserializeTopicRow(row) {
|
|
977
|
+
const self = this;
|
|
978
|
+
const topic = {
|
|
979
|
+
name: row.name,
|
|
980
|
+
created: self._parseDate(row.created),
|
|
981
|
+
updated: self._parseDate(row.updated),
|
|
982
|
+
deleted: self._parseDate(row.deleted),
|
|
983
|
+
touched: self._parseDate(row.touched),
|
|
984
|
+
read: row.read || 0,
|
|
985
|
+
recv: row.recv || 0,
|
|
986
|
+
seq: row.seq || 0,
|
|
987
|
+
clear: row.clear || 0,
|
|
988
|
+
_deleted: row._deleted === 1
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// Parse JSON fields
|
|
992
|
+
if (row.defacs) topic.defacs = self._parseJSON(row.defacs);
|
|
993
|
+
if (row.creds) topic.creds = self._parseJSON(row.creds);
|
|
994
|
+
if (row.public) topic.public = self._parseJSON(row.public);
|
|
995
|
+
if (row.trusted) topic.trusted = self._parseJSON(row.trusted);
|
|
996
|
+
if (row.private) topic.private = self._parseJSON(row.private);
|
|
997
|
+
if (row._aux) topic._aux = self._parseJSON(row._aux);
|
|
998
|
+
if (row.tags) topic.tags = self._parseJSON(row.tags);
|
|
999
|
+
if (row.acs) topic.acs = self._parseJSON(row.acs);
|
|
1000
|
+
|
|
1001
|
+
return topic;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Serialize subscription for storage.
|
|
1006
|
+
*/
|
|
1007
|
+
_serializeSubscription(dst, topicName, uid, sub) {
|
|
1008
|
+
const res = dst ? Object.assign({}, dst) : {
|
|
1009
|
+
topic: topicName,
|
|
1010
|
+
uid: uid
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
SUBSCRIPTION_FIELDS.forEach(function(f) {
|
|
1014
|
+
if (sub.hasOwnProperty(f)) {
|
|
1015
|
+
res[f] = sub[f];
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
return res;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Deserialize subscription row from database.
|
|
1024
|
+
*/
|
|
1025
|
+
_deserializeSubscriptionRow(row) {
|
|
1026
|
+
const self = this;
|
|
1027
|
+
return {
|
|
1028
|
+
topic: row.topic,
|
|
1029
|
+
uid: row.uid,
|
|
1030
|
+
updated: self._parseDate(row.updated),
|
|
1031
|
+
mode: row.mode,
|
|
1032
|
+
read: row.read || 0,
|
|
1033
|
+
recv: row.recv || 0,
|
|
1034
|
+
clear: row.clear || 0,
|
|
1035
|
+
lastSeen: self._parseDate(row.lastSeen),
|
|
1036
|
+
userAgent: row.userAgent
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Serialize message for storage.
|
|
1042
|
+
*/
|
|
1043
|
+
_serializeMessage(dst, msg) {
|
|
1044
|
+
const res = dst ? Object.assign({}, dst) : {};
|
|
1045
|
+
|
|
1046
|
+
MESSAGE_FIELDS.forEach(function(f) {
|
|
1047
|
+
if (msg.hasOwnProperty(f)) {
|
|
1048
|
+
if (f === 'head' || f === 'content') {
|
|
1049
|
+
// JSON stringify head and content
|
|
1050
|
+
res[f] = typeof msg[f] === 'object' && msg[f] !== null ? JSON.stringify(msg[f]) : msg[f];
|
|
1051
|
+
} else if (f === 'ts') {
|
|
1052
|
+
// Convert Date objects to ISO string for SQLite
|
|
1053
|
+
res[f] = msg[f] instanceof Date ? msg[f].toISOString() : msg[f];
|
|
1054
|
+
} else {
|
|
1055
|
+
res[f] = msg[f];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
return res;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Deserialize message row from database.
|
|
1065
|
+
*/
|
|
1066
|
+
_deserializeMessageRow(row) {
|
|
1067
|
+
const self = this;
|
|
1068
|
+
const msg = {
|
|
1069
|
+
topic: row.topic,
|
|
1070
|
+
seq: row.seq,
|
|
1071
|
+
ts: row.ts ? new Date(row.ts) : null, // Convert ISO string back to Date
|
|
1072
|
+
_status: row._status || 0,
|
|
1073
|
+
from: row.from_uid
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
if (row.head) msg.head = self._parseJSON(row.head);
|
|
1077
|
+
if (row.content) msg.content = self._parseJSON(row.content);
|
|
1078
|
+
|
|
1079
|
+
return msg;
|
|
1080
|
+
}
|
|
1081
|
+
}
|