@telecrypt-io/storage 0.5.28 → 0.5.29

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.
@@ -10,12 +10,12 @@
10
10
  * config (persistent crypto store, session credentials, etc.) is
11
11
  * platform-specific and stays with the caller.
12
12
  */
13
- import { withMatrixMutationAbort, withTreeMutation, } from "../TeleCryptIOStorage.js";
13
+ import { readFileEventMetadata, withMatrixMutationAbort, withTreeMutation, } from "../TeleCryptIOStorage.js";
14
14
  import { EventType, MatrixError, RoomCreateTypeField, RoomType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "matrix-js-sdk";
15
15
  import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
16
16
  import { Method } from "matrix-js-sdk/lib/http-api/method.js";
17
- import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, NonEmptyTreeError, RoomCreationAmbiguousError, RoomCleanupIncompleteError, StorageError, UndecryptableFileError, } from "./errors.js";
18
- import { waitForCondition } from "./poll.js";
17
+ import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, NonEmptyTreeError, RoomCreationAmbiguousError, RoomCleanupIncompleteError, StorageError, } from "./errors.js";
18
+ import { ConditionTimeoutError, waitForCondition } from "./poll.js";
19
19
  import { validateMatrixEventId } from "./constants.js";
20
20
  import { validateName } from "./validation.js";
