@anuma/sdk 6.31.3 → 6.31.4

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.
@@ -32167,6 +32167,22 @@ async function uploadFileToDropbox(accessToken, filename, content, folder = DEFA
32167
32167
  }
32168
32168
  return response.json();
32169
32169
  }
32170
+ async function getDropboxFileMetadata(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
32171
+ const response = await fetch(`${DROPBOX_API_URL}/files/get_metadata`, {
32172
+ method: "POST",
32173
+ headers: {
32174
+ Authorization: `Bearer ${accessToken}`,
32175
+ "Content-Type": "application/json"
32176
+ },
32177
+ body: JSON.stringify({ path: `${folder}/${filename}` })
32178
+ });
32179
+ if (response.status === 409) return null;
32180
+ if (!response.ok) {
32181
+ const errorText = await response.text();
32182
+ throw new Error(`Dropbox get metadata failed: ${response.status} - ${errorText}`);
32183
+ }
32184
+ return response.json();
32185
+ }
32170
32186
  async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
32171
32187
  await ensureBackupFolder(accessToken, folder);
32172
32188
  const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
@@ -32232,27 +32248,61 @@ async function downloadDropboxFile(accessToken, path) {
32232
32248
  }
32233
32249
  return response.blob();
32234
32250
  }
32235
- async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
32236
- const files = await listDropboxFiles(accessToken, folder);
32237
- return files.find((f) => f.name === filename) || null;
32238
- }
32239
32251
 
32240
32252
  // src/lib/backup/dropbox/backup.ts
32241
32253
  var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
