@roeehrl/tinode-sdk 0.25.1-sqlite.7 → 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.7",
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",
@@ -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
+ }
package/types/index.d.ts CHANGED
@@ -265,6 +265,8 @@ declare module '@roeehrl/tinode-sdk' {
265
265
  duration: number;
266
266
  filename?: string;
267
267
  size?: number;
268
+ /** Promise that resolves to the upload URL (for out-of-band uploads) */
269
+ urlPromise?: Promise<string>;
268
270
  }
269
271
 
270
272
  export interface AttachmentDesc {
@@ -660,6 +662,91 @@ declare module '@roeehrl/tinode-sdk' {
660
662
  cancel(): void;
661
663
  }
662
664
 
665
+ // ==========================================================================
666
+ // LargeFileHelperNative (React Native)
667
+ // ==========================================================================
668
+
669
+ /**
670
+ * React Native file upload helper.
671
+ * Uses file URIs instead of Blob objects for compatibility.
672
+ */
673
+ export class LargeFileHelperNative {
674
+ constructor(tinode: Tinode, version?: string);
675
+
676
+ /**
677
+ * Upload a file from a URI (React Native specific).
678
+ * @param uri File URI (e.g., 'file:///path/to/audio.m4a')
679
+ * @param filename Filename for the upload
680
+ * @param mimetype MIME type of the file
681
+ * @param size File size in bytes (optional, for progress calculation)
682
+ * @param avatarFor Topic name if the upload represents an avatar
683
+ * @param onProgress Progress callback (0..1)
684
+ * @param onSuccess Success callback
685
+ * @param onFailure Failure callback
686
+ * @returns Promise resolved with the upload URL
687
+ */
688
+ uploadUri(
689
+ uri: string,
690
+ filename: string,
691
+ mimetype: string,
692
+ size?: number,
693
+ avatarFor?: string,
694
+ onProgress?: (progress: number) => void,
695
+ onSuccess?: (ctrl: ControlMessage) => void,
696
+ onFailure?: (ctrl: ControlMessage | null) => void,
697
+ ): Promise<string>;
698
+
699
+ /**
700
+ * Upload a file from a URI to a specific base URL.
701
+ */
702
+ uploadUriWithBaseUrl(
703
+ baseUrl: string,
704
+ uri: string,
705
+ filename: string,
706
+ mimetype: string,
707
+ size?: number,
708
+ avatarFor?: string,
709
+ onProgress?: (progress: number) => void,
710
+ onSuccess?: (ctrl: ControlMessage) => void,
711
+ onFailure?: (ctrl: ControlMessage | null) => void,
712
+ ): Promise<string>;
713
+
714
+ /**
715
+ * Upload (compatibility method - prefer uploadUri in React Native).
716
+ * Accepts objects with uri property for RN file objects.
717
+ */
718
+ upload(
719
+ data: { uri: string; name?: string; type?: string; size?: number } | File | Blob,
720
+ avatarFor?: string,
721
+ onProgress?: (progress: number) => void,
722
+ onSuccess?: (ctrl: ControlMessage) => void,
723
+ onFailure?: (ctrl: ControlMessage | null) => void,
724
+ ): Promise<string>;
725
+
726
+ /**
727
+ * Get download configuration for use with expo-file-system.
728
+ * @param relativeUrl Relative URL to the file
729
+ * @returns Object with url and headers for download
730
+ */
731
+ getDownloadConfig(relativeUrl: string): {
732
+ url: string;
733
+ headers: Record<string, string>;
734
+ };
735
+
736
+ /**
737
+ * Download (placeholder - use expo-file-system instead).
738
+ */
739
+ download(
740
+ relativeUrl: string,
741
+ filename?: string,
742
+ mimetype?: string,
743
+ onProgress?: (loaded: number) => void,
744
+ onError?: (error: Error | string) => void,
745
+ ): Promise<string>;
746
+
747
+ cancel(): void;
748
+ }
749
+
663
750
  // ==========================================================================
664
751
  // Main Tinode Class
665
752
  // ==========================================================================
package/umd/tinode.dev.js CHANGED
@@ -770,6 +770,16 @@ class Connection {
770
770
  if (this.#socket && this.#socket.readyState == this.#socket.OPEN) {
771
771
  this.#socket.send(msg);
772
772
  } else {
773
+ if (this.onDisconnect) {
774
+ setTimeout(() => {
775
+ if (this.onDisconnect) {
776
+ this.onDisconnect(new _comm_error_js__WEBPACK_IMPORTED_MODULE_0__["default"](NETWORK_ERROR_TEXT, NETWORK_ERROR), NETWORK_ERROR);
777
+ }
778
+ }, 0);
779
+ }
780
+ if (this.#socket) {
781
+ this.#socket = null;
782
+ }
773
783
  throw new Error("Websocket is not connected");
774
784
  }
775
785
  };
@@ -944,6 +954,10 @@ class DB {
944
954
  return !!this.db;
945
955
  }
946
956
  updTopic(topic) {
957
+ if (topic?._new) {
958
+ console.log('[DB] updTopic DEFERRED - topic not yet confirmed by server:', topic.name);
959
+ return Promise.resolve();
960
+ }
947
961
  console.log('[DB] updTopic CALLED:', topic?.name, 'shouldDelegate:', this.#shouldDelegate());
948
962
  if (this.#shouldDelegate()) {
949
963
  return this.#delegateStorage.updTopic(topic);
@@ -5551,7 +5565,7 @@ __webpack_require__.r(__webpack_exports__);
5551
5565
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5552
5566
  /* harmony export */ PACKAGE_VERSION: function() { return /* binding */ PACKAGE_VERSION; }
5553
5567
  /* harmony export */ });
5554
- const PACKAGE_VERSION = "0.25.1-sqlite.6";
5568
+ const PACKAGE_VERSION = "0.25.1-sqlite.8";
5555
5569
 
5556
5570
  /***/ })
5557
5571