21
21
  import { getDeletedTreeIds, isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
@@ -27,14 +27,12 @@ import { getDeletedTreeIds, isFileDeleted, isTreeDeleted, markFileDeleted, markT
27
27
  * server-advised backoff. Mirrors the harness library suite's
28
28
  * `withRateLimitRetry`; must not mask non-rate-limit failures.
29
29
  */
30
- const RATE_LIMIT_RETRIES = 6;
31
30
  const RATE_LIMIT_DEFAULT_DELAY_MS = 15_000;
32
- const RATE_LIMIT_MAX_DELAY_MS = 30_000;
33
- const RATE_LIMIT_MAX_TOTAL_DELAY_MS = 90_000;
31
+ // JavaScript timers cannot represent a longer delay reliably. The operation's
32
+ // own deadline remains the authority that stops retrying.
33
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
34
34
  const MAX_DELETION_ROOMS = 4096;
35
35
  const MAX_DELETION_DEPTH = 128;
36
- const MAX_FILE_VERSION_CHAIN = 128;
37
- const MAX_LIST_ITEMS = 10000;
38
36
  const DEFAULT_MUTATION_TIMEOUT_MS = 5 * 60_000;
39
37
  const MAX_MUTATION_TIMEOUT_MS = 15 * 60_000;
40
38
  function createOperationDeadline(options) {
@@ -79,24 +77,17 @@ async function raceOperationDeadline(deadline, pending, kind) {
79
77
  finally {
80
78
  if (onAbort)
81
79
  deadline.signal.removeEventListener("abort", onAbort);
82
- void pending.catch(() => undefined);
83
80
  }
84
81
  }
85
82
  async function withOperationDeadline(options, operation, kind = "read") {
86
83
  const deadline = createOperationDeadline(options);
87
- let pending;
88
84
  try {
89
85
  ensureOperationActive(deadline.signal);
90
- pending = Promise.resolve().then(() => operation(deadline.signal));
86
+ const pending = Promise.resolve().then(() => operation(deadline.signal));
91
87
  return await raceOperationDeadline(deadline, pending, kind);
92
88
  }
93
89
  finally {
94
90
  deadline.close();
95
- // A caller-facing deadline must not wait for a broken provider, but the
96
- // late result remains observed so an ignored AbortSignal cannot create an
97
- // unhandled rejection.
98
- if (pending)
99
- void pending.catch(() => undefined);
100
91
  }
101
92
  }
102
93
  function isRateLimited(error) {
@@ -107,16 +98,28 @@ function isRateLimited(error) {
107
98
  const isRateLimitError = error.isRateLimitError;
108
99
  return typeof isRateLimitError === "function" && isRateLimitError.call(error) === true;
109
100
  }
101
+ function requiresMutationReconciliation(error) {
102
+ return (error instanceof MutationOutcomeUnknownError ||
103
+ error instanceof RoomCreationAmbiguousError ||
104
+ error instanceof RoomCleanupIncompleteError ||
105
+ (error instanceof Error &&
106
+ error.cleanupIncomplete === true));
107
+ }
110
108
  async function withRateLimitRetry(operation, signal) {
111
- let totalDelay = 0;
112
- for (let attempt = 0;; attempt++) {
109
+ for (;;) {
113
110
  if (signal?.aborted)
114
111
  throw new StorageError("operation cancelled");
115
112
  try {
116
113
  return await operation();
117
114
  }
118
115
  catch (error) {
119
- if (!isRateLimited(error) || attempt >= RATE_LIMIT_RETRIES)
116
+ // A mutation that may have committed, or whose compensating cleanup is
117
+ // incomplete, must be reconciled by its owner before any retry. A
118
+ // server rate-limit response does not make repeating room creation safe.
119
+ if (requiresMutationReconciliation(error)) {
120
+ throw error;
121
+ }
122
+ if (!isRateLimited(error))
120
123
  throw error;
121
124
  // The server's retry_after_ms only covers one token; tree operations
122
125
  // need several (kick + leave + forget). Wait at least 15s so the
@@ -131,12 +134,7 @@ async function withRateLimitRetry(operation, signal) {
131
134
  }
132
135
  }
133
136
  }
134
- retryAfter = Math.min(retryAfter, RATE_LIMIT_MAX_DELAY_MS);
135
- const remaining = RATE_LIMIT_MAX_TOTAL_DELAY_MS - totalDelay;
136
- if (remaining <= 0)
137
- throw error;
138
- retryAfter = Math.min(retryAfter, remaining);
139
- totalDelay += retryAfter;
137
+ retryAfter = Math.min(retryAfter, MAX_TIMER_DELAY_MS);
140
138
  await new Promise((resolve, reject) => {
141
139
  let timer;
142
140
  const onAbort = () => {
@@ -169,10 +167,12 @@ async function resolveTree(storage, treeId, signal) {
169
167
  signal,
170
168
  });
171
169
  }
172
- catch {
170
+ catch (error) {
173
171
  if (signal?.aborted)
174
172
  throw new StorageError("operation cancelled");
175
- throw new StorageError("storage space not found");
173
+ if (error instanceof ConditionTimeoutError)
174
+ throw new StorageError("storage space not found");
175
+ throw new StorageError("storage space lookup failed", { cause: error });
176
176
  }
177
177
  }
178
178
  function isMarkedTreeDeleted(storage, treeId) {
@@ -197,10 +197,12 @@ async function resolveFile(storage, tree, fileId, signal) {
197
197
  try {
198
198
  return await waitForCondition(() => (isMarkedFileDeleted(storage, tree.id, fileId) ? null : tree.getFile(fileId)), { timeoutMs: 15000, signal });
199
199
  }
200
- catch {
200
+ catch (error) {
201
201
  if (signal?.aborted)
202
202
  throw new StorageError("operation cancelled");
203
- throw new StorageError("file not found");
203
+ if (error instanceof ConditionTimeoutError)
204
+ throw new StorageError("file not found");
205
+ throw new StorageError("file lookup failed", { cause: error });
204
206
  }
205
207
  }
206
208
  function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large") {
@@ -223,8 +225,8 @@ function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large")
223
225
  try {
224
226
  children = tree.getDirectories();
225
227
  }
226
- catch {
227
- throw new StorageError("could not enumerate storage space descendants safely");
228
+ catch (error) {
229
+ throw new StorageError("could not enumerate storage space descendants safely", { cause: error });
228
230
  }
229
231
  if (children.length > MAX_DELETION_ROOMS) {
230
232
  throw new StorageError(tooLargeMessage);
@@ -250,8 +252,8 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
250
252
  .filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
251
253
  !isConfirmedDeletedFile(storage.getClient(), tree.id, file));
252
254
  }
253
- catch {
254
- throw new StorageError("could not enumerate storage files safely");
255
+ catch (error) {
256
+ throw new StorageError("could not enumerate storage files safely", { cause: error });
255
257
  }
256
258
  if (files.length > 0)
257
259
  throw new NonEmptyTreeError(tree.id);
@@ -360,8 +362,8 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
360
362
  try {
361
363
  directories = tree.getDirectories();
362
364
  }
363
- catch {
364
- throw new StorageError("delete graph is unsafe");
365
+ catch (error) {
366
+ throw new StorageError("delete graph is unsafe", { cause: error });
365
367
  }
366
368
  for (const child of directories)
367
369
  addEdge(id, child.id);
@@ -455,8 +457,10 @@ async function unlinkExternalParents(storage, rootId, externalParents, signal) {
455
457
  try {
456
458
  await relinkExternalParents(storage, rootId, externalParents, new AbortController().signal);
457
459
  }
458
- catch {
459
- throw new StorageError("delete graph unlink cleanup is incomplete");
460
+ catch (cleanupError) {
461
+ throw new StorageError("delete graph unlink cleanup is incomplete", {
462
+ cause: new AggregateError([error, cleanupError], "delete graph unlink and relink both failed", { cause: error }),
463
+ });
460
464
  }
461
465
  throw error;
462
466
  }
@@ -554,7 +558,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
554
558
  if (error instanceof MutationOutcomeUnknownError || error instanceof MutationPartialError)
555
559
  throw error;
556
560
  if (completedRoomIds.length > 0) {
557
- throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped");
561
+ throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped", { cause: error });
558
562
  }
559
563
  throw error;
560
564
  }
@@ -565,14 +569,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
565
569
  * may otherwise leave a stale invite visible until the next sync.
566
570
  */
567
571
  function removeRoomFromLocalStore(client, roomId) {
568
- try {
569
- client.store?.removeRoom(roomId);
570
- }
571
- catch {
572
- // Server-side leave/forget already completed; a store implementation that
573
- // cannot evict synchronously must not turn authoritative cleanup into a
574
- // false failure.
575
- }
572
+ client.store?.removeRoom(roomId);
576
573
  }
577
574
  function isActiveMembership(membership) {
578
575
  return membership === "join" || membership === "invite";
@@ -582,12 +579,11 @@ function isRevocableMembership(membership) {
582
579
  }
583
580
  function safePartialDetail(error) {
584
581
  // Provider/Matrix errors can contain response bodies, URLs, or credentials.
585
- // StorageError messages are authored by this package and are already
586
- // bounded; retain those local policy details so callers still get useful
587
- // owner/self-target diagnostics.
582
+ // StorageError messages are authored by this package; retain those local
583
+ // policy details so callers still get useful owner/self-target diagnostics.
588
584
  if (!(error instanceof StorageError) || error instanceof MutationOutcomeUnknownError)
589
585
  return undefined;
590
- return error.message.length <= 256 ? error.message : undefined;
586
+ return error.message;
591
587
  }
592
588
  function toArrayBuffer(bytes) {
593
589
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
@@ -599,16 +595,12 @@ export async function createVault(storage, name, options) {
599
595
  tree = await withRateLimitRetry(() => storage.createTree(name, signal), signal);
600
596
  }
601
597
  catch (error) {
602
- if (error instanceof RoomCleanupIncompleteError ||
603
- error instanceof RoomCreationAmbiguousError ||
604
- error instanceof MutationOutcomeUnknownError ||
605
- (error instanceof Error &&
606
- error.cleanupIncomplete === true)) {
598
+ if (requiresMutationReconciliation(error)) {
607
599
  throw error;
608
600
  }
609
601
  if (signal.aborted)
610
602
  throw new StorageError("operation cancelled");
611
- throw new StorageError("create vault failed");
603
+ throw new StorageError("create vault failed", { cause: error });
612
604
  }
613
605
  ensureOperationActive(signal);
614
606
  return { id: tree.id, name };
@@ -646,7 +638,7 @@ export async function joinVault(storage, vaultId, options) {
646
638
  membership = null;
647
639
  }
648
640
  else {
649
- throw new StorageError("join failed");
641
+ throw new StorageError("join failed", { cause: err });
650
642
  }
651
643
  }
652
644
  if (membership === "join")
@@ -664,7 +656,7 @@ export async function joinVault(storage, vaultId, options) {
664
656
  }
665
657
  if (signal.aborted)
666
658
  throw new StorageError("operation cancelled");
667
- throw new StorageError("join failed");
659
+ throw new StorageError("join failed", { cause: err });
668
660
  }
669
661
  ensureOperationActive(signal);
670
662
  return { vaultId, joined: true };
@@ -700,8 +692,6 @@ export async function listPendingInvites(storage, options) {
700
692
  return withOperationDeadline(options, async (signal) => {
701
693
  const client = storage.getClient();
702
694
  const rooms = client.getRooms();
703
- if (rooms.length > MAX_LIST_ITEMS)
704
- throw new StorageError("invite list is too large");
705
695
  const invites = [];
706
696
  for (const room of rooms) {
707
697
  ensureOperationActive(signal);
@@ -820,7 +810,7 @@ export async function declineInvite(storage, vaultId, options) {
820
810
  throw error;
821
811
  if (signal.aborted)
822
812
  throw new StorageError("operation cancelled");
823
- throw new StorageError("decline failed");
813
+ throw new StorageError("decline failed", { cause: error });
824
814
  }
825
815
  ensureOperationActive(signal);
826
816
  return { vaultId, declined: true };
@@ -893,7 +883,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
893
883
  if (error instanceof MutationOutcomeUnknownError)
894
884
  throw error;
895
885
  if (completedRoomIds.size > 0) {
896
- throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error));
886
+ throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error), { cause: error });
897
887
  }
898
888
  if (error instanceof StorageError &&
899
889
  (error.message === "share failed" ||
@@ -902,7 +892,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
902
892
  error.message === "operation cancelled")) {
903
893
  throw error;
904
894
  }
905
- throw new StorageError("share failed");
895
+ throw new StorageError("share failed", { cause: error });
906
896
  }
907
897
  return { vaultId, userId, role };
908
898
  }, operation.signal);
@@ -968,7 +958,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
968
958
  if (error instanceof MutationOutcomeUnknownError)
969
959
  throw error;
970
960
  if (completedRoomIds.size > 0) {
971
- throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error));
961
+ throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error), { cause: error });
972
962
  }
973
963
  if (error instanceof StorageError &&
974
964
  (error.message.includes("owner") ||
@@ -976,7 +966,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
976
966
  error.message === "operation cancelled")) {
977
967
  throw error;
978
968
  }
979
- throw new StorageError("unshare failed");
969
+ throw new StorageError("unshare failed", { cause: error });
980
970
  }
981
971
  return { vaultId, userId, removed: true };
982
972
  }, operation.signal);
@@ -992,10 +982,10 @@ export async function listMembers(storage, vaultId, options) {
992
982
  try {
993
983
  return await withRateLimitRetry(() => storage.listMembers(tree, { signal }), signal);
994
984
  }
995
- catch {
985
+ catch (error) {
996
986
  if (signal.aborted)
997
987
  throw new StorageError("operation cancelled");
998
- throw new StorageError("list members failed");
988
+ throw new StorageError("list members failed", { cause: error });
999
989
  }
1000
990
  });
1001
991
  }
@@ -1008,16 +998,12 @@ export async function listFiles(storage, treeId, options) {
1008
998
  const files = tree
1009
999
  .listFiles()
1010
1000
  .filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
1011
- if (files.length > MAX_LIST_ITEMS)
1012
- throw new StorageError("file list is too large");
1013
1001
  return files.map((f) => ({ id: f.id, name: f.getName() }));
1014
1002
  }
1015
1003
  catch (error) {
1016
1004
  if (signal.aborted)
1017
1005
  throw new StorageError("operation cancelled");
1018
- if (error instanceof StorageError && error.message === "file list is too large")
1019
- throw error;
1020
- throw new StorageError("list files failed");
1006
+ throw new StorageError("list files failed", { cause: error });
1021
1007
  }
1022
1008
  });
