Skip to content

Commit f14cf39

Browse files
feat: support concurrent chunk uploads (#174)
1 parent 2460ef6 commit f14cf39

3 files changed

Lines changed: 148 additions & 29 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "appwrite",
33
"homepage": "https://appwrite.io/support",
44
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
5-
"version": "25.1.1",
5+
"version": "25.2.0",
66
"license": "BSD-3-Clause",
77
"main": "dist/cjs/sdk.js",
88
"exports": {

src/client.ts

Lines changed: 145 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,11 @@ class Client {
354354
endpoint: string;
355355
endpointRealtime: string;
356356
project: string;
357+
key: string;
357358
jwt: string;
358359
locale: string;
359360
session: string;
361+
forwardeduseragent: string;
360362
devkey: string;
361363
cookie: string;
362364
impersonateuserid: string;
@@ -366,9 +368,11 @@ class Client {
366368
endpoint: 'https://cloud.appwrite.io/v1',
367369
endpointRealtime: '',
368370
project: '',
371+
key: '',
369372
jwt: '',
370373
locale: '',
371374
session: '',
375+
forwardeduseragent: '',
372376
devkey: '',
373377
cookie: '',
374378
impersonateuserid: '',
@@ -380,9 +384,9 @@ class Client {
380384
*/
381385
headers: Headers = {
382386
'x-sdk-name': 'Web',
383-
'x-sdk-platform': 'client',
387+
'x-sdk-platform': 'server',
384388
'x-sdk-language': 'web',
385-
'x-sdk-version': '25.1.1',
389+
'x-sdk-version': '25.2.0',
386390
'X-Appwrite-Response-Format': '1.9.5',
387391
};
388392

@@ -456,6 +460,20 @@ class Client {
456460
this.config.project = value;
457461
return this;
458462
}
463+
/**
464+
* Set Key
465+
*
466+
* Your secret API key
467+
*
468+
* @param value string
469+
*
470+
* @return {this}
471+
*/
472+
setKey(value: string): this {
473+
this.headers['X-Appwrite-Key'] = value;
474+
this.config.key = value;
475+
return this;
476+
}
459477
/**
460478
* Set JWT
461479
*
@@ -496,6 +514,20 @@ class Client {
496514
this.config.session = value;
497515
return this;
498516
}
517+
/**
518+
* Set ForwardedUserAgent
519+
*
520+
* The user agent string of the client that made the request
521+
*
522+
* @param value string
523+
*
524+
* @return {this}
525+
*/
526+
setForwardedUserAgent(value: string): this {
527+
this.headers['X-Forwarded-User-Agent'] = value;
528+
this.config.forwardeduseragent = value;
529+
return this;
530+
}
499531
/**
500532
* Set DevKey
501533
*
@@ -918,44 +950,131 @@ class Client {
918950
return await this.call(method, url, headers, originalPayload);
919951
}
920952

921-
let start = 0;
922-
let response = null;
953+
const totalChunks = Math.ceil(file.size / Client.CHUNK_SIZE);
954+
955+
// Upload first chunk alone to get the upload ID
956+
const firstChunkEnd = Math.min(Client.CHUNK_SIZE, file.size);
957+
const firstChunkHeaders = { ...headers, 'content-range': `bytes 0-${firstChunkEnd - 1}/${file.size}` };
958+
const firstChunk = file.slice(0, firstChunkEnd);
959+
const firstPayload = { ...originalPayload };
960+
firstPayload[fileParam] = new File([firstChunk], file.name);
961+
962+
let response = await this.call(method, url, firstChunkHeaders, firstPayload);
963+
const uploadId = response?.$id;
964+
965+
if (onProgress && typeof onProgress === 'function') {
966+
onProgress({
967+
$id: uploadId,
968+
progress: Math.round((firstChunkEnd / file.size) * 100),
969+
sizeUploaded: firstChunkEnd,
970+
chunksTotal: totalChunks,
971+
chunksUploaded: 1
972+
});
973+
}
923974

924-
while (start < file.size) {
925-
let end = start + Client.CHUNK_SIZE; // Prepare end for the next chunk
926-
if (end >= file.size) {
927-
end = file.size; // Adjust for the last chunk to include the last byte
928-
}
975+
if (totalChunks === 1) {
976+
return response;
977+
}
978+
979+
// Prepare remaining chunks
980+
const chunks: { start: number; end: number }[] = [];
981+
for (let i = 1; i < totalChunks; i++) {
982+
const start = i * Client.CHUNK_SIZE;
983+
const end = Math.min(start + Client.CHUNK_SIZE, file.size);
984+
chunks.push({ start, end });
985+
}
929986

930-
headers['content-range'] = `bytes ${start}-${end-1}/${file.size}`;
931-
const chunk = file.slice(start, end);
987+
// Upload remaining chunks with max concurrency of 8
988+
const CONCURRENCY = 8;
989+
let completedCount = 1;
990+
let uploadedBytes = firstChunkEnd;
991+
let lastResponse = response;
992+
let finalResponse = null;
993+
let rejected = false;
994+
995+
const isUploadComplete = (chunkResponse: any) => {
996+
const chunksUploaded = chunkResponse?.chunksUploaded;
997+
const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks;
998+
return typeof chunksUploaded === 'number' && typeof chunksTotal === 'number' && chunksUploaded >= chunksTotal;
999+
};
9321000

933-
let payload = { ...originalPayload };
934-
payload[fileParam] = new File([chunk], file.name);
1001+
const uploadChunk = async (chunk: typeof chunks[0]) => {
1002+
const chunkHeaders = { ...headers };
1003+
if (uploadId) {
1004+
chunkHeaders['x-appwrite-id'] = uploadId;
1005+
}
1006+
chunkHeaders['content-range'] = `bytes ${chunk.start}-${chunk.end - 1}/${file.size}`;
1007+
1008+
const chunkBlob = file.slice(chunk.start, chunk.end);
1009+
const chunkPayload = { ...originalPayload };
1010+
chunkPayload[fileParam] = new File([chunkBlob], file.name);
9351011

936-
response = await this.call(method, url, headers, payload);
1012+
const chunkResponse = await this.call(method, url, chunkHeaders, chunkPayload);
1013+
1014+
if (rejected) {
1015+
return chunkResponse;
1016+
}
1017+
1018+
completedCount++;
1019+
uploadedBytes += (chunk.end - chunk.start);
1020+
1021+
lastResponse = chunkResponse;
1022+
if (isUploadComplete(chunkResponse)) {
1023+
finalResponse = chunkResponse;
1024+
}
9371025

9381026
if (onProgress && typeof onProgress === 'function') {
9391027
onProgress({
940-
$id: response.$id,
941-
progress: Math.round((end / file.size) * 100),
942-
sizeUploaded: end,
943-
chunksTotal: Math.ceil(file.size / Client.CHUNK_SIZE),
944-
chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE)
1028+
$id: uploadId,
1029+
progress: Math.round((uploadedBytes / file.size) * 100),
1030+
sizeUploaded: uploadedBytes,
1031+
chunksTotal: totalChunks,
1032+
chunksUploaded: completedCount
9451033
});
9461034
}
9471035

948-
if (response && response.$id) {
949-
headers['x-appwrite-id'] = response.$id;
950-
}
1036+
return chunkResponse;
1037+
};
9511038

952-
start = end;
953-
}
1039+
await new Promise<void>((resolve, reject) => {
1040+
let nextChunk = 0;
1041+
let inFlight = 0;
1042+
let completed = 0;
1043+
1044+
const uploadNext = () => {
1045+
if (rejected) {
1046+
return;
1047+
}
1048+
1049+
if (completed === chunks.length) {
1050+
resolve();
1051+
return;
1052+
}
1053+
1054+
while (inFlight < CONCURRENCY && nextChunk < chunks.length) {
1055+
const chunk = chunks[nextChunk++];
1056+
inFlight++;
1057+
1058+
uploadChunk(chunk)
1059+
.then(() => {
1060+
inFlight--;
1061+
completed++;
1062+
uploadNext();
1063+
})
1064+
.catch((error) => {
1065+
rejected = true;
1066+
reject(error);
1067+
});
1068+
}
1069+
};
1070+
1071+
uploadNext();
1072+
});
9541073

955-
return response;
1074+
return finalResponse ?? lastResponse;
9561075
}
9571076

958-
async ping(): Promise<string> {
1077+
async ping(): Promise<unknown> {
9591078
return this.call('GET', new URL(this.config.endpoint + '/ping'));
9601079
}
9611080

0 commit comments

Comments
 (0)