Skip to content

Commit ae369e8

Browse files
Merge pull request #107 from appwrite/concurrent-chunk-uploads-version-bump-1-9-x
chore: bump SDK version to 0.31.0
2 parents 884afab + dbf442e commit ae369e8

3 files changed

Lines changed: 123 additions & 24 deletions

File tree

docs/examples/avatars/get-screenshot.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const result = avatars.getScreenshot({
2020
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15', // optional
2121
fullpage: true, // optional
2222
locale: 'en-US', // optional
23-
timezone: Timezone.AmericaNewYork, // optional
23+
timezone: Timezone.AfricaAbidjan, // optional
2424
latitude: 37.7749, // optional
2525
longitude: -122.4194, // optional
2626
accuracy: 100, // optional

src/services/realtime.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,13 @@ export class Realtime {
239239
try {
240240
const connectionId = ++this.connectionId;
241241
const WebSocketCtor: any = WebSocket;
242-
const socket = (this.socket = new WebSocketCtor(url, undefined, {
243-
headers: {
244-
Origin: `appwrite-${Platform.OS}://${this.client.config.platform}`
245-
}
246-
}));
242+
const socket = (this.socket = Platform.OS === 'web'
243+
? new WebSocketCtor(url)
244+
: new WebSocketCtor(url, undefined, {
245+
headers: {
246+
Origin: `appwrite-${Platform.OS}://${this.client.config.platform}`
247+
}
248+
}));
247249

248250
socket.addEventListener('open', () => {
249251
if (connectionId !== this.connectionId) {

src/services/storage.ts

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -195,42 +195,139 @@ export class Storage extends Service {
195195
} catch(e) {
196196
}
197197

198-
let timestamp = new Date().getTime();
199-
while (offset < size) {
200-
let end = Math.min(offset + Service.CHUNK_SIZE - 1, size - 1);
198+
const totalChunks = Math.ceil(size / Service.CHUNK_SIZE);
201199

202-
apiHeaders['content-range'] = 'bytes ' + offset + '-' + end + '/' + size;
203-
if (response && response.$id) {
204-
apiHeaders['x-appwrite-id'] = response.$id;
205-
}
200+
// Upload first chunk alone to get the upload ID
201+
if (offset === 0) {
202+
const firstChunkEnd = Math.min(Service.CHUNK_SIZE, size);
203+
const firstChunkHeaders = { ...apiHeaders, 'content-range': 'bytes 0-' + (firstChunkEnd - 1) + '/' + size };
206204

207-
let chunk = await FileSystem.readAsStringAsync(file.uri, {
205+
let firstChunk = await FileSystem.readAsStringAsync(file.uri, {
208206
encoding: FileSystem.EncodingType.Base64,
209-
position: offset,
207+
position: 0,
210208
length: Service.CHUNK_SIZE
211209
});
212-
var path = `data:${file.type};base64,${chunk}`;
210+
var firstPath = `data:${file.type};base64,${firstChunk}`;
213211
if (RNPlatform.OS.toLowerCase() === 'android') {
214-
path = FileSystem.cacheDirectory + '/tmp_chunk_' + timestamp;
215-
await FileSystem.writeAsStringAsync(path, chunk, {encoding: FileSystem.EncodingType.Base64});
212+
firstPath = FileSystem.cacheDirectory + '/tmp_chunk_' + new Date().getTime();
213+
await FileSystem.writeAsStringAsync(firstPath, firstChunk, {encoding: FileSystem.EncodingType.Base64});
216214
}
217215

218-
payload['file'] = { uri: path, name: file.name, type: file.type };
216+
payload['file'] = { uri: firstPath, name: file.name, type: file.type };
219217

220-
response = await this.client.call('post', uri, apiHeaders, payload);
218+
response = await this.client.call('post', uri, firstChunkHeaders, payload);
219+
offset = firstChunkEnd;
221220

222221
if (onProgress) {
223222
onProgress({
224223
$id: response.$id,
225224
progress: (offset / size) * 100,
226225
sizeUploaded: offset,
227-
chunksTotal: response.chunksTotal,
228-
chunksUploaded: response.chunksUploaded
226+
chunksTotal: totalChunks,
227+
chunksUploaded: 1
229228
});
230229
}
231-
offset += Service.CHUNK_SIZE;
232230
}
233-
return response;
231+
232+
if (offset >= size) {
233+
return response;
234+
}
235+
236+
const uploadId = response?.$id;
237+
const chunks: { index: number; start: number; end: number }[] = [];
238+
const startChunkIndex = Math.ceil(offset / Service.CHUNK_SIZE);
239+
for (let i = startChunkIndex; i < totalChunks; i++) {
240+
const start = i * Service.CHUNK_SIZE;
241+
const end = Math.min(start + Service.CHUNK_SIZE, size);
242+
chunks.push({ index: i, start, end });
243+
}
244+
245+
// Upload remaining chunks with max concurrency of 8
246+
const CONCURRENCY = 8;
247+
let completedCount = startChunkIndex;
248+
let uploadedBytes = offset;
249+
let finalResponse = null;
250+
let failed = false;
251+
252+
const isUploadComplete = (chunkResponse: any) => {
253+
const chunksUploaded = chunkResponse?.chunksUploaded;
254+
const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks;
255+
return typeof chunksUploaded === 'number' && typeof chunksTotal === 'number' && chunksUploaded >= chunksTotal;
256+
};
257+
258+
const uploadChunk = async (chunk: typeof chunks[0]) => {
259+
const chunkHeaders = { ...apiHeaders };
260+
if (uploadId) {
261+
chunkHeaders['x-appwrite-id'] = uploadId;
262+
}
263+
chunkHeaders['content-range'] = 'bytes ' + chunk.start + '-' + (chunk.end - 1) + '/' + size;
264+
265+
const chunkData = await FileSystem.readAsStringAsync(file.uri, {
266+
encoding: FileSystem.EncodingType.Base64,
267+
position: chunk.start,
268+
length: chunk.end - chunk.start
269+
});
270+
271+
let chunkPath = `data:${file.type};base64,${chunkData}`;
272+
if (RNPlatform.OS.toLowerCase() === 'android') {
273+
chunkPath = FileSystem.cacheDirectory + '/tmp_chunk_' + new Date().getTime() + '_' + chunk.index;
274+
await FileSystem.writeAsStringAsync(chunkPath, chunkData, {encoding: FileSystem.EncodingType.Base64});
275+
}
276+
277+
const chunkPayload = { ...payload };
278+
chunkPayload['file'] = { uri: chunkPath, name: file.name, type: file.type };
279+
280+
const chunkResponse = await this.client.call('post', uri, chunkHeaders, chunkPayload);
281+
282+
if (failed) {
283+
return chunkResponse;
284+
}
285+
286+
completedCount++;
287+
uploadedBytes += (chunk.end - chunk.start);
288+
289+
response = chunkResponse;
290+
if (isUploadComplete(chunkResponse)) {
291+
finalResponse = chunkResponse;
292+
}
293+
294+
if (onProgress) {
295+
onProgress({
296+
$id: uploadId,
297+
progress: (uploadedBytes / size) * 100,
298+
sizeUploaded: uploadedBytes,
299+
chunksTotal: totalChunks,
300+
chunksUploaded: completedCount
301+
});
302+
}
303+
304+
return chunkResponse;
305+
};
306+
307+
// Process with limited concurrency using a worker pool
308+
const queue = [...chunks];
309+
const workers: Promise<void>[] = [];
310+
const workerCount = Math.min(CONCURRENCY, queue.length);
311+
312+
for (let i = 0; i < workerCount; i++) {
313+
workers.push(
314+
(async () => {
315+
while (!failed && queue.length > 0) {
316+
const chunk = queue.shift()!;
317+
try {
318+
await uploadChunk(chunk);
319+
} catch (error) {
320+
failed = true;
321+
throw error;
322+
}
323+
}
324+
})()
325+
);
326+
}
327+
328+
await Promise.all(workers);
329+
330+
return finalResponse ?? response;
234331
}
235332

236333
/**

0 commit comments

Comments
 (0)