@arke-institute/sdk 3.6.0 → 3.6.2

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.
@@ -43,6 +43,7 @@ __export(operations_exports, {
43
43
  isCasConflictError: () => isCasConflictError,
44
44
  scanFileList: () => scanFileList,
45
45
  scanFileSystemEntries: () => scanFileSystemEntries,
46
+ uploadToEntity: () => uploadToEntity,
46
47
  uploadTree: () => uploadTree,
47
48
  verifyCid: () => verifyCid,
48
49
  withCasRetry: () => withCasRetry
@@ -859,6 +860,185 @@ function buildUploadTree(items) {
859
860
  return { files, folders };
860
861
  }
861
862
 
863
+ // src/operations/upload/single.ts
864
+ var MAX_CAS_RETRIES = 3;
865
+ async function uploadToEntity(client, entityId, items, options = {}) {
866
+ if (items.length === 0) {
867
+ throw new Error("At least one upload item is required");
868
+ }
869
+ const { onProgress } = options;
870
+ let progressState = {
871
+ phase: "preparing",
872
+ totalBytes: 0,
873
+ uploadedBytes: 0,
874
+ completedFiles: 0,
875
+ totalFiles: items.length
876
+ };
877
+ const reportProgress = (update) => {
878
+ progressState = { ...progressState, ...update };
879
+ onProgress?.(progressState);
880
+ };
881
+ reportProgress({ phase: "preparing" });
882
+ const prepared = await Promise.all(
883
+ items.map((item) => prepareItem(item))
884
+ );
885
+ const keys = prepared.map((p) => p.key);
886
+ const duplicates = keys.filter((key, i) => keys.indexOf(key) !== i);
887
+ if (duplicates.length > 0) {
888
+ const uniqueDupes = [...new Set(duplicates)];
889
+ throw new Error(
890
+ `Duplicate content keys detected: ${uniqueDupes.map((k) => `"${k}"`).join(", ")}. Each file must have a unique key. Provide explicit keys to resolve.`
891
+ );
892
+ }
893
+ const totalBytes = prepared.reduce((sum, p) => sum + p.size, 0);
894
+ reportProgress({ totalBytes });
895
+ const uploadInfos = await Promise.all(
896
+ prepared.map(async (item) => {
897
+ const { data: presigned, error } = await client.api.POST(
898
+ "/entities/{id}/content/upload-url",
899
+ {
900
+ params: { path: { id: entityId } },
901
+ body: {
902
+ cid: item.cid,
903
+ content_type: item.contentType,
904
+ size: item.size
905
+ }
906
+ }
907
+ );
908
+ if (error || !presigned) {
909
+ throw new Error(
910
+ `Failed to get upload URL for ${item.key}: ${JSON.stringify(error)}`
911
+ );
912
+ }
913
+ return {
914
+ ...item,
915
+ uploadUrl: presigned.upload_url
916
+ };
917
+ })
918
+ );
919
+ reportProgress({ phase: "uploading", uploadedBytes: 0 });
920
+ let uploadedBytes = 0;
921
+ await Promise.all(
922
+ uploadInfos.map(async (item) => {
923
+ const response = await fetch(item.uploadUrl, {
924
+ method: "PUT",
925
+ headers: { "Content-Type": item.contentType },
926
+ body: item.bytes
927
+ });
928
+ if (!response.ok) {
929
+ throw new Error(
930
+ `Upload to R2 failed for ${item.key}: ${response.statusText}`
931
+ );
932
+ }
933
+ uploadedBytes += item.size;
934
+ reportProgress({ uploadedBytes });
935
+ })
936
+ );
937
+ const { data: tipData, error: tipError } = await client.api.GET(
938
+ "/entities/{id}/tip",
939
+ { params: { path: { id: entityId } } }
940
+ );
941
+ if (tipError || !tipData) {
942
+ throw new Error(`Failed to get entity tip: ${JSON.stringify(tipError)}`);
943
+ }
944
+ const prevCid = tipData.cid;
945
+ let currentTip = prevCid;
946
+ reportProgress({ phase: "completing", completedFiles: 0 });
947
+ const contents = [];
948
+ let finalCid = currentTip;
949
+ for (let i = 0; i < uploadInfos.length; i++) {
950
+ const item = uploadInfos[i];
951
+ const result = await completeWithRetry(client, entityId, item, currentTip);
952
+ currentTip = result.cid;
953
+ finalCid = result.cid;
954
+ contents.push({
955
+ key: item.key,
956
+ cid: result.contentCid,
957
+ size: item.size,
958
+ contentType: item.contentType,
959
+ filename: item.filename
960
+ });
961
+ reportProgress({ completedFiles: i + 1 });
962
+ }
963
+ return {
964
+ id: entityId,
965
+ cid: finalCid,
966
+ prevCid,
967
+ contents
968
+ };
969
+ }
970
+ async function prepareItem(item) {
971
+ const { data } = item;
972
+ let bytes;
973
+ if (data instanceof Blob) {
974
+ bytes = await data.arrayBuffer();
975
+ } else if (data instanceof ArrayBuffer) {
976
+ bytes = data;
977
+ } else {
978
+ const buffer = new ArrayBuffer(data.byteLength);
979
+ new Uint8Array(buffer).set(data);
980
+ bytes = buffer;
981
+ }
982
+ const size = bytes.byteLength;
983
+ let filename = item.filename;
984
+ let contentType = item.contentType;
985
+ if (data instanceof File) {
986
+ filename = filename ?? data.name;
987
+ contentType = contentType ?? (data.type || getMimeType(data.name));
988
+ }
989
+ contentType = contentType ?? "application/octet-stream";
990
+ if (contentType === "application/octet-stream" && filename) {
991
+ contentType = getMimeType(filename);
992
+ }
993
+ const cid = await computeCid(new Uint8Array(bytes));
994
+ let key = item.key;
995
+ if (!key && filename) {
996
+ const lastDot = filename.lastIndexOf(".");
997
+ key = lastDot > 0 ? filename.substring(0, lastDot) : filename;
998
+ }
999
+ if (!key) {
1000
+ key = `file_${cid.slice(-8)}`;
1001
+ }
1002
+ return { key, bytes, size, contentType, filename, cid };
1003
+ }
1004
+ async function completeWithRetry(client, entityId, item, expectTip) {
1005
+ let tip = expectTip;
1006
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
1007
+ const { data, error, response } = await client.api.POST(
1008
+ "/entities/{id}/content/complete",
1009
+ {
1010
+ params: { path: { id: entityId } },
1011
+ body: {
1012
+ key: item.key,
1013
+ cid: item.cid,
1014
+ size: item.size,
1015
+ content_type: item.contentType,
1016
+ filename: item.filename,
1017
+ expect_tip: tip
1018
+ }
1019
+ }
1020
+ );
1021
+ if (data) {
1022
+ return { cid: data.cid, contentCid: data.content.cid };
1023
+ }
1024
+ if (response?.status === 409) {
1025
+ const { data: freshTip, error: tipError } = await client.api.GET(
1026
+ "/entities/{id}/tip",
1027
+ { params: { path: { id: entityId } } }
1028
+ );
1029
+ if (tipError || !freshTip) {
1030
+ throw new Error(`Failed to refresh tip: ${JSON.stringify(tipError)}`);
1031
+ }
1032
+ tip = freshTip.cid;
1033
+ continue;
1034
+ }
1035
+ throw new Error(
1036
+ `Failed to complete upload for ${item.key}: ${JSON.stringify(error)}`
1037
+ );
1038
+ }
1039
+ throw new Error(`Max CAS retries exceeded for ${item.key}`);
1040
+ }
1041
+
862
1042
  // src/operations/folders.ts
863
1043
  var FolderOperations = class {
864
1044
  constructor(client) {
@@ -1056,6 +1236,7 @@ async function withCasRetry(callbacks, options) {
1056
1236
  isCasConflictError,
1057
1237
  scanFileList,
1058
1238
  scanFileSystemEntries,
1239
+ uploadToEntity,
1059
1240
  uploadTree,
1060
1241
  verifyCid,
1061
1242
  withCasRetry