@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
package/src/db.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Helper methods for dealing with IndexedDB cache of messages, users, and topics.
|
|
3
|
+
*
|
|
4
|
+
* @copyright 2015-2025 Tinode LLC.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// NOTE TO DEVELOPERS:
|
|
9
|
+
// Localizable strings should be double quoted "строка на другом языке",
|
|
10
|
+
// non-localizable strings should be single quoted 'non-localized'.
|
|
11
|
+
|
|
12
|
+
const DB_VERSION = 3;
|
|
13
|
+
const DB_NAME = 'tinode-web';
|
|
14
|
+
|
|
15
|
+
let IDBProvider;
|
|
16
|
+
|
|
17
|
+
// Custom storage provider (e.g., SQLiteStorage for React Native)
|
|
18
|
+
let _storageProvider = null;
|
|
19
|
+
|
|
20
|
+
export default class DB {
|
|
21
|
+
#onError = _ => {};
|
|
22
|
+
#logger = _ => {};
|
|
23
|
+
|
|
24
|
+
// Instance of IndexDB.
|
|
25
|
+
db = null;
|
|
26
|
+
// Indicator that the cache is disabled.
|
|
27
|
+
disabled = true;
|
|
28
|
+
// Reference to custom storage provider (if using delegation)
|
|
29
|
+
#delegateStorage = null;
|
|
30
|
+
|
|
31
|
+
constructor(onError, logger) {
|
|
32
|
+
this.#onError = onError || this.#onError;
|
|
33
|
+
this.#logger = logger || this.#logger;
|
|
34
|
+
|
|
35
|
+
// If a custom storage provider is set, use it instead of IndexedDB
|
|
36
|
+
if (_storageProvider) {
|
|
37
|
+
this.#delegateStorage = _storageProvider;
|
|
38
|
+
this.disabled = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Helper to check if we should delegate to custom storage
|
|
43
|
+
#shouldDelegate() {
|
|
44
|
+
return this.#delegateStorage !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#mapObjects(source, callback, context) {
|
|
48
|
+
if (!this.db) {
|
|
49
|
+
return disabled ?
|
|
50
|
+
Promise.resolve([]) :
|
|
51
|
+
Promise.reject(new Error("not initialized"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const trx = this.db.transaction([source]);
|
|
56
|
+
trx.onerror = event => {
|
|
57
|
+
this.#logger('PCache', 'mapObjects', source, event.target.error);
|
|
58
|
+
reject(event.target.error);
|
|
59
|
+
};
|
|
60
|
+
trx.objectStore(source).getAll().onsuccess = event => {
|
|
61
|
+
if (callback) {
|
|
62
|
+
event.target.result.forEach(topic => {
|
|
63
|
+
callback.call(context, topic);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
resolve(event.target.result);
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize persistent cache: open or create/upgrade if needed.
|
|
73
|
+
* @returns {Promise} promise to be resolved/rejected when the DB is initialized.
|
|
74
|
+
*/
|
|
75
|
+
initDatabase() {
|
|
76
|
+
console.log('[DB] initDatabase CALLED, shouldDelegate:', this.#shouldDelegate(), 'delegateStorage:', !!this.#delegateStorage);
|
|
77
|
+
// Delegate to custom storage if set
|
|
78
|
+
if (this.#shouldDelegate()) {
|
|
79
|
+
console.log('[DB] initDatabase DELEGATING to SQLiteStorage');
|
|
80
|
+
return this.#delegateStorage.initDatabase().then(result => {
|
|
81
|
+
console.log('[DB] initDatabase: SQLiteStorage initialized successfully');
|
|
82
|
+
return result;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
console.log('[DB] initDatabase using IndexedDB');
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
// Open the database and initialize callbacks.
|
|
89
|
+
const req = IDBProvider.open(DB_NAME, DB_VERSION);
|
|
90
|
+
req.onsuccess = event => {
|
|
91
|
+
this.db = event.target.result;
|
|
92
|
+
this.disabled = false;
|
|
93
|
+
|
|
94
|
+
// This handler is called when a different tab tries to upgrade the database.
|
|
95
|
+
this.db.onversionchange = _ => {
|
|
96
|
+
this.#logger('PCache', "another tab tries to upgrade DB, shutting down");
|
|
97
|
+
this.db.close();
|
|
98
|
+
this.db = null;
|
|
99
|
+
this.disabled = true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
resolve(this.db);
|
|
103
|
+
};
|
|
104
|
+
req.onerror = event => {
|
|
105
|
+
this.#logger('PCache', "failed to initialize", event);
|
|
106
|
+
reject(event.target.error);
|
|
107
|
+
this.#onError(event.target.error);
|
|
108
|
+
};
|
|
109
|
+
req.onupgradeneeded = event => {
|
|
110
|
+
this.db = event.target.result;
|
|
111
|
+
|
|
112
|
+
this.db.onerror = event => {
|
|
113
|
+
this.#logger('PCache', "failed to create storage", event);
|
|
114
|
+
this.#onError(event.target.error);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Individual object stores.
|
|
118
|
+
|
|
119
|
+
// Alternatively could use event.oldVersion and event.newVersion
|
|
120
|
+
// to determine which object stores to create or upgrade.
|
|
121
|
+
|
|
122
|
+
if (!this.db.objectStoreNames.contains('topic')) {
|
|
123
|
+
// Object store (table) for topics. The primary key is the topic name.
|
|
124
|
+
this.db.createObjectStore('topic', {
|
|
125
|
+
keyPath: 'name'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!this.db.objectStoreNames.contains('user')) {
|
|
130
|
+
// Users object store. UID is the primary key.
|
|
131
|
+
this.db.createObjectStore('user', {
|
|
132
|
+
keyPath: 'uid'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!this.db.objectStoreNames.contains('subscription')) {
|
|
137
|
+
// Subscriptions object store topic <-> user. Topic name + UID is the primary key.
|
|
138
|
+
this.db.createObjectStore('subscription', {
|
|
139
|
+
keyPath: ['topic', 'uid']
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!this.db.objectStoreNames.contains('message')) {
|
|
144
|
+
// Messages object store. The primary key is topic name + seq.
|
|
145
|
+
this.db.createObjectStore('message', {
|
|
146
|
+
keyPath: ['topic', 'seq']
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.db.objectStoreNames.contains('dellog')) {
|
|
151
|
+
// Records of deleted message ranges. The primary key is topic name + low seq.
|
|
152
|
+
const dellog = this.db.createObjectStore('dellog', {
|
|
153
|
+
keyPath: ['topic', 'low', 'hi']
|
|
154
|
+
});
|
|
155
|
+
if (!dellog.indexNames.contains('topic_clear')) {
|
|
156
|
+
dellog.createIndex('topic_clear', ['topic', 'clear'], {
|
|
157
|
+
unique: false
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Delete persistent cache.
|
|
167
|
+
*/
|
|
168
|
+
deleteDatabase() {
|
|
169
|
+
// Delegate to custom storage if set
|
|
170
|
+
if (this.#shouldDelegate()) {
|
|
171
|
+
return this.#delegateStorage.deleteDatabase();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Close connection, otherwise operations will fail with 'onblocked'.
|
|
175
|
+
if (this.db) {
|
|
176
|
+
this.db.close();
|
|
177
|
+
this.db = null;
|
|
178
|
+
}
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const req = IDBProvider.deleteDatabase(DB_NAME);
|
|
181
|
+
req.onblocked = _ => {
|
|
182
|
+
if (this.db) {
|
|
183
|
+
this.db.close();
|
|
184
|
+
}
|
|
185
|
+
const err = new Error("blocked");
|
|
186
|
+
this.#logger('PCache', 'deleteDatabase', err);
|
|
187
|
+
reject(err);
|
|
188
|
+
};
|
|
189
|
+
req.onsuccess = _ => {
|
|
190
|
+
this.db = null;
|
|
191
|
+
this.disabled = true;
|
|
192
|
+
resolve(true);
|
|
193
|
+
};
|
|
194
|
+
req.onerror = event => {
|
|
195
|
+
this.#logger('PCache', 'deleteDatabase', event.target.error);
|
|
196
|
+
reject(event.target.error);
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if persistent cache is ready for use.
|
|
203
|
+
* @memberOf DB
|
|
204
|
+
* @returns {boolean} <code>true</code> if cache is ready, <code>false</code> otherwise.
|
|
205
|
+
*/
|
|
206
|
+
isReady() {
|
|
207
|
+
// Delegate to custom storage if set
|
|
208
|
+
if (this.#shouldDelegate()) {
|
|
209
|
+
return this.#delegateStorage.isReady();
|
|
210
|
+
}
|
|
211
|
+
return !!this.db;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Topics.
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Save to cache or update topic in persistent cache.
|
|
218
|
+
* @memberOf DB
|
|
219
|
+
* @param {Topic} topic - topic to be added or updated.
|
|
220
|
+
* @returns {Promise} promise resolved/rejected on operation completion.
|
|
221
|
+
*/
|
|
222
|
+
updTopic(topic) {
|
|
223
|
+
console.log('[DB] updTopic CALLED:', topic?.name, 'shouldDelegate:', this.#shouldDelegate());
|
|
224
|
+
// Delegate to custom storage if set
|
|
225
|
+
if (this.#shouldDelegate()) {
|
|
226
|
+
return this.#delegateStorage.updTopic(topic);
|
|
227
|
+
}
|
|
228
|
+
if (!this.isReady()) {
|
|
229
|
+
return this.disabled ?
|
|
230
|
+
Promise.resolve() :
|
|
231
|
+
Promise.reject(new Error("not initialized"));
|
|
232
|
+
}
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const trx = this.db.transaction(['topic'], 'readwrite');
|
|
235
|
+
trx.oncomplete = event => {
|
|
236
|
+
resolve(event.target.result);
|
|
237
|
+
};
|
|
238
|
+
trx.onerror = event => {
|
|
239
|
+
this.#logger('PCache', 'updTopic', event.target.error);
|
|
240
|
+
reject(event.target.error);
|
|
241
|
+
};
|
|
242
|
+
const req = trx.objectStore('topic').get(topic.name);
|
|
243
|
+
req.onsuccess = _ => {
|
|
244
|
+
trx.objectStore('topic').put(DB.#serializeTopic(req.result, topic));
|
|
245
|
+
trx.commit();
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Mark or unmark topic as deleted.
|
|
252
|
+
* @memberOf DB
|
|
253
|
+
* @param {string} name - name of the topic to mark or unmark.
|
|
254
|
+
* @param {boolean} deleted - status
|
|
255
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
256
|
+
*/
|
|
257
|
+
markTopicAsDeleted(name, deleted) {
|
|
258
|
+
// Delegate to custom storage if set
|
|
259
|
+
if (this.#shouldDelegate()) {
|
|
260
|
+
return this.#delegateStorage.markTopicAsDeleted(name, deleted);
|
|
261
|
+
}
|
|
262
|
+
if (!this.isReady()) {
|
|
263
|
+
return this.disabled ?
|
|
264
|
+
Promise.resolve() :
|
|
265
|
+
Promise.reject(new Error("not initialized"));
|
|
266
|
+
}
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const trx = this.db.transaction(['topic'], 'readwrite');
|
|
269
|
+
trx.oncomplete = event => {
|
|
270
|
+
resolve(event.target.result);
|
|
271
|
+
};
|
|
272
|
+
trx.onerror = event => {
|
|
273
|
+
this.#logger('PCache', 'markTopicAsDeleted', event.target.error);
|
|
274
|
+
reject(event.target.error);
|
|
275
|
+
};
|
|
276
|
+
const req = trx.objectStore('topic').get(name);
|
|
277
|
+
req.onsuccess = event => {
|
|
278
|
+
const topic = event.target.result;
|
|
279
|
+
if (topic && topic._deleted != deleted) {
|
|
280
|
+
topic._deleted = deleted;
|
|
281
|
+
trx.objectStore('topic').put(topic);
|
|
282
|
+
}
|
|
283
|
+
trx.commit();
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Remove topic from persistent cache.
|
|
290
|
+
* @memberOf DB
|
|
291
|
+
* @param {string} name - name of the topic to remove from database.
|
|
292
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
293
|
+
*/
|
|
294
|
+
remTopic(name) {
|
|
295
|
+
// Delegate to custom storage if set
|
|
296
|
+
if (this.#shouldDelegate()) {
|
|
297
|
+
return this.#delegateStorage.remTopic(name);
|
|
298
|
+
}
|
|
299
|
+
if (!this.isReady()) {
|
|
300
|
+
return this.disabled ?
|
|
301
|
+
Promise.resolve() :
|
|
302
|
+
Promise.reject(new Error("not initialized"));
|
|
303
|
+
}
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
const trx = this.db.transaction(['topic', 'subscription', 'message'], 'readwrite');
|
|
306
|
+
trx.oncomplete = event => {
|
|
307
|
+
resolve(event.target.result);
|
|
308
|
+
};
|
|
309
|
+
trx.onerror = event => {
|
|
310
|
+
this.#logger('PCache', 'remTopic', event.target.error);
|
|
311
|
+
reject(event.target.error);
|
|
312
|
+
};
|
|
313
|
+
trx.objectStore('topic').delete(IDBKeyRange.only(name));
|
|
314
|
+
trx.objectStore('subscription').delete(IDBKeyRange.bound([name, '-'], [name, '~']));
|
|
315
|
+
trx.objectStore('message').delete(IDBKeyRange.bound([name, 0], [name, Number.MAX_SAFE_INTEGER]));
|
|
316
|
+
trx.commit();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Execute a callback for each stored topic.
|
|
322
|
+
* @memberOf DB
|
|
323
|
+
* @param {function} callback - function to call for each topic.
|
|
324
|
+
* @param {Object} context - the value or <code>this</code> inside the callback.
|
|
325
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
326
|
+
*/
|
|
327
|
+
mapTopics(callback, context) {
|
|
328
|
+
console.log('[DB] mapTopics CALLED, shouldDelegate:', this.#shouldDelegate());
|
|
329
|
+
// Delegate to custom storage if set
|
|
330
|
+
if (this.#shouldDelegate()) {
|
|
331
|
+
console.log('[DB] mapTopics DELEGATING to SQLiteStorage');
|
|
332
|
+
return this.#delegateStorage.mapTopics(callback, context).then(result => {
|
|
333
|
+
console.log('[DB] mapTopics: SQLiteStorage returned', result ? result.length : 0, 'topics');
|
|
334
|
+
return result;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
console.log('[DB] mapTopics using IndexedDB');
|
|
338
|
+
return this.#mapObjects('topic', callback, context);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Copy data from serialized object to topic.
|
|
343
|
+
* @memberOf DB
|
|
344
|
+
* @param {Topic} topic - target to deserialize to.
|
|
345
|
+
* @param {Object} src - serialized data to copy from.
|
|
346
|
+
*/
|
|
347
|
+
deserializeTopic(topic, src) {
|
|
348
|
+
// Delegate to custom storage if set
|
|
349
|
+
if (this.#shouldDelegate()) {
|
|
350
|
+
return this.#delegateStorage.deserializeTopic(topic, src);
|
|
351
|
+
}
|
|
352
|
+
DB.#deserializeTopic(topic, src);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Users.
|
|
356
|
+
/**
|
|
357
|
+
* Add or update user object in the persistent cache.
|
|
358
|
+
* @memberOf DB
|
|
359
|
+
* @param {string} uid - ID of the user to save or update.
|
|
360
|
+
* @param {Object} pub - user's <code>public</code> information.
|
|
361
|
+
* @returns {Promise} promise resolved/rejected on operation completion.
|
|
362
|
+
*/
|
|
363
|
+
updUser(uid, pub) {
|
|
364
|
+
// Delegate to custom storage if set
|
|
365
|
+
if (this.#shouldDelegate()) {
|
|
366
|
+
return this.#delegateStorage.updUser(uid, pub);
|
|
367
|
+
}
|
|
368
|
+
if (arguments.length < 2 || pub === undefined) {
|
|
369
|
+
// No point inupdating user with invalid data.
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!this.isReady()) {
|
|
373
|
+
return this.disabled ?
|
|
374
|
+
Promise.resolve() :
|
|
375
|
+
Promise.reject(new Error("not initialized"));
|
|
376
|
+
}
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const trx = this.db.transaction(['user'], 'readwrite');
|
|
379
|
+
trx.oncomplete = event => {
|
|
380
|
+
resolve(event.target.result);
|
|
381
|
+
};
|
|
382
|
+
trx.onerror = event => {
|
|
383
|
+
this.#logger('PCache', 'updUser', event.target.error);
|
|
384
|
+
reject(event.target.error);
|
|
385
|
+
};
|
|
386
|
+
trx.objectStore('user').put({
|
|
387
|
+
uid: uid,
|
|
388
|
+
public: pub
|
|
389
|
+
});
|
|
390
|
+
trx.commit();
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Remove user from persistent cache.
|
|
396
|
+
* @memberOf DB
|
|
397
|
+
* @param {string} uid - ID of the user to remove from the cache.
|
|
398
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
399
|
+
*/
|
|
400
|
+
remUser(uid) {
|
|
401
|
+
// Delegate to custom storage if set
|
|
402
|
+
if (this.#shouldDelegate()) {
|
|
403
|
+
return this.#delegateStorage.remUser(uid);
|
|
404
|
+
}
|
|
405
|
+
if (!this.isReady()) {
|
|
406
|
+
return this.disabled ?
|
|
407
|
+
Promise.resolve() :
|
|
408
|
+
Promise.reject(new Error("not initialized"));
|
|
409
|
+
}
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
const trx = this.db.transaction(['user'], 'readwrite');
|
|
412
|
+
trx.oncomplete = event => {
|
|
413
|
+
resolve(event.target.result);
|
|
414
|
+
};
|
|
415
|
+
trx.onerror = event => {
|
|
416
|
+
this.#logger('PCache', 'remUser', event.target.error);
|
|
417
|
+
reject(event.target.error);
|
|
418
|
+
};
|
|
419
|
+
trx.objectStore('user').delete(IDBKeyRange.only(uid));
|
|
420
|
+
trx.commit();
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Execute a callback for each stored user.
|
|
426
|
+
* @memberOf DB
|
|
427
|
+
* @param {function} callback - function to call for each topic.
|
|
428
|
+
* @param {Object} context - the value or <code>this</code> inside the callback.
|
|
429
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
430
|
+
*/
|
|
431
|
+
mapUsers(callback, context) {
|
|
432
|
+
// Delegate to custom storage if set
|
|
433
|
+
if (this.#shouldDelegate()) {
|
|
434
|
+
return this.#delegateStorage.mapUsers(callback, context);
|
|
435
|
+
}
|
|
436
|
+
return this.#mapObjects('user', callback, context);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Read a single user from persistent cache.
|
|
441
|
+
* @memberOf DB
|
|
442
|
+
* @param {string} uid - ID of the user to fetch from cache.
|
|
443
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
444
|
+
*/
|
|
445
|
+
getUser(uid) {
|
|
446
|
+
// Delegate to custom storage if set
|
|
447
|
+
if (this.#shouldDelegate()) {
|
|
448
|
+
return this.#delegateStorage.getUser(uid);
|
|
449
|
+
}
|
|
450
|
+
if (!this.isReady()) {
|
|
451
|
+
return this.disabled ?
|
|
452
|
+
Promise.resolve() :
|
|
453
|
+
Promise.reject(new Error("not initialized"));
|
|
454
|
+
}
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const trx = this.db.transaction(['user']);
|
|
457
|
+
trx.oncomplete = event => {
|
|
458
|
+
const user = event.target.result;
|
|
459
|
+
resolve({
|
|
460
|
+
user: user.uid,
|
|
461
|
+
public: user.public
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
trx.onerror = event => {
|
|
465
|
+
this.#logger('PCache', 'getUser', event.target.error);
|
|
466
|
+
reject(event.target.error);
|
|
467
|
+
};
|
|
468
|
+
trx.objectStore('user').get(uid);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Subscriptions.
|
|
473
|
+
/**
|
|
474
|
+
* Add or update subscription in persistent cache.
|
|
475
|
+
* @memberOf DB
|
|
476
|
+
* @param {string} topicName - name of the topic which owns the message.
|
|
477
|
+
* @param {string} uid - ID of the subscribed user.
|
|
478
|
+
* @param {Object} sub - subscription to save.
|
|
479
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
480
|
+
*/
|
|
481
|
+
updSubscription(topicName, uid, sub) {
|
|
482
|
+
// Delegate to custom storage if set
|
|
483
|
+
if (this.#shouldDelegate()) {
|
|
484
|
+
return this.#delegateStorage.updSubscription(topicName, uid, sub);
|
|
485
|
+
}
|
|
486
|
+
if (!this.isReady()) {
|
|
487
|
+
return this.disabled ?
|
|
488
|
+
Promise.resolve() :
|
|
489
|
+
Promise.reject(new Error("not initialized"));
|
|
490
|
+
}
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
const trx = this.db.transaction(['subscription'], 'readwrite');
|
|
493
|
+
trx.oncomplete = event => {
|
|
494
|
+
resolve(event.target.result);
|
|
495
|
+
};
|
|
496
|
+
trx.onerror = event => {
|
|
497
|
+
this.#logger('PCache', 'updSubscription', event.target.error);
|
|
498
|
+
reject(event.target.error);
|
|
499
|
+
};
|
|
500
|
+
trx.objectStore('subscription').get([topicName, uid]).onsuccess = (event) => {
|
|
501
|
+
trx.objectStore('subscription').put(DB.#serializeSubscription(event.target.result, topicName, uid, sub));
|
|
502
|
+
trx.commit();
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Execute a callback for each cached subscription in a given topic.
|
|
509
|
+
* @memberOf DB
|
|
510
|
+
* @param {string} topicName - name of the topic which owns the subscriptions.
|
|
511
|
+
* @param {function} callback - function to call for each subscription.
|
|
512
|
+
* @param {Object} context - the value or <code>this</code> inside the callback.
|
|
513
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
514
|
+
*/
|
|
515
|
+
mapSubscriptions(topicName, callback, context) {
|
|
516
|
+
// Delegate to custom storage if set
|
|
517
|
+
if (this.#shouldDelegate()) {
|
|
518
|
+
return this.#delegateStorage.mapSubscriptions(topicName, callback, context);
|
|
519
|
+
}
|
|
520
|
+
if (!this.isReady()) {
|
|
521
|
+
return this.disabled ?
|
|
522
|
+
Promise.resolve([]) :
|
|
523
|
+
Promise.reject(new Error("not initialized"));
|
|
524
|
+
}
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
const trx = this.db.transaction(['subscription']);
|
|
527
|
+
trx.onerror = (event) => {
|
|
528
|
+
this.#logger('PCache', 'mapSubscriptions', event.target.error);
|
|
529
|
+
reject(event.target.error);
|
|
530
|
+
};
|
|
531
|
+
trx.objectStore('subscription').getAll(IDBKeyRange.bound([topicName, '-'], [topicName, '~'])).onsuccess = (event) => {
|
|
532
|
+
if (callback) {
|
|
533
|
+
event.target.result.forEach((topic) => {
|
|
534
|
+
callback.call(context, topic);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
resolve(event.target.result);
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Messages.
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Save message to persistent cache.
|
|
546
|
+
* @memberOf DB
|
|
547
|
+
* @param {Object} msg - message to save.
|
|
548
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
549
|
+
*/
|
|
550
|
+
addMessage(msg) {
|
|
551
|
+
// Delegate to custom storage if set
|
|
552
|
+
if (this.#shouldDelegate()) {
|
|
553
|
+
return this.#delegateStorage.addMessage(msg);
|
|
554
|
+
}
|
|
555
|
+
if (!this.isReady()) {
|
|
556
|
+
return this.disabled ?
|
|
557
|
+
Promise.resolve() :
|
|
558
|
+
Promise.reject(new Error("not initialized"));
|
|
559
|
+
}
|
|
560
|
+
return new Promise((resolve, reject) => {
|
|
561
|
+
const trx = this.db.transaction(['message'], 'readwrite');
|
|
562
|
+
trx.onsuccess = event => {
|
|
563
|
+
resolve(event.target.result);
|
|
564
|
+
};
|
|
565
|
+
trx.onerror = event => {
|
|
566
|
+
this.#logger('PCache', 'addMessage', event.target.error);
|
|
567
|
+
reject(event.target.error);
|
|
568
|
+
};
|
|
569
|
+
trx.objectStore('message').add(DB.#serializeMessage(null, msg));
|
|
570
|
+
trx.commit();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Update delivery status of a message stored in persistent cache.
|
|
576
|
+
* @memberOf DB
|
|
577
|
+
* @param {string} topicName - name of the topic which owns the message.
|
|
578
|
+
* @param {number} seq - ID of the message to update
|
|
579
|
+
* @param {number} status - new delivery status of the message.
|
|
580
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
581
|
+
*/
|
|
582
|
+
updMessageStatus(topicName, seq, status) {
|
|
583
|
+
// Delegate to custom storage if set
|
|
584
|
+
if (this.#shouldDelegate()) {
|
|
585
|
+
return this.#delegateStorage.updMessageStatus(topicName, seq, status);
|
|
586
|
+
}
|
|
587
|
+
if (!this.isReady()) {
|
|
588
|
+
return this.disabled ?
|
|
589
|
+
Promise.resolve() :
|
|
590
|
+
Promise.reject(new Error("not initialized"));
|
|
591
|
+
}
|
|
592
|
+
return new Promise((resolve, reject) => {
|
|
593
|
+
const trx = this.db.transaction(['message'], 'readwrite');
|
|
594
|
+
trx.onsuccess = event => {
|
|
595
|
+
resolve(event.target.result);
|
|
596
|
+
};
|
|
597
|
+
trx.onerror = event => {
|
|
598
|
+
this.#logger('PCache', 'updMessageStatus', event.target.error);
|
|
599
|
+
reject(event.target.error);
|
|
600
|
+
};
|
|
601
|
+
const req = trx.objectStore('message').get(IDBKeyRange.only([topicName, seq]));
|
|
602
|
+
req.onsuccess = event => {
|
|
603
|
+
const src = req.result || event.target.result;
|
|
604
|
+
if (!src || src._status == status) {
|
|
605
|
+
trx.commit();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
trx.objectStore('message').put(DB.#serializeMessage(src, {
|
|
609
|
+
topic: topicName,
|
|
610
|
+
seq: seq,
|
|
611
|
+
_status: status
|
|
612
|
+
}));
|
|
613
|
+
trx.commit();
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Remove one or more messages from persistent cache.
|
|
620
|
+
* @memberOf DB
|
|
621
|
+
* @param {string} topicName - name of the topic which owns the message.
|
|
622
|
+
* @param {number} from - id of the message to remove or lower boundary when removing range (inclusive).
|
|
623
|
+
* @param {number=} to - upper boundary (exclusive) when removing a range of messages.
|
|
624
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
625
|
+
*/
|
|
626
|
+
remMessages(topicName, from, to) {
|
|
627
|
+
// Delegate to custom storage if set
|
|
628
|
+
if (this.#shouldDelegate()) {
|
|
629
|
+
return this.#delegateStorage.remMessages(topicName, from, to);
|
|
630
|
+
}
|
|
631
|
+
if (!this.isReady()) {
|
|
632
|
+
return this.disabled ?
|
|
633
|
+
Promise.resolve() :
|
|
634
|
+
Promise.reject(new Error("not initialized"));
|
|
635
|
+
}
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
if (!from && !to) {
|
|
638
|
+
from = 0;
|
|
639
|
+
to = Number.MAX_SAFE_INTEGER;
|
|
640
|
+
}
|
|
641
|
+
const range = to > 0 ? IDBKeyRange.bound([topicName, from], [topicName, to], false, true) :
|
|
642
|
+
IDBKeyRange.only([topicName, from]);
|
|
643
|
+
const trx = this.db.transaction(['message'], 'readwrite');
|
|
644
|
+
trx.onsuccess = event => {
|
|
645
|
+
resolve(event.target.result);
|
|
646
|
+
};
|
|
647
|
+
trx.onerror = event => {
|
|
648
|
+
this.#logger('PCache', 'remMessages', event.target.error);
|
|
649
|
+
reject(event.target.error);
|
|
650
|
+
};
|
|
651
|
+
trx.objectStore('message').delete(range);
|
|
652
|
+
trx.commit();
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Retrieve messages from persistent store.
|
|
658
|
+
* @memberOf DB
|
|
659
|
+
* @param {string} topicName - name of the topic to retrieve messages from.
|
|
660
|
+
* @param {function} callback to call for each retrieved message.
|
|
661
|
+
* @param {GetDataType} query - parameters of the message range to retrieve.
|
|
662
|
+
*
|
|
663
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
664
|
+
*/
|
|
665
|
+
readMessages(topicName, query, callback, context) {
|
|
666
|
+
console.log('[DB] readMessages CALLED:', topicName, 'shouldDelegate:', this.#shouldDelegate(), 'delegateStorage:', !!this.#delegateStorage);
|
|
667
|
+
// Delegate to custom storage if set
|
|
668
|
+
if (this.#shouldDelegate()) {
|
|
669
|
+
console.log('[DB] readMessages DELEGATING to SQLiteStorage');
|
|
670
|
+
return this.#delegateStorage.readMessages(topicName, query, callback, context);
|
|
671
|
+
}
|
|
672
|
+
console.log('[DB] readMessages NOT delegating, using IndexedDB');
|
|
673
|
+
|
|
674
|
+
query = query || {};
|
|
675
|
+
|
|
676
|
+
if (!this.isReady()) {
|
|
677
|
+
return this.disabled ?
|
|
678
|
+
Promise.resolve([]) :
|
|
679
|
+
Promise.reject(new Error("not initialized"));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const trx = this.db.transaction(['message']);
|
|
683
|
+
let result = [];
|
|
684
|
+
|
|
685
|
+
// Handle individual message ranges.
|
|
686
|
+
if (Array.isArray(query.ranges)) {
|
|
687
|
+
return new Promise((resolve, reject) => {
|
|
688
|
+
trx.onerror = event => {
|
|
689
|
+
this.#logger('PCache', 'readMessages', event.target.error);
|
|
690
|
+
reject(event.target.error);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
let count = 0;
|
|
694
|
+
query.ranges.forEach(range => {
|
|
695
|
+
const key = range.hi ? IDBKeyRange.bound([topicName, range.low], [topicName, range.hi], false, true) :
|
|
696
|
+
IDBKeyRange.only([topicName, range.low]);
|
|
697
|
+
trx.objectStore('message').getAll(key).onsuccess = event => {
|
|
698
|
+
const msgs = event.target.result;
|
|
699
|
+
if (msgs) {
|
|
700
|
+
if (callback) {
|
|
701
|
+
callback.call(context, msgs);
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(msgs)) {
|
|
704
|
+
result = result.concat(msgs);
|
|
705
|
+
} else {
|
|
706
|
+
result.push(msgs);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
count++;
|
|
710
|
+
if (count == query.ranges.length) {
|
|
711
|
+
resolve(result);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Handle single range.
|
|
719
|
+
return new Promise((resolve, reject) => {
|
|
720
|
+
const since = query.since > 0 ? query.since : 0;
|
|
721
|
+
const before = query.before > 0 ? query.before : Number.MAX_SAFE_INTEGER;
|
|
722
|
+
const limit = query.limit | 0;
|
|
723
|
+
|
|
724
|
+
trx.onerror = event => {
|
|
725
|
+
this.#logger('PCache', 'readMessages', event.target.error);
|
|
726
|
+
reject(event.target.error);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const range = IDBKeyRange.bound([topicName, since], [topicName, before], false, true);
|
|
730
|
+
// Iterate in descending order.
|
|
731
|
+
trx.objectStore('message').openCursor(range, 'prev')
|
|
732
|
+
.onsuccess = event => {
|
|
733
|
+
const cursor = event.target.result;
|
|
734
|
+
if (cursor) {
|
|
735
|
+
if (callback) {
|
|
736
|
+
callback.call(context, cursor.value);
|
|
737
|
+
}
|
|
738
|
+
result.push(cursor.value);
|
|
739
|
+
if (limit <= 0 || result.length < limit) {
|
|
740
|
+
cursor.continue();
|
|
741
|
+
} else {
|
|
742
|
+
resolve(result);
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
resolve(result);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Delete log
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Add records of deleted messages.
|
|
755
|
+
* @memberOf DB
|
|
756
|
+
* @param {string} topicName - name of the topic which owns the message.
|
|
757
|
+
* @param {number} delId - id of the deletion transaction.
|
|
758
|
+
* @param {Array.<IdRange>} ranges - message to save.
|
|
759
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
760
|
+
*/
|
|
761
|
+
addDelLog(topicName, delId, ranges) {
|
|
762
|
+
// Delegate to custom storage if set
|
|
763
|
+
if (this.#shouldDelegate()) {
|
|
764
|
+
return this.#delegateStorage.addDelLog(topicName, delId, ranges);
|
|
765
|
+
}
|
|
766
|
+
if (!this.isReady()) {
|
|
767
|
+
return this.disabled ?
|
|
768
|
+
Promise.resolve() :
|
|
769
|
+
Promise.reject(new Error("not initialized"));
|
|
770
|
+
}
|
|
771
|
+
return new Promise((resolve, reject) => {
|
|
772
|
+
const trx = this.db.transaction(['dellog'], 'readwrite');
|
|
773
|
+
trx.onsuccess = event => {
|
|
774
|
+
resolve(event.target.result);
|
|
775
|
+
};
|
|
776
|
+
trx.onerror = event => {
|
|
777
|
+
this.#logger('PCache', 'addDelLog', event.target.error);
|
|
778
|
+
reject(event.target.error);
|
|
779
|
+
};
|
|
780
|
+
ranges.forEach(r => trx.objectStore('dellog').add({
|
|
781
|
+
topic: topicName,
|
|
782
|
+
clear: delId,
|
|
783
|
+
low: r.low,
|
|
784
|
+
hi: r.hi || (r.low + 1)
|
|
785
|
+
}));
|
|
786
|
+
trx.commit();
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Retrieve deleted message records from persistent store.
|
|
792
|
+
* @memberOf DB
|
|
793
|
+
* @param {string} topicName - name of the topic to retrieve records for.
|
|
794
|
+
* @param {GetDataType} query - parameters of the message range to retrieve.
|
|
795
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
796
|
+
*/
|
|
797
|
+
readDelLog(topicName, query) {
|
|
798
|
+
// Delegate to custom storage if set
|
|
799
|
+
if (this.#shouldDelegate()) {
|
|
800
|
+
return this.#delegateStorage.readDelLog(topicName, query);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
query = query || {};
|
|
804
|
+
|
|
805
|
+
if (!this.isReady()) {
|
|
806
|
+
return this.disabled ?
|
|
807
|
+
Promise.resolve([]) :
|
|
808
|
+
Promise.reject(new Error("not initialized"));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const trx = this.db.transaction(['dellog']);
|
|
812
|
+
let result = [];
|
|
813
|
+
|
|
814
|
+
// Handle individual message ranges.
|
|
815
|
+
if (Array.isArray(query.ranges)) {
|
|
816
|
+
return new Promise((resolve, reject) => {
|
|
817
|
+
trx.onerror = event => {
|
|
818
|
+
this.#logger('PCache', 'readDelLog', event.target.error);
|
|
819
|
+
reject(event.target.error);
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
let count = 0;
|
|
823
|
+
query.ranges.forEach(range => {
|
|
824
|
+
const hi = range.hi || (range.low + 1);
|
|
825
|
+
const key = IDBKeyRange.bound([topicName, 0, range.low], [topicName, hi, Number.MAX_SAFE_INTEGER], false, true);
|
|
826
|
+
trx.objectStore('dellog').getAll(key).onsuccess = event => {
|
|
827
|
+
const entries = event.target.result;
|
|
828
|
+
if (entries) {
|
|
829
|
+
if (Array.isArray(entries)) {
|
|
830
|
+
result = result.concat(entries.map(entry => {
|
|
831
|
+
return {
|
|
832
|
+
low: entry.low,
|
|
833
|
+
hi: entry.hi
|
|
834
|
+
};
|
|
835
|
+
}));
|
|
836
|
+
} else {
|
|
837
|
+
result.push({
|
|
838
|
+
low: entries.low,
|
|
839
|
+
hi: entries.hi
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
count++;
|
|
844
|
+
if (count == query.ranges.length) {
|
|
845
|
+
resolve(result);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
const since = query.since > 0 ? query.since : 0;
|
|
854
|
+
const before = query.before > 0 ? query.before : Number.MAX_SAFE_INTEGER;
|
|
855
|
+
const limit = query.limit | 0;
|
|
856
|
+
|
|
857
|
+
trx.onerror = event => {
|
|
858
|
+
this.#logger('PCache', 'readDelLog', event.target.error);
|
|
859
|
+
reject(event.target.error);
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
let count = 0;
|
|
863
|
+
const result = [];
|
|
864
|
+
const range = IDBKeyRange.bound([topicName, 0, since], [topicName, before, Number.MAX_SAFE_INTEGER], false, true);
|
|
865
|
+
trx.objectStore('dellog').openCursor(range, 'prev')
|
|
866
|
+
.onsuccess = event => {
|
|
867
|
+
const cursor = event.target.result;
|
|
868
|
+
if (cursor) {
|
|
869
|
+
result.push({
|
|
870
|
+
low: cursor.value.low,
|
|
871
|
+
hi: cursor.value.hi
|
|
872
|
+
});
|
|
873
|
+
count += cursor.value.hi - cursor.value.low;
|
|
874
|
+
if (limit <= 0 || count < limit) {
|
|
875
|
+
cursor.continue();
|
|
876
|
+
} else {
|
|
877
|
+
resolve(result);
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
resolve(result);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Retrieve the latest 'clear' ID for the given topic.
|
|
888
|
+
* @param {string} topicName
|
|
889
|
+
* @return {Promise} promise resolved/rejected on operation completion.
|
|
890
|
+
*/
|
|
891
|
+
maxDelId(topicName) {
|
|
892
|
+
// Delegate to custom storage if set
|
|
893
|
+
if (this.#shouldDelegate()) {
|
|
894
|
+
return this.#delegateStorage.maxDelId(topicName);
|
|
895
|
+
}
|
|
896
|
+
if (!this.isReady()) {
|
|
897
|
+
return this.disabled ?
|
|
898
|
+
Promise.resolve(0) :
|
|
899
|
+
Promise.reject(new Error("not initialized"));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
const trx = this.db.transaction(['dellog']);
|
|
904
|
+
trx.onerror = event => {
|
|
905
|
+
this.#logger('PCache', 'maxDelId', event.target.error);
|
|
906
|
+
reject(event.target.error);
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const index = trx.objectStore('dellog').index('topic_clear');
|
|
910
|
+
index.openCursor(IDBKeyRange.bound([topicName, 0], [topicName, Number.MAX_SAFE_INTEGER]), 'prev')
|
|
911
|
+
.onsuccess = event => {
|
|
912
|
+
if (event.target.result) {
|
|
913
|
+
resolve(event.target.result.value);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Private methods.
|
|
920
|
+
|
|
921
|
+
// Serializable topic fields.
|
|
922
|
+
static #topic_fields = ['created', 'updated', 'deleted', 'touched', 'read', 'recv', 'seq',
|
|
923
|
+
'clear', 'defacs', 'creds', 'public', 'trusted', 'private', '_aux', '_deleted'
|
|
924
|
+
];
|
|
925
|
+
|
|
926
|
+
// Copy data from src to Topic object.
|
|
927
|
+
static #deserializeTopic(topic, src) {
|
|
928
|
+
DB.#topic_fields.forEach((f) => {
|
|
929
|
+
if (src.hasOwnProperty(f)) {
|
|
930
|
+
topic[f] = src[f];
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
if (Array.isArray(src.tags)) {
|
|
934
|
+
topic._tags = src.tags;
|
|
935
|
+
}
|
|
936
|
+
if (src.acs) {
|
|
937
|
+
topic.setAccessMode(src.acs);
|
|
938
|
+
}
|
|
939
|
+
topic.seq |= 0;
|
|
940
|
+
topic.read |= 0;
|
|
941
|
+
topic.unread = Math.max(0, topic.seq - topic.read);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Copy values from 'src' to 'dst'. Allocate dst if it's null or undefined.
|
|
945
|
+
static #serializeTopic(dst, src) {
|
|
946
|
+
const res = dst || {
|
|
947
|
+
name: src.name
|
|
948
|
+
};
|
|
949
|
+
DB.#topic_fields.forEach(f => {
|
|
950
|
+
if (src.hasOwnProperty(f)) {
|
|
951
|
+
res[f] = src[f];
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
if (Array.isArray(src._tags)) {
|
|
955
|
+
res.tags = src._tags;
|
|
956
|
+
}
|
|
957
|
+
if (src.acs) {
|
|
958
|
+
res.acs = src.getAccessMode().jsonHelper();
|
|
959
|
+
}
|
|
960
|
+
return res;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
static #serializeSubscription(dst, topicName, uid, sub) {
|
|
964
|
+
const fields = ['updated', 'mode', 'read', 'recv', 'clear', 'lastSeen', 'userAgent'];
|
|
965
|
+
const res = dst || {
|
|
966
|
+
topic: topicName,
|
|
967
|
+
uid: uid
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
fields.forEach((f) => {
|
|
971
|
+
if (sub.hasOwnProperty(f)) {
|
|
972
|
+
res[f] = sub[f];
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return res;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
static #serializeMessage(dst, msg) {
|
|
980
|
+
// Serializable fields.
|
|
981
|
+
const fields = ['topic', 'seq', 'ts', '_status', 'from', 'head', 'content'];
|
|
982
|
+
const res = dst || {};
|
|
983
|
+
fields.forEach((f) => {
|
|
984
|
+
if (msg.hasOwnProperty(f)) {
|
|
985
|
+
res[f] = msg[f];
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
return res;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* To use DB in a non browser context, supply indexedDB provider.
|
|
993
|
+
* @static
|
|
994
|
+
* @memberof DB
|
|
995
|
+
* @param idbProvider indexedDB provider, e.g. for node <code>require('fake-indexeddb')</code>.
|
|
996
|
+
*/
|
|
997
|
+
static setDatabaseProvider(idbProvider) {
|
|
998
|
+
IDBProvider = idbProvider;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Set a custom storage provider (e.g., SQLiteStorage for React Native).
|
|
1003
|
+
* Must be called BEFORE creating Tinode instance with persist: true.
|
|
1004
|
+
* @static
|
|
1005
|
+
* @memberof DB
|
|
1006
|
+
* @param {Object} storage - Storage implementation with the same interface as DB class.
|
|
1007
|
+
*/
|
|
1008
|
+
static setStorageProvider(storage) {
|
|
1009
|
+
_storageProvider = storage;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Get the current storage provider (if any).
|
|
1014
|
+
* @static
|
|
1015
|
+
* @memberof DB
|
|
1016
|
+
* @returns {Object|null} The custom storage provider, or null if using default IndexedDB.
|
|
1017
|
+
*/
|
|
1018
|
+
static getStorageProvider() {
|
|
1019
|
+
return _storageProvider;
|
|
1020
|
+
}
|
|
1021
|
+
}
|