1023
1009
  }
@@ -1030,8 +1016,6 @@ export async function listSubfolders(storage, parentId, options) {
1030
1016
  const directories = tree
1031
1017
  .getDirectories()
1032
1018
  .filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
1033
- if (directories.length > MAX_LIST_ITEMS)
1034
- throw new StorageError("folder list is too large");
1035
1019
  return directories.map((d) => ({ id: d.id, name: d.room.name }));
1036
1020
  });
1037
1021
  }
@@ -1044,16 +1028,12 @@ export async function createSubfolder(storage, parentId, name, options) {
1044
1028
  sub = await withRateLimitRetry(() => storage.createSubtree(tree, name, signal), signal);
1045
1029
  }
1046
1030
  catch (error) {
1047
- if (error instanceof RoomCleanupIncompleteError ||
1048
- error instanceof RoomCreationAmbiguousError ||
1049
- error instanceof MutationOutcomeUnknownError ||
1050
- (error instanceof Error &&
1051
- error.cleanupIncomplete === true)) {
1031
+ if (requiresMutationReconciliation(error)) {
1052
1032
  throw error;
1053
1033
  }
1054
1034
  if (signal.aborted)
1055
1035
  throw new StorageError("operation cancelled");
1056
- throw new StorageError("create folder failed");
1036
+ throw new StorageError("create folder failed", { cause: error });
1057
1037
  }
1058
1038
  ensureOperationActive(signal);
1059
1039
  return { id: sub.id, name };
@@ -1071,12 +1051,7 @@ async function renameTree(storage, treeId, name, options) {
1071
1051
  const current = storage.getTree(treeId);
1072
1052
  if (current?.room.name === name)
1073
1053
  return current;
1074
- try {
1075
- await storage.refreshRoomState(treeId, { signal });
1076
- }
1077
- catch {
1078
- return null;
1079
- }
1054
+ await storage.refreshRoomState(treeId, { signal });
1080
1055
  const refreshed = storage.getTree(treeId);
1081
1056
  return refreshed?.room.name === name ? refreshed : null;
1082
1057
  }, { timeoutMs: 15000, signal });
