@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 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;