@telecrypt-io/storage 0.5.23 → 0.5.25

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.
@@ -7,6 +7,7 @@ import { FileTooLargeError, RecoveryAlreadyConfiguredError, RecoveryRestoreError
7
7
  import { MAX_MEDIA_FILE_BYTES, MAX_MATRIX_IDENTIFIER_LENGTH, validateCanonicalMatrixUserId, validateMatrixDeviceId, validateMatrixEventId, validateMatrixRoomId, } from "./core/constants.js";
8
8
  import { raceWithAbort, readBoundedResponseBody } from "./core/http.js";
9
9
  import { validateName } from "./core/validation.js";
10
+ import { isTreeDeleted } from "./deletion-markers.js";
10
11
  const recoveryMutationQueues = new WeakMap();
11
12
  const treeMutationQueues = new WeakMap();
12
13
  const TREE_SYNC_TIMEOUT_MS = 15000;
@@ -1244,6 +1245,8 @@ export class TeleCryptIOStorage {
1244
1245
  for (const room of rooms) {
1245
1246
  if (signal?.aborted)
1246
1247
  throw new StorageError("operation cancelled");
1248
+ if (isTreeDeleted(this.client, room.roomId))
1249
+ continue;
1247
1250
  const tree = this.client.unstableGetFileTreeSpace(room.roomId);
1248
1251
  if (tree)
1249
1252
  trees.push(this.decorateTreeSpace(tree));
@@ -1373,6 +1376,8 @@ export class TeleCryptIOStorage {
1373
1376
  }, signal);
1374
1377
  }
1375
1378
  getTree(roomId) {
1379
+ if (isTreeDeleted(this.client, roomId))
1380
+ return null;
1376
1381
  const tree = this.client.unstableGetFileTreeSpace(roomId);
1377
1382
  return tree ? this.decorateTreeSpace(tree) : null;
1378
1383
  }
@@ -18,6 +18,7 @@ import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, N
18
18
  import { waitForCondition } from "./poll.js";
19
19
  import { validateMatrixEventId } from "./constants.js";
20
20
  import { validateName } from "./validation.js";
21
+ import { getDeletedTreeIds, isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
21
22
  /**
22
23
  * Production Synapse enforces the built-in rc_messages budget (per-account
23
24
  * burst 10, refill 1/5s) on every room/state mutation the tree operations
@@ -175,12 +176,27 @@ async function resolveTree(storage, treeId, signal) {
175
176
  throw new StorageError("storage space not found");
176
177
  }
177
178
  }
179
+ function isMarkedTreeDeleted(storage, treeId) {
180
+ if (typeof storage.getClient !== "function")
181
+ return false;
182
+ return isTreeDeleted(storage.getClient(), treeId);
183
+ }
184
+ function isMarkedFileDeleted(storage, treeId, fileId) {
185
+ if (typeof storage.getClient !== "function")
186
+ return false;
187
+ return isFileDeleted(storage.getClient(), treeId, fileId);
188
+ }
178
189
  /** As `resolveTree`, but for a specific file within an already-resolved
179
190
  * vault or folder — covers the same settling window for a file another
180
191
  * process/session just uploaded. */
181
- async function resolveFile(tree, fileId, signal) {
192
+ async function resolveFile(storage, tree, fileId, signal) {
193
+ if (signal?.aborted)
194
+ throw new StorageError("operation cancelled");
195
+ if (isMarkedFileDeleted(storage, tree.id, fileId)) {
196
+ throw new StorageError("file not found");
197
+ }
182
198
  try {
183
- return await waitForCondition(() => tree.getFile(fileId), { timeoutMs: 15000, signal });
199
+ return await waitForCondition(() => (isMarkedFileDeleted(storage, tree.id, fileId) ? null : tree.getFile(fileId)), { timeoutMs: 15000, signal });
184
200
  }
185
201
  catch {
186
202
  if (signal?.aborted)
@@ -225,12 +241,14 @@ function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large")
225
241
  * observe each result before removing their containing room. This also
226
242
  * prevents a folder/vault delete from silently deleting a nested shared tree.
227
243
  */
228
- function assertTreeEmptyForDeletion(tree, spaces) {
244
+ function assertTreeEmptyForDeletion(storage, tree, spaces) {
229
245
  if (spaces.length !== 1)
230
246
  throw new NonEmptyTreeError(tree.id);
231
247
  let files;
232
248
  try {
233
- files = tree.listAllFiles();
249
+ files = tree
250
+ .listAllFiles()
251
+ .filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
234
252
  }
235
253
  catch {
236
254
  throw new StorageError("could not enumerate storage files safely");
@@ -480,8 +498,6 @@ async function relinkExternalParents(storage, rootId, externalParents, signal) {
480
498
  }
481
499
  }
482
500
  }
483
- const deletedTreeIds = new WeakMap();
484
- const deletedRoomIds = new WeakMap();
485
501
  function isGoneError(error) {
486
502
  return (error instanceof MatrixError &&
487
503
  (error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
@@ -540,12 +556,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
540
556
  throw error;
541
557
  }
542
558
  removeRoomFromLocalStore(client, roomId);
543
- let deleted = deletedRoomIds.get(client);
544
- if (!deleted) {
545
- deleted = new Set();
546
- deletedRoomIds.set(client, deleted);
547
- }
548
- deleted.add(roomId);
559
+ markTreeDeleted(client, roomId);
549
560
  }
550
561
  /**
551
562
  * The SDK normally evicts a room as part of forget(). Keep the local-store
@@ -693,6 +704,8 @@ export async function listPendingInvites(storage, options) {
693
704
  const invites = [];
694
705
  for (const room of rooms) {
695
706
  ensureOperationActive(signal);
707
+ if (isMarkedTreeDeleted(storage, room.roomId))
708
+ continue;
696
709
  if (room.getMyMembership() !== "invite")
697
710
  continue;
698
711
  if (hasActiveSpaceParent(room))
@@ -991,7 +1004,9 @@ export async function listFiles(storage, treeId, options) {
991
1004
  try {
992
1005
  await storage.refreshRoomState(treeId, { signal });
993
1006
  ensureOperationActive(signal);
994
- const files = tree.listFiles();
1007
+ const files = tree
1008
+ .listFiles()
1009
+ .filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
995
1010
  if (files.length > MAX_LIST_ITEMS)
996
1011
  throw new StorageError("file list is too large");
997
1012
  return files.map((f) => ({ id: f.id, name: f.getName() }));
@@ -1011,7 +1026,9 @@ export async function listSubfolders(storage, parentId, options) {
1011
1026
  const tree = await resolveTree(storage, parentId, signal);
1012
1027
  await storage.refreshRoomState(parentId, { signal });
1013
1028
  ensureOperationActive(signal);
1014
- const directories = tree.getDirectories();
1029
+ const directories = tree
1030
+ .getDirectories()
1031
+ .filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
1015
1032
  if (directories.length > MAX_LIST_ITEMS)
1016
1033
  throw new StorageError("folder list is too large");
1017
1034
  return directories.map((d) => ({ id: d.id, name: d.room.name }));
@@ -1084,21 +1101,16 @@ export async function renameFolder(storage, folderId, name, options) {
1084
1101
  async function deleteTree(storage, treeId, options) {
1085
1102
  const operation = createOperationDeadline(options);
1086
1103
  const client = storage.getClient();
1087
- let deleted = deletedTreeIds.get(client);
1088
- if (!deleted) {
1089
- deleted = new Set();
1090
- deletedTreeIds.set(client, deleted);
1091
- }
1092
1104
  try {
1093
1105
  ensureOperationActive(operation.signal);
1094
1106
  const pending = withTreeMutation(client, async () => {
1095
- if (deleted.has(treeId))
1107
+ if (isTreeDeleted(client, treeId))
1096
1108
  return { id: treeId, deleted: true };
1097
1109
  const tree = await resolveTree(storage, treeId, operation.signal);
1098
1110
  // Read reconciliation state after this operation reaches the single
1099
1111
  // client queue; a preceding delete may have populated the set while we
1100
1112
  // were waiting for its turn.
1101
- const removedRooms = deletedRoomIds.get(client) ?? new Set();
1113
+ const removedRooms = getDeletedTreeIds(client);
1102
1114
  let graph;
1103
1115
  try {
1104
1116
  // Re-read every locally visible room before taking the immutable graph
@@ -1110,12 +1122,11 @@ async function deleteTree(storage, treeId, options) {
1110
1122
  const ids = new Set(activeSpaces.map((space) => space.id));
1111
1123
  if (!ids.has(tree.id)) {
1112
1124
  if (removedRooms.has(tree.id)) {
1113
- deleted.add(treeId);
1114
1125
  return { id: treeId, deleted: true };
1115
1126
  }
1116
1127
  throw new StorageError("delete graph is unsafe");
1117
1128
  }
1118
- assertTreeEmptyForDeletion(tree, activeSpaces);
1129
+ assertTreeEmptyForDeletion(storage, tree, activeSpaces);
1119
1130
  graph = validateDeletionGraph(client, tree, ids, removedRooms);
1120
1131
  }
1121
1132
  catch (error) {
@@ -1165,7 +1176,6 @@ async function deleteTree(storage, treeId, options) {
1165
1176
  throw error;
1166
1177
  throw new StorageError("delete failed");
1167
1178
  }
1168
- deleted.add(treeId);
1169
1179
  return { id: treeId, deleted: true };
1170
1180
  }, operation.signal);
1171
1181
  return await raceOperationDeadline(operation, pending, "mutation");
@@ -1186,7 +1196,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
1186
1196
  validateName(name, "file name");
1187
1197
  return withOperationDeadline(options, async (signal) => {
1188
1198
  const tree = await resolveTree(storage, treeId, signal);
1189
- const branch = await resolveFile(tree, fileId, signal);
1199
+ const branch = await resolveFile(storage, tree, fileId, signal);
1190
1200
  try {
1191
1201
  const client = typeof storage.getClient === "function" ? storage.getClient() : undefined;
1192
1202
  await withRateLimitRetry(() => client ? withMatrixMutationAbort(client, () => branch.setName(name), signal) : branch.setName(name), signal);
@@ -1291,7 +1301,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
1291
1301
  export async function deleteFile(storage, treeId, fileId, options) {
1292
1302
  return withOperationDeadline(options, async (signal) => {
1293
1303
  const tree = await resolveTree(storage, treeId, signal);
1294
- const branch = await resolveFile(tree, fileId, signal);
1304
+ const branch = await resolveFile(storage, tree, fileId, signal);
1295
1305
  const versions = await resolveFileVersions(branch, signal);
1296
1306
  const mediaIds = [...new Set(versions.map(({ mediaId }) => mediaId))];
1297
1307
  await deleteFileMedia(storage, mediaIds, signal);
@@ -1328,6 +1338,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
1328
1338
  const detail = "Matrix deletion completed but the inactive file state could not be verified";
1329
1339
  throw new MutationPartialError("delete file", completedIds, detail);
1330
1340
  }
1341
+ for (const { branch: version } of versions) {
1342
+ markFileDeleted(storage.getClient(), tree.id, version.id);
1343
+ }
1331
1344
  ensureOperationActive(signal);
1332
1345
  return { id: fileId, deleted: true };
1333
1346
  }, "mutation");
@@ -1370,7 +1383,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
1370
1383
  export async function downloadFile(storage, treeId, fileId, options) {
1371
1384
  return withOperationDeadline(options, async (signal) => {
1372
1385
  const tree = await resolveTree(storage, treeId, signal);
1373
- const branch = await resolveFile(tree, fileId, signal);
1386
+ const branch = await resolveFile(storage, tree, fileId, signal);
1374
1387
  let result;
1375
1388
  try {
1376
1389
  result = await storage.downloadFile(branch, signal);
@@ -1404,7 +1417,7 @@ function tsToIso(ts) {
1404
1417
  export async function getFileDetails(storage, treeId, fileId, options) {
1405
1418
  return withOperationDeadline(options, async (signal) => {
1406
1419
  const tree = await resolveTree(storage, treeId, signal);
1407
- const branch = await resolveFile(tree, fileId, signal);
1420
+ const branch = await resolveFile(storage, tree, fileId, signal);
1408
1421
  const name = branch.getName();
1409
1422
  let mimetype = null;
1410
1423
  let size = null;
@@ -0,0 +1,8 @@
1
+ import type { MatrixClient } from "matrix-js-sdk";
2
+ /** Records a confirmed room deletion for this MatrixClient's lifetime. */
3
+ export declare function markTreeDeleted(client: MatrixClient, roomId: string): void;
4
+ /** Records a confirmed file deletion for this MatrixClient's lifetime. */
5
+ export declare function markFileDeleted(client: MatrixClient, roomId: string, fileId: string): void;
6
+ export declare function isTreeDeleted(client: MatrixClient, roomId: string): boolean;
7
+ export declare function getDeletedTreeIds(client: MatrixClient): ReadonlySet<string>;
8
+ export declare function isFileDeleted(client: MatrixClient, roomId: string, fileId: string): boolean;
@@ -0,0 +1,32 @@
1
+ const markersByClient = new WeakMap();
2
+ function markersFor(client) {
3
+ let markers = markersByClient.get(client);
4
+ if (!markers) {
5
+ markers = { trees: new Set(), files: new Map() };
6
+ markersByClient.set(client, markers);
7
+ }
8
+ return markers;
9
+ }
10
+ /** Records a confirmed room deletion for this MatrixClient's lifetime. */
11
+ export function markTreeDeleted(client, roomId) {
12
+ markersFor(client).trees.add(roomId);
13
+ }
14
+ /** Records a confirmed file deletion for this MatrixClient's lifetime. */
15
+ export function markFileDeleted(client, roomId, fileId) {
16
+ const markers = markersFor(client);
17
+ let files = markers.files.get(roomId);
18
+ if (!files) {
19
+ files = new Set();
20
+ markers.files.set(roomId, files);
21
+ }
22
+ files.add(fileId);
23
+ }
24
+ export function isTreeDeleted(client, roomId) {
25
+ return markersFor(client).trees.has(roomId);
26
+ }
27
+ export function getDeletedTreeIds(client) {
28
+ return markersFor(client).trees;
29
+ }
30
+ export function isFileDeleted(client, roomId, fileId) {
31
+ return markersFor(client).files.get(roomId)?.has(fileId) ?? false;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",