@@ -1086,7 +1061,7 @@ async function renameTree(storage, treeId, name, options) {
1086
1061
  throw error;
1087
1062
  if (signal.aborted)
1088
1063
  throw new StorageError("operation cancelled");
1089
- throw new StorageError("rename failed");
1064
+ throw new StorageError("rename failed", { cause: error });
1090
1065
  }
1091
1066
  return { id: treeId, name };
1092
1067
  }, "mutation");
@@ -1132,13 +1107,9 @@ async function deleteTree(storage, treeId, options) {
1132
1107
  graph = validateDeletionGraph(client, tree, ids, removedRooms);
1133
1108
  }
1134
1109
  catch (error) {
1135
- if (error instanceof MutationOutcomeUnknownError ||
1136
- error instanceof MutationPartialError ||
1137
- error instanceof NonEmptyTreeError)
1138
- throw error;
1139
1110
  if (error instanceof StorageError)
1140
1111
  throw error;
1141
- throw new StorageError("delete failed");
1112
+ throw new StorageError("delete failed", { cause: error });
1142
1113
  }
1143
1114
  // Keep external parent links intact until the room deletion succeeds.
1144
1115
  // Unlinking first would silently detach a live vault on failure.
@@ -1146,16 +1117,9 @@ async function deleteTree(storage, treeId, options) {
1146
1117
  await unlinkExternalParents(storage, tree.id, graph.externalParents, operation.signal);
1147
1118
  }
