@roeehrl/tinode-sdk 0.25.1-sqlite.6 → 0.25.1-sqlite.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/connection.js +14 -0
- package/src/db.js +8 -0
- package/src/index.native.js +9 -1
- package/src/large-file.native.js +365 -0
- package/src/storage-sqlite.js +112 -154
- package/types/index.d.ts +87 -0
- package/umd/tinode.dev.js +15 -1
- package/umd/tinode.dev.js.map +1 -1
- package/umd/tinode.prod.js +1 -1
- package/umd/tinode.prod.js.map +1 -1
- package/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roeehrl/tinode-sdk",
|
|
3
3
|
"description": "Tinode SDK fork with Storage interface for SQLite persistence in React Native",
|
|
4
|
-
"version": "0.25.1-sqlite.
|
|
4
|
+
"version": "0.25.1-sqlite.8",
|
|
5
5
|
"types": "./types/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"format": "js-beautify -r src/*.js",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"vers": "echo \"export const PACKAGE_VERSION = \\\"`node -p -e \"require('./package.json').version\"`\\\";\" > version.js",
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
|
-
"browserslist": [
|
|
16
|
+
"browserslist": [
|
|
17
|
+
"defaults"
|
|
18
|
+
],
|
|
17
19
|
"repository": {
|
|
18
20
|
"type": "git",
|
|
19
21
|
"url": "git+https://github.com/roeehrl/tinode-js.git"
|
package/src/connection.js
CHANGED
|
@@ -489,6 +489,20 @@ export default class Connection {
|
|
|
489
489
|
if (this.#socket && (this.#socket.readyState == this.#socket.OPEN)) {
|
|
490
490
|
this.#socket.send(msg);
|
|
491
491
|
} else {
|
|
492
|
+
// Socket is not connected - trigger disconnect callback if not already done
|
|
493
|
+
// This handles the case where the server dies but onclose hasn't fired yet
|
|
494
|
+
if (this.onDisconnect) {
|
|
495
|
+
// Use setTimeout to avoid blocking the throw
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
if (this.onDisconnect) {
|
|
498
|
+
this.onDisconnect(new CommError(NETWORK_ERROR_TEXT, NETWORK_ERROR), NETWORK_ERROR);
|
|
499
|
+
}
|
|
500
|
+
}, 0);
|
|
501
|
+
}
|
|
502
|
+
// Clean up the socket reference if it exists but isn't connected
|
|
503
|
+
if (this.#socket) {
|
|
504
|
+
this.#socket = null;
|
|
505
|
+
}
|
|
492
506
|
throw new Error("Websocket is not connected");
|
|
493
507
|
}
|
|
494
508
|
};
|
package/src/db.js
CHANGED
|
@@ -220,6 +220,14 @@ export default class DB {
|
|
|
220
220
|
* @returns {Promise} promise resolved/rejected on operation completion.
|
|
221
221
|
*/
|
|
222
222
|
updTopic(topic) {
|
|
223
|
+
// Skip topics that haven't been confirmed by the server yet.
|
|
224
|
+
// The _new flag is true for topics created locally but not yet subscribed.
|
|
225
|
+
// Only persist after subscribe succeeds and server assigns the real topic name.
|
|
226
|
+
if (topic?._new) {
|
|
227
|
+
console.log('[DB] updTopic DEFERRED - topic not yet confirmed by server:', topic.name);
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
}
|
|
230
|
+
|
|
223
231
|
console.log('[DB] updTopic CALLED:', topic?.name, 'shouldDelegate:', this.#shouldDelegate());
|
|
224
232
|
// Delegate to custom storage if set
|
|
225
233
|
if (this.#shouldDelegate()) {
|
package/src/index.native.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* This is the entry point for React Native (iOS/Android) environments.
|
|
5
5
|
* Metro bundler automatically picks this file for native platforms.
|
|
6
6
|
*
|
|
7
|
-
* Exports
|
|
7
|
+
* Exports:
|
|
8
|
+
* - SQLiteStorage for persistent storage using expo-sqlite
|
|
9
|
+
* - LargeFileHelperNative for file uploads using file URIs
|
|
8
10
|
*
|
|
9
11
|
* @module tinode-sdk
|
|
10
12
|
* @copyright 2015-2025 Tinode LLC, Activon
|
|
@@ -33,3 +35,9 @@ export {
|
|
|
33
35
|
default as SQLiteStorage
|
|
34
36
|
}
|
|
35
37
|
from './storage-sqlite.js';
|
|
38
|
+
|
|
39
|
+
// Export LargeFileHelperNative for React Native file uploads
|
|
40
|
+
export {
|
|
41
|
+
default as LargeFileHelperNative
|
|
42
|
+
}
|
|
43
|
+
from './large-file.native.js';
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Large file upload/download utilities for React Native.
|
|
3
|
+
* Provides file upload support using file URIs instead of Blob objects.
|
|
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 2015-2025 Tinode LLC, Activon
|
|
9
|
+
* @license Apache 2.0
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import CommError from './comm-error.js';
|
|
14
|
+
import {
|
|
15
|
+
isUrlRelative
|
|
16
|
+
} from './utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @class LargeFileHelperNative - utilities for uploading and downloading files out of band in React Native.
|
|
20
|
+
* Don't instantiate this class directly. Use {Tinode.getLargeFileHelper} instead.
|
|
21
|
+
* @memberof Tinode
|
|
22
|
+
*
|
|
23
|
+
* @param {Tinode} tinode - the main Tinode object.
|
|
24
|
+
* @param {string} version - protocol version, i.e. '0'.
|
|
25
|
+
*/
|
|
26
|
+
export default class LargeFileHelperNative {
|
|
27
|
+
constructor(tinode, version) {
|
|
28
|
+
this._tinode = tinode;
|
|
29
|
+
this._version = version;
|
|
30
|
+
|
|
31
|
+
this._apiKey = tinode._apiKey;
|
|
32
|
+
this._authToken = tinode.getAuthToken();
|
|
33
|
+
|
|
34
|
+
// Ongoing requests (using AbortController for fetch).
|
|
35
|
+
this._abortControllers = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the upload URL.
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
_buildUploadUrl(baseUrl) {
|
|
43
|
+
let url = `/v${this._version}/file/u/`;
|
|
44
|
+
if (baseUrl) {
|
|
45
|
+
let base = baseUrl;
|
|
46
|
+
if (base.endsWith('/')) {
|
|
47
|
+
base = base.slice(0, -1);
|
|
48
|
+
}
|
|
49
|
+
if (base.startsWith('http://') || base.startsWith('https://')) {
|
|
50
|
+
url = base + url;
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`Invalid base URL '${baseUrl}'`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build headers for upload request.
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
_buildHeaders() {
|
|
63
|
+
const headers = {
|
|
64
|
+
'X-Tinode-APIKey': this._apiKey,
|
|
65
|
+
};
|
|
66
|
+
if (this._authToken) {
|
|
67
|
+
headers['X-Tinode-Auth'] = `Token ${this._authToken.token}`;
|
|
68
|
+
}
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start uploading a file from a URI (React Native).
|
|
74
|
+
*
|
|
75
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
76
|
+
*
|
|
77
|
+
* @param {string} uri - File URI (e.g., 'file:///path/to/audio.m4a')
|
|
78
|
+
* @param {string} filename - Filename for the upload
|
|
79
|
+
* @param {string} mimetype - MIME type of the file
|
|
80
|
+
* @param {number} size - File size in bytes (optional, for progress calculation)
|
|
81
|
+
* @param {string} avatarFor - Topic name if the upload represents an avatar
|
|
82
|
+
* @param {function} onProgress - Progress callback. Takes one {float} parameter 0..1
|
|
83
|
+
* @param {function} onSuccess - Success callback. Called with server control message.
|
|
84
|
+
* @param {function} onFailure - Failure callback. Called with error or null.
|
|
85
|
+
*
|
|
86
|
+
* @returns {Promise<string>} Promise resolved with the upload URL.
|
|
87
|
+
*/
|
|
88
|
+
uploadUri(uri, filename, mimetype, size, avatarFor, onProgress, onSuccess, onFailure) {
|
|
89
|
+
const baseUrl = (this._tinode._secure ? 'https://' : 'http://') + this._tinode._host;
|
|
90
|
+
return this.uploadUriWithBaseUrl(baseUrl, uri, filename, mimetype, size, avatarFor, onProgress, onSuccess, onFailure);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Start uploading a file from a URI to a specific base URL.
|
|
95
|
+
*
|
|
96
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
97
|
+
*
|
|
98
|
+
* @param {string} baseUrl - Base URL of upload server.
|
|
99
|
+
* @param {string} uri - File URI (e.g., 'file:///path/to/audio.m4a')
|
|
100
|
+
* @param {string} filename - Filename for the upload
|
|
101
|
+
* @param {string} mimetype - MIME type of the file
|
|
102
|
+
* @param {number} size - File size in bytes (optional)
|
|
103
|
+
* @param {string} avatarFor - Topic name if the upload represents an avatar
|
|
104
|
+
* @param {function} onProgress - Progress callback
|
|
105
|
+
* @param {function} onSuccess - Success callback
|
|
106
|
+
* @param {function} onFailure - Failure callback
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<string>} Promise resolved with the upload URL.
|
|
109
|
+
*/
|
|
110
|
+
uploadUriWithBaseUrl(baseUrl, uri, filename, mimetype, size, avatarFor, onProgress, onSuccess, onFailure) {
|
|
111
|
+
const instance = this;
|
|
112
|
+
const url = this._buildUploadUrl(baseUrl);
|
|
113
|
+
const headers = this._buildHeaders();
|
|
114
|
+
|
|
115
|
+
// Create AbortController for cancellation
|
|
116
|
+
const abortController = new AbortController();
|
|
117
|
+
this._abortControllers.push(abortController);
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
try {
|
|
121
|
+
// Build FormData with file URI (React Native specific)
|
|
122
|
+
const formData = new FormData();
|
|
123
|
+
|
|
124
|
+
// React Native FormData accepts objects with uri, type, and name
|
|
125
|
+
formData.append('file', {
|
|
126
|
+
uri: uri,
|
|
127
|
+
type: mimetype,
|
|
128
|
+
name: filename,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
formData.append('id', this._tinode.getNextUniqueId());
|
|
132
|
+
|
|
133
|
+
if (avatarFor) {
|
|
134
|
+
formData.append('topic', avatarFor);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Use fetch with upload progress via XMLHttpRequest
|
|
138
|
+
// Note: fetch doesn't support upload progress, so we use XMLHttpRequest
|
|
139
|
+
const xhr = new XMLHttpRequest();
|
|
140
|
+
|
|
141
|
+
xhr.open('POST', url, true);
|
|
142
|
+
|
|
143
|
+
// Set headers
|
|
144
|
+
Object.keys(headers).forEach(key => {
|
|
145
|
+
xhr.setRequestHeader(key, headers[key]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Handle abort
|
|
149
|
+
abortController.signal.addEventListener('abort', () => {
|
|
150
|
+
xhr.abort();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Progress tracking
|
|
154
|
+
if (onProgress || instance.onProgress) {
|
|
155
|
+
xhr.upload.onprogress = (e) => {
|
|
156
|
+
if (e.lengthComputable) {
|
|
157
|
+
const progress = e.loaded / e.total;
|
|
158
|
+
if (onProgress) {
|
|
159
|
+
onProgress(progress);
|
|
160
|
+
}
|
|
161
|
+
if (instance.onProgress) {
|
|
162
|
+
instance.onProgress(progress);
|
|
163
|
+
}
|
|
164
|
+
} else if (size > 0) {
|
|
165
|
+
// Use provided size if available
|
|
166
|
+
const progress = Math.min(e.loaded / size, 1);
|
|
167
|
+
if (onProgress) {
|
|
168
|
+
onProgress(progress);
|
|
169
|
+
}
|
|
170
|
+
if (instance.onProgress) {
|
|
171
|
+
instance.onProgress(progress);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
xhr.onload = function() {
|
|
178
|
+
let pkt;
|
|
179
|
+
try {
|
|
180
|
+
pkt = JSON.parse(this.response);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
instance._tinode.logger("ERROR: Invalid server response in LargeFileHelperNative", this.response);
|
|
183
|
+
pkt = {
|
|
184
|
+
ctrl: {
|
|
185
|
+
code: this.status,
|
|
186
|
+
text: this.statusText
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.status >= 200 && this.status < 300) {
|
|
192
|
+
const uploadUrl = pkt.ctrl.params.url;
|
|
193
|
+
resolve(uploadUrl);
|
|
194
|
+
if (onSuccess) {
|
|
195
|
+
onSuccess(pkt.ctrl);
|
|
196
|
+
}
|
|
197
|
+
} else if (this.status >= 400) {
|
|
198
|
+
const error = new CommError(pkt.ctrl.text, pkt.ctrl.code);
|
|
199
|
+
reject(error);
|
|
200
|
+
if (onFailure) {
|
|
201
|
+
onFailure(pkt.ctrl);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
instance._tinode.logger("ERROR: Unexpected server response status", this.status, this.response);
|
|
205
|
+
reject(new Error(`Unexpected status: ${this.status}`));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
xhr.onerror = function(e) {
|
|
210
|
+
const error = e || new Error("Upload failed");
|
|
211
|
+
reject(error);
|
|
212
|
+
if (onFailure) {
|
|
213
|
+
onFailure(null);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
xhr.onabort = function() {
|
|
218
|
+
const error = new Error("Upload cancelled by user");
|
|
219
|
+
reject(error);
|
|
220
|
+
if (onFailure) {
|
|
221
|
+
onFailure(null);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
xhr.send(formData);
|
|
226
|
+
|
|
227
|
+
} catch (err) {
|
|
228
|
+
reject(err);
|
|
229
|
+
if (onFailure) {
|
|
230
|
+
onFailure(null);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Upload a Blob or File (for compatibility with web code).
|
|
238
|
+
* In React Native, this converts the blob to a data URI if possible,
|
|
239
|
+
* but it's recommended to use uploadUri instead.
|
|
240
|
+
*
|
|
241
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
242
|
+
*
|
|
243
|
+
* @param {Blob|File} data - Data to upload
|
|
244
|
+
* @param {string} avatarFor - Topic name if avatar
|
|
245
|
+
* @param {function} onProgress - Progress callback
|
|
246
|
+
* @param {function} onSuccess - Success callback
|
|
247
|
+
* @param {function} onFailure - Failure callback
|
|
248
|
+
*
|
|
249
|
+
* @returns {Promise<string>} Promise resolved with the upload URL.
|
|
250
|
+
*/
|
|
251
|
+
upload(data, avatarFor, onProgress, onSuccess, onFailure) {
|
|
252
|
+
// For React Native compatibility, try to handle File/Blob objects
|
|
253
|
+
// This is mainly for backward compatibility - prefer uploadUri
|
|
254
|
+
if (data && typeof data === 'object') {
|
|
255
|
+
// Check if it has a uri property (React Native file object)
|
|
256
|
+
if (data.uri) {
|
|
257
|
+
return this.uploadUri(
|
|
258
|
+
data.uri,
|
|
259
|
+
data.name || 'file',
|
|
260
|
+
data.type || 'application/octet-stream',
|
|
261
|
+
data.size || 0,
|
|
262
|
+
avatarFor,
|
|
263
|
+
onProgress,
|
|
264
|
+
onSuccess,
|
|
265
|
+
onFailure
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if it's a proper File object with arrayBuffer (unlikely in RN)
|
|
270
|
+
if (typeof data.arrayBuffer === 'function') {
|
|
271
|
+
// This won't work well in React Native, log warning
|
|
272
|
+
console.warn('LargeFileHelperNative: Blob/File upload not fully supported in React Native. Use uploadUri instead.');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const error = new Error('React Native requires file URI for upload. Use uploadUri instead of upload.');
|
|
277
|
+
if (onFailure) {
|
|
278
|
+
onFailure(null);
|
|
279
|
+
}
|
|
280
|
+
return Promise.reject(error);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Download a file. Not fully implemented for React Native.
|
|
285
|
+
* Use expo-file-system for downloads in React Native.
|
|
286
|
+
*
|
|
287
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
288
|
+
*
|
|
289
|
+
* @param {string} relativeUrl - URL to download from
|
|
290
|
+
* @param {string} filename - Filename
|
|
291
|
+
* @param {string} mimetype - MIME type
|
|
292
|
+
* @param {function} onProgress - Progress callback
|
|
293
|
+
* @param {function} onError - Error callback
|
|
294
|
+
*
|
|
295
|
+
* @returns {Promise<string>} Promise resolved with local file path.
|
|
296
|
+
*/
|
|
297
|
+
async download(relativeUrl, filename, mimetype, onProgress, onError) {
|
|
298
|
+
if (!isUrlRelative(relativeUrl)) {
|
|
299
|
+
const error = `The URL '${relativeUrl}' must be relative, not absolute`;
|
|
300
|
+
if (onError) {
|
|
301
|
+
onError(error);
|
|
302
|
+
}
|
|
303
|
+
throw new Error(error);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!this._authToken) {
|
|
307
|
+
const error = "Must authenticate first";
|
|
308
|
+
if (onError) {
|
|
309
|
+
onError(error);
|
|
310
|
+
}
|
|
311
|
+
throw new Error(error);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// For React Native, we need to use expo-file-system or react-native-fs
|
|
315
|
+
// This is a placeholder that returns the authorized URL
|
|
316
|
+
// The caller should use expo-file-system to download
|
|
317
|
+
console.warn('LargeFileHelperNative.download: Use expo-file-system for file downloads in React Native');
|
|
318
|
+
|
|
319
|
+
// Return the authorized URL for the caller to download
|
|
320
|
+
const baseUrl = (this._tinode._secure ? 'https://' : 'http://') + this._tinode._host;
|
|
321
|
+
const fullUrl = baseUrl + relativeUrl + '&asatt=1';
|
|
322
|
+
|
|
323
|
+
return fullUrl;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get an authorized download URL for use with expo-file-system.
|
|
328
|
+
*
|
|
329
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
330
|
+
*
|
|
331
|
+
* @param {string} relativeUrl - Relative URL to the file
|
|
332
|
+
* @returns {Object} Object with url and headers for download
|
|
333
|
+
*/
|
|
334
|
+
getDownloadConfig(relativeUrl) {
|
|
335
|
+
if (!isUrlRelative(relativeUrl)) {
|
|
336
|
+
throw new Error(`The URL '${relativeUrl}' must be relative, not absolute`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const baseUrl = (this._tinode._secure ? 'https://' : 'http://') + this._tinode._host;
|
|
340
|
+
|
|
341
|
+
// Add asatt=1 to request content-disposition: attachment
|
|
342
|
+
const separator = relativeUrl.includes('?') ? '&' : '?';
|
|
343
|
+
const fullUrl = baseUrl + relativeUrl + separator + 'asatt=1';
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
url: fullUrl,
|
|
347
|
+
headers: this._buildHeaders(),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Try to cancel all ongoing uploads.
|
|
353
|
+
* @memberof Tinode.LargeFileHelperNative#
|
|
354
|
+
*/
|
|
355
|
+
cancel() {
|
|
356
|
+
this._abortControllers.forEach(controller => {
|
|
357
|
+
try {
|
|
358
|
+
controller.abort();
|
|
359
|
+
} catch (e) {
|
|
360
|
+
// Ignore errors during abort
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
this._abortControllers = [];
|
|
364
|
+
}
|
|
365
|
+
}
|