32242
- async function pushConversationToDropbox(database, conversationId, userAddress, token, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
32254
+ var MAX_LISTING_FAILURES = 3;
32255
+ function createDropboxFileIndex(backupFolder) {
32256
+ let pending;
32257
+ let failures = 0;
32258
+ return {
32259
+ get(token) {
32260
+ if (!pending) {
32261
+ if (failures >= MAX_LISTING_FAILURES) {
32262
+ return Promise.reject(
32263
+ new Error("The backup folder listing failed repeatedly; skipped for this run")
32264
+ );
32265
+ }
32266
+ pending = listDropboxFiles(token, backupFolder).then((files) => {
32267
+ const byName = /* @__PURE__ */ new Map();
32268
+ for (const file of files) {
32269
+ if (!byName.has(file.name)) byName.set(file.name, file);
32270
+ }
32271
+ return byName;
32272
+ }).catch((err) => {
32273
+ pending = void 0;
32274
+ failures++;
32275
+ throw err;
32276
+ });
32277
+ }
32278
+ return pending;
32279
+ }
32280
+ };
32281
+ }
32282
+ async function readLocalUpdatedAt(database, conversationId) {
32283
+ const { Q: Q19 } = await import("@nozbe/watermelondb");
32284
+ const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
32285
+ const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
32286
+ return match ? match.updatedAt : null;
32287
+ }
32288
+ async function pushConversationToDropbox(database, conversationId, userAddress, token, fileIndex, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
32243
32289
  try {
32244
32290
  await deps.requestEncryptionKey(userAddress);
32245
32291
  const filename = `${conversationId}.json`;
32246
- const existingFile = await findDropboxFile(token, filename, backupFolder);
32292
+ const index = await fileIndex.get(token);
32293
+ const existingFile = index.get(filename);
32247
32294
  if (existingFile) {
32248
- const { Q: Q19 } = await import("@nozbe/watermelondb");
32249
- const conversationsCollection = database.get("conversations");
32250
- const records = await conversationsCollection.query(Q19.where("conversation_id", conversationId)).fetch();
32251
- if (records.length > 0) {
32252
- const conversation = conversationToStoredRaw(records[0]);
32253
- const localUpdated = conversation.updatedAt.getTime();
32254
- const remoteModified = new Date(existingFile.server_modified).getTime();
32255
- if (localUpdated <= remoteModified) {
32295
+ const localUpdatedAt = await readLocalUpdatedAt(database, conversationId);
32296
+ const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
32297
+ if (localUpdated !== null && localUpdated <= new Date(existingFile.server_modified).getTime()) {
32298
+ return "skipped";
32299
+ }
32300
+ const current = await getDropboxFileMetadata(token, filename, backupFolder);
32301
+ if (!current) {
32302
+ index.delete(filename);
32303
+ } else {
32304
+ index.set(filename, current);
32305
+ if (localUpdated !== null && localUpdated <= new Date(current.server_modified).getTime()) {
32256
32306
  return "skipped";
32257
32307
  }
32258
32308
  }
@@ -32261,7 +32311,8 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
32261
32311
  if (!exportResult.success || !exportResult.blob) {
32262
32312
  return "failed";
32263
32313
  }
32264
- await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
32314
+ const uploaded = await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
32315
+ index.set(filename, uploaded);
32265
32316
  return "uploaded";
32266
32317
  } catch (err) {
32267
32318
  if (isAuthError(err) && !_retried) {
@@ -32272,6 +32323,7 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
32272
32323
  conversationId,
32273
32324
  userAddress,
32274
32325
  newToken,
32326
+ fileIndex,
32275
32327
  deps,
32276
32328
  backupFolder,
32277
32329
  true
@@ -32295,6 +32347,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
32295
32347
  }
32296
32348
  let uploaded = 0;
32297
32349
  let skipped = 0;
32350
+ const fileIndex = createDropboxFileIndex(backupFolder);
32298
32351
  for (let i = 0; i < conversations.length; i++) {
32299
32352
  const conv = conversations[i];
32300
32353
  onProgress?.(i + 1, total);
@@ -32303,6 +32356,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
32303
32356
  conv.conversationId,
32304
32357
  userAddress,
32305
32358
  token,
32359
+ fileIndex,
32306
32360
  deps,
32307
32361
  backupFolder
32308
32362
  );
@@ -32866,29 +32920,52 @@ async function listDriveFiles(accessToken, folderId) {
32866
32920
  const data = await response.json();
32867
32921
  return data.files ?? [];
32868
32922
  }
32869
- async function downloadDriveFile(accessToken, fileId) {
32870
- const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
32871
- headers: { Authorization: `Bearer ${accessToken}` }
32872
- });
32873
- if (!response.ok) {
32874
- throw new Error(`Failed to download file: ${response.status}`);
32875
- }
32876
- return response.blob();
32923
+ var DRIVE_MAX_PAGE_SIZE = 1e3;
32924
+ async function listAllDriveFiles(accessToken, folderId) {
32925
+ const query = `'${escapeQueryValue(folderId)}' in parents and trashed=false`;
32926
+ const fields = encodeURIComponent("nextPageToken,files(id,name,createdTime,modifiedTime,size)");
32927
+ const files = [];
32928
+ let pageToken;
32929
+ do {
32930
+ const tokenParam = pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "";
32931
+ const response = await fetch(
32932
+ `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=${DRIVE_MAX_PAGE_SIZE}${tokenParam}`,
32933
+ {
32934
+ headers: { Authorization: `Bearer ${accessToken}` }
32935
+ }
32936
+ );
32937
+ if (!response.ok) {
32938
+ throw new Error(`Failed to list files: ${response.status}`);
32939
+ }
32940
+ const data = await response.json();
32941
+ files.push(...data.files ?? []);
32942
+ pageToken = data.nextPageToken;
32943
+ } while (pageToken);
32944
+ return files;
32877
32945
  }
32878
- async function findDriveFile(accessToken, folderId, filename) {
32879
- const query = `'${escapeQueryValue(folderId)}' in parents and name='${escapeQueryValue(filename)}' and trashed=false`;
32880
- const fields = "files(id,name,createdTime,modifiedTime,size)";
32946
+ async function getDriveFileMetadata(accessToken, fileId) {
32947
+ const fields = encodeURIComponent("id,name,createdTime,modifiedTime,size,trashed");
32881
32948
  const response = await fetch(
32882
- `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1`,
32949
+ `${DRIVE_API_URL}/files/${encodeURIComponent(fileId)}?fields=${fields}`,
32883
32950
  {
32884
32951
  headers: { Authorization: `Bearer ${accessToken}` }
32885
32952
  }
32886
32953
  );
32954
+ if (response.status === 404) return null;
32887
32955
  if (!response.ok) {
32888
- throw new Error(`Failed to find file: ${response.status}`);
32956
+ throw new Error(`Failed to read file: ${response.status}`);
32889
32957
  }
32890
32958
  const data = await response.json();
32891
- return data.files?.[0] ?? null;
32959
+ return data.trashed ? null : data;
32960
+ }
32961
+ async function downloadDriveFile(accessToken, fileId) {
32962
+ const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
32963
+ headers: { Authorization: `Bearer ${accessToken}` }
32964
+ });
32965
+ if (!response.ok) {
32966
+ throw new Error(`Failed to download file: ${response.status}`);
32967
+ }
32968
+ return response.blob();
32892
32969
  }
32893
32970
 
32894
32971
  // src/lib/backup/google/backup.ts
@@ -32910,28 +32987,59 @@ async function getConversationsFolder(token, requestDriveAccess, rootFolder, sub
32910
32987
  throw err;
32911
32988
  }
32912
32989
  }
32913
- async function pushConversationToDrive(database, conversationId, userAddress, token, deps, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER, _retried = false) {
32990
+ var MAX_LISTING_FAILURES2 = 3;
32991
+ function createDriveFileIndex(folderId) {
32992
+ let pending;
32993
+ let failures = 0;
32994
+ return {
32995
+ get(token) {
32996
+ if (!pending) {
32997
+ if (failures >= MAX_LISTING_FAILURES2) {
32998
+ return Promise.reject(
32999
+ new Error("The backup folder listing failed repeatedly; skipped for this run")
33000
+ );
33001
+ }
33002
+ pending = listAllDriveFiles(token, folderId).then((files) => {
33003
+ const byName = /* @__PURE__ */ new Map();
33004
+ for (const file of files) {
33005
+ if (!byName.has(file.name)) byName.set(file.name, file);
33006
+ }
33007
+ return byName;
33008
+ }).catch((err) => {
33009
+ pending = void 0;
33010
+ failures++;
33011
+ throw err;
33012
+ });
33013
+ }
33014
+ return pending;
33015
+ }
33016
+ };
33017
+ }
33018
+ async function readLocalUpdatedAt2(database, conversationId) {
33019
+ const { Q: Q19 } = await import("@nozbe/watermelondb");
33020
+ const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
33021
+ const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
33022
+ return match ? match.updatedAt : null;
33023
+ }
33024
+ async function pushConversationToDrive(database, conversationId, userAddress, token, folderId, fileIndex, deps, _retried = false) {
32914
33025
  try {
32915
33026
  await deps.requestEncryptionKey(userAddress);
32916
- const folderResult = await getConversationsFolder(
32917
- token,
32918
- deps.requestDriveAccess,
32919
- rootFolder,
32920
- subfolder
32921
- );
32922
- if (!folderResult) return "failed";
32923
- const { folderId, token: activeToken } = folderResult;
32924
33027
  const filename = `${conversationId}.json`;
32925
- const existingFile = await findDriveFile(activeToken, folderId, filename);
33028
+ const index = await fileIndex.get(token);
33029
+ let existingFile = index.get(filename);
32926
33030
  if (existingFile) {
32927
- const { Q: Q19 } = await import("@nozbe/watermelondb");
32928
- const conversationsCollection = database.get("conversations");
32929
- const records = await conversationsCollection.query(Q19.where("conversation_id", conversationId)).fetch();
32930
- if (records.length > 0) {
32931
- const conversation = conversationToStoredRaw(records[0]);
32932
- const localUpdated = conversation.updatedAt.getTime();
32933
- const remoteModified = new Date(existingFile.modifiedTime).getTime();
32934
- if (localUpdated <= remoteModified) {
33031
+ const localUpdatedAt = await readLocalUpdatedAt2(database, conversationId);
33032
+ const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
33033
+ if (localUpdated !== null && localUpdated <= new Date(existingFile.modifiedTime).getTime()) {
33034
+ return "skipped";
33035
+ }
33036
+ const current = await getDriveFileMetadata(token, existingFile.id);
33037
+ if (!current) {
33038
+ index.delete(filename);
33039
+ existingFile = void 0;
33040
+ } else {
33041
+ index.set(filename, current);
33042
+ if (localUpdated !== null && localUpdated <= new Date(current.modifiedTime).getTime()) {
32935
33043
  return "skipped";
32936
33044
  }
32937
33045
  }
@@ -32940,10 +33048,19 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
32940
33048
  if (!exportResult.success || !exportResult.blob) {
32941
33049
  return "failed";
32942
33050
  }
33051
+ const now = (/* @__PURE__ */ new Date()).toISOString();
32943
33052
  if (existingFile) {
32944
- await updateDriveFile(activeToken, existingFile.id, exportResult.blob);
33053
+ await updateDriveFile(token, existingFile.id, exportResult.blob);
33054
+ index.set(filename, { ...existingFile, modifiedTime: now });
32945
33055
  } else {
32946
- await uploadFileToDrive(activeToken, folderId, exportResult.blob, filename);
33056
+ const created = await uploadFileToDrive(token, folderId, exportResult.blob, filename);
33057
+ index.set(filename, {
33058
+ id: created.id,
33059
+ name: created.name,
33060
+ createdTime: now,
33061
+ modifiedTime: now,
33062
+ size: String(exportResult.blob.size)
33063
+ });
32947
33064
  }
32948
33065
  return "uploaded";
32949
33066
  } catch (err) {
@@ -32955,9 +33072,9 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
32955
33072
  conversationId,
32956
33073
  userAddress,
32957
33074
  newToken,
33075
+ folderId,
33076
+ fileIndex,
32958
33077
  deps,
32959
- rootFolder,
32960
- subfolder,
32961
33078
  true
32962
33079
  );
32963
33080
  } catch {
@@ -32978,7 +33095,8 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
32978
33095
  if (!folderResult) {
32979
33096
  return { success: false, uploaded: 0, skipped: 0, total: 0 };
32980
33097
  }
32981
- const { token: activeToken } = folderResult;
33098
+ const { folderId, token: activeToken } = folderResult;
33099
+ const fileIndex = createDriveFileIndex(folderId);
32982
33100
  const { Q: Q19 } = await import("@nozbe/watermelondb");
32983
33101
  const conversationsCollection = database.get("conversations");
32984
33102
  const records = await conversationsCollection.query(Q19.where("is_deleted", false)).fetch();
@@ -32997,9 +33115,9 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
32997
33115
  conv.conversationId,
32998
33116
  userAddress,
32999
33117
  activeToken,
33000
- deps,
33001
- rootFolder,
33002
- subfolder
33118
+ folderId,
33119
+ fileIndex,
33120
+ deps
33003
33121
  );
33004
33122
  if (result === "uploaded") uploaded++;
33005
33123
  if (result === "skipped") skipped++;
@@ -30194,6 +30194,22 @@ async function uploadFileToDropbox(accessToken, filename, content, folder = DEFA
30194
30194
  }
30195
30195
  return response.json();
30196
30196
  }
30197
+ async function getDropboxFileMetadata(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
30198
+ const response = await fetch(`${DROPBOX_API_URL}/files/get_metadata`, {
30199
+ method: "POST",
30200
+ headers: {
30201
+ Authorization: `Bearer ${accessToken}`,
30202
+ "Content-Type": "application/json"
30203
+ },
30204
+ body: JSON.stringify({ path: `${folder}/${filename}` })
30205
+ });
30206
+ if (response.status === 409) return null;
30207
+ if (!response.ok) {
30208
+ const errorText = await response.text();
30209
+ throw new Error(`Dropbox get metadata failed: ${response.status} - ${errorText}`);
30210
+ }
30211
+ return response.json();
30212
+ }
30197
30213
  async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
30198
30214
  await ensureBackupFolder(accessToken, folder);
30199
30215
  const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
@@ -30259,27 +30275,61 @@ async function downloadDropboxFile(accessToken, path) {
30259
30275
  }
30260
30276
  return response.blob();
30261
30277
  }
30262
- async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
30263
- const files = await listDropboxFiles(accessToken, folder);
30264
- return files.find((f) => f.name === filename) || null;
30265
- }
30266
30278
 
30267
30279
  // src/lib/backup/dropbox/backup.ts
30268
30280
  var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
30269
- async function pushConversationToDropbox(database, conversationId, userAddress, token, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
30281
+ var MAX_LISTING_FAILURES = 3;
30282
+ function createDropboxFileIndex(backupFolder) {
30283
+ let pending;
30284
+ let failures = 0;
30285
+ return {
30286
+ get(token) {
30287
+ if (!pending) {
30288
+ if (failures >= MAX_LISTING_FAILURES) {
30289
+ return Promise.reject(
30290
+ new Error("The backup folder listing failed repeatedly; skipped for this run")
30291
+ );
30292
+ }
30293
+ pending = listDropboxFiles(token, backupFolder).then((files) => {
30294
+ const byName = /* @__PURE__ */ new Map();
30295
+ for (const file of files) {
30296
+ if (!byName.has(file.name)) byName.set(file.name, file);
30297
+ }
30298
+ return byName;
30299
+ }).catch((err) => {
30300
+ pending = void 0;
30301
+ failures++;
30302
+ throw err;
30303
+ });
30304
+ }
30305
+ return pending;
30306
+ }
30307
+ };
30308
+ }
30309
+ async function readLocalUpdatedAt(database, conversationId) {
30310
+ const { Q: Q19 } = await import("@nozbe/watermelondb");
30311
+ const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
30312
+ const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
30313
+ return match ? match.updatedAt : null;
30314
+ }
30315
+ async function pushConversationToDropbox(database, conversationId, userAddress, token, fileIndex, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
30270
30316
  try {
30271
30317
  await deps.requestEncryptionKey(userAddress);
30272
30318
  const filename = `${conversationId}.json`;
30273
- const existingFile = await findDropboxFile(token, filename, backupFolder);
30319
+ const index = await fileIndex.get(token);
30320
+ const existingFile = index.get(filename);
30274
30321
  if (existingFile) {
30275
- const { Q: Q19 } = await import("@nozbe/watermelondb");
30276
- const conversationsCollection = database.get("conversations");
30277
- const records = await conversationsCollection.query(Q19.where("conversation_id", conversationId)).fetch();
30278
- if (records.length > 0) {
30279
- const conversation = conversationToStoredRaw(records[0]);
30280
- const localUpdated = conversation.updatedAt.getTime();
30281
- const remoteModified = new Date(existingFile.server_modified).getTime();
30282
- if (localUpdated <= remoteModified) {
30322
+ const localUpdatedAt = await readLocalUpdatedAt(database, conversationId);
30323
+ const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
30324
+ if (localUpdated !== null && localUpdated <= new Date(existingFile.server_modified).getTime()) {
30325
+ return "skipped";
30326
+ }
30327
+ const current = await getDropboxFileMetadata(token, filename, backupFolder);
30328
+ if (!current) {
30329
+ index.delete(filename);
30330
+ } else {
30331
+ index.set(filename, current);
30332
+ if (localUpdated !== null && localUpdated <= new Date(current.server_modified).getTime()) {
30283
30333
  return "skipped";
30284
30334
  }
30285
30335
  }
@@ -30288,7 +30338,8 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
30288
30338
  if (!exportResult.success || !exportResult.blob) {
30289
30339
  return "failed";
30290
30340
  }
30291
- await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
30341
+ const uploaded = await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
30342
+ index.set(filename, uploaded);
30292
30343
  return "uploaded";
30293
30344
  } catch (err) {
30294
30345
  if (isAuthError(err) && !_retried) {
@@ -30299,6 +30350,7 @@ async function pushConversationToDropbox(database, conversationId, userAddress,
30299
30350
  conversationId,
30300
30351
  userAddress,
30301
30352
  newToken,
30353
+ fileIndex,
30302
30354
  deps,
30303
30355
  backupFolder,
30304
30356
  true
@@ -30322,6 +30374,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
30322
30374
  }
30323
30375
  let uploaded = 0;
30324
30376
  let skipped = 0;
30377
+ const fileIndex = createDropboxFileIndex(backupFolder);
30325
30378
  for (let i = 0; i < conversations.length; i++) {
30326
30379
  const conv = conversations[i];
30327
30380
  onProgress?.(i + 1, total);
@@ -30330,6 +30383,7 @@ async function performDropboxExport(database, userAddress, token, deps, onProgre
30330
30383
  conv.conversationId,
30331
30384
  userAddress,
30332
30385
  token,
30386
+ fileIndex,
30333
30387
  deps,
30334
30388
  backupFolder
30335
30389
  );
@@ -30898,29 +30952,52 @@ async function listDriveFiles(accessToken, folderId) {
30898
30952
  const data = await response.json();
30899
30953
  return data.files ?? [];
30900
30954
  }
30901
- async function downloadDriveFile(accessToken, fileId) {
30902
- const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
30903
- headers: { Authorization: `Bearer ${accessToken}` }
30904
- });
30905
- if (!response.ok) {
30906
- throw new Error(`Failed to download file: ${response.status}`);
30907
- }
30908
- return response.blob();
30955
+ var DRIVE_MAX_PAGE_SIZE = 1e3;
30956
+ async function listAllDriveFiles(accessToken, folderId) {
30957
+ const query = `'${escapeQueryValue(folderId)}' in parents and trashed=false`;
30958
+ const fields = encodeURIComponent("nextPageToken,files(id,name,createdTime,modifiedTime,size)");
30959
+ const files = [];
30960
+ let pageToken;
30961
+ do {
30962
+ const tokenParam = pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "";
30963
+ const response = await fetch(
30964
+ `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=${DRIVE_MAX_PAGE_SIZE}${tokenParam}`,
30965
+ {
30966
+ headers: { Authorization: `Bearer ${accessToken}` }
30967
+ }
30968
+ );
30969
+ if (!response.ok) {
30970
+ throw new Error(`Failed to list files: ${response.status}`);
30971
+ }
30972
+ const data = await response.json();
30973
+ files.push(...data.files ?? []);
30974
+ pageToken = data.nextPageToken;
30975
+ } while (pageToken);
30976
+ return files;
30909
30977
  }
30910
- async function findDriveFile(accessToken, folderId, filename) {
30911
- const query = `'${escapeQueryValue(folderId)}' in parents and name='${escapeQueryValue(filename)}' and trashed=false`;
30912
- const fields = "files(id,name,createdTime,modifiedTime,size)";
30978
+ async function getDriveFileMetadata(accessToken, fileId) {
30979
+ const fields = encodeURIComponent("id,name,createdTime,modifiedTime,size,trashed");
30913
30980
  const response = await fetch(
30914
- `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1`,
30981
+ `${DRIVE_API_URL}/files/${encodeURIComponent(fileId)}?fields=${fields}`,
30915
30982
  {
30916
30983
  headers: { Authorization: `Bearer ${accessToken}` }
30917
30984
  }
30918
30985
  );
30986
+ if (response.status === 404) return null;
30919
30987
  if (!response.ok) {
30920
- throw new Error(`Failed to find file: ${response.status}`);
30988
+ throw new Error(`Failed to read file: ${response.status}`);
30921
30989
  }
30922
30990
  const data = await response.json();
30923
- return data.files?.[0] ?? null;
30991
+ return data.trashed ? null : data;
30992
+ }
30993
+ async function downloadDriveFile(accessToken, fileId) {
30994
+ const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
30995
+ headers: { Authorization: `Bearer ${accessToken}` }
30996
+ });
30997
+ if (!response.ok) {
30998
+ throw new Error(`Failed to download file: ${response.status}`);
30999
+ }
31000
+ return response.blob();
30924
31001
  }
30925
31002
 
30926
31003
  // src/lib/backup/google/backup.ts
@@ -30942,28 +31019,59 @@ async function getConversationsFolder(token, requestDriveAccess, rootFolder, sub
30942
31019
  throw err;
30943
31020
  }
30944
31021
  }
30945
- async function pushConversationToDrive(database, conversationId, userAddress, token, deps, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER, _retried = false) {
31022
+ var MAX_LISTING_FAILURES2 = 3;
31023
+ function createDriveFileIndex(folderId) {
31024
+ let pending;
31025
+ let failures = 0;
31026
+ return {
31027
+ get(token) {
31028
+ if (!pending) {
31029
+ if (failures >= MAX_LISTING_FAILURES2) {
31030
+ return Promise.reject(
31031
+ new Error("The backup folder listing failed repeatedly; skipped for this run")
31032
+ );
31033
+ }
31034
+ pending = listAllDriveFiles(token, folderId).then((files) => {
31035
+ const byName = /* @__PURE__ */ new Map();
31036
+ for (const file of files) {
31037
+ if (!byName.has(file.name)) byName.set(file.name, file);
31038
+ }
31039
+ return byName;
31040
+ }).catch((err) => {
31041
+ pending = void 0;
31042
+ failures++;
31043
+ throw err;
31044
+ });
31045
+ }
31046
+ return pending;
31047
+ }
31048
+ };
31049
+ }
31050
+ async function readLocalUpdatedAt2(database, conversationId) {
31051
+ const { Q: Q19 } = await import("@nozbe/watermelondb");
31052
+ const records = await database.get("conversations").query(Q19.where("conversation_id", conversationId)).fetch();
31053
+ const match = records.map(conversationToStoredRaw).find((c) => c.conversationId === conversationId);
31054
+ return match ? match.updatedAt : null;
31055
+ }
31056
+ async function pushConversationToDrive(database, conversationId, userAddress, token, folderId, fileIndex, deps, _retried = false) {
30946
31057
  try {
30947
31058
  await deps.requestEncryptionKey(userAddress);
30948
- const folderResult = await getConversationsFolder(
30949
- token,
30950
- deps.requestDriveAccess,
30951
- rootFolder,
30952
- subfolder
30953
- );
30954
- if (!folderResult) return "failed";
30955
- const { folderId, token: activeToken } = folderResult;
30956
31059
  const filename = `${conversationId}.json`;
30957
- const existingFile = await findDriveFile(activeToken, folderId, filename);
31060
+ const index = await fileIndex.get(token);
31061
+ let existingFile = index.get(filename);
30958
31062
  if (existingFile) {
30959
- const { Q: Q19 } = await import("@nozbe/watermelondb");
30960
- const conversationsCollection = database.get("conversations");
30961
- const records = await conversationsCollection.query(Q19.where("conversation_id", conversationId)).fetch();
30962
- if (records.length > 0) {
30963
- const conversation = conversationToStoredRaw(records[0]);
30964
- const localUpdated = conversation.updatedAt.getTime();
30965
- const remoteModified = new Date(existingFile.modifiedTime).getTime();
30966
- if (localUpdated <= remoteModified) {
31063
+ const localUpdatedAt = await readLocalUpdatedAt2(database, conversationId);
31064
+ const localUpdated = localUpdatedAt ? localUpdatedAt.getTime() : null;
31065
+ if (localUpdated !== null && localUpdated <= new Date(existingFile.modifiedTime).getTime()) {
31066
+ return "skipped";
31067
+ }
31068
+ const current = await getDriveFileMetadata(token, existingFile.id);
31069
+ if (!current) {
31070
+ index.delete(filename);
31071
+ existingFile = void 0;
31072
+ } else {
31073
+ index.set(filename, current);
31074
+ if (localUpdated !== null && localUpdated <= new Date(current.modifiedTime).getTime()) {
30967
31075
  return "skipped";
30968
31076
  }
30969
31077
  }
@@ -30972,10 +31080,19 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
30972
31080
  if (!exportResult.success || !exportResult.blob) {
30973
31081
  return "failed";
30974
31082
  }
31083
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30975
31084
  if (existingFile) {
30976
- await updateDriveFile(activeToken, existingFile.id, exportResult.blob);
31085
+ await updateDriveFile(token, existingFile.id, exportResult.blob);
31086
+ index.set(filename, { ...existingFile, modifiedTime: now });
30977
31087
  } else {
30978
- await uploadFileToDrive(activeToken, folderId, exportResult.blob, filename);
31088
+ const created = await uploadFileToDrive(token, folderId, exportResult.blob, filename);
31089
+ index.set(filename, {
31090
+ id: created.id,
31091
+ name: created.name,
31092
+ createdTime: now,
31093
+ modifiedTime: now,
31094
+ size: String(exportResult.blob.size)
31095
+ });
30979
31096
  }
30980
31097
  return "uploaded";
30981
31098
  } catch (err) {
@@ -30987,9 +31104,9 @@ async function pushConversationToDrive(database, conversationId, userAddress, to
30987
31104
  conversationId,
30988
31105
  userAddress,
30989
31106
  newToken,
31107
+ folderId,
31108
+ fileIndex,
30990
31109
  deps,
30991
- rootFolder,
30992
- subfolder,
30993
31110
  true
30994
31111
  );
30995
31112
  } catch {
@@ -31010,7 +31127,8 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
31010
31127
  if (!folderResult) {
31011
31128
  return { success: false, uploaded: 0, skipped: 0, total: 0 };
31012
31129
  }
31013
- const { token: activeToken } = folderResult;
31130
+ const { folderId, token: activeToken } = folderResult;
31131
+ const fileIndex = createDriveFileIndex(folderId);
31014
31132
  const { Q: Q19 } = await import("@nozbe/watermelondb");
31015
31133
  const conversationsCollection = database.get("conversations");
31016
31134
  const records = await conversationsCollection.query(Q19.where("is_deleted", false)).fetch();
@@ -31029,9 +31147,9 @@ async function performGoogleDriveExport(database, userAddress, token, deps, onPr
31029
31147
  conv.conversationId,
31030
31148
  userAddress,
31031
31149
  activeToken,
31032
- deps,
31033
- rootFolder,
31034
- subfolder
31150
+ folderId,
31151
+ fileIndex,
31152
+ deps
31035
31153
  );
31036
31154
  if (result === "uploaded") uploaded++;
31037
31155
  if (result === "skipped") skipped++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "6.31.3",
3
+ "version": "6.31.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",