@telecrypt-io/storage 0.5.27 → 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.
- package/README.md +22 -8
- package/dist/TeleCryptIOStorage.d.ts +8 -10
- package/dist/TeleCryptIOStorage.js +244 -211
- package/dist/core/errors.d.ts +9 -9
- package/dist/core/errors.js +18 -18
- package/dist/core/http.d.ts +20 -14
- package/dist/core/http.js +165 -91
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +15 -0
- package/dist/core/oidc.js +533 -215
- package/dist/core/operations.js +187 -267
- package/dist/core/poll.d.ts +3 -0
- package/dist/core/poll.js +7 -1
- package/dist/core/validation.d.ts +2 -1
- package/dist/core/validation.js +8 -5
- package/package.json +1 -1
package/dist/core/operations.js
CHANGED
|
@@ -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,
|
|
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,15 +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
|
-
|
|
33
|
-
|
|
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
|
-
const DELETION_REFRESH_CONCURRENCY = 8;
|
|
36
35
|
const MAX_DELETION_DEPTH = 128;
|
|
37
|
-
const MAX_FILE_VERSION_CHAIN = 128;
|
|
38
|
-
const MAX_LIST_ITEMS = 10000;
|
|
39
36
|
const DEFAULT_MUTATION_TIMEOUT_MS = 5 * 60_000;
|
|
40
37
|
const MAX_MUTATION_TIMEOUT_MS = 15 * 60_000;
|
|
41
38
|
function createOperationDeadline(options) {
|
|
@@ -80,24 +77,17 @@ async function raceOperationDeadline(deadline, pending, kind) {
|
|
|
80
77
|
finally {
|
|
81
78
|
if (onAbort)
|
|
82
79
|
deadline.signal.removeEventListener("abort", onAbort);
|
|
83
|
-
void pending.catch(() => undefined);
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
82
|
async function withOperationDeadline(options, operation, kind = "read") {
|
|
87
83
|
const deadline = createOperationDeadline(options);
|
|
88
|
-
let pending;
|
|
89
84
|
try {
|
|
90
85
|
ensureOperationActive(deadline.signal);
|
|
91
|
-
pending = Promise.resolve().then(() => operation(deadline.signal));
|
|
86
|
+
const pending = Promise.resolve().then(() => operation(deadline.signal));
|
|
92
87
|
return await raceOperationDeadline(deadline, pending, kind);
|
|
93
88
|
}
|
|
94
89
|
finally {
|
|
95
90
|
deadline.close();
|
|
96
|
-
// A caller-facing deadline must not wait for a broken provider, but the
|
|
97
|
-
// late result remains observed so an ignored AbortSignal cannot create an
|
|
98
|
-
// unhandled rejection.
|
|
99
|
-
if (pending)
|
|
100
|
-
void pending.catch(() => undefined);
|
|
101
91
|
}
|
|
102
92
|
}
|
|
103
93
|
function isRateLimited(error) {
|
|
@@ -108,16 +98,28 @@ function isRateLimited(error) {
|
|
|
108
98
|
const isRateLimitError = error.isRateLimitError;
|
|
109
99
|
return typeof isRateLimitError === "function" && isRateLimitError.call(error) === true;
|
|
110
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
|
+
}
|
|
111
108
|
async function withRateLimitRetry(operation, signal) {
|
|
112
|
-
|
|
113
|
-
for (let attempt = 0;; attempt++) {
|
|
109
|
+
for (;;) {
|
|
114
110
|
if (signal?.aborted)
|
|
115
111
|
throw new StorageError("operation cancelled");
|
|
116
112
|
try {
|
|
117
113
|
return await operation();
|
|
118
114
|
}
|
|
119
115
|
catch (error) {
|
|
120
|
-
|
|
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))
|
|
121
123
|
throw error;
|
|
122
124
|
// The server's retry_after_ms only covers one token; tree operations
|
|
123
125
|
// need several (kick + leave + forget). Wait at least 15s so the
|
|
@@ -132,12 +134,7 @@ async function withRateLimitRetry(operation, signal) {
|
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
|
-
retryAfter = Math.min(retryAfter,
|
|
136
|
-
const remaining = RATE_LIMIT_MAX_TOTAL_DELAY_MS - totalDelay;
|
|
137
|
-
if (remaining <= 0)
|
|
138
|
-
throw error;
|
|
139
|
-
retryAfter = Math.min(retryAfter, remaining);
|
|
140
|
-
totalDelay += retryAfter;
|
|
137
|
+
retryAfter = Math.min(retryAfter, MAX_TIMER_DELAY_MS);
|
|
141
138
|
await new Promise((resolve, reject) => {
|
|
142
139
|
let timer;
|
|
143
140
|
const onAbort = () => {
|
|
@@ -170,10 +167,12 @@ async function resolveTree(storage, treeId, signal) {
|
|
|
170
167
|
signal,
|
|
171
168
|
});
|
|
172
169
|
}
|
|
173
|
-
catch {
|
|
170
|
+
catch (error) {
|
|
174
171
|
if (signal?.aborted)
|
|
175
172
|
throw new StorageError("operation cancelled");
|
|
176
|
-
|
|
173
|
+
if (error instanceof ConditionTimeoutError)
|
|
174
|
+
throw new StorageError("storage space not found");
|
|
175
|
+
throw new StorageError("storage space lookup failed", { cause: error });
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
178
|
function isMarkedTreeDeleted(storage, treeId) {
|
|
@@ -198,10 +197,12 @@ async function resolveFile(storage, tree, fileId, signal) {
|
|
|
198
197
|
try {
|
|
199
198
|
return await waitForCondition(() => (isMarkedFileDeleted(storage, tree.id, fileId) ? null : tree.getFile(fileId)), { timeoutMs: 15000, signal });
|
|
200
199
|
}
|
|
201
|
-
catch {
|
|
200
|
+
catch (error) {
|
|
202
201
|
if (signal?.aborted)
|
|
203
202
|
throw new StorageError("operation cancelled");
|
|
204
|
-
|
|
203
|
+
if (error instanceof ConditionTimeoutError)
|
|
204
|
+
throw new StorageError("file not found");
|
|
205
|
+
throw new StorageError("file lookup failed", { cause: error });
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large") {
|
|
@@ -224,8 +225,8 @@ function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large")
|
|
|
224
225
|
try {
|
|
225
226
|
children = tree.getDirectories();
|
|
226
227
|
}
|
|
227
|
-
catch {
|
|
228
|
-
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 });
|
|
229
230
|
}
|
|
230
231
|
if (children.length > MAX_DELETION_ROOMS) {
|
|
231
232
|
throw new StorageError(tooLargeMessage);
|
|
@@ -251,13 +252,13 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
251
252
|
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
|
|
252
253
|
!isConfirmedDeletedFile(storage.getClient(), tree.id, file));
|
|
253
254
|
}
|
|
254
|
-
catch {
|
|
255
|
-
throw new StorageError("could not enumerate storage files safely");
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw new StorageError("could not enumerate storage files safely", { cause: error });
|
|
256
257
|
}
|
|
257
258
|
if (files.length > 0)
|
|
258
259
|
throw new NonEmptyTreeError(tree.id);
|
|
259
260
|
}
|
|
260
|
-
async function refreshDeletionRooms(storage, signal) {
|
|
261
|
+
async function refreshDeletionRooms(storage, rootId, signal) {
|
|
261
262
|
const client = storage.getClient();
|
|
262
263
|
const http = client.http;
|
|
263
264
|
// A real MatrixClient always exposes its authenticated HTTP transport. A
|
|
@@ -265,48 +266,31 @@ async function refreshDeletionRooms(storage, signal) {
|
|
|
265
266
|
if (typeof http?.authedRequest !== "function") {
|
|
266
267
|
throw new StorageError("delete graph refresh is unavailable");
|
|
267
268
|
}
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
throw new StorageError("
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
for (const
|
|
278
|
-
if (!
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
269
|
+
await storage.refreshRoomState(rootId, { signal });
|
|
270
|
+
if (signal?.aborted)
|
|
271
|
+
throw new StorageError("operation cancelled");
|
|
272
|
+
// The refreshed root owns its parent relation. Verify its one supported
|
|
273
|
+
// external parent directly; unrelated local rooms are outside this deletion.
|
|
274
|
+
const parentEvents = readRelationEvents(client, rootId, EventType.SpaceParent);
|
|
275
|
+
if (parentEvents === null)
|
|
276
|
+
throw new StorageError("delete graph is unsafe");
|
|
277
|
+
const parentIds = new Set();
|
|
278
|
+
for (const event of parentEvents) {
|
|
279
|
+
if (!isActiveRelationEvent(event))
|
|
280
|
+
continue;
|
|
281
|
+
const parentId = relationStateKey(event);
|
|
282
|
+
if (!parentId)
|
|
283
|
+
throw new StorageError("delete graph is unsafe");
|
|
284
|
+
if (parentId !== rootId)
|
|
285
|
+
parentIds.add(parentId);
|
|
284
286
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
while (!failed && !signal?.aborted) {
|
|
291
|
-
const roomId = roomIdsToRefresh[nextRoom++];
|
|
292
|
-
if (roomId === undefined)
|
|
293
|
-
return;
|
|
294
|
-
try {
|
|
295
|
-
await storage.refreshRoomState(roomId, { signal });
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
failed = true;
|
|
299
|
-
firstError = error;
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
const workerCount = Math.min(DELETION_REFRESH_CONCURRENCY, roomIdsToRefresh.length);
|
|
305
|
-
await Promise.all(Array.from({ length: workerCount }, () => refreshWorker()));
|
|
287
|
+
if (parentIds.size > 1)
|
|
288
|
+
throw new StorageError("delete graph is unsafe");
|
|
289
|
+
const parentId = parentIds.values().next().value;
|
|
290
|
+
if (parentId)
|
|
291
|
+
await storage.refreshRoomState(parentId, { signal });
|
|
306
292
|
if (signal?.aborted)
|
|
307
293
|
throw new StorageError("operation cancelled");
|
|
308
|
-
if (failed)
|
|
309
|
-
throw firstError;
|
|
310
294
|
}
|
|
311
295
|
async function refreshTreeSpaces(storage, spaces, signal) {
|
|
312
296
|
for (const space of spaces) {
|
|
@@ -378,8 +362,8 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
|
378
362
|
try {
|
|
379
363
|
directories = tree.getDirectories();
|
|
380
364
|
}
|
|
381
|
-
catch {
|
|
382
|
-
throw new StorageError("delete graph is unsafe");
|
|
365
|
+
catch (error) {
|
|
366
|
+
throw new StorageError("delete graph is unsafe", { cause: error });
|
|
383
367
|
}
|
|
384
368
|
for (const child of directories)
|
|
385
369
|
addEdge(id, child.id);
|
|
@@ -420,31 +404,11 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
|
420
404
|
if (id !== root.id || externalParents.includes(parentId)) {
|
|
421
405
|
throw new StorageError("delete graph is unsafe");
|
|
422
406
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
const rooms = client.getRooms();
|
|
428
|
-
if (rooms.length > MAX_DELETION_ROOMS)
|
|
429
|
-
throw new StorageError("delete graph is too large");
|
|
430
|
-
for (const room of rooms) {
|
|
431
|
-
const parentId = room.roomId;
|
|
432
|
-
const childEvents = readRelationEvents(client, parentId, EventType.SpaceChild);
|
|
433
|
-
if (childEvents === null)
|
|
434
|
-
throw new StorageError("delete graph is unsafe");
|
|
435
|
-
for (const event of childEvents) {
|
|
436
|
-
if (!isActiveRelationEvent(event))
|
|
437
|
-
continue;
|
|
438
|
-
const childId = relationStateKey(event);
|
|
439
|
-
if (!childId)
|
|
440
|
-
throw new StorageError("delete graph is unsafe");
|
|
441
|
-
if (ids.has(childId) && !ids.has(parentId)) {
|
|
442
|
-
if (childId === root.id) {
|
|
443
|
-
if (!externalParents.includes(parentId))
|
|
444
|
-
externalParents.push(parentId);
|
|
445
|
-
continue;
|
|
407
|
+
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, id);
|
|
408
|
+
if (!childLink?.some((candidate) => isActiveRelationEvent(candidate) && relationStateKey(candidate, id) === id)) {
|
|
409
|
+
throw new StorageError("delete graph is unsafe");
|
|
446
410
|
}
|
|
447
|
-
|
|
411
|
+
externalParents.push(parentId);
|
|
448
412
|
}
|
|
449
413
|
}
|
|
450
414
|
}
|
|
@@ -493,8 +457,10 @@ async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
493
457
|
try {
|
|
494
458
|
await relinkExternalParents(storage, rootId, externalParents, new AbortController().signal);
|
|
495
459
|
}
|
|
496
|
-
catch {
|
|
497
|
-
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
|
+
});
|
|
498
464
|
}
|
|
499
465
|
throw error;
|
|
500
466
|
}
|
|
@@ -524,60 +490,78 @@ function isGoneError(error) {
|
|
|
524
490
|
(error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
|
|
525
491
|
}
|
|
526
492
|
async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
if (member.userId
|
|
542
|
-
(membership
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (isGoneError(error))
|
|
493
|
+
const completedRoomIds = [];
|
|
494
|
+
const markRoomMutationComplete = () => {
|
|
495
|
+
if (!completedRoomIds.includes(roomId))
|
|
496
|
+
completedRoomIds.push(roomId);
|
|
497
|
+
};
|
|
498
|
+
try {
|
|
499
|
+
const client = storage.getClient();
|
|
500
|
+
const tree = storage.getTree(roomId);
|
|
501
|
+
if (!tree)
|
|
502
|
+
throw new StorageError("delete graph is unsafe");
|
|
503
|
+
const self = client.getUserId();
|
|
504
|
+
if (!self)
|
|
505
|
+
throw new StorageError("delete graph is unsafe");
|
|
506
|
+
const members = await storage.listMembers(tree, { signal });
|
|
507
|
+
if (members.some((member) => member.userId !== self && member.role === "owner" &&
|
|
508
|
+
(member.membership === "join" || member.membership === "invite" || member.membership === "knock"))) {
|
|
509
|
+
throw new StorageError("delete will not kick another room owner");
|
|
510
|
+
}
|
|
511
|
+
for (const member of members) {
|
|
512
|
+
const membership = member.membership;
|
|
513
|
+
if (member.userId === self ||
|
|
514
|
+
(membership !== "join" && membership !== "invite" && membership !== "knock")) {
|
|
550
515
|
continue;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.kick(roomId, member.userId, "Room deleted"), signal), signal);
|
|
519
|
+
markRoomMutationComplete();
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (isGoneError(error))
|
|
556
523
|
continue;
|
|
524
|
+
if (error instanceof MatrixError && error.errcode === "M_FORBIDDEN") {
|
|
525
|
+
const currentMembership = await storage.getRoomMembership(roomId, member.userId, { signal });
|
|
526
|
+
if (currentMembership === "leave" ||
|
|
527
|
+
currentMembership === "ban" ||
|
|
528
|
+
!currentMembership) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
557
531
|
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const ownMembership = await storage.getRoomMembership(roomId, undefined, { signal });
|
|
536
|
+
if (ownMembership === "join" || ownMembership === "invite" || ownMembership === "knock") {
|
|
537
|
+
try {
|
|
538
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.leave(roomId), signal), signal);
|
|
539
|
+
markRoomMutationComplete();
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (!isGoneError(error))
|
|
543
|
+
throw error;
|
|
558
544
|
}
|
|
559
|
-
throw error;
|
|
560
545
|
}
|
|
561
|
-
}
|
|
562
|
-
const ownMembership = await storage.getRoomMembership(roomId, undefined, { signal });
|
|
563
|
-
if (ownMembership === "join" || ownMembership === "invite" || ownMembership === "knock") {
|
|
564
546
|
try {
|
|
565
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.
|
|
547
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.forget(roomId), signal), signal);
|
|
548
|
+
markRoomMutationComplete();
|
|
566
549
|
}
|
|
567
550
|
catch (error) {
|
|
568
551
|
if (!isGoneError(error))
|
|
569
552
|
throw error;
|
|
570
553
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.forget(roomId), signal), signal);
|
|
554
|
+
removeRoomFromLocalStore(client, roomId);
|
|
555
|
+
markTreeDeleted(client, roomId);
|
|
574
556
|
}
|
|
575
557
|
catch (error) {
|
|
576
|
-
if (
|
|
558
|
+
if (error instanceof MutationOutcomeUnknownError || error instanceof MutationPartialError)
|
|
577
559
|
throw error;
|
|
560
|
+
if (completedRoomIds.length > 0) {
|
|
561
|
+
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped", { cause: error });
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
578
564
|
}
|
|
579
|
-
removeRoomFromLocalStore(client, roomId);
|
|
580
|
-
markTreeDeleted(client, roomId);
|
|
581
565
|
}
|
|
582
566
|
/**
|
|
583
567
|
* The SDK normally evicts a room as part of forget(). Keep the local-store
|
|
@@ -585,14 +569,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
585
569
|
* may otherwise leave a stale invite visible until the next sync.
|
|
586
570
|
*/
|
|
587
571
|
function removeRoomFromLocalStore(client, roomId) {
|
|
588
|
-
|
|
589
|
-
client.store?.removeRoom(roomId);
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
// Server-side leave/forget already completed; a store implementation that
|
|
593
|
-
// cannot evict synchronously must not turn authoritative cleanup into a
|
|
594
|
-
// false failure.
|
|
595
|
-
}
|
|
572
|
+
client.store?.removeRoom(roomId);
|
|
596
573
|
}
|
|
597
574
|
function isActiveMembership(membership) {
|
|
598
575
|
return membership === "join" || membership === "invite";
|
|
@@ -602,12 +579,11 @@ function isRevocableMembership(membership) {
|
|
|
602
579
|
}
|
|
603
580
|
function safePartialDetail(error) {
|
|
604
581
|
// Provider/Matrix errors can contain response bodies, URLs, or credentials.
|
|
605
|
-
// StorageError messages are authored by this package
|
|
606
|
-
//
|
|
607
|
-
// 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.
|
|
608
584
|
if (!(error instanceof StorageError) || error instanceof MutationOutcomeUnknownError)
|
|
609
585
|
return undefined;
|
|
610
|
-
return error.message
|
|
586
|
+
return error.message;
|
|
611
587
|
}
|
|
612
588
|
function toArrayBuffer(bytes) {
|
|
613
589
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
@@ -619,16 +595,12 @@ export async function createVault(storage, name, options) {
|
|
|
619
595
|
tree = await withRateLimitRetry(() => storage.createTree(name, signal), signal);
|
|
620
596
|
}
|
|
621
597
|
catch (error) {
|
|
622
|
-
if (error
|
|
623
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
624
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
625
|
-
(error instanceof Error &&
|
|
626
|
-
error.cleanupIncomplete === true)) {
|
|
598
|
+
if (requiresMutationReconciliation(error)) {
|
|
627
599
|
throw error;
|
|
628
600
|
}
|
|
629
601
|
if (signal.aborted)
|
|
630
602
|
throw new StorageError("operation cancelled");
|
|
631
|
-
throw new StorageError("create vault failed");
|
|
603
|
+
throw new StorageError("create vault failed", { cause: error });
|
|
632
604
|
}
|
|
633
605
|
ensureOperationActive(signal);
|
|
634
606
|
return { id: tree.id, name };
|
|
@@ -666,7 +638,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
666
638
|
membership = null;
|
|
667
639
|
}
|
|
668
640
|
else {
|
|
669
|
-
throw new StorageError("join failed");
|
|
641
|
+
throw new StorageError("join failed", { cause: err });
|
|
670
642
|
}
|
|
671
643
|
}
|
|
672
644
|
if (membership === "join")
|
|
@@ -684,7 +656,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
684
656
|
}
|
|
685
657
|
if (signal.aborted)
|
|
686
658
|
throw new StorageError("operation cancelled");
|
|
687
|
-
throw new StorageError("join failed");
|
|
659
|
+
throw new StorageError("join failed", { cause: err });
|
|
688
660
|
}
|
|
689
661
|
ensureOperationActive(signal);
|
|
690
662
|
return { vaultId, joined: true };
|
|
@@ -720,8 +692,6 @@ export async function listPendingInvites(storage, options) {
|
|
|
720
692
|
return withOperationDeadline(options, async (signal) => {
|
|
721
693
|
const client = storage.getClient();
|
|
722
694
|
const rooms = client.getRooms();
|
|
723
|
-
if (rooms.length > MAX_LIST_ITEMS)
|
|
724
|
-
throw new StorageError("invite list is too large");
|
|
725
695
|
const invites = [];
|
|
726
696
|
for (const room of rooms) {
|
|
727
697
|
ensureOperationActive(signal);
|
|
@@ -840,7 +810,7 @@ export async function declineInvite(storage, vaultId, options) {
|
|
|
840
810
|
throw error;
|
|
841
811
|
if (signal.aborted)
|
|
842
812
|
throw new StorageError("operation cancelled");
|
|
843
|
-
throw new StorageError("decline failed");
|
|
813
|
+
throw new StorageError("decline failed", { cause: error });
|
|
844
814
|
}
|
|
845
815
|
ensureOperationActive(signal);
|
|
846
816
|
return { vaultId, declined: true };
|
|
@@ -913,7 +883,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
913
883
|
if (error instanceof MutationOutcomeUnknownError)
|
|
914
884
|
throw error;
|
|
915
885
|
if (completedRoomIds.size > 0) {
|
|
916
|
-
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error));
|
|
886
|
+
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
917
887
|
}
|
|
918
888
|
if (error instanceof StorageError &&
|
|
919
889
|
(error.message === "share failed" ||
|
|
@@ -922,7 +892,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
922
892
|
error.message === "operation cancelled")) {
|
|
923
893
|
throw error;
|
|
924
894
|
}
|
|
925
|
-
throw new StorageError("share failed");
|
|
895
|
+
throw new StorageError("share failed", { cause: error });
|
|
926
896
|
}
|
|
927
897
|
return { vaultId, userId, role };
|
|
928
898
|
}, operation.signal);
|
|
@@ -988,7 +958,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
988
958
|
if (error instanceof MutationOutcomeUnknownError)
|
|
989
959
|
throw error;
|
|
990
960
|
if (completedRoomIds.size > 0) {
|
|
991
|
-
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error));
|
|
961
|
+
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
992
962
|
}
|
|
993
963
|
if (error instanceof StorageError &&
|
|
994
964
|
(error.message.includes("owner") ||
|
|
@@ -996,7 +966,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
996
966
|
error.message === "operation cancelled")) {
|
|
997
967
|
throw error;
|
|
998
968
|
}
|
|
999
|
-
throw new StorageError("unshare failed");
|
|
969
|
+
throw new StorageError("unshare failed", { cause: error });
|
|
1000
970
|
}
|
|
1001
971
|
return { vaultId, userId, removed: true };
|
|
1002
972
|
}, operation.signal);
|
|
@@ -1012,10 +982,10 @@ export async function listMembers(storage, vaultId, options) {
|
|
|
1012
982
|
try {
|
|
1013
983
|
return await withRateLimitRetry(() => storage.listMembers(tree, { signal }), signal);
|
|
1014
984
|
}
|
|
1015
|
-
catch {
|
|
985
|
+
catch (error) {
|
|
1016
986
|
if (signal.aborted)
|
|
1017
987
|
throw new StorageError("operation cancelled");
|
|
1018
|
-
throw new StorageError("list members failed");
|
|
988
|
+
throw new StorageError("list members failed", { cause: error });
|
|
1019
989
|
}
|
|
1020
990
|
});
|
|
1021
991
|
}
|
|
@@ -1028,16 +998,12 @@ export async function listFiles(storage, treeId, options) {
|
|
|
1028
998
|
const files = tree
|
|
1029
999
|
.listFiles()
|
|
1030
1000
|
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
1031
|
-
if (files.length > MAX_LIST_ITEMS)
|
|
1032
|
-
throw new StorageError("file list is too large");
|
|
1033
1001
|
return files.map((f) => ({ id: f.id, name: f.getName() }));
|
|
1034
1002
|
}
|
|
1035
1003
|
catch (error) {
|
|
1036
1004
|
if (signal.aborted)
|
|
1037
1005
|
throw new StorageError("operation cancelled");
|
|
1038
|
-
|
|
1039
|
-
throw error;
|
|
1040
|
-
throw new StorageError("list files failed");
|
|
1006
|
+
throw new StorageError("list files failed", { cause: error });
|
|
1041
1007
|
}
|
|
1042
1008
|
});
|
|
1043
1009
|
}
|
|
@@ -1050,8 +1016,6 @@ export async function listSubfolders(storage, parentId, options) {
|
|
|
1050
1016
|
const directories = tree
|
|
1051
1017
|
.getDirectories()
|
|
1052
1018
|
.filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
|
|
1053
|
-
if (directories.length > MAX_LIST_ITEMS)
|
|
1054
|
-
throw new StorageError("folder list is too large");
|
|
1055
1019
|
return directories.map((d) => ({ id: d.id, name: d.room.name }));
|
|
1056
1020
|
});
|
|
1057
1021
|
}
|
|
@@ -1064,16 +1028,12 @@ export async function createSubfolder(storage, parentId, name, options) {
|
|
|
1064
1028
|
sub = await withRateLimitRetry(() => storage.createSubtree(tree, name, signal), signal);
|
|
1065
1029
|
}
|
|
1066
1030
|
catch (error) {
|
|
1067
|
-
if (error
|
|
1068
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
1069
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
1070
|
-
(error instanceof Error &&
|
|
1071
|
-
error.cleanupIncomplete === true)) {
|
|
1031
|
+
if (requiresMutationReconciliation(error)) {
|
|
1072
1032
|
throw error;
|
|
1073
1033
|
}
|
|
1074
1034
|
if (signal.aborted)
|
|
1075
1035
|
throw new StorageError("operation cancelled");
|
|
1076
|
-
throw new StorageError("create folder failed");
|
|
1036
|
+
throw new StorageError("create folder failed", { cause: error });
|
|
1077
1037
|
}
|
|
1078
1038
|
ensureOperationActive(signal);
|
|
1079
1039
|
return { id: sub.id, name };
|
|
@@ -1091,12 +1051,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1091
1051
|
const current = storage.getTree(treeId);
|
|
1092
1052
|
if (current?.room.name === name)
|
|
1093
1053
|
return current;
|
|
1094
|
-
|
|
1095
|
-
await storage.refreshRoomState(treeId, { signal });
|
|
1096
|
-
}
|
|
1097
|
-
catch {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1054
|
+
await storage.refreshRoomState(treeId, { signal });
|
|
1100
1055
|
const refreshed = storage.getTree(treeId);
|
|
1101
1056
|
return refreshed?.room.name === name ? refreshed : null;
|
|
1102
1057
|
}, { timeoutMs: 15000, signal });
|
|
@@ -1106,7 +1061,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1106
1061
|
throw error;
|
|
1107
1062
|
if (signal.aborted)
|
|
1108
1063
|
throw new StorageError("operation cancelled");
|
|
1109
|
-
throw new StorageError("rename failed");
|
|
1064
|
+
throw new StorageError("rename failed", { cause: error });
|
|
1110
1065
|
}
|
|
1111
1066
|
return { id: treeId, name };
|
|
1112
1067
|
}, "mutation");
|
|
@@ -1134,10 +1089,11 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1134
1089
|
const removedRooms = getDeletedTreeIds(client);
|
|
1135
1090
|
let graph;
|
|
1136
1091
|
try {
|
|
1137
|
-
// Re-read
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1092
|
+
// Re-read the candidate graph and any external parents it identifies
|
|
1093
|
+
// before taking the immutable graph snapshot. Unrelated rooms are not
|
|
1094
|
+
// part of this operation and must not block deletion merely because
|
|
1095
|
+
// their membership is not joined.
|
|
1096
|
+
await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1141
1097
|
const spaces = snapshotTreeSpaces(tree, "delete graph is too large");
|
|
1142
1098
|
const activeSpaces = spaces.filter((space) => !removedRooms.has(space.id));
|
|
1143
1099
|
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
@@ -1151,13 +1107,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1151
1107
|
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
1152
1108
|
}
|
|
1153
1109
|
catch (error) {
|
|
1154
|
-
if (error instanceof
|
|
1155
|
-
error instanceof MutationPartialError ||
|
|
1156
|
-
error instanceof NonEmptyTreeError)
|
|
1157
|
-
throw error;
|
|
1158
|
-
if (error instanceof StorageError && error.message === "operation cancelled")
|
|
1110
|
+
if (error instanceof StorageError)
|
|
1159
1111
|
throw error;
|
|
1160
|
-
throw new StorageError("delete failed");
|
|
1112
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1161
1113
|
}
|
|
1162
1114
|
// Keep external parent links intact until the room deletion succeeds.
|
|
1163
1115
|
// Unlinking first would silently detach a live vault on failure.
|
|
@@ -1165,16 +1117,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1165
1117
|
await unlinkExternalParents(storage, tree.id, graph.externalParents, operation.signal);
|
|
1166
1118
|
}
|
|
1167
1119
|
catch (error) {
|
|
1168
|
-
if (error instanceof
|
|
1169
|
-
throw error;
|
|
1170
|
-
if (error instanceof MutationPartialError)
|
|
1171
|
-
throw error;
|
|
1172
|
-
if (error instanceof StorageError && error.message === "delete graph unlink cleanup is incomplete") {
|
|
1173
|
-
throw error;
|
|
1174
|
-
}
|
|
1175
|
-
if (error instanceof StorageError && error.message === "operation cancelled")
|
|
1120
|
+
if (error instanceof StorageError)
|
|
1176
1121
|
throw error;
|
|
1177
|
-
throw new StorageError("delete failed");
|
|
1122
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1178
1123
|
}
|
|
1179
1124
|
try {
|
|
1180
1125
|
await deleteRoomDeterministically(storage, tree.id, operation.signal);
|
|
@@ -1186,16 +1131,14 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1186
1131
|
try {
|
|
1187
1132
|
await relinkExternalParents(storage, tree.id, graph.externalParents, new AbortController().signal);
|
|
1188
1133
|
}
|
|
1189
|
-
catch {
|
|
1190
|
-
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
|
+
});
|
|
1191
1138
|
}
|
|
1192
|
-
if (error instanceof
|
|
1193
|
-
throw error;
|
|
1194
|
-
if (error instanceof MutationPartialError)
|
|
1195
|
-
throw error;
|
|
1196
|
-
if (error instanceof StorageError && error.message === "operation cancelled")
|
|
1139
|
+
if (error instanceof StorageError)
|
|
1197
1140
|
throw error;
|
|
1198
|
-
throw new StorageError("delete failed");
|
|
1141
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1199
1142
|
}
|
|
1200
1143
|
return { id: treeId, deleted: true };
|
|
1201
1144
|
}, operation.signal);
|
|
@@ -1235,7 +1178,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1235
1178
|
throw error;
|
|
1236
1179
|
if (signal.aborted)
|
|
1237
1180
|
throw new StorageError("operation cancelled");
|
|
1238
|
-
throw new StorageError("rename file failed");
|
|
1181
|
+
throw new StorageError("rename file failed", { cause: error });
|
|
1239
1182
|
}
|
|
1240
1183
|
return { id: fileId, name };
|
|
1241
1184
|
}, "mutation");
|
|
@@ -1254,11 +1197,11 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1254
1197
|
try {
|
|
1255
1198
|
history = await branch.getVersionHistory();
|
|
1256
1199
|
}
|
|
1257
|
-
catch {
|
|
1258
|
-
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 });
|
|
1259
1202
|
}
|
|
1260
|
-
if (!Array.isArray(history) || history.length === 0
|
|
1261
|
-
throw new StorageError("file version history is invalid
|
|
1203
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
1204
|
+
throw new StorageError("file version history is invalid");
|
|
1262
1205
|
}
|
|
1263
1206
|
const eventIds = new Set();
|
|
1264
1207
|
const mediaIds = new Set();
|
|
@@ -1272,8 +1215,8 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1272
1215
|
try {
|
|
1273
1216
|
validateMatrixEventId(version.id, "file version event ID");
|
|
1274
1217
|
}
|
|
1275
|
-
catch {
|
|
1276
|
-
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 });
|
|
1277
1220
|
}
|
|
1278
1221
|
if (eventIds.has(version.id)) {
|
|
1279
1222
|
throw new StorageError("file version history contains a cycle");
|
|
@@ -1283,17 +1226,14 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1283
1226
|
try {
|
|
1284
1227
|
fileInfo = await version.getFileInfo();
|
|
1285
1228
|
}
|
|
1286
|
-
catch {
|
|
1287
|
-
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 });
|
|
1288
1231
|
}
|
|
1289
1232
|
const mediaId = fileInfo?.info?.url;
|
|
1290
1233
|
if (typeof mediaId !== "string" || mediaId.length === 0) {
|
|
1291
1234
|
throw new StorageError("encrypted file version has no media identifier");
|
|
1292
1235
|
}
|
|
1293
1236
|
mediaIds.add(mediaId);
|
|
1294
|
-
if (mediaIds.size > MAX_FILE_VERSION_CHAIN) {
|
|
1295
|
-
throw new StorageError("file media version history is too large");
|
|
1296
|
-
}
|
|
1297
1237
|
versions.push({ branch: version, mediaId });
|
|
1298
1238
|
}
|
|
1299
1239
|
return versions;
|
|
@@ -1316,7 +1256,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1316
1256
|
throw error;
|
|
1317
1257
|
if (signal?.aborted)
|
|
1318
1258
|
throw new StorageError("operation cancelled");
|
|
1319
|
-
throw new StorageError("delete file media failed");
|
|
1259
|
+
throw new StorageError("delete file media failed", { cause: error });
|
|
1320
1260
|
}
|
|
1321
1261
|
}
|
|
1322
1262
|
export async function deleteFile(storage, treeId, fileId, options) {
|
|
@@ -1341,9 +1281,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1341
1281
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1342
1282
|
throw error;
|
|
1343
1283
|
if (signal.aborted) {
|
|
1344
|
-
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 });
|
|
1345
1285
|
}
|
|
1346
|
-
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 });
|
|
1347
1287
|
}
|
|
1348
1288
|
try {
|
|
1349
1289
|
await waitForCondition(async () => {
|
|
@@ -1357,7 +1297,7 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1357
1297
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1358
1298
|
throw error;
|
|
1359
1299
|
const detail = "Matrix deletion completed but the inactive file state could not be verified";
|
|
1360
|
-
throw new MutationPartialError("delete file", completedIds, detail);
|
|
1300
|
+
throw new MutationPartialError("delete file", completedIds, detail, { cause: error });
|
|
1361
1301
|
}
|
|
1362
1302
|
for (const { branch: version } of versions) {
|
|
1363
1303
|
markFileDeleted(storage.getClient(), tree.id, version.id);
|
|
@@ -1376,7 +1316,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1376
1316
|
throw error;
|
|
1377
1317
|
if (signal.aborted)
|
|
1378
1318
|
throw new StorageError("operation cancelled");
|
|
1379
|
-
throw new StorageError("upload failed");
|
|
1319
|
+
throw new StorageError("upload failed", { cause: error });
|
|
1380
1320
|
});
|
|
1381
1321
|
// The create-file request is acknowledged before the event necessarily
|
|
1382
1322
|
// arrives in this client's sync timeline. A caller can otherwise report a
|
|
@@ -1395,7 +1335,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1395
1335
|
catch (error) {
|
|
1396
1336
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1397
1337
|
throw error;
|
|
1398
|
-
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 });
|
|
1399
1339
|
}
|
|
1400
1340
|
ensureOperationActive(signal);
|
|
1401
1341
|
return { id: fileId, name, mimetype };
|
|
@@ -1410,11 +1350,11 @@ export async function downloadFile(storage, treeId, fileId, options) {
|
|
|
1410
1350
|
result = await storage.downloadFile(branch, signal);
|
|
1411
1351
|
}
|
|
1412
1352
|
catch (error) {
|
|
1413
|
-
if (error instanceof
|
|
1353
|
+
if (error instanceof StorageError)
|
|
1414
1354
|
throw error;
|
|
1415
1355
|
if (signal.aborted)
|
|
1416
1356
|
throw new StorageError("operation cancelled");
|
|
1417
|
-
throw new StorageError("download failed");
|
|
1357
|
+
throw new StorageError("download failed", { cause: error });
|
|
1418
1358
|
}
|
|
1419
1359
|
ensureOperationActive(signal);
|
|
1420
1360
|
return {
|
|
@@ -1444,41 +1384,21 @@ export async function getFileDetails(storage, treeId, fileId, options) {
|
|
|
1444
1384
|
let size = null;
|
|
1445
1385
|
let createdAt = null;
|
|
1446
1386
|
let updatedAt = null;
|
|
1447
|
-
try {
|
|
1448
|
-
ensureOperationActive(signal);
|
|
1449
|
-
const { info } = await branch.getFileInfo();
|
|
1450
|
-
if (info) {
|
|
1451
|
-
if (typeof info["mimetype"] === "string")
|
|
1452
|
-
mimetype = info["mimetype"];
|
|
1453
|
-
if (typeof info["size"] === "number")
|
|
1454
|
-
size = info["size"];
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
catch {
|
|
1458
|
-
if (signal.aborted)
|
|
1459
|
-
throw new StorageError("operation cancelled");
|
|
1460
|
-
// Partial metadata is fine — UI shows "—" for unknown fields.
|
|
1461
|
-
}
|
|
1462
1387
|
try {
|
|
1463
1388
|
ensureOperationActive(signal);
|
|
1464
1389
|
const event = await branch.getFileEvent();
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
if (size == null && typeof infoBlock?.["size"] === "number") {
|
|
1471
|
-
size = infoBlock["size"];
|
|
1472
|
-
}
|
|
1473
|
-
const eventAny = event;
|
|
1474
|
-
const ts = eventAny.getTs?.() ?? eventAny.origin_server_ts;
|
|
1475
|
-
createdAt = tsToIso(ts);
|
|
1390
|
+
const metadata = readFileEventMetadata(event.getContent());
|
|
1391
|
+
mimetype = metadata.mimetype;
|
|
1392
|
+
size = metadata.size;
|
|
1393
|
+
createdAt = tsToIso(event.getTs());
|
|
1476
1394
|
updatedAt = createdAt;
|
|
1477
1395
|
}
|
|
1478
|
-
catch {
|
|
1396
|
+
catch (error) {
|
|
1479
1397
|
if (signal.aborted)
|
|
1480
1398
|
throw new StorageError("operation cancelled");
|
|
1481
|
-
|
|
1399
|
+
if (error instanceof StorageError)
|
|
1400
|
+
throw error;
|
|
1401
|
+
throw new StorageError("get file details failed", { cause: error });
|
|
1482
1402
|
}
|
|
1483
1403
|
ensureOperationActive(signal);
|
|
1484
1404
|
return { name, mimetype, size, createdAt, updatedAt };
|
|
@@ -1498,8 +1418,8 @@ async function getTreeDetails(storage, treeId, signal) {
|
|
|
1498
1418
|
try {
|
|
1499
1419
|
memberCount = room.getJoinedMemberCount();
|
|
1500
1420
|
}
|
|
1501
|
-
catch {
|
|
1502
|
-
|
|
1421
|
+
catch (error) {
|
|
1422
|
+
throw new StorageError("member count lookup failed", { cause: error });
|
|
1503
1423
|
}
|
|
1504
1424
|
}
|
|
1505
1425
|
return {
|