@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/utils.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Utilities used in multiple places.
|
|
3
|
+
*
|
|
4
|
+
* @copyright 2015-2025 Tinode LLC.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
import AccessMode from './access-mode.js';
|
|
9
|
+
import {
|
|
10
|
+
DEL_CHAR,
|
|
11
|
+
LOCAL_SEQID
|
|
12
|
+
} from './config.js';
|
|
13
|
+
|
|
14
|
+
// Attempt to convert date and AccessMode strings to objects.
|
|
15
|
+
export function jsonParseHelper(key, val) {
|
|
16
|
+
// Try to convert string timestamps with optional milliseconds to Date,
|
|
17
|
+
// e.g. 2015-09-02T01:45:43[.123]Z
|
|
18
|
+
if (typeof val == 'string' && val.length >= 20 && val.length <= 24 && ['ts', 'touched', 'updated', 'created', 'when', 'deleted', 'expires'].includes(key)) {
|
|
19
|
+
const date = new Date(val);
|
|
20
|
+
if (!isNaN(date)) {
|
|
21
|
+
return date;
|
|
22
|
+
}
|
|
23
|
+
} else if (key === 'acs' && typeof val === 'object') {
|
|
24
|
+
return new AccessMode(val);
|
|
25
|
+
}
|
|
26
|
+
return val;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Checks if URL is a relative url, i.e. has no 'scheme://', including the case of missing scheme '//'.
|
|
30
|
+
// The scheme is expected to be RFC-compliant, e.g. [a-z][a-z0-9+.-]*
|
|
31
|
+
// example.html - ok
|
|
32
|
+
// https:example.com - not ok.
|
|
33
|
+
// http:/example.com - not ok.
|
|
34
|
+
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
|
|
35
|
+
export function isUrlRelative(url) {
|
|
36
|
+
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isValidDate(d) {
|
|
40
|
+
return (d instanceof Date) && !isNaN(d) && (d.getTime() != 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// RFC3339 formater of Date
|
|
44
|
+
export function rfc3339DateString(d) {
|
|
45
|
+
if (!isValidDate(d)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pad = function(val, sp) {
|
|
50
|
+
sp = sp || 2;
|
|
51
|
+
return '0'.repeat(sp - ('' + val).length) + val;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const millis = d.getUTCMilliseconds();
|
|
55
|
+
return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) +
|
|
56
|
+
'T' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) +
|
|
57
|
+
(millis ? '.' + pad(millis, 3) : '') + 'Z';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Recursively merge src's own properties to dst.
|
|
61
|
+
// Array and Date objects are shallow-copied.
|
|
62
|
+
export function mergeObj(dst, src) {
|
|
63
|
+
if (typeof src != 'object') {
|
|
64
|
+
if (src === undefined) {
|
|
65
|
+
return dst;
|
|
66
|
+
}
|
|
67
|
+
if (src === DEL_CHAR) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return src;
|
|
71
|
+
}
|
|
72
|
+
// JS is crazy: typeof null is 'object'.
|
|
73
|
+
if (src === null) {
|
|
74
|
+
return dst;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle Date
|
|
78
|
+
if (src instanceof Date && !isNaN(src)) {
|
|
79
|
+
return (!dst || !(dst instanceof Date) || isNaN(dst) || dst < src) ? src : dst;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Access mode
|
|
83
|
+
if (src instanceof AccessMode) {
|
|
84
|
+
return new AccessMode(src);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle Array
|
|
88
|
+
if (src instanceof Array) {
|
|
89
|
+
return src;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!dst || dst === DEL_CHAR) {
|
|
93
|
+
dst = src.constructor();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (let prop in src) {
|
|
97
|
+
if (src.hasOwnProperty(prop) && (prop != '_noForwarding')) {
|
|
98
|
+
try {
|
|
99
|
+
dst[prop] = mergeObj(dst[prop], src[prop]);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn("Error merging property:", prop, err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return dst;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Update object stored in a cache. Returns updated value.
|
|
109
|
+
export function mergeToCache(cache, key, newval) {
|
|
110
|
+
cache[key] = mergeObj(cache[key], newval);
|
|
111
|
+
return cache[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Strips all values from an object of they evaluate to false or if their name starts with '_'.
|
|
115
|
+
// Used on all outgoing object before serialization to string.
|
|
116
|
+
export function simplify(obj) {
|
|
117
|
+
Object.keys(obj).forEach((key) => {
|
|
118
|
+
if (key[0] == '_') {
|
|
119
|
+
// Strip fields like "obj._key".
|
|
120
|
+
delete obj[key];
|
|
121
|
+
} else if (!obj[key]) {
|
|
122
|
+
// Strip fields which evaluate to false.
|
|
123
|
+
delete obj[key];
|
|
124
|
+
} else if (Array.isArray(obj[key]) && obj[key].length == 0) {
|
|
125
|
+
// Strip empty arrays.
|
|
126
|
+
delete obj[key];
|
|
127
|
+
} else if (!obj[key]) {
|
|
128
|
+
// Strip fields which evaluate to false.
|
|
129
|
+
delete obj[key];
|
|
130
|
+
} else if (obj[key] instanceof Date) {
|
|
131
|
+
// Strip invalid or zero date.
|
|
132
|
+
if (!isValidDate(obj[key])) {
|
|
133
|
+
delete obj[key];
|
|
134
|
+
}
|
|
135
|
+
} else if (typeof obj[key] == 'object') {
|
|
136
|
+
simplify(obj[key]);
|
|
137
|
+
// Strip empty objects.
|
|
138
|
+
if (Object.getOwnPropertyNames(obj[key]).length == 0) {
|
|
139
|
+
delete obj[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return obj;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
// Trim whitespace, convert to lowercase, strip empty, short, and duplicate elements elements.
|
|
148
|
+
// If the result is an empty array, add a single element "\u2421" (Unicode Del character).
|
|
149
|
+
export function normalizeArray(arr) {
|
|
150
|
+
let out = [];
|
|
151
|
+
if (Array.isArray(arr)) {
|
|
152
|
+
// Trim, throw away very short and empty tags.
|
|
153
|
+
for (let i = 0, l = arr.length; i < l; i++) {
|
|
154
|
+
let t = arr[i];
|
|
155
|
+
if (t) {
|
|
156
|
+
t = t.trim().toLowerCase();
|
|
157
|
+
if (t.length > 1) {
|
|
158
|
+
out.push(t);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
out = out.sort().filter((item, pos, ary) => {
|
|
163
|
+
return !pos || item != ary[pos - 1];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (out.length == 0) {
|
|
167
|
+
// Add single tag with a Unicode Del character, otherwise an ampty array
|
|
168
|
+
// is ambiguos. The Del tag will be stripped by the server.
|
|
169
|
+
out.push(DEL_CHAR);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Convert input to valid ranges of IDs.
|
|
175
|
+
export function normalizeRanges(ranges, maxSeq) {
|
|
176
|
+
if (!Array.isArray(ranges)) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sort ranges in accending order by low, then descending by hi.
|
|
181
|
+
ranges.sort((r1, r2) => {
|
|
182
|
+
if (r1.low < r2.low) {
|
|
183
|
+
return -1;
|
|
184
|
+
}
|
|
185
|
+
if (r1.low == r2.low) {
|
|
186
|
+
return (r2.hi | 0) - r1.hi;
|
|
187
|
+
}
|
|
188
|
+
return 1;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Remove pending messages from ranges possibly clipping some ranges.
|
|
192
|
+
ranges = ranges.reduce((out, r) => {
|
|
193
|
+
if (r.low < LOCAL_SEQID && r.low > 0) {
|
|
194
|
+
if (!r.hi || r.hi < LOCAL_SEQID) {
|
|
195
|
+
out.push(r);
|
|
196
|
+
} else {
|
|
197
|
+
// Clip hi to max allowed value.
|
|
198
|
+
out.push({
|
|
199
|
+
low: r.low,
|
|
200
|
+
hi: maxSeq + 1
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
// Merge overlapping ranges.
|
|
208
|
+
ranges = ranges.reduce((out, r) => {
|
|
209
|
+
if (out.length == 0) {
|
|
210
|
+
out.push(r);
|
|
211
|
+
} else {
|
|
212
|
+
let prev = out[out.length - 1];
|
|
213
|
+
if (r.low <= prev.hi) {
|
|
214
|
+
prev.hi = Math.max(prev.hi, r.hi);
|
|
215
|
+
} else {
|
|
216
|
+
out.push(r);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
return ranges;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Convert array of IDs to array of ranges.
|
|
226
|
+
export function listToRanges(list) {
|
|
227
|
+
// Sort the list in ascending order
|
|
228
|
+
list.sort((a, b) => a - b);
|
|
229
|
+
// Convert the array of IDs to ranges.
|
|
230
|
+
return list.reduce((out, id) => {
|
|
231
|
+
if (out.length == 0) {
|
|
232
|
+
// First element.
|
|
233
|
+
out.push({
|
|
234
|
+
low: id
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
let prev = out[out.length - 1];
|
|
238
|
+
if ((!prev.hi && (id != prev.low + 1)) || (id > prev.hi)) {
|
|
239
|
+
// New range.
|
|
240
|
+
out.push({
|
|
241
|
+
low: id
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
// Expand existing range.
|
|
245
|
+
prev.hi = prev.hi ? Math.max(prev.hi, id + 1) : id + 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}, []);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cuts 'clip' range out of the 'src' range.
|
|
253
|
+
// Returns an array with 0, 1 or 2 elements.
|
|
254
|
+
export function clipOutRange(src, clip) {
|
|
255
|
+
if (clip.hi <= src.low || clip.low >= src.hi) {
|
|
256
|
+
// Clip is completely outside of src, no intersection.
|
|
257
|
+
return [src];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if (clip.low <= src.low) {
|
|
262
|
+
if (clip.hi >= src.hi) {
|
|
263
|
+
// The source range is completely inside the clipping range.
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
// Partial clipping at the top.
|
|
267
|
+
return [{
|
|
268
|
+
low: clip.hi,
|
|
269
|
+
hi: src.hi
|
|
270
|
+
}];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Range on the lower end.
|
|
274
|
+
const result = [{
|
|
275
|
+
low: src.low,
|
|
276
|
+
hi: clip.low
|
|
277
|
+
}];
|
|
278
|
+
if (clip.hi < src.hi) {
|
|
279
|
+
// Maybe a range on the higher end, if clip is completely inside the source.
|
|
280
|
+
result.push({
|
|
281
|
+
low: clip.hi,
|
|
282
|
+
hi: src.hi
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Cuts 'src' range to be completely within 'clip' range.
|
|
290
|
+
// Returns clipped range or null if 'src' is outside of 'clip'.
|
|
291
|
+
export function clipInRange(src, clip) {
|
|
292
|
+
if (clip.hi <= src.low || clip.low >= src.hi) {
|
|
293
|
+
// The src is completely outside of the clip, no intersection.
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (src.low >= clip.low && src.hi <= clip.hi) {
|
|
298
|
+
// Src is completely within the clip, return the entire src.
|
|
299
|
+
return src;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Partial overlap.
|
|
303
|
+
return {
|
|
304
|
+
low: Math.max(src.low, clip.low),
|
|
305
|
+
hi: Math.min(src.hi, clip.hi)
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return result;
|
|
309
|
+
}
|