1148
1119
  catch (error) {
1149
- if (error instanceof MutationOutcomeUnknownError)
1150
- throw error;
1151
- if (error instanceof MutationPartialError)
1152
- throw error;
1153
- if (error instanceof StorageError && error.message === "delete graph unlink cleanup is incomplete") {
1154
- throw error;
1155
- }
1156
1120
  if (error instanceof StorageError)
1157
1121
  throw error;
1158
- throw new StorageError("delete failed");
1122
+ throw new StorageError("delete failed", { cause: error });
1159
1123
  }
1160
1124
  try {
1161
1125
  await deleteRoomDeterministically(storage, tree.id, operation.signal);
@@ -1167,16 +1131,14 @@ async function deleteTree(storage, treeId, options) {
1167
1131
  try {
1168
1132
  await relinkExternalParents(storage, tree.id, graph.externalParents, new AbortController().signal);
1169
1133
  }
1170
- catch {
1171
- throw new StorageError("delete cleanup is incomplete");
1134
+ catch (cleanupError) {
1135
+ throw new StorageError("delete cleanup is incomplete", {
1136
+ cause: new AggregateError([error, cleanupError], "delete and external-parent relink both failed", { cause: error }),
1137
+ });
1172
1138
  }
1173
- if (error instanceof MutationOutcomeUnknownError)
1174
- throw error;
1175
- if (error instanceof MutationPartialError)
1176
- throw error;
1177
1139
  if (error instanceof StorageError)
1178
1140
  throw error;
1179
- throw new StorageError("delete failed");
1141
+ throw new StorageError("delete failed", { cause: error });
1180
1142
  }
1181
1143
  return { id: treeId, deleted: true };
1182
1144
  }, operation.signal);
