@trustgraph/react-state 1.5.2 → 1.5.3
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/dist/index.cjs +548 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +548 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/state/chunked-download.d.ts +56 -0
- package/dist/state/chunked-download.d.ts.map +1 -0
- package/dist/state/chunked-upload.d.ts +84 -0
- package/dist/state/chunked-upload.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4642,6 +4642,552 @@ const useNodeDetails = (nodeId, flowId) => {
|
|
|
4642
4642
|
};
|
|
4643
4643
|
};
|
|
4644
4644
|
|
|
4645
|
+
// Default chunk size: 5MB (matches backend default)
|
|
4646
|
+
const DEFAULT_CHUNK_SIZE$1 = 5 * 1024 * 1024;
|
|
4647
|
+
// Maximum parallel chunk uploads
|
|
4648
|
+
const DEFAULT_PARALLEL_UPLOADS = 3;
|
|
4649
|
+
/**
|
|
4650
|
+
* Hook for managing chunked document uploads with progress tracking
|
|
4651
|
+
*
|
|
4652
|
+
* Features:
|
|
4653
|
+
* - Automatic chunking of large files
|
|
4654
|
+
* - Parallel chunk uploads for performance
|
|
4655
|
+
* - Progress tracking (bytes, percentage, chunks)
|
|
4656
|
+
* - Pause/resume support
|
|
4657
|
+
* - Cancel support
|
|
4658
|
+
* - Resumability after interruption
|
|
4659
|
+
*
|
|
4660
|
+
* @param options - Configuration options for the upload
|
|
4661
|
+
* @returns Upload state and control methods
|
|
4662
|
+
*/
|
|
4663
|
+
const useChunkedUpload = (options = {}) => {
|
|
4664
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE$1, parallelUploads = DEFAULT_PARALLEL_UPLOADS, onProgress, onComplete, onError, } = options;
|
|
4665
|
+
const socket = reactProvider.useSocket();
|
|
4666
|
+
const connectionState = reactProvider.useConnectionState();
|
|
4667
|
+
const queryClient = reactQuery.useQueryClient();
|
|
4668
|
+
const notify = useNotification();
|
|
4669
|
+
// Upload state
|
|
4670
|
+
const [progress, setProgress] = react.useState({
|
|
4671
|
+
totalBytes: 0,
|
|
4672
|
+
bytesUploaded: 0,
|
|
4673
|
+
percentage: 0,
|
|
4674
|
+
totalChunks: 0,
|
|
4675
|
+
chunksUploaded: 0,
|
|
4676
|
+
pendingChunks: [],
|
|
4677
|
+
status: "idle",
|
|
4678
|
+
});
|
|
4679
|
+
// Refs for managing upload lifecycle
|
|
4680
|
+
const abortControllerRef = react.useRef(null);
|
|
4681
|
+
const isPausedRef = react.useRef(false);
|
|
4682
|
+
const currentFileRef = react.useRef(null);
|
|
4683
|
+
const uploadIdRef = react.useRef(null);
|
|
4684
|
+
// Update progress and notify callback
|
|
4685
|
+
const updateProgress = react.useCallback((updates) => {
|
|
4686
|
+
setProgress((prev) => {
|
|
4687
|
+
const next = { ...prev, ...updates };
|
|
4688
|
+
onProgress?.(next);
|
|
4689
|
+
return next;
|
|
4690
|
+
});
|
|
4691
|
+
}, [onProgress]);
|
|
4692
|
+
// Read a chunk from the file as base64
|
|
4693
|
+
const readChunkAsBase64 = react.useCallback(async (file, chunkIndex, chunkSz) => {
|
|
4694
|
+
const start = chunkIndex * chunkSz;
|
|
4695
|
+
const end = Math.min(start + chunkSz, file.size);
|
|
4696
|
+
const blob = file.slice(start, end);
|
|
4697
|
+
return new Promise((resolve, reject) => {
|
|
4698
|
+
const reader = new FileReader();
|
|
4699
|
+
reader.onloadend = () => {
|
|
4700
|
+
const dataUrl = reader.result;
|
|
4701
|
+
// Extract base64 from data URL
|
|
4702
|
+
const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
|
|
4703
|
+
resolve(base64);
|
|
4704
|
+
};
|
|
4705
|
+
reader.onerror = () => reject(new Error("Failed to read file chunk"));
|
|
4706
|
+
reader.readAsDataURL(blob);
|
|
4707
|
+
});
|
|
4708
|
+
}, []);
|
|
4709
|
+
// Upload chunks in parallel with concurrency limit
|
|
4710
|
+
const uploadChunks = react.useCallback(async (file, uploadId, pendingChunks, chunkSz, totalChunks) => {
|
|
4711
|
+
const remaining = [...pendingChunks];
|
|
4712
|
+
let completedCount = totalChunks - remaining.length;
|
|
4713
|
+
let bytesUploaded = completedCount * chunkSz;
|
|
4714
|
+
// Process chunks with limited parallelism
|
|
4715
|
+
const uploadNextBatch = async () => {
|
|
4716
|
+
while (remaining.length > 0 && !abortControllerRef.current?.signal.aborted) {
|
|
4717
|
+
// Wait if paused
|
|
4718
|
+
if (isPausedRef.current) {
|
|
4719
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4720
|
+
continue;
|
|
4721
|
+
}
|
|
4722
|
+
// Take up to parallelUploads chunks
|
|
4723
|
+
const batch = remaining.splice(0, parallelUploads);
|
|
4724
|
+
// Upload batch in parallel
|
|
4725
|
+
const results = await Promise.allSettled(batch.map(async (chunkIndex) => {
|
|
4726
|
+
const content = await readChunkAsBase64(file, chunkIndex, chunkSz);
|
|
4727
|
+
await socket.librarian().uploadChunk(uploadId, chunkIndex, content);
|
|
4728
|
+
return chunkIndex;
|
|
4729
|
+
}));
|
|
4730
|
+
// Process results
|
|
4731
|
+
for (const result of results) {
|
|
4732
|
+
if (result.status === "fulfilled") {
|
|
4733
|
+
completedCount++;
|
|
4734
|
+
// Calculate actual bytes for this chunk
|
|
4735
|
+
const chunkIdx = result.value;
|
|
4736
|
+
const chunkStart = chunkIdx * chunkSz;
|
|
4737
|
+
const chunkEnd = Math.min(chunkStart + chunkSz, file.size);
|
|
4738
|
+
bytesUploaded += chunkEnd - chunkStart;
|
|
4739
|
+
updateProgress({
|
|
4740
|
+
chunksUploaded: completedCount,
|
|
4741
|
+
bytesUploaded,
|
|
4742
|
+
percentage: Math.round((bytesUploaded / file.size) * 100),
|
|
4743
|
+
pendingChunks: [...remaining],
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
else {
|
|
4747
|
+
// Re-add failed chunk to retry
|
|
4748
|
+
const failedIndex = batch[results.indexOf(result)];
|
|
4749
|
+
remaining.push(failedIndex);
|
|
4750
|
+
console.warn(`Chunk ${failedIndex} failed, will retry:`, result.reason);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
};
|
|
4755
|
+
await uploadNextBatch();
|
|
4756
|
+
}, [socket, parallelUploads, readChunkAsBase64, updateProgress]);
|
|
4757
|
+
/**
|
|
4758
|
+
* Start a new chunked upload
|
|
4759
|
+
*/
|
|
4760
|
+
const upload = react.useCallback(async (params) => {
|
|
4761
|
+
const { file, title, comments = "", tags = [], collection, documentId } = params;
|
|
4762
|
+
// Validate connection
|
|
4763
|
+
if (connectionState?.status !== "authenticated" &&
|
|
4764
|
+
connectionState?.status !== "unauthenticated") {
|
|
4765
|
+
const error = "Not connected to server";
|
|
4766
|
+
updateProgress({ status: "error", error });
|
|
4767
|
+
onError?.(error);
|
|
4768
|
+
return null;
|
|
4769
|
+
}
|
|
4770
|
+
// Reset state
|
|
4771
|
+
abortControllerRef.current = new AbortController();
|
|
4772
|
+
isPausedRef.current = false;
|
|
4773
|
+
currentFileRef.current = file;
|
|
4774
|
+
const docId = documentId || createDocId();
|
|
4775
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
4776
|
+
const pendingChunks = Array.from({ length: totalChunks }, (_, i) => i);
|
|
4777
|
+
updateProgress({
|
|
4778
|
+
totalBytes: file.size,
|
|
4779
|
+
bytesUploaded: 0,
|
|
4780
|
+
percentage: 0,
|
|
4781
|
+
totalChunks,
|
|
4782
|
+
chunksUploaded: 0,
|
|
4783
|
+
pendingChunks,
|
|
4784
|
+
status: "preparing",
|
|
4785
|
+
error: undefined,
|
|
4786
|
+
uploadId: undefined,
|
|
4787
|
+
documentId: undefined,
|
|
4788
|
+
});
|
|
4789
|
+
try {
|
|
4790
|
+
// Initialize upload session
|
|
4791
|
+
const metadata = {
|
|
4792
|
+
id: docId,
|
|
4793
|
+
time: Math.floor(Date.now() / 1000),
|
|
4794
|
+
kind: file.type || "application/octet-stream",
|
|
4795
|
+
title,
|
|
4796
|
+
comments,
|
|
4797
|
+
user: "trustgraph", // Will be set by server based on auth
|
|
4798
|
+
collection: collection || "default",
|
|
4799
|
+
tags,
|
|
4800
|
+
};
|
|
4801
|
+
const beginResponse = await socket
|
|
4802
|
+
.librarian()
|
|
4803
|
+
.beginUpload(metadata, file.size, chunkSize);
|
|
4804
|
+
const uploadId = beginResponse["upload-id"];
|
|
4805
|
+
uploadIdRef.current = uploadId;
|
|
4806
|
+
updateProgress({
|
|
4807
|
+
status: "uploading",
|
|
4808
|
+
uploadId,
|
|
4809
|
+
});
|
|
4810
|
+
// Upload all chunks
|
|
4811
|
+
await uploadChunks(file, uploadId, pendingChunks, chunkSize, totalChunks);
|
|
4812
|
+
// Check if cancelled
|
|
4813
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
4814
|
+
return null;
|
|
4815
|
+
}
|
|
4816
|
+
// Complete the upload
|
|
4817
|
+
updateProgress({ status: "completing" });
|
|
4818
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
4819
|
+
const finalDocId = completeResponse["document-id"];
|
|
4820
|
+
updateProgress({
|
|
4821
|
+
status: "completed",
|
|
4822
|
+
documentId: finalDocId,
|
|
4823
|
+
percentage: 100,
|
|
4824
|
+
});
|
|
4825
|
+
// Invalidate documents cache
|
|
4826
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
4827
|
+
notify.success(`Upload complete: ${title}`);
|
|
4828
|
+
onComplete?.(finalDocId);
|
|
4829
|
+
return finalDocId;
|
|
4830
|
+
}
|
|
4831
|
+
catch (err) {
|
|
4832
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
4833
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
4834
|
+
notify.error(`Upload failed: ${errorMsg}`);
|
|
4835
|
+
onError?.(errorMsg);
|
|
4836
|
+
return null;
|
|
4837
|
+
}
|
|
4838
|
+
}, [
|
|
4839
|
+
socket,
|
|
4840
|
+
connectionState,
|
|
4841
|
+
chunkSize,
|
|
4842
|
+
queryClient,
|
|
4843
|
+
notify,
|
|
4844
|
+
updateProgress,
|
|
4845
|
+
uploadChunks,
|
|
4846
|
+
onComplete,
|
|
4847
|
+
onError,
|
|
4848
|
+
]);
|
|
4849
|
+
/**
|
|
4850
|
+
* Resume an interrupted upload
|
|
4851
|
+
*/
|
|
4852
|
+
const resume = react.useCallback(async (params) => {
|
|
4853
|
+
const { uploadId, file } = params;
|
|
4854
|
+
// Validate connection
|
|
4855
|
+
if (connectionState?.status !== "authenticated" &&
|
|
4856
|
+
connectionState?.status !== "unauthenticated") {
|
|
4857
|
+
const error = "Not connected to server";
|
|
4858
|
+
updateProgress({ status: "error", error });
|
|
4859
|
+
onError?.(error);
|
|
4860
|
+
return null;
|
|
4861
|
+
}
|
|
4862
|
+
abortControllerRef.current = new AbortController();
|
|
4863
|
+
isPausedRef.current = false;
|
|
4864
|
+
currentFileRef.current = file;
|
|
4865
|
+
uploadIdRef.current = uploadId;
|
|
4866
|
+
updateProgress({
|
|
4867
|
+
status: "preparing",
|
|
4868
|
+
uploadId,
|
|
4869
|
+
});
|
|
4870
|
+
try {
|
|
4871
|
+
// Get current upload status
|
|
4872
|
+
const status = await socket.librarian().getUploadStatus(uploadId);
|
|
4873
|
+
if (status["upload-state"] === "completed") {
|
|
4874
|
+
updateProgress({ status: "completed" });
|
|
4875
|
+
return null;
|
|
4876
|
+
}
|
|
4877
|
+
if (status["upload-state"] === "expired") {
|
|
4878
|
+
throw new Error("Upload session has expired");
|
|
4879
|
+
}
|
|
4880
|
+
const totalChunks = status["total-chunks"];
|
|
4881
|
+
const missingChunks = status["missing-chunks"];
|
|
4882
|
+
const bytesReceived = status["bytes-received"];
|
|
4883
|
+
const totalBytes = status["total-bytes"];
|
|
4884
|
+
updateProgress({
|
|
4885
|
+
totalBytes,
|
|
4886
|
+
bytesUploaded: bytesReceived,
|
|
4887
|
+
percentage: Math.round((bytesReceived / totalBytes) * 100),
|
|
4888
|
+
totalChunks,
|
|
4889
|
+
chunksUploaded: totalChunks - missingChunks.length,
|
|
4890
|
+
pendingChunks: missingChunks,
|
|
4891
|
+
status: "uploading",
|
|
4892
|
+
});
|
|
4893
|
+
// Upload missing chunks
|
|
4894
|
+
const effectiveChunkSize = status["chunk-size"] || chunkSize;
|
|
4895
|
+
await uploadChunks(file, uploadId, missingChunks, effectiveChunkSize, totalChunks);
|
|
4896
|
+
// Check if cancelled
|
|
4897
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
4898
|
+
return null;
|
|
4899
|
+
}
|
|
4900
|
+
// Complete the upload
|
|
4901
|
+
updateProgress({ status: "completing" });
|
|
4902
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
4903
|
+
const finalDocId = completeResponse["document-id"];
|
|
4904
|
+
updateProgress({
|
|
4905
|
+
status: "completed",
|
|
4906
|
+
documentId: finalDocId,
|
|
4907
|
+
percentage: 100,
|
|
4908
|
+
});
|
|
4909
|
+
// Invalidate documents cache
|
|
4910
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
4911
|
+
notify.success("Upload resumed and completed");
|
|
4912
|
+
onComplete?.(finalDocId);
|
|
4913
|
+
return finalDocId;
|
|
4914
|
+
}
|
|
4915
|
+
catch (err) {
|
|
4916
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
4917
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
4918
|
+
notify.error(`Resume failed: ${errorMsg}`);
|
|
4919
|
+
onError?.(errorMsg);
|
|
4920
|
+
return null;
|
|
4921
|
+
}
|
|
4922
|
+
}, [
|
|
4923
|
+
socket,
|
|
4924
|
+
connectionState,
|
|
4925
|
+
chunkSize,
|
|
4926
|
+
queryClient,
|
|
4927
|
+
notify,
|
|
4928
|
+
updateProgress,
|
|
4929
|
+
uploadChunks,
|
|
4930
|
+
onComplete,
|
|
4931
|
+
onError,
|
|
4932
|
+
]);
|
|
4933
|
+
/**
|
|
4934
|
+
* Pause the current upload
|
|
4935
|
+
*/
|
|
4936
|
+
const pause = react.useCallback(() => {
|
|
4937
|
+
if (progress.status === "uploading") {
|
|
4938
|
+
isPausedRef.current = true;
|
|
4939
|
+
updateProgress({ status: "paused" });
|
|
4940
|
+
}
|
|
4941
|
+
}, [progress.status, updateProgress]);
|
|
4942
|
+
/**
|
|
4943
|
+
* Resume a paused upload (not to be confused with resuming an interrupted upload)
|
|
4944
|
+
*/
|
|
4945
|
+
const unpause = react.useCallback(() => {
|
|
4946
|
+
if (progress.status === "paused") {
|
|
4947
|
+
isPausedRef.current = false;
|
|
4948
|
+
updateProgress({ status: "uploading" });
|
|
4949
|
+
}
|
|
4950
|
+
}, [progress.status, updateProgress]);
|
|
4951
|
+
/**
|
|
4952
|
+
* Cancel the current upload
|
|
4953
|
+
*/
|
|
4954
|
+
const cancel = react.useCallback(async () => {
|
|
4955
|
+
abortControllerRef.current?.abort();
|
|
4956
|
+
if (uploadIdRef.current) {
|
|
4957
|
+
try {
|
|
4958
|
+
await socket.librarian().abortUpload(uploadIdRef.current);
|
|
4959
|
+
}
|
|
4960
|
+
catch (err) {
|
|
4961
|
+
console.warn("Failed to abort upload on server:", err);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
updateProgress({
|
|
4965
|
+
status: "cancelled",
|
|
4966
|
+
pendingChunks: [],
|
|
4967
|
+
});
|
|
4968
|
+
uploadIdRef.current = null;
|
|
4969
|
+
currentFileRef.current = null;
|
|
4970
|
+
}, [socket, updateProgress]);
|
|
4971
|
+
/**
|
|
4972
|
+
* Reset the upload state to idle
|
|
4973
|
+
*/
|
|
4974
|
+
const reset = react.useCallback(() => {
|
|
4975
|
+
abortControllerRef.current?.abort();
|
|
4976
|
+
uploadIdRef.current = null;
|
|
4977
|
+
currentFileRef.current = null;
|
|
4978
|
+
isPausedRef.current = false;
|
|
4979
|
+
setProgress({
|
|
4980
|
+
totalBytes: 0,
|
|
4981
|
+
bytesUploaded: 0,
|
|
4982
|
+
percentage: 0,
|
|
4983
|
+
totalChunks: 0,
|
|
4984
|
+
chunksUploaded: 0,
|
|
4985
|
+
pendingChunks: [],
|
|
4986
|
+
status: "idle",
|
|
4987
|
+
});
|
|
4988
|
+
}, []);
|
|
4989
|
+
return {
|
|
4990
|
+
// Current progress state
|
|
4991
|
+
progress,
|
|
4992
|
+
// Control methods
|
|
4993
|
+
upload,
|
|
4994
|
+
resume,
|
|
4995
|
+
pause,
|
|
4996
|
+
unpause,
|
|
4997
|
+
cancel,
|
|
4998
|
+
reset,
|
|
4999
|
+
// Convenience flags
|
|
5000
|
+
isIdle: progress.status === "idle",
|
|
5001
|
+
isUploading: progress.status === "uploading",
|
|
5002
|
+
isPaused: progress.status === "paused",
|
|
5003
|
+
isCompleted: progress.status === "completed",
|
|
5004
|
+
isError: progress.status === "error",
|
|
5005
|
+
};
|
|
5006
|
+
};
|
|
5007
|
+
|
|
5008
|
+
// Default chunk size for downloads: 1MB
|
|
5009
|
+
const DEFAULT_CHUNK_SIZE = 1024 * 1024;
|
|
5010
|
+
/**
|
|
5011
|
+
* Decode base64 string to Uint8Array
|
|
5012
|
+
*/
|
|
5013
|
+
const base64ToUint8Array = (base64) => {
|
|
5014
|
+
const binaryString = atob(base64);
|
|
5015
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
5016
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
5017
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
5018
|
+
}
|
|
5019
|
+
return bytes;
|
|
5020
|
+
};
|
|
5021
|
+
/**
|
|
5022
|
+
* Trigger browser download of a Blob
|
|
5023
|
+
*/
|
|
5024
|
+
const triggerBrowserDownload = (blob, filename) => {
|
|
5025
|
+
const url = URL.createObjectURL(blob);
|
|
5026
|
+
const link = document.createElement("a");
|
|
5027
|
+
link.href = url;
|
|
5028
|
+
link.download = filename;
|
|
5029
|
+
document.body.appendChild(link);
|
|
5030
|
+
link.click();
|
|
5031
|
+
document.body.removeChild(link);
|
|
5032
|
+
URL.revokeObjectURL(url);
|
|
5033
|
+
};
|
|
5034
|
+
/**
|
|
5035
|
+
* Hook for managing streamed document downloads with progress tracking
|
|
5036
|
+
*
|
|
5037
|
+
* Features:
|
|
5038
|
+
* - Streams large documents via WebSocket streaming response
|
|
5039
|
+
* - Progress tracking (chunks received, percentage)
|
|
5040
|
+
* - Cancel support
|
|
5041
|
+
* - Returns Blob or triggers browser download
|
|
5042
|
+
*
|
|
5043
|
+
* @param options - Configuration options for the download
|
|
5044
|
+
* @returns Download state and control methods
|
|
5045
|
+
*/
|
|
5046
|
+
const useChunkedDownload = (options = {}) => {
|
|
5047
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE, onProgress, onComplete, onError, } = options;
|
|
5048
|
+
const socket = reactProvider.useSocket();
|
|
5049
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5050
|
+
const notify = useNotification();
|
|
5051
|
+
// Download state
|
|
5052
|
+
const [progress, setProgress] = react.useState({
|
|
5053
|
+
totalChunks: 0,
|
|
5054
|
+
chunksReceived: 0,
|
|
5055
|
+
percentage: 0,
|
|
5056
|
+
status: "idle",
|
|
5057
|
+
});
|
|
5058
|
+
// Refs for managing download lifecycle
|
|
5059
|
+
const cancelledRef = react.useRef(false);
|
|
5060
|
+
const chunksRef = react.useRef(new Map());
|
|
5061
|
+
// Update progress and notify callback
|
|
5062
|
+
const updateProgress = react.useCallback((updates) => {
|
|
5063
|
+
setProgress((prev) => {
|
|
5064
|
+
const next = { ...prev, ...updates };
|
|
5065
|
+
onProgress?.(next);
|
|
5066
|
+
return next;
|
|
5067
|
+
});
|
|
5068
|
+
}, [onProgress]);
|
|
5069
|
+
/**
|
|
5070
|
+
* Download a document via streaming and return as Blob
|
|
5071
|
+
*/
|
|
5072
|
+
const download = react.useCallback((params) => {
|
|
5073
|
+
const { documentId, mimeType = "application/octet-stream", filename } = params;
|
|
5074
|
+
// Validate connection
|
|
5075
|
+
if (connectionState?.status !== "authenticated" &&
|
|
5076
|
+
connectionState?.status !== "unauthenticated") {
|
|
5077
|
+
const error = "Not connected to server";
|
|
5078
|
+
updateProgress({ status: "error", error });
|
|
5079
|
+
onError?.(error);
|
|
5080
|
+
return Promise.resolve(null);
|
|
5081
|
+
}
|
|
5082
|
+
// Reset state
|
|
5083
|
+
cancelledRef.current = false;
|
|
5084
|
+
chunksRef.current = new Map();
|
|
5085
|
+
updateProgress({
|
|
5086
|
+
totalChunks: 0,
|
|
5087
|
+
chunksReceived: 0,
|
|
5088
|
+
percentage: 0,
|
|
5089
|
+
status: "downloading",
|
|
5090
|
+
error: undefined,
|
|
5091
|
+
documentId,
|
|
5092
|
+
});
|
|
5093
|
+
return new Promise((resolve) => {
|
|
5094
|
+
const onChunk = (content, chunkIndex, totalChunks, complete) => {
|
|
5095
|
+
// Check for cancellation
|
|
5096
|
+
if (cancelledRef.current) {
|
|
5097
|
+
return;
|
|
5098
|
+
}
|
|
5099
|
+
// Store chunk
|
|
5100
|
+
const chunkData = base64ToUint8Array(content);
|
|
5101
|
+
chunksRef.current.set(chunkIndex, chunkData);
|
|
5102
|
+
const chunksReceived = chunksRef.current.size;
|
|
5103
|
+
const percentage = totalChunks > 0
|
|
5104
|
+
? Math.round((chunksReceived / totalChunks) * 100)
|
|
5105
|
+
: 0;
|
|
5106
|
+
updateProgress({
|
|
5107
|
+
totalChunks,
|
|
5108
|
+
chunksReceived,
|
|
5109
|
+
percentage,
|
|
5110
|
+
});
|
|
5111
|
+
// If complete, reassemble and return
|
|
5112
|
+
if (complete) {
|
|
5113
|
+
if (cancelledRef.current) {
|
|
5114
|
+
resolve(null);
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
// Reassemble chunks in order
|
|
5118
|
+
const orderedChunks = [];
|
|
5119
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
5120
|
+
const chunk = chunksRef.current.get(i);
|
|
5121
|
+
if (chunk) {
|
|
5122
|
+
orderedChunks.push(chunk);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const blob = new Blob(orderedChunks, { type: mimeType });
|
|
5126
|
+
updateProgress({
|
|
5127
|
+
status: "completed",
|
|
5128
|
+
percentage: 100,
|
|
5129
|
+
chunksReceived: totalChunks,
|
|
5130
|
+
});
|
|
5131
|
+
// Trigger browser download if filename provided
|
|
5132
|
+
if (filename) {
|
|
5133
|
+
triggerBrowserDownload(blob, filename);
|
|
5134
|
+
}
|
|
5135
|
+
notify.success("Download complete");
|
|
5136
|
+
onComplete?.(blob, documentId);
|
|
5137
|
+
resolve(blob);
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
const onStreamError = (error) => {
|
|
5141
|
+
if (cancelledRef.current) {
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
updateProgress({ status: "error", error });
|
|
5145
|
+
notify.error(`Download failed: ${error}`);
|
|
5146
|
+
onError?.(error);
|
|
5147
|
+
resolve(null);
|
|
5148
|
+
};
|
|
5149
|
+
// Start streaming download
|
|
5150
|
+
socket.librarian().streamDocument(documentId, onChunk, onStreamError, chunkSize);
|
|
5151
|
+
});
|
|
5152
|
+
}, [socket, connectionState, chunkSize, notify, updateProgress, onComplete, onError]);
|
|
5153
|
+
/**
|
|
5154
|
+
* Cancel the current download
|
|
5155
|
+
*/
|
|
5156
|
+
const cancel = react.useCallback(() => {
|
|
5157
|
+
cancelledRef.current = true;
|
|
5158
|
+
chunksRef.current = new Map();
|
|
5159
|
+
updateProgress({
|
|
5160
|
+
status: "cancelled",
|
|
5161
|
+
});
|
|
5162
|
+
}, [updateProgress]);
|
|
5163
|
+
/**
|
|
5164
|
+
* Reset the download state to idle
|
|
5165
|
+
*/
|
|
5166
|
+
const reset = react.useCallback(() => {
|
|
5167
|
+
cancelledRef.current = true;
|
|
5168
|
+
chunksRef.current = new Map();
|
|
5169
|
+
setProgress({
|
|
5170
|
+
totalChunks: 0,
|
|
5171
|
+
chunksReceived: 0,
|
|
5172
|
+
percentage: 0,
|
|
5173
|
+
status: "idle",
|
|
5174
|
+
});
|
|
5175
|
+
}, []);
|
|
5176
|
+
return {
|
|
5177
|
+
// Current progress state
|
|
5178
|
+
progress,
|
|
5179
|
+
// Control methods
|
|
5180
|
+
download,
|
|
5181
|
+
cancel,
|
|
5182
|
+
reset,
|
|
5183
|
+
// Convenience flags
|
|
5184
|
+
isIdle: progress.status === "idle",
|
|
5185
|
+
isDownloading: progress.status === "downloading",
|
|
5186
|
+
isCompleted: progress.status === "completed",
|
|
5187
|
+
isError: progress.status === "error",
|
|
5188
|
+
};
|
|
5189
|
+
};
|
|
5190
|
+
|
|
4645
5191
|
Object.defineProperty(exports, "ConnectionStateContext", {
|
|
4646
5192
|
enumerable: true,
|
|
4647
5193
|
get: function () { return reactProvider.ConnectionStateContext; }
|
|
@@ -4669,6 +5215,8 @@ exports.useActivity = useActivity;
|
|
|
4669
5215
|
exports.useAgentTools = useAgentTools;
|
|
4670
5216
|
exports.useChat = useChat;
|
|
4671
5217
|
exports.useChatSession = useChatSession;
|
|
5218
|
+
exports.useChunkedDownload = useChunkedDownload;
|
|
5219
|
+
exports.useChunkedUpload = useChunkedUpload;
|
|
4672
5220
|
exports.useCollections = useCollections;
|
|
4673
5221
|
exports.useConversation = useConversation;
|
|
4674
5222
|
exports.useDocumentEmbeddingsQuery = useDocumentEmbeddingsQuery;
|