@telecrypt-io/storage 0.5.28 → 0.5.30
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 +17 -7
- package/dist/TeleCryptIOStorage.d.ts +8 -4
- package/dist/TeleCryptIOStorage.js +244 -183
- 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/oidc.d.ts +15 -0
- package/dist/core/oidc.js +533 -215
- package/dist/core/operations.js +151 -194
- package/dist/core/poll.d.ts +3 -0
- package/dist/core/poll.js +7 -1
- 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,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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,42 +197,13 @@ 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
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large") {
|
|
207
|
-
const spaces = [];
|
|
208
|
-
const seen = new Set();
|
|
209
|
-
const pending = [{ tree: root, depth: 0 }];
|
|
210
|
-
while (pending.length > 0) {
|
|
211
|
-
const next = pending.pop();
|
|
212
|
-
if (!next)
|
|
213
|
-
continue;
|
|
214
|
-
const { tree, depth } = next;
|
|
215
|
-
if (seen.has(tree.id))
|
|
216
|
-
continue;
|
|
217
|
-
if (depth > MAX_DELETION_DEPTH || spaces.length >= MAX_DELETION_ROOMS) {
|
|
218
|
-
throw new StorageError(tooLargeMessage);
|
|
219
|
-
}
|
|
220
|
-
seen.add(tree.id);
|
|
221
|
-
spaces.push(tree);
|
|
222
|
-
let children;
|
|
223
|
-
try {
|
|
224
|
-
children = tree.getDirectories();
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
throw new StorageError("could not enumerate storage space descendants safely");
|
|
228
|
-
}
|
|
229
|
-
if (children.length > MAX_DELETION_ROOMS) {
|
|
230
|
-
throw new StorageError(tooLargeMessage);
|
|
231
|
-
}
|
|
232
|
-
for (const child of children)
|
|
233
|
-
pending.push({ tree: child, depth: depth + 1 });
|
|
203
|
+
if (error instanceof ConditionTimeoutError)
|
|
204
|
+
throw new StorageError("file not found");
|
|
205
|
+
throw new StorageError("file lookup failed", { cause: error });
|
|
234
206
|
}
|
|
235
|
-
return spaces;
|
|
236
207
|
}
|
|
237
208
|
/**
|
|
238
209
|
* Deleting a tree is deliberately a one-room operation. File versions are
|
|
@@ -250,8 +221,8 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
250
221
|
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
|
|
251
222
|
!isConfirmedDeletedFile(storage.getClient(), tree.id, file));
|
|
252
223
|
}
|
|
253
|
-
catch {
|
|
254
|
-
throw new StorageError("could not enumerate storage files safely");
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new StorageError("could not enumerate storage files safely", { cause: error });
|
|
255
226
|
}
|
|
256
227
|
if (files.length > 0)
|
|
257
228
|
throw new NonEmptyTreeError(tree.id);
|
|
@@ -316,6 +287,62 @@ function isActiveRelationEvent(event) {
|
|
|
316
287
|
!Array.isArray(content) &&
|
|
317
288
|
Object.keys(content).length > 0);
|
|
318
289
|
}
|
|
290
|
+
function activeTreeDirectories(client, tree) {
|
|
291
|
+
const childEvents = readRelationEvents(client, tree.id, EventType.SpaceChild);
|
|
292
|
+
if (childEvents === null)
|
|
293
|
+
throw new StorageError("delete graph is unsafe");
|
|
294
|
+
const activeChildIds = new Set();
|
|
295
|
+
for (const event of childEvents) {
|
|
296
|
+
if (!isActiveRelationEvent(event))
|
|
297
|
+
continue;
|
|
298
|
+
const childId = relationStateKey(event);
|
|
299
|
+
if (!childId)
|
|
300
|
+
throw new StorageError("delete graph is unsafe");
|
|
301
|
+
activeChildIds.add(childId);
|
|
302
|
+
}
|
|
303
|
+
const knownChildren = new Map(tree.getDirectories().map((child) => [child.id, child]));
|
|
304
|
+
return [...activeChildIds].map((childId) => {
|
|
305
|
+
const child = knownChildren.get(childId);
|
|
306
|
+
if (!child)
|
|
307
|
+
throw new StorageError("delete graph is unsafe");
|
|
308
|
+
return child;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function snapshotTreeSpaces(root, tooLargeMessage = "storage tree is too large", activeRelationClient) {
|
|
312
|
+
const spaces = [];
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
const pending = [{ tree: root, depth: 0 }];
|
|
315
|
+
while (pending.length > 0) {
|
|
316
|
+
const next = pending.pop();
|
|
317
|
+
if (!next)
|
|
318
|
+
continue;
|
|
319
|
+
const { tree, depth } = next;
|
|
320
|
+
if (seen.has(tree.id))
|
|
321
|
+
continue;
|
|
322
|
+
if (depth > MAX_DELETION_DEPTH || spaces.length >= MAX_DELETION_ROOMS) {
|
|
323
|
+
throw new StorageError(tooLargeMessage);
|
|
324
|
+
}
|
|
325
|
+
seen.add(tree.id);
|
|
326
|
+
spaces.push(tree);
|
|
327
|
+
let children;
|
|
328
|
+
try {
|
|
329
|
+
children = activeRelationClient
|
|
330
|
+
? activeTreeDirectories(activeRelationClient, tree)
|
|
331
|
+
: tree.getDirectories();
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error instanceof StorageError)
|
|
335
|
+
throw error;
|
|
336
|
+
throw new StorageError("could not enumerate storage space descendants safely", { cause: error });
|
|
337
|
+
}
|
|
338
|
+
if (children.length > MAX_DELETION_ROOMS) {
|
|
339
|
+
throw new StorageError(tooLargeMessage);
|
|
340
|
+
}
|
|
341
|
+
for (const child of children)
|
|
342
|
+
pending.push({ tree: child, depth: depth + 1 });
|
|
343
|
+
}
|
|
344
|
+
return spaces;
|
|
345
|
+
}
|
|
319
346
|
/**
|
|
320
347
|
* A deleted MSC3089 branch is represented by an authoritative empty state
|
|
321
348
|
* event. Inactive branches with metadata are historical versions, and a
|
|
@@ -356,13 +383,7 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
|
356
383
|
const tree = client.unstableGetFileTreeSpace(id);
|
|
357
384
|
if (!tree)
|
|
358
385
|
throw new StorageError("delete graph is unsafe");
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
directories = tree.getDirectories();
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
throw new StorageError("delete graph is unsafe");
|
|
365
|
-
}
|
|
386
|
+
const directories = activeTreeDirectories(client, tree);
|
|
366
387
|
for (const child of directories)
|
|
367
388
|
addEdge(id, child.id);
|
|
368
389
|
const childEvents = readRelationEvents(client, id, EventType.SpaceChild);
|
|
@@ -455,8 +476,10 @@ async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
455
476
|
try {
|
|
456
477
|
await relinkExternalParents(storage, rootId, externalParents, new AbortController().signal);
|
|
457
478
|
}
|
|
458
|
-
catch {
|
|
459
|
-
throw new StorageError("delete graph unlink cleanup is incomplete"
|
|
479
|
+
catch (cleanupError) {
|
|
480
|
+
throw new StorageError("delete graph unlink cleanup is incomplete", {
|
|
481
|
+
cause: new AggregateError([error, cleanupError], "delete graph unlink and relink both failed", { cause: error }),
|
|
482
|
+
});
|
|
460
483
|
}
|
|
461
484
|
throw error;
|
|
462
485
|
}
|
|
@@ -554,7 +577,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
554
577
|
if (error instanceof MutationOutcomeUnknownError || error instanceof MutationPartialError)
|
|
555
578
|
throw error;
|
|
556
579
|
if (completedRoomIds.length > 0) {
|
|
557
|
-
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped");
|
|
580
|
+
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped", { cause: error });
|
|
558
581
|
}
|
|
559
582
|
throw error;
|
|
560
583
|
}
|
|
@@ -565,14 +588,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
565
588
|
* may otherwise leave a stale invite visible until the next sync.
|
|
566
589
|
*/
|
|
567
590
|
function removeRoomFromLocalStore(client, roomId) {
|
|
568
|
-
|
|
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
|
-
}
|
|
591
|
+
client.store?.removeRoom(roomId);
|
|
576
592
|
}
|
|
577
593
|
function isActiveMembership(membership) {
|
|
578
594
|
return membership === "join" || membership === "invite";
|
|
@@ -582,12 +598,11 @@ function isRevocableMembership(membership) {
|
|
|
582
598
|
}
|
|
583
599
|
function safePartialDetail(error) {
|
|
584
600
|
// Provider/Matrix errors can contain response bodies, URLs, or credentials.
|
|
585
|
-
// StorageError messages are authored by this package
|
|
586
|
-
//
|
|
587
|
-
// owner/self-target diagnostics.
|
|
601
|
+
// StorageError messages are authored by this package; retain those local
|
|
602
|
+
// policy details so callers still get useful owner/self-target diagnostics.
|
|
588
603
|
if (!(error instanceof StorageError) || error instanceof MutationOutcomeUnknownError)
|
|
589
604
|
return undefined;
|
|
590
|
-
return error.message
|
|
605
|
+
return error.message;
|
|
591
606
|
}
|
|
592
607
|
function toArrayBuffer(bytes) {
|
|
593
608
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
@@ -599,16 +614,12 @@ export async function createVault(storage, name, options) {
|
|
|
599
614
|
tree = await withRateLimitRetry(() => storage.createTree(name, signal), signal);
|
|
600
615
|
}
|
|
601
616
|
catch (error) {
|
|
602
|
-
if (error
|
|
603
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
604
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
605
|
-
(error instanceof Error &&
|
|
606
|
-
error.cleanupIncomplete === true)) {
|
|
617
|
+
if (requiresMutationReconciliation(error)) {
|
|
607
618
|
throw error;
|
|
608
619
|
}
|
|
609
620
|
if (signal.aborted)
|
|
610
621
|
throw new StorageError("operation cancelled");
|
|
611
|
-
throw new StorageError("create vault failed");
|
|
622
|
+
throw new StorageError("create vault failed", { cause: error });
|
|
612
623
|
}
|
|
613
624
|
ensureOperationActive(signal);
|
|
614
625
|
return { id: tree.id, name };
|
|
@@ -646,7 +657,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
646
657
|
membership = null;
|
|
647
658
|
}
|
|
648
659
|
else {
|
|
649
|
-
throw new StorageError("join failed");
|
|
660
|
+
throw new StorageError("join failed", { cause: err });
|
|
650
661
|
}
|
|
651
662
|
}
|
|
652
663
|
if (membership === "join")
|
|
@@ -664,7 +675,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
664
675
|
}
|
|
665
676
|
if (signal.aborted)
|
|
666
677
|
throw new StorageError("operation cancelled");
|
|
667
|
-
throw new StorageError("join failed");
|
|
678
|
+
throw new StorageError("join failed", { cause: err });
|
|
668
679
|
}
|
|
669
680
|
ensureOperationActive(signal);
|
|
670
681
|
return { vaultId, joined: true };
|
|
@@ -700,8 +711,6 @@ export async function listPendingInvites(storage, options) {
|
|
|
700
711
|
return withOperationDeadline(options, async (signal) => {
|
|
701
712
|
const client = storage.getClient();
|
|
702
713
|
const rooms = client.getRooms();
|
|
703
|
-
if (rooms.length > MAX_LIST_ITEMS)
|
|
704
|
-
throw new StorageError("invite list is too large");
|
|
705
714
|
const invites = [];
|
|
706
715
|
for (const room of rooms) {
|
|
707
716
|
ensureOperationActive(signal);
|
|
@@ -820,7 +829,7 @@ export async function declineInvite(storage, vaultId, options) {
|
|
|
820
829
|
throw error;
|
|
821
830
|
if (signal.aborted)
|
|
822
831
|
throw new StorageError("operation cancelled");
|
|
823
|
-
throw new StorageError("decline failed");
|
|
832
|
+
throw new StorageError("decline failed", { cause: error });
|
|
824
833
|
}
|
|
825
834
|
ensureOperationActive(signal);
|
|
826
835
|
return { vaultId, declined: true };
|
|
@@ -893,7 +902,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
893
902
|
if (error instanceof MutationOutcomeUnknownError)
|
|
894
903
|
throw error;
|
|
895
904
|
if (completedRoomIds.size > 0) {
|
|
896
|
-
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error));
|
|
905
|
+
throw new MutationPartialError("share", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
897
906
|
}
|
|
898
907
|
if (error instanceof StorageError &&
|
|
899
908
|
(error.message === "share failed" ||
|
|
@@ -902,7 +911,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
902
911
|
error.message === "operation cancelled")) {
|
|
903
912
|
throw error;
|
|
904
913
|
}
|
|
905
|
-
throw new StorageError("share failed");
|
|
914
|
+
throw new StorageError("share failed", { cause: error });
|
|
906
915
|
}
|
|
907
916
|
return { vaultId, userId, role };
|
|
908
917
|
}, operation.signal);
|
|
@@ -968,7 +977,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
968
977
|
if (error instanceof MutationOutcomeUnknownError)
|
|
969
978
|
throw error;
|
|
970
979
|
if (completedRoomIds.size > 0) {
|
|
971
|
-
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error));
|
|
980
|
+
throw new MutationPartialError("unshare", [...completedRoomIds], safePartialDetail(error), { cause: error });
|
|
972
981
|
}
|
|
973
982
|
if (error instanceof StorageError &&
|
|
974
983
|
(error.message.includes("owner") ||
|
|
@@ -976,7 +985,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
976
985
|
error.message === "operation cancelled")) {
|
|
977
986
|
throw error;
|
|
978
987
|
}
|
|
979
|
-
throw new StorageError("unshare failed");
|
|
988
|
+
throw new StorageError("unshare failed", { cause: error });
|
|
980
989
|
}
|
|
981
990
|
return { vaultId, userId, removed: true };
|
|
982
991
|
}, operation.signal);
|
|
@@ -992,10 +1001,10 @@ export async function listMembers(storage, vaultId, options) {
|
|
|
992
1001
|
try {
|
|
993
1002
|
return await withRateLimitRetry(() => storage.listMembers(tree, { signal }), signal);
|
|
994
1003
|
}
|
|
995
|
-
catch {
|
|
1004
|
+
catch (error) {
|
|
996
1005
|
if (signal.aborted)
|
|
997
1006
|
throw new StorageError("operation cancelled");
|
|
998
|
-
throw new StorageError("list members failed");
|
|
1007
|
+
throw new StorageError("list members failed", { cause: error });
|
|
999
1008
|
}
|
|
1000
1009
|
});
|
|
1001
1010
|
}
|
|
@@ -1008,16 +1017,12 @@ export async function listFiles(storage, treeId, options) {
|
|
|
1008
1017
|
const files = tree
|
|
1009
1018
|
.listFiles()
|
|
1010
1019
|
.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
1020
|
return files.map((f) => ({ id: f.id, name: f.getName() }));
|
|
1014
1021
|
}
|
|
1015
1022
|
catch (error) {
|
|
1016
1023
|
if (signal.aborted)
|
|
1017
1024
|
throw new StorageError("operation cancelled");
|
|
1018
|
-
|
|
1019
|
-
throw error;
|
|
1020
|
-
throw new StorageError("list files failed");
|
|
1025
|
+
throw new StorageError("list files failed", { cause: error });
|
|
1021
1026
|
}
|
|
1022
1027
|
});
|
|
1023
1028
|
}
|
|
@@ -1027,11 +1032,8 @@ export async function listSubfolders(storage, parentId, options) {
|
|
|
1027
1032
|
const tree = await resolveTree(storage, parentId, signal);
|
|
1028
1033
|
await storage.refreshRoomState(parentId, { signal });
|
|
1029
1034
|
ensureOperationActive(signal);
|
|
1030
|
-
const directories = tree
|
|
1031
|
-
.getDirectories()
|
|
1035
|
+
const directories = activeTreeDirectories(storage.getClient(), tree)
|
|
1032
1036
|
.filter((directory) => !isMarkedTreeDeleted(storage, directory.id));
|
|
1033
|
-
if (directories.length > MAX_LIST_ITEMS)
|
|
1034
|
-
throw new StorageError("folder list is too large");
|
|
1035
1037
|
return directories.map((d) => ({ id: d.id, name: d.room.name }));
|
|
1036
1038
|
});
|
|
1037
1039
|
}
|
|
@@ -1044,16 +1046,12 @@ export async function createSubfolder(storage, parentId, name, options) {
|
|
|
1044
1046
|
sub = await withRateLimitRetry(() => storage.createSubtree(tree, name, signal), signal);
|
|
1045
1047
|
}
|
|
1046
1048
|
catch (error) {
|
|
1047
|
-
if (error
|
|
1048
|
-
error instanceof RoomCreationAmbiguousError ||
|
|
1049
|
-
error instanceof MutationOutcomeUnknownError ||
|
|
1050
|
-
(error instanceof Error &&
|
|
1051
|
-
error.cleanupIncomplete === true)) {
|
|
1049
|
+
if (requiresMutationReconciliation(error)) {
|
|
1052
1050
|
throw error;
|
|
1053
1051
|
}
|
|
1054
1052
|
if (signal.aborted)
|
|
1055
1053
|
throw new StorageError("operation cancelled");
|
|
1056
|
-
throw new StorageError("create folder failed");
|
|
1054
|
+
throw new StorageError("create folder failed", { cause: error });
|
|
1057
1055
|
}
|
|
1058
1056
|
ensureOperationActive(signal);
|
|
1059
1057
|
return { id: sub.id, name };
|
|
@@ -1071,12 +1069,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1071
1069
|
const current = storage.getTree(treeId);
|
|
1072
1070
|
if (current?.room.name === name)
|
|
1073
1071
|
return current;
|
|
1074
|
-
|
|
1075
|
-
await storage.refreshRoomState(treeId, { signal });
|
|
1076
|
-
}
|
|
1077
|
-
catch {
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1072
|
+
await storage.refreshRoomState(treeId, { signal });
|
|
1080
1073
|
const refreshed = storage.getTree(treeId);
|
|
1081
1074
|
return refreshed?.room.name === name ? refreshed : null;
|
|
1082
1075
|
}, { timeoutMs: 15000, signal });
|
|
@@ -1086,7 +1079,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1086
1079
|
throw error;
|
|
1087
1080
|
if (signal.aborted)
|
|
1088
1081
|
throw new StorageError("operation cancelled");
|
|
1089
|
-
throw new StorageError("rename failed");
|
|
1082
|
+
throw new StorageError("rename failed", { cause: error });
|
|
1090
1083
|
}
|
|
1091
1084
|
return { id: treeId, name };
|
|
1092
1085
|
}, "mutation");
|
|
@@ -1119,7 +1112,7 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1119
1112
|
// part of this operation and must not block deletion merely because
|
|
1120
1113
|
// their membership is not joined.
|
|
1121
1114
|
await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1122
|
-
const spaces = snapshotTreeSpaces(tree, "delete graph is too large");
|
|
1115
|
+
const spaces = snapshotTreeSpaces(tree, "delete graph is too large", client);
|
|
1123
1116
|
const activeSpaces = spaces.filter((space) => !removedRooms.has(space.id));
|
|
1124
1117
|
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
1125
1118
|
if (!ids.has(tree.id)) {
|
|
@@ -1132,13 +1125,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1132
1125
|
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
1133
1126
|
}
|
|
1134
1127
|
catch (error) {
|
|
1135
|
-
if (error instanceof MutationOutcomeUnknownError ||
|
|
1136
|
-
error instanceof MutationPartialError ||
|
|
1137
|
-
error instanceof NonEmptyTreeError)
|
|
1138
|
-
throw error;
|
|
1139
1128
|
if (error instanceof StorageError)
|
|
1140
1129
|
throw error;
|
|
1141
|
-
throw new StorageError("delete failed");
|
|
1130
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1142
1131
|
}
|
|
1143
1132
|
// Keep external parent links intact until the room deletion succeeds.
|
|
1144
1133
|
// Unlinking first would silently detach a live vault on failure.
|
|
@@ -1146,16 +1135,9 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1146
1135
|
await unlinkExternalParents(storage, tree.id, graph.externalParents, operation.signal);
|
|
1147
1136
|
}
|
|
1148
1137
|
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
1138
|
if (error instanceof StorageError)
|
|
1157
1139
|
throw error;
|
|
1158
|
-
throw new StorageError("delete failed");
|
|
1140
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1159
1141
|
}
|
|
1160
1142
|
try {
|
|
1161
1143
|
await deleteRoomDeterministically(storage, tree.id, operation.signal);
|
|
@@ -1167,16 +1149,14 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1167
1149
|
try {
|
|
1168
1150
|
await relinkExternalParents(storage, tree.id, graph.externalParents, new AbortController().signal);
|
|
1169
1151
|
}
|
|
1170
|
-
catch {
|
|
1171
|
-
throw new StorageError("delete cleanup is incomplete"
|
|
1152
|
+
catch (cleanupError) {
|
|
1153
|
+
throw new StorageError("delete cleanup is incomplete", {
|
|
1154
|
+
cause: new AggregateError([error, cleanupError], "delete and external-parent relink both failed", { cause: error }),
|
|
1155
|
+
});
|
|
1172
1156
|
}
|
|
1173
|
-
if (error instanceof MutationOutcomeUnknownError)
|
|
1174
|
-
throw error;
|
|
1175
|
-
if (error instanceof MutationPartialError)
|
|
1176
|
-
throw error;
|
|
1177
1157
|
if (error instanceof StorageError)
|
|
1178
1158
|
throw error;
|
|
1179
|
-
throw new StorageError("delete failed");
|
|
1159
|
+
throw new StorageError("delete failed", { cause: error });
|
|
1180
1160
|
}
|
|
1181
1161
|
return { id: treeId, deleted: true };
|
|
1182
1162
|
}, operation.signal);
|
|
@@ -1216,7 +1196,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1216
1196
|
throw error;
|
|
1217
1197
|
if (signal.aborted)
|
|
1218
1198
|
throw new StorageError("operation cancelled");
|
|
1219
|
-
throw new StorageError("rename file failed");
|
|
1199
|
+
throw new StorageError("rename file failed", { cause: error });
|
|
1220
1200
|
}
|
|
1221
1201
|
return { id: fileId, name };
|
|
1222
1202
|
}, "mutation");
|
|
@@ -1235,11 +1215,11 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1235
1215
|
try {
|
|
1236
1216
|
history = await branch.getVersionHistory();
|
|
1237
1217
|
}
|
|
1238
|
-
catch {
|
|
1239
|
-
throw new StorageError("could not resolve file version history safely");
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
throw new StorageError("could not resolve file version history safely", { cause: error });
|
|
1240
1220
|
}
|
|
1241
|
-
if (!Array.isArray(history) || history.length === 0
|
|
1242
|
-
throw new StorageError("file version history is invalid
|
|
1221
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
1222
|
+
throw new StorageError("file version history is invalid");
|
|
1243
1223
|
}
|
|
1244
1224
|
const eventIds = new Set();
|
|
1245
1225
|
const mediaIds = new Set();
|
|
@@ -1253,8 +1233,8 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1253
1233
|
try {
|
|
1254
1234
|
validateMatrixEventId(version.id, "file version event ID");
|
|
1255
1235
|
}
|
|
1256
|
-
catch {
|
|
1257
|
-
throw new StorageError("file version history contains an invalid event ID");
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
throw new StorageError("file version history contains an invalid event ID", { cause: error });
|
|
1258
1238
|
}
|
|
1259
1239
|
if (eventIds.has(version.id)) {
|
|
1260
1240
|
throw new StorageError("file version history contains a cycle");
|
|
@@ -1264,17 +1244,14 @@ async function resolveFileVersions(branch, signal) {
|
|
|
1264
1244
|
try {
|
|
1265
1245
|
fileInfo = await version.getFileInfo();
|
|
1266
1246
|
}
|
|
1267
|
-
catch {
|
|
1268
|
-
throw new StorageError("could not resolve encrypted file version safely");
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
throw new StorageError("could not resolve encrypted file version safely", { cause: error });
|
|
1269
1249
|
}
|
|
1270
1250
|
const mediaId = fileInfo?.info?.url;
|
|
1271
1251
|
if (typeof mediaId !== "string" || mediaId.length === 0) {
|
|
1272
1252
|
throw new StorageError("encrypted file version has no media identifier");
|
|
1273
1253
|
}
|
|
1274
1254
|
mediaIds.add(mediaId);
|
|
1275
|
-
if (mediaIds.size > MAX_FILE_VERSION_CHAIN) {
|
|
1276
|
-
throw new StorageError("file media version history is too large");
|
|
1277
|
-
}
|
|
1278
1255
|
versions.push({ branch: version, mediaId });
|
|
1279
1256
|
}
|
|
1280
1257
|
return versions;
|
|
@@ -1297,7 +1274,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1297
1274
|
throw error;
|
|
1298
1275
|
if (signal?.aborted)
|
|
1299
1276
|
throw new StorageError("operation cancelled");
|
|
1300
|
-
throw new StorageError("delete file media failed");
|
|
1277
|
+
throw new StorageError("delete file media failed", { cause: error });
|
|
1301
1278
|
}
|
|
1302
1279
|
}
|
|
1303
1280
|
export async function deleteFile(storage, treeId, fileId, options) {
|
|
@@ -1322,9 +1299,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1322
1299
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1323
1300
|
throw error;
|
|
1324
1301
|
if (signal.aborted) {
|
|
1325
|
-
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled");
|
|
1302
|
+
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup was cancelled", { cause: error });
|
|
1326
1303
|
}
|
|
1327
|
-
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped");
|
|
1304
|
+
throw new MutationPartialError("delete file", completedIds, "media was deleted but Matrix event cleanup stopped", { cause: error });
|
|
1328
1305
|
}
|
|
1329
1306
|
try {
|
|
1330
1307
|
await waitForCondition(async () => {
|
|
@@ -1338,7 +1315,7 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1338
1315
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1339
1316
|
throw error;
|
|
1340
1317
|
const detail = "Matrix deletion completed but the inactive file state could not be verified";
|
|
1341
|
-
throw new MutationPartialError("delete file", completedIds, detail);
|
|
1318
|
+
throw new MutationPartialError("delete file", completedIds, detail, { cause: error });
|
|
1342
1319
|
}
|
|
1343
1320
|
for (const { branch: version } of versions) {
|
|
1344
1321
|
markFileDeleted(storage.getClient(), tree.id, version.id);
|
|
@@ -1357,7 +1334,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1357
1334
|
throw error;
|
|
1358
1335
|
if (signal.aborted)
|
|
1359
1336
|
throw new StorageError("operation cancelled");
|
|
1360
|
-
throw new StorageError("upload failed");
|
|
1337
|
+
throw new StorageError("upload failed", { cause: error });
|
|
1361
1338
|
});
|
|
1362
1339
|
// The create-file request is acknowledged before the event necessarily
|
|
1363
1340
|
// arrives in this client's sync timeline. A caller can otherwise report a
|
|
@@ -1376,7 +1353,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
|
|
|
1376
1353
|
catch (error) {
|
|
1377
1354
|
if (error instanceof MutationOutcomeUnknownError)
|
|
1378
1355
|
throw error;
|
|
1379
|
-
throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally");
|
|
1356
|
+
throw new MutationPartialError("upload file", [fileId], "upload completed but the file could not be observed locally", { cause: error });
|
|
1380
1357
|
}
|
|
1381
1358
|
ensureOperationActive(signal);
|
|
1382
1359
|
return { id: fileId, name, mimetype };
|
|
@@ -1391,11 +1368,11 @@ export async function downloadFile(storage, treeId, fileId, options) {
|
|
|
1391
1368
|
result = await storage.downloadFile(branch, signal);
|
|
1392
1369
|
}
|
|
1393
1370
|
catch (error) {
|
|
1394
|
-
if (error instanceof
|
|
1371
|
+
if (error instanceof StorageError)
|
|
1395
1372
|
throw error;
|
|
1396
1373
|
if (signal.aborted)
|
|
1397
1374
|
throw new StorageError("operation cancelled");
|
|
1398
|
-
throw new StorageError("download failed");
|
|
1375
|
+
throw new StorageError("download failed", { cause: error });
|
|
1399
1376
|
}
|
|
1400
1377
|
ensureOperationActive(signal);
|
|
1401
1378
|
return {
|
|
@@ -1425,41 +1402,21 @@ export async function getFileDetails(storage, treeId, fileId, options) {
|
|
|
1425
1402
|
let size = null;
|
|
1426
1403
|
let createdAt = null;
|
|
1427
1404
|
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
1405
|
try {
|
|
1444
1406
|
ensureOperationActive(signal);
|
|
1445
1407
|
const event = await branch.getFileEvent();
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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);
|
|
1408
|
+
const metadata = readFileEventMetadata(event.getContent());
|
|
1409
|
+
mimetype = metadata.mimetype;
|
|
1410
|
+
size = metadata.size;
|
|
1411
|
+
createdAt = tsToIso(event.getTs());
|
|
1457
1412
|
updatedAt = createdAt;
|
|
1458
1413
|
}
|
|
1459
|
-
catch {
|
|
1414
|
+
catch (error) {
|
|
1460
1415
|
if (signal.aborted)
|
|
1461
1416
|
throw new StorageError("operation cancelled");
|
|
1462
|
-
|
|
1417
|
+
if (error instanceof StorageError)
|
|
1418
|
+
throw error;
|
|
1419
|
+
throw new StorageError("get file details failed", { cause: error });
|
|
1463
1420
|
}
|
|
1464
1421
|
ensureOperationActive(signal);
|
|
1465
1422
|
return { name, mimetype, size, createdAt, updatedAt };
|
|
@@ -1479,8 +1436,8 @@ async function getTreeDetails(storage, treeId, signal) {
|
|
|
1479
1436
|
try {
|
|
1480
1437
|
memberCount = room.getJoinedMemberCount();
|
|
1481
1438
|
}
|
|
1482
|
-
catch {
|
|
1483
|
-
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
throw new StorageError("member count lookup failed", { cause: error });
|
|
1484
1441
|
}
|
|
1485
1442
|
}
|
|
1486
1443
|
return {
|