@@ -1216,7 +1178,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
1216
1178
  throw error;
1217
1179
  if (signal.aborted)
1218
1180
  throw new StorageError("operation cancelled");
1219
- throw new StorageError("rename file failed");
1181
+ throw new StorageError("rename file failed", { cause: error });
1220
1182
  }
1221
1183
  return { id: fileId, name };
1222
1184
  }, "mutation");
@@ -1235,11 +1197,11 @@ async function resolveFileVersions(branch, signal) {
1235
1197
  try {
1236
1198
  history = await branch.getVersionHistory();
1237
1199
  }
1238
- catch {
1239
- throw new StorageError("could not resolve file version history safely");
1200
+ catch (error) {
1201
+ throw new StorageError("could not resolve file version history safely", { cause: error });
1240
1202
  }
1241
- if (!Array.isArray(history) || history.length === 0 || history.length > MAX_FILE_VERSION_CHAIN) {
1242
- throw new StorageError("file version history is invalid or too large");
1203
+ if (!Array.isArray(history) || history.length === 0) {
1204
+ throw new StorageError("file version history is invalid");
1243
1205
  }
1244
1206
  const eventIds = new Set();
1245
1207
  const mediaIds = new Set();
@@ -1253,8 +1215,8 @@ async function resolveFileVersions(branch, signal) {
1253
1215
  try {
1254
1216
  validateMatrixEventId(version.id, "file version event ID");
1255
1217
  }
1256
- catch {
1257
- throw new StorageError("file version history contains an invalid event ID");
1218
+ catch (error) {
1219
+ throw new StorageError("file version history contains an invalid event ID", { cause: error });
1258
1220
  }
1259
1221
  if (eventIds.has(version.id)) {
1260
1222
  throw new StorageError("file version history contains a cycle");
@@ -1264,17 +1226,14 @@ async function resolveFileVersions(branch, signal) {
1264
1226
  try {
1265
1227
  fileInfo = await version.getFileInfo();
1266
1228
  }
1267
- catch {
1268
- throw new StorageError("could not resolve encrypted file version safely");
1229
+ catch (error) {
1230
+ throw new StorageError("could not resolve encrypted file version safely", { cause: error });
1269
1231
  }
1270
1232
  const mediaId = fileInfo?.info?.url;
1271
1233
  if (typeof mediaId !== "string" || mediaId.length === 0) {
1272
1234
  throw new StorageError("encrypted file version has no media identifier");
1273
1235
  }
1274
1236
  mediaIds.add(mediaId);
1275
- if (mediaIds.size > MAX_FILE_VERSION_CHAIN) {
1276
- throw new StorageError("file media version history is too large");
1277
- }
1278
1237
  versions.push({ branch: version, mediaId });
1279
1238
  }
1280
1239
  return versions;
@@ -1297,7 +1256,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
1297
1256
  throw error;
1298
1257
  if (signal?.aborted)
1299
1258
  throw new StorageError("operation cancelled");
1300
- throw new StorageError("delete file media failed");
1259
+ throw new StorageError("delete file media failed", { cause: error });
1301
1260
  }
1302
1261
  }
1303
1262
  export async function deleteFile(storage, treeId, fileId, options) {
@@ -1322,9 +1281,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
1322
1281
  if (error instanceof MutationOutcomeUnknownError)
1323
1282
  throw error;
1324
1283
  if (signal.aborted) {
1325
- throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled");
1284
+ throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled", { cause: error });
1326
1285
  }
1327
- throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped");
1286
+ throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped", { cause: error });
1328
1287
  }
1329
1288
  try {
1330
1289
  await waitForCondition(async () => {
@@ -1338,7 +1297,7 @@ export async function deleteFile(storage, treeId, fileId, options) {
1338
1297
  if (error instanceof MutationOutcomeUnknownError)
1339
1298
  throw error;
1340
1299
  const detail = "Matrix deletion completed but the inactive file state could not be verified";
1341
- throw new MutationPartialError("delete file", completedIds, detail);
1300
+ throw new MutationPartialError("delete file", completedIds, detail, { cause: error });
1342
1301
  }
1343
1302
  for (const { branch: version } of versions) {
1344
1303
  markFileDeleted(storage.getClient(), tree.id, version.id);
@@ -1357,7 +1316,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
1357
1316
  throw error;
1358
1317
  if (signal.aborted)
1359
1318
  throw new StorageError("operation cancelled");
1360
- throw new StorageError("upload failed");
1319
+ throw new StorageError("upload failed", { cause: error });
1361
1320
  });
1362
1321
  // The create-file request is acknowledged before the event necessarily
1363
1322
  // arrives in this client's sync timeline. A caller can otherwise report a
@@ -1376,7 +1335,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
1376
1335
  catch (error) {
1377
1336
  if (error instanceof MutationOutcomeUnknownError)
1378
1337
  throw error;
1379
- throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally");
1338
+ throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally", { cause: error });
1380
1339
  }
1381
1340
  ensureOperationActive(signal);
1382
1341
  return { id: fileId, name, mimetype };
@@ -1391,11 +1350,11 @@ export async function downloadFile(storage, treeId, fileId, options) {
1391
1350
  result = await storage.downloadFile(branch, signal);
1392
1351
  }
1393
1352
  catch (error) {
1394
- if (error instanceof UndecryptableFileError)
1353
+ if (error instanceof StorageError)
1395
1354
  throw error;
1396
1355
  if (signal.aborted)
1397
1356
  throw new StorageError("operation cancelled");
1398
- throw new StorageError("download failed");
1357
+ throw new StorageError("download failed", { cause: error });
1399
1358
  }
1400
1359
  ensureOperationActive(signal);
1401
1360
  return {
@@ -1425,41 +1384,21 @@ export async function getFileDetails(storage, treeId, fileId, options) {
1425
1384
  let size = null;
1426
1385
  let createdAt = null;
1427
1386
  let updatedAt = null;
1428
- try {
1429
- ensureOperationActive(signal);
1430
- const { info } = await branch.getFileInfo();
1431
- if (info) {
1432
- if (typeof info["mimetype"] === "string")
1433
- mimetype = info["mimetype"];
1434
- if (typeof info["size"] === "number")
1435
- size = info["size"];
1436
- }
1437
- }
1438
- catch {
1439
- if (signal.aborted)
1440
- throw new StorageError("operation cancelled");
1441
- // Partial metadata is fine — UI shows "—" for unknown fields.
1442
- }
1443
1387
  try {
1444
1388
  ensureOperationActive(signal);
1445
1389
  const event = await branch.getFileEvent();
1446
- const content = event.getContent();
1447
- const infoBlock = content["info"];
1448
- if (!mimetype && typeof infoBlock?.["mimetype"] === "string") {
1449
- mimetype = infoBlock["mimetype"];
1450
- }
1451
- if (size == null && typeof infoBlock?.["size"] === "number") {
1452
- size = infoBlock["size"];
1453
- }
1454
- const eventAny = event;
1455
- const ts = eventAny.getTs?.() ?? eventAny.origin_server_ts;
1456
- createdAt = tsToIso(ts);
1390
+ const metadata = readFileEventMetadata(event.getContent());
1391
+ mimetype = metadata.mimetype;
1392
+ size = metadata.size;
1393
+ createdAt = tsToIso(event.getTs());
1457
1394
  updatedAt = createdAt;
1458
1395
  }
1459
- catch {
1396
+ catch (error) {
1460
1397
  if (signal.aborted)
1461
1398
  throw new StorageError("operation cancelled");
1462
- // Same as above.
1399
+ if (error instanceof StorageError)
1400
+ throw error;
1401
+ throw new StorageError("get file details failed", { cause: error });
1463
1402
  }
1464
1403
  ensureOperationActive(signal);
1465
1404
  return { name, mimetype, size, createdAt, updatedAt };
@@ -1479,8 +1418,8 @@ async function getTreeDetails(storage, treeId, signal) {
1479
1418
  try {
1480
1419
  memberCount = room.getJoinedMemberCount();
1481
1420
  }
1482
- catch {
1483
- memberCount = null;
1421
+ catch (error) {
1422
+ throw new StorageError("member count lookup failed", { cause: error });
1484
1423
  }
1485
1424
  }
1486
1425
  return {
@@ -1,3 +1,6 @@
1
+ export declare class ConditionTimeoutError extends Error {
2
+ constructor(timeoutMs: number);
3
+ }
1
4
  /**
2
5
  * A freshly-synced client doesn't always see a room/event the *instant* the
3
6
  * server has committed it — e.g. a room this same session just created can
package/dist/core/poll.js CHANGED
@@ -1,3 +1,9 @@
1
+ export class ConditionTimeoutError extends Error {
2
+ constructor(timeoutMs) {
3
+ super(`timed out after ${timeoutMs}ms waiting for condition`);
4
+ this.name = "ConditionTimeoutError";
5
+ }
6
+ }
1
7
  /**
2
8
  * A freshly-synced client doesn't always see a room/event the *instant* the
3
9
  * server has committed it — e.g. a room this same session just created can
@@ -12,7 +18,7 @@ export async function waitForCondition(check, opts) {
12
18
  const timeoutMs = opts?.timeoutMs ?? 15000;
13
19
  const intervalMs = opts?.intervalMs ?? 300;
14
20
  const deadline = Date.now() + timeoutMs;
15
- const timeoutError = () => new Error(`timed out after ${timeoutMs}ms waiting for condition`);
21
+ const timeoutError = () => new ConditionTimeoutError(timeoutMs);
16
22
  const abortError = () => {
17
23
  const reason = opts?.signal?.reason;
18
24
  return reason instanceof Error ? reason : new Error("condition wait aborted");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.28",
3
+ "version": "0.5.29",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",