From bf8f32a55d9836ab0cc2a0ceb5453a89f3e3fc5e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 21 May 2026 17:19:47 +0400 Subject: [PATCH 1/4] feat: support concurrent chunk uploads --- package-lock.json | 4 +- package.json | 2 +- src/client.ts | 123 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 101 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 685364d8..bbeff03c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@appwrite.io/console", - "version": "13.0.0", + "version": "13.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@appwrite.io/console", - "version": "13.0.0", + "version": "13.1.0", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" diff --git a/package.json b/package.json index 93313761..17c9e470 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@appwrite.io/console", "homepage": "https://appwrite.io/support", "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", - "version": "13.0.0", + "version": "13.1.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { diff --git a/src/client.ts b/src/client.ts index 8db723f2..97e80fa4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -390,8 +390,8 @@ class Client { 'x-sdk-name': 'Console', 'x-sdk-platform': 'console', 'x-sdk-language': 'web', - 'x-sdk-version': '13.0.0', - 'X-Appwrite-Response-Format': '1.9.4', + 'x-sdk-version': '13.1.0', + 'X-Appwrite-Response-Format': '1.9.5', }; /** @@ -978,41 +978,114 @@ class Client { return await this.call(method, url, headers, originalPayload); } - let start = 0; - let response = null; + const totalChunks = Math.ceil(file.size / Client.CHUNK_SIZE); + + // Upload first chunk alone to get the upload ID + const firstChunkEnd = Math.min(Client.CHUNK_SIZE, file.size); + const firstChunkHeaders = { ...headers, 'content-range': `bytes 0-${firstChunkEnd - 1}/${file.size}` }; + const firstChunk = file.slice(0, firstChunkEnd); + const firstPayload = { ...originalPayload }; + firstPayload[fileParam] = new File([firstChunk], file.name); + + let response = await this.call(method, url, firstChunkHeaders, firstPayload); + const uploadId = response?.$id; + + if (onProgress && typeof onProgress === 'function') { + onProgress({ + $id: uploadId, + progress: Math.round((firstChunkEnd / file.size) * 100), + sizeUploaded: firstChunkEnd, + chunksTotal: totalChunks, + chunksUploaded: 1 + }); + } - while (start < file.size) { - let end = start + Client.CHUNK_SIZE; // Prepare end for the next chunk - if (end >= file.size) { - end = file.size; // Adjust for the last chunk to include the last byte - } + if (totalChunks === 1) { + return response; + } + + // Prepare remaining chunks + const chunks: { index: number; start: number; end: number }[] = []; + for (let i = 1; i < totalChunks; i++) { + const start = i * Client.CHUNK_SIZE; + const end = Math.min(start + Client.CHUNK_SIZE, file.size); + chunks.push({ index: i, start, end }); + } - headers['content-range'] = `bytes ${start}-${end-1}/${file.size}`; - const chunk = file.slice(start, end); + // Upload remaining chunks with max concurrency of 8 + const CONCURRENCY = 8; + let completedCount = 1; + let uploadedBytes = firstChunkEnd; + let lastResponse = response; - let payload = { ...originalPayload }; - payload[fileParam] = new File([chunk], file.name); + const isUploadComplete = (chunkResponse: any) => { + const chunksUploaded = chunkResponse?.chunksUploaded; + const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks; + return typeof chunksUploaded === 'number' && typeof chunksTotal === 'number' && chunksUploaded >= chunksTotal; + }; - response = await this.call(method, url, headers, payload); + const uploadChunk = async (chunk: typeof chunks[0]) => { + const chunkHeaders = { ...headers }; + if (uploadId) { + chunkHeaders['x-appwrite-id'] = uploadId; + } + chunkHeaders['content-range'] = `bytes ${chunk.start}-${chunk.end - 1}/${file.size}`; + + const chunkBlob = file.slice(chunk.start, chunk.end); + const chunkPayload = { ...originalPayload }; + chunkPayload[fileParam] = new File([chunkBlob], file.name); + + const chunkResponse = await this.call(method, url, chunkHeaders, chunkPayload); + + completedCount++; + uploadedBytes += (chunk.end - chunk.start); + + if (isUploadComplete(chunkResponse)) { + lastResponse = chunkResponse; + } if (onProgress && typeof onProgress === 'function') { onProgress({ - $id: response.$id, - progress: Math.round((end / file.size) * 100), - sizeUploaded: end, - chunksTotal: Math.ceil(file.size / Client.CHUNK_SIZE), - chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE) + $id: uploadId, + progress: Math.round((uploadedBytes / file.size) * 100), + sizeUploaded: uploadedBytes, + chunksTotal: totalChunks, + chunksUploaded: completedCount }); } - if (response && response.$id) { - headers['x-appwrite-id'] = response.$id; - } + return chunkResponse; + }; - start = end; - } + await new Promise((resolve, reject) => { + let nextChunk = 0; + let inFlight = 0; + let completed = 0; + + const uploadNext = () => { + if (completed === chunks.length) { + resolve(); + return; + } + + while (inFlight < CONCURRENCY && nextChunk < chunks.length) { + const chunk = chunks[nextChunk++]; + inFlight++; + + uploadChunk(chunk) + .then(() => { + inFlight--; + completed++; + uploadNext(); + }) + .catch(reject); + } + }; + + uploadNext(); + }); - return response; + return lastResponse; } async ping(): Promise { From d6d6961e89bd02ecf58d09e7ac39aebeea3fc42e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 21 May 2026 20:45:56 +0400 Subject: [PATCH 2/4] feat: support concurrent chunk uploads --- src/client.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client.ts b/src/client.ts index 97e80fa4..e162b40e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1005,11 +1005,11 @@ class Client { } // Prepare remaining chunks - const chunks: { index: number; start: number; end: number }[] = []; + const chunks: { start: number; end: number }[] = []; for (let i = 1; i < totalChunks; i++) { const start = i * Client.CHUNK_SIZE; const end = Math.min(start + Client.CHUNK_SIZE, file.size); - chunks.push({ index: i, start, end }); + chunks.push({ start, end }); } // Upload remaining chunks with max concurrency of 8 @@ -1017,6 +1017,7 @@ class Client { let completedCount = 1; let uploadedBytes = firstChunkEnd; let lastResponse = response; + let finalResponse = null; const isUploadComplete = (chunkResponse: any) => { const chunksUploaded = chunkResponse?.chunksUploaded; @@ -1040,8 +1041,9 @@ class Client { completedCount++; uploadedBytes += (chunk.end - chunk.start); + lastResponse = chunkResponse; if (isUploadComplete(chunkResponse)) { - lastResponse = chunkResponse; + finalResponse = chunkResponse; } if (onProgress && typeof onProgress === 'function') { @@ -1085,10 +1087,10 @@ class Client { uploadNext(); }); - return lastResponse; + return finalResponse ?? lastResponse; } - async ping(): Promise { + async ping(): Promise { return this.call('GET', new URL(this.config.endpoint + '/ping')); } From 8eb98667767368a51955fe955fb58efa1367082a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 21 May 2026 21:10:25 +0400 Subject: [PATCH 3/4] feat: support concurrent chunk uploads --- src/client.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index e162b40e..1f9035ae 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1063,8 +1063,13 @@ class Client { let nextChunk = 0; let inFlight = 0; let completed = 0; + let rejected = false; const uploadNext = () => { + if (rejected) { + return; + } + if (completed === chunks.length) { resolve(); return; @@ -1080,7 +1085,10 @@ class Client { completed++; uploadNext(); }) - .catch(reject); + .catch((error) => { + rejected = true; + reject(error); + }); } }; From ae294de73a390af9fadcf634d6c162c55bc9ace7 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 21 May 2026 21:32:55 +0400 Subject: [PATCH 4/4] feat: support concurrent chunk uploads --- src/client.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 1f9035ae..c1ca5831 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1018,6 +1018,7 @@ class Client { let uploadedBytes = firstChunkEnd; let lastResponse = response; let finalResponse = null; + let rejected = false; const isUploadComplete = (chunkResponse: any) => { const chunksUploaded = chunkResponse?.chunksUploaded; @@ -1037,6 +1038,10 @@ class Client { chunkPayload[fileParam] = new File([chunkBlob], file.name); const chunkResponse = await this.call(method, url, chunkHeaders, chunkPayload); + + if (rejected) { + return chunkResponse; + } completedCount++; uploadedBytes += (chunk.end - chunk.start); @@ -1063,7 +1068,6 @@ class Client { let nextChunk = 0; let inFlight = 0; let completed = 0; - let rejected = false; const uploadNext = () => { if (rejected) { @@ -1098,7 +1102,7 @@ class Client { return finalResponse ?? lastResponse; } - async ping(): Promise { + async ping(): Promise { return this.call('GET', new URL(this.config.endpoint + '/ping')); }