@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 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.6",
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": ["defaults"],
16
+ "browserslist": [
17
+ "defaults"
18
+ ],
17
19
  "repository": {
18
20
  "type": "git",
19
21
  "url": "git+https://github.com/roeehrl/tinode-js.git"
package/src/connection.js CHANGED
@@ -489,6 +489,20 @@ export default class Connection {
489
489
  if (this.#socket && (this.#socket.readyState == this.#socket.OPEN)) {
490
490
  this.#socket.send(msg);
491
491
  } else {
492
+ // Socket is not connected - trigger disconnect callback if not already done
493
+ // This handles the case where the server dies but onclose hasn't fired yet
494
+ if (this.onDisconnect) {
495
+ // Use setTimeout to avoid blocking the throw
496
+ setTimeout(() => {
497
+ if (this.onDisconnect) {
498
+ this.onDisconnect(new CommError(NETWORK_ERROR_TEXT, NETWORK_ERROR), NETWORK_ERROR);
499
+ }
500
+ }, 0);
501
+ }
502
+ // Clean up the socket reference if it exists but isn't connected
503
+ if (this.#socket) {
504
+ this.#socket = null;
505
+ }
492
506
  throw new Error("Websocket is not connected");
493
507
  }
494
508
  };
package/src/db.js CHANGED
@@ -220,6 +220,14 @@ export default class DB {
220
220
  * @returns {Promise} promise resolved/rejected on operation completion.
221
221
  */
222
222
  updTopic(topic) {
223
+ // Skip topics that haven't been confirmed by the server yet.
224
+ // The _new flag is true for topics created locally but not yet subscribed.
225
+ // Only persist after subscribe succeeds and server assigns the real topic name.
226
+ if (topic?._new) {
227
+ console.log('[DB] updTopic DEFERRED - topic not yet confirmed by server:', topic.name);
228
+ return Promise.resolve();
229
+ }
230
+
223
231
  console.log('[DB] updTopic CALLED:', topic?.name, 'shouldDelegate:', this.#shouldDelegate());
224
232
  // Delegate to custom storage if set
225
233
  if (this.#shouldDelegate()) {
@@ -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 SQLiteStorage for persistent storage using expo-sqlite.